fix: resolve 3 upstream bugs (summarize, ChromaSync, HealthMonitor) (#1566)

* fix: resolve 3 upstream bugs in summarize, ChromaSync, and HealthMonitor

1. summarize.ts: Skip summary when transcript has no assistant message.
   Prevents error loop where empty transcripts cause repeated failed
   summarize attempts (~30 errors/day observed in production).

2. ChromaSync.ts: Fallback to chroma_update_documents when add fails
   with "IDs already exist". Handles partial writes after MCP timeout
   without waiting for next backfill cycle.

3. HealthMonitor.ts: Replace HTTP-based isPortInUse with atomic socket
   bind on Unix. Eliminates TOCTOU race when two sessions start
   simultaneously (HTTP check is non-atomic — both see "port free"
   before either completes listen()). Updated tests accordingly.

All three bugs are pre-existing in v10.5.5. Confirmed via log analysis
of 543K lines over 17 days of production usage across two servers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: add CONTRIB_NOTES.md to gitignore

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address CodeRabbit review on PR #1566

- HealthMonitor: add APPROVED OVERRIDE annotation for Win32 HTTP fallback
- ChromaSync: replace chroma_update_documents with delete+add for proper
  upsert (update only modifies existing IDs, silently ignores missing ones)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Alessandro Costa <alessandro@claudio.dev>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alessandro Costa
2026-04-04 19:15:08 -03:00
committed by GitHub
parent 5a27420809
commit 64cce2bf10
5 changed files with 177 additions and 52 deletions
+93 -38
View File
@@ -1,4 +1,5 @@
import { describe, it, expect, beforeEach, afterEach, mock } from 'bun:test';
import { describe, it, expect, beforeEach, afterEach, mock, spyOn } from 'bun:test';
import net from 'net';
import {
isPortInUse,
waitForHealth,
@@ -15,45 +16,73 @@ describe('HealthMonitor', () => {
});
describe('isPortInUse', () => {
it('should return true for occupied port (health check succeeds)', async () => {
global.fetch = mock(() => Promise.resolve({ ok: true } as Response));
// Note: Since we are on Linux (as per session_context), isPortInUse uses 'net'
// instead of 'fetch'. We need to mock 'net.createServer().listen()'
it('should return true for occupied port (EADDRINUSE)', async () => {
// Create a specific mock for this test
const createServerMock = mock(() => ({
once: mock((event: string, cb: Function) => {
if (event === 'error') {
// Trigger EADDRINUSE immediately
setTimeout(() => cb({ code: 'EADDRINUSE' }), 0);
}
}),
listen: mock(() => {})
}));
const spy = spyOn(net, 'createServer').mockImplementation(createServerMock as any);
const result = await isPortInUse(37777);
expect(result).toBe(true);
expect(global.fetch).toHaveBeenCalledWith('http://127.0.0.1:37777/api/health');
expect(net.createServer).toHaveBeenCalled();
spy.mockRestore();
});
it('should return false for free port (connection refused)', async () => {
global.fetch = mock(() => Promise.reject(new Error('ECONNREFUSED')));
it('should return false for free port (listening succeeds)', async () => {
const closeMock = mock((cb: Function) => cb());
const createServerMock = mock(() => ({
once: mock((event: string, cb: Function) => {
if (event === 'listening') {
// Trigger listening success
setTimeout(() => cb(), 0);
}
}),
listen: mock(() => {}),
close: closeMock
}));
const spy = spyOn(net, 'createServer').mockImplementation(createServerMock as any);
const result = await isPortInUse(39999);
expect(result).toBe(false);
expect(net.createServer).toHaveBeenCalled();
expect(closeMock).toHaveBeenCalled();
spy.mockRestore();
});
it('should return false when health check returns non-ok', async () => {
global.fetch = mock(() => Promise.resolve({ ok: false, status: 503 } as Response));
const result = await isPortInUse(37777);
expect(result).toBe(false);
});
it('should return false on network timeout', async () => {
global.fetch = mock(() => Promise.reject(new Error('ETIMEDOUT')));
const result = await isPortInUse(37777);
expect(result).toBe(false);
});
it('should return false on fetch failed error', async () => {
global.fetch = mock(() => Promise.reject(new Error('fetch failed')));
it('should return false for other socket errors', async () => {
const createServerMock = mock(() => ({
once: mock((event: string, cb: Function) => {
if (event === 'error') {
// Trigger other error (e.g., EACCES)
setTimeout(() => cb({ code: 'EACCES' }), 0);
}
}),
listen: mock(() => {})
}));
const spy = spyOn(net, 'createServer').mockImplementation(createServerMock as any);
const result = await isPortInUse(37777);
expect(result).toBe(false);
spy.mockRestore();
});
});
@@ -203,54 +232,80 @@ describe('HealthMonitor', () => {
describe('waitForPortFree', () => {
it('should return true immediately when port is already free', async () => {
global.fetch = mock(() => Promise.reject(new Error('ECONNREFUSED')));
const createServerMock = mock(() => ({
once: mock((event: string, cb: Function) => {
if (event === 'listening') setTimeout(() => cb(), 0);
}),
listen: mock(() => {}),
close: mock((cb: Function) => cb())
}));
const spy = spyOn(net, 'createServer').mockImplementation(createServerMock as any);
const start = Date.now();
const result = await waitForPortFree(39999, 5000);
const elapsed = Date.now() - start;
expect(result).toBe(true);
// Should return quickly
expect(elapsed).toBeLessThan(1000);
spy.mockRestore();
});
it('should timeout when port remains occupied', async () => {
global.fetch = mock(() => Promise.resolve({ ok: true } as Response));
const createServerMock = mock(() => ({
once: mock((event: string, cb: Function) => {
if (event === 'error') setTimeout(() => cb({ code: 'EADDRINUSE' }), 0);
}),
listen: mock(() => {})
}));
const spy = spyOn(net, 'createServer').mockImplementation(createServerMock as any);
const start = Date.now();
const result = await waitForPortFree(37777, 1500);
const elapsed = Date.now() - start;
expect(result).toBe(false);
// Should take close to timeout duration
expect(elapsed).toBeGreaterThanOrEqual(1400);
expect(elapsed).toBeLessThan(2500);
spy.mockRestore();
});
it('should succeed when port becomes free', async () => {
let callCount = 0;
global.fetch = mock(() => {
callCount++;
// Port occupied for first 2 checks, then free
if (callCount < 3) {
return Promise.resolve({ ok: true } as Response);
}
return Promise.reject(new Error('ECONNREFUSED'));
});
const spy = spyOn(net, 'createServer').mockImplementation(() => ({
once: mock((event: string, cb: Function) => {
callCount++;
// Port occupied for first 2 checks, then free
if (callCount < 3) {
if (event === 'error') setTimeout(() => cb({ code: 'EADDRINUSE' }), 0);
} else {
if (event === 'listening') setTimeout(() => cb(), 0);
}
}),
listen: mock(() => {}),
close: mock((cb: Function) => cb())
} as any));
const result = await waitForPortFree(37777, 5000);
expect(result).toBe(true);
expect(callCount).toBeGreaterThanOrEqual(3);
spy.mockRestore();
});
it('should use default timeout when not specified', async () => {
global.fetch = mock(() => Promise.reject(new Error('ECONNREFUSED')));
const createServerMock = mock(() => ({
once: mock((event: string, cb: Function) => {
if (event === 'listening') setTimeout(() => cb(), 0);
}),
listen: mock(() => {}),
close: mock((cb: Function) => cb())
}));
const spy = spyOn(net, 'createServer').mockImplementation(createServerMock as any);
// Just verify it doesn't throw and returns quickly
const result = await waitForPortFree(39999);
expect(result).toBe(true);
spy.mockRestore();
});
});
});