import test from 'node:test'; import assert from 'node:assert/strict'; import { AgentGatewayError, createHermesAgentService, resolveHermesAgentConfig } from '../src/hermes-agent-service.js'; const origin = 'http://127.0.0.1:4173'; const accessCode = 'test-agent-access-code-2026'; const configured = () => resolveHermesAgentConfig({ TIMMY_AGENT_ENABLED: 'true', TIMMY_AGENT_ACCESS_TOKEN: accessCode, TIMMY_PUBLIC_ORIGIN: origin, TIMMY_AGENT_WORKDIR: '/tmp/timmy-agent-workspace', }); function deferred() { let resolve; let reject; const promise = new Promise((res, rej) => { resolve = res; reject = rej; }); return { promise, resolve, reject }; } async function unlock(service, token) { await service.unlock({ origin, accessCode }); return token; } test('chat requests beyond the queue depth get a stable sanitized overload fallback', async () => { let clock = 0; const gate = deferred(); let started = 0; const service = createHermesAgentService({ config: { ...configured(), maxQueueDepth: 1 }, randomToken: () => 'queue-cookie', now: () => clock, runTurn: async () => { started += 1; return gate.promise; }, }); await unlock(service); const first = service.chat({ origin, cookieToken: 'queue-cookie', payload: { message: 'first', ledger: [] } }); await new Promise(resolve => setImmediate(resolve)); assert.equal(started, 1); const second = service.chat({ origin, cookieToken: 'queue-cookie', payload: { message: 'second', ledger: [] } }); await new Promise(resolve => setImmediate(resolve)); assert.equal(started, 1, 'second request waits in the bounded queue'); await assert.rejects(() => service.chat({ origin, cookieToken: 'queue-cookie', payload: { message: 'third', ledger: [] } }), error => { assert.equal(error.status, 503); assert.match(error.message, /busy|try again/i); assert.doesNotMatch(error.message, /spawn|hermes|session|token|workdir/i); return true; }, 'request beyond max queue depth fails fast with sanitized copy'); gate.resolve({ reply: 'eventually', sessionId: 'private-session' }); assert.match((await first).reply, /eventually/); assert.match((await second).reply, /eventually/); }); test('a chat request abandoned by its browser is cancelled without running Hermes and frees its slot', async () => { let clock = 0; const gate = deferred(); const started = []; const service = createHermesAgentService({ config: { ...configured(), maxQueueDepth: 2 }, randomToken: () => 'disconnect-cookie', now: () => clock, runTurn: async ({ signal }) => { started.push('run'); return new Promise((resolve, reject) => { signal.addEventListener('abort', () => reject(new Error('aborted'))); gate.promise.then(resolve, reject); }); }, }); await unlock(service, 'disconnect-cookie'); const active = service.chat({ origin, cookieToken: 'disconnect-cookie', payload: { message: 'holding slot', ledger: [] } }); await new Promise(resolve => setImmediate(resolve)); const abandonSignal = new AbortController(); const abandoned = service.chat({ origin, cookieToken: 'disconnect-cookie', payload: { message: 'queued', ledger: [] }, signal: abandonSignal.signal }); await new Promise(resolve => setImmediate(resolve)); abandonSignal.abort(new Error('client disconnected')); await assert.rejects(() => abandoned, error => error.status === 503 && /closed|disconnect/i.test(error.message)); gate.resolve({ reply: 'late answer', sessionId: 'private-session' }); assert.match((await active).reply, /late answer/); assert.deepEqual(started, ['run'], 'the cancelled request never invoked a Hermes turn'); });