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, urgentSymptomCopy } from '../src/domain.js'; // Pinned literally so any drift in the authoritative copy fails this suite. const URGENT_MESSAGE_COPY = 'These reported symptoms can need prompt medical care. Contact a clinician or urgent service now; call emergency services for heavy or nonstop bleeding, fainting, or severe worsening symptoms.'; assert.equal(urgentSymptomCopy.flagsMessage, URGENT_MESSAGE_COPY); 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); } }); // The review-reported false negatives plus their case/punctuation/contraction // variants. Each phrase is proven twice: once against the detector and once // through the authoritative service gate, which must override before any // Hermes call happens. const REVIEW_REGRESSION_PHRASES = [ 'I have a fever right now', 'I HAVE A FEVER RIGHT NOW!', 'my fever is 103', 'My fever is 103.', 'my fever is 103.5 degrees', 'fever started this morning', 'Fever started this morning?', 'severe pain in the abdomen.', 'Severe pain in THE abdomen!!', 'severe pain around the abdomen', "I've had a fever since monday", ]; test('review-reported false negatives escalate at the detector for every variant', async () => { for (const phrase of REVIEW_REGRESSION_PHRASES) { const result = detectUrgentText(phrase); assert.equal(result.urgent, true, JSON.stringify(phrase)); assert.equal(result.message, URGENT_MESSAGE_COPY, phrase); } }); test('review-reported false negatives are intercepted at the service boundary with zero Hermes calls', async () => { for (const phrase of REVIEW_REGRESSION_PHRASES) { const calls = []; const service = createHermesAgentService({ config: configured(), randomToken: () => 'review-regression-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: 'review-regression-cookie', payload: { message: phrase, ledger: [] } }); assert.equal(calls.length, 0, `${JSON.stringify(phrase)} 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); } }); 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); });