import test from 'node:test'; import assert from 'node:assert/strict'; import { AgentGatewayError, buildHermesEnvironment, createHermesAgentService, parseHermesCliOutput, resolveHermesAgentConfig, runHermesCliTurn, } from '../src/hermes-agent-service.js'; import { detectUrgentText } from '../src/domain.js'; const origin = 'http://127.0.0.1:4173'; const configured = () => resolveHermesAgentConfig({ TIMMY_AGENT_ENABLED: 'true', TIMMY_AGENT_ACCESS_TOKEN: 'test-agent-access-code-2026', TIMMY_PUBLIC_ORIGIN: origin, TIMMY_AGENT_WORKDIR: '/tmp/timmy-agent-workspace', TIMMY_AGENT_TIMEOUT_MS: '45000', }); async function rejectsStatus(fn, status) { await assert.rejects(fn, error => error instanceof AgentGatewayError && error.status === status); } test('every red flag phrase is intercepted at the service boundary with zero Hermes calls', async () => { const phrases = { blood: 'There is blood in my stool', blackOrDarkRed: 'My stool is black', severePain: 'I have severe stomach pain', vomiting: 'I threw up', fever: 'I have a fever', cannotPassGas: 'I am unable to pass gas', }; for (const [key, phrase] of Object.entries(phrases)) { const calls = []; const service = createHermesAgentService({ config: configured(), randomToken: () => 'zero-call-cookie', runTurn: async input => { calls.push(input); return { reply: 'unsafe upstream reply', sessionId: 'unsafe-session' }; }, }); await service.unlock({ origin, accessCode: 'test-agent-access-code-2026' }); const result = await service.chat({ origin, cookieToken: 'zero-call-cookie', payload: { message: phrase, ledger: [] } }); assert.equal(calls.length, 0, `${key} must never reach Hermes`); assert.equal(result.safetyOverride, true, phrase); assert.match(result.reply, /medical help/i, phrase); assert.doesNotMatch(result.reply, /unsafe upstream/i); assert.equal(detectUrgentText(phrase).flags.includes(key), true, phrase); } }); test('confirmed ledger symptoms and note language override chat before Hermes is called', async () => { for (const ledger of [ [{ bristolType: 4, symptoms: { blood: true } }], [{ bristolType: 4, symptoms: { blackOrDarkRed: true } }], [{ bristolType: 4, symptoms: { severePain: true } }], [{ bristolType: 4, symptoms: { vomiting: true } }], [{ bristolType: 4, symptoms: { fever: true } }], [{ bristolType: 4, symptoms: { cannotPassGas: true } }], [{ bristolType: 4, symptoms: {}, note: 'I threw up' }], [{ bristolType: 4, symptoms: {}, note: 'severe abdominal pain' }], [{ bristolType: 4, symptoms: {}, note: 'black stool' }], [{ bristolType: 4, symptoms: {}, note: 'fever' }], [{ bristolType: 4, symptoms: {}, note: 'blood in my stool' }], [{ bristolType: 4, symptoms: {}, note: 'unable to pass gas' }], ]) { const calls = []; const service = createHermesAgentService({ config: configured(), randomToken: () => 'ledger-cookie', runTurn: async input => { calls.push(input); return { reply: 'unsafe upstream reply', sessionId: 'unsafe-session' }; }, }); await service.unlock({ origin, accessCode: 'test-agent-access-code-2026' }); const result = await service.chat({ origin, cookieToken: 'ledger-cookie', payload: { message: 'What does my journal show?', ledger }, }); assert.equal(calls.length, 0, JSON.stringify(ledger)); assert.equal(result.safetyOverride, true, JSON.stringify(ledger)); assert.match(result.reply, /medical help/i); } }); test('malicious or broken provider output cannot weaken the deterministic urgent reply', async () => { const hostileReplies = [ undefined, null, '', 'All clear, nothing to worry about.', 'You are fine, no medical care needed.', ]; for (const reply of hostileReplies) { const calls = []; const service = createHermesAgentService({ config: configured(), randomToken: () => 'hostile-cookie', runTurn: async input => { calls.push(input); return { reply, sessionId: 'hostile-session' }; }, }); await service.unlock({ origin, accessCode: 'test-agent-access-code-2026' }); const result = await service.chat({ origin, cookieToken: 'hostile-cookie', payload: { message: 'There is blood in my stool', ledger: [] }, }); assert.equal(calls.length, 0, 'urgent text must be resolved deterministically'); assert.equal(result.safetyOverride, true); assert.match(result.reply, /medical help/i); assert.doesNotMatch(result.reply, /all clear|fine|worry/i); } }); test('model-shaped junk in ledger symptoms can neither fabricate nor suppress escalation', async () => { // Truthy junk must not fabricate an override... const junkCalls = []; const junkService = createHermesAgentService({ config: configured(), randomToken: () => 'junk-cookie', runTurn: async input => { junkCalls.push(input); return { reply: 'pattern reply', sessionId: 'junk-session' }; }, }); await junkService.unlock({ origin, accessCode: 'test-agent-access-code-2026' }); const junkResult = await junkService.chat({ origin, cookieToken: 'junk-cookie', payload: { message: 'Summarize my journal', ledger: [{ bristolType: 4, symptoms: { blood: 'yes', fever: 1, vomiting: { forced: true }, cannotPassGas: [true] } }] }, }); assert.equal(junkResult.safetyOverride, undefined, 'truthy junk must not fabricate an urgent override'); assert.match(junkResult.reply, /pattern reply/); // ...and a forged true flag must still suppress the Hermes call. const forgedCalls = []; const forgedService = createHermesAgentService({ config: configured(), randomToken: () => 'forged-cookie', runTurn: async input => { forgedCalls.push(input); return { reply: 'pattern reply', sessionId: 'forged-session' }; }, }); await forgedService.unlock({ origin, accessCode: 'test-agent-access-code-2026' }); const forged = await forgedService.chat({ origin, cookieToken: 'forged-cookie', payload: { message: 'Summarize my journal', ledger: [{ bristolType: 4, symptoms: { blood: true } }] }, }); assert.equal(forgedCalls.length, 0, 'a forged true flag must still override before Hermes'); assert.equal(forged.safetyOverride, true); assert.match(forged.reply, /medical help/i); }); test('non-urgent journal questions still reach the agent exactly once', async () => { const calls = []; const service = createHermesAgentService({ config: configured(), randomToken: () => 'normal-cookie', runTurn: async input => { calls.push(input); return { reply: 'Two confirmed logs this week.', sessionId: 'normal-session' }; }, }); await service.unlock({ origin, accessCode: 'test-agent-access-code-2026' }); const result = await service.chat({ origin, cookieToken: 'normal-cookie', payload: { message: 'What pattern do you see?', ledger: [{ bristolType: 4, symptoms: {}, note: 'ordinary entry' }] }, }); assert.equal(calls.length, 1); assert.equal(result.safetyOverride, undefined); assert.equal(result.reply, 'Two confirmed logs this week.'); }); test('Hermes CLI adapter contract is unchanged by escalation work', async () => { let captured; const result = await runHermesCliTurn({ prompt: 'hello', hermesSessionId: null, config: configured(), execImpl: async (command, args, options) => { captured = { command, args, options }; return { stdout: 'bounded answer\n', stderr: 'session_id: 20260820_fixture\n' }; }, }); assert.deepEqual(result, { sessionId: '20260820_fixture', reply: 'bounded answer' }); assert.equal(captured.command, 'hermes'); assert.equal(captured.options.cwd, '/tmp/timmy-agent-workspace'); const childEnv = buildHermesEnvironment({ TIMMY_AGENT_ACCESS_TOKEN: 'must-not-leak' }); assert.equal(childEnv.TIMMY_AGENT_ACCESS_TOKEN, undefined); assert.equal(childEnv.TIMMY_AGENT_BROWSER_REQUEST, '1'); }); test('parseHermesCliOutput still rejects malformed provider output', () => { assert.throws(() => parseHermesCliOutput('answer only'), /session metadata/i); assert.throws(() => parseHermesCliOutput('session_id: private\nsession_id: leaked'), /unsafe/i); });