All checks were successful
Quality gates / quality (pull_request) Successful in 1m28s
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
191 lines
8.1 KiB
JavaScript
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);
|
|
});
|