timmy-talking-turd/tests/escalation-boundary.test.js
Timmy e05d7d1e1a
All checks were successful
Quality gates / quality (pull_request) Successful in 1m28s
test: expand deterministic symptom escalation regression suite
Issue #21 (epic #5). Prove model behavior can never suppress or soften
urgent symptom handling.

- Pin the six authoritative red flags, their canonical order, and the
  exact urgent copy as read-only exports; hostile provider output
  cannot reassemble it.
- Grow the positive text matrix from 24 to 56 pinned clinical phrases
  across all flags (bloody stool, dark-red/black descriptions, severe
  abdominal pain variants, vomiting tenses/slang, fever phrasings,
  inability to pass gas) and pin 27 idiomatic negatives that must not
  escalate (threw up my hands, yellow fever history class, black tea).
- Authoritative boundary suite: every red-flag phrase and confirmed
  ledger symptom/note intercepts chat with zero Hermes calls;
  malicious/missing provider replies cannot weaken the deterministic
  response; truthy junk symptoms can neither fabricate nor suppress
  escalation.
- Wiring suite: the single chat gate screens urgency before any agent
  turn in both service and browser code; detection stays centralized
  in the frozen domain pattern table.

RED evidence: URGENT_MESSAGE unexported, 9 matrix misses (bleeding from
my rectum, bloody stool/poop, severe pain in my abdomen), 8 false
positives (I threw up my hands, feverish about the election).

GREEN: 87/87 npm test, syntax/diff gates clean, 0 vulnerabilities,
browser suites pass with zero /api/agent/chat calls on urgent input.

Closes #21
2026-08-22 20:57:04 +00:00

191 lines
8.1 KiB
JavaScript

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);
});