Quality gates / quality (pull_request) Failing after 2m50s
Chat now has to show its work. Every answer that states a count, a date, or a visible form cites the confirmed records behind it, and following a citation opens exactly that log. Questions the journal cannot answer (food safety, medicine, what a symptom means) get boundary copy with no borrowed evidence, and a sparse window refuses to describe a trend instead of naming one. The Hermes path is held to the same rule: the server gives each ledger record a positional ref, the prompt requires inline [entry:L<n>] citations, and the browser resolves them against the ledger it sent. A ref that matches no record is stripped and the answer is labelled unverified rather than rendered as a source, so an invented citation cannot look like evidence. The release recording now exercises the grounded local answer, its citation sheet, the refusal, and a Hermes reply whose refs resolve, each asserted in the browser with fictional logs.
155 lines
5.9 KiB
JavaScript
155 lines
5.9 KiB
JavaScript
import test from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
|
||
import { answerFromLedger, buildChatLedger, resolveAgentReply } from '../src/grounded-chat.js';
|
||
|
||
const DAY = 86400000;
|
||
const NOW = Date.parse('2026-09-20T12:00:00.000Z');
|
||
const entry = (id, daysAgo, bristolType, extra = {}) => ({
|
||
id,
|
||
occurredAt: new Date(NOW - daysAgo * DAY).toISOString(),
|
||
bristolType,
|
||
color: 'brown',
|
||
urgency: 1,
|
||
discomfort: 1,
|
||
note: '',
|
||
photoDataUrl: '',
|
||
symptoms: {},
|
||
...extra,
|
||
});
|
||
|
||
test('a frequency question answers with the count from the window and cites exactly those logs', () => {
|
||
const answer = answerFromLedger(
|
||
'How many times have I logged in the last four weeks?',
|
||
[entry('a', 2, 4), entry('b', 5, 3), entry('c', 9, 6), entry('d', 12, 4), entry('old', 40, 2)],
|
||
{ now: NOW },
|
||
);
|
||
|
||
assert.equal(answer.kind, 'frequency');
|
||
assert.equal(answer.grounded, true);
|
||
assert.match(answer.text, /4 confirmed logs/);
|
||
assert.match(answer.text, /last 28 days/);
|
||
assert.deepEqual(answer.citations.map(citation => citation.id).sort(), ['a', 'b', 'c', 'd']);
|
||
// The out-of-window log is never counted and never offered as a source.
|
||
assert.equal(answer.citations.some(citation => citation.id === 'old'), false);
|
||
});
|
||
|
||
test('a recency question cites the one log it names, even when the ledger is out of order', () => {
|
||
const answer = answerFromLedger(
|
||
'When did I last log something?',
|
||
[entry('middle', 6, 5), entry('newest', 1, 3), entry('older', 11, 2)],
|
||
{ now: NOW },
|
||
);
|
||
|
||
assert.equal(answer.kind, 'latest');
|
||
assert.equal(answer.grounded, true);
|
||
assert.equal(answer.citations.length, 1);
|
||
assert.equal(answer.citations[0].id, 'newest');
|
||
assert.equal(answer.citations[0].bristolType, 3);
|
||
assert.match(answer.text, /most recent confirmed log/i);
|
||
assert.match(answer.text, /Type 3/);
|
||
});
|
||
|
||
test('a question the journal cannot answer refuses instead of reassuring', () => {
|
||
const answer = answerFromLedger(
|
||
'Is it safe to eat tacos with this?',
|
||
[entry('a', 2, 4), entry('b', 5, 3), entry('c', 9, 6)],
|
||
{ now: NOW },
|
||
);
|
||
|
||
assert.equal(answer.kind, 'unsupported');
|
||
assert.equal(answer.grounded, false);
|
||
assert.deepEqual(answer.citations, []);
|
||
assert.match(answer.text, /cannot (clear|decide|tell)/i);
|
||
// Boundary copy may not invent a verdict about health or food.
|
||
assert.doesNotMatch(answer.text, /safe to eat|it is fine|you should eat|diagnos/i);
|
||
});
|
||
|
||
test('a pattern question on a sparse window refuses to describe a trend', () => {
|
||
const answer = answerFromLedger(
|
||
'What pattern do you see?',
|
||
[entry('a', 2, 4), entry('b', 5, 6)],
|
||
{ now: NOW },
|
||
);
|
||
|
||
assert.equal(answer.kind, 'insufficient');
|
||
assert.equal(answer.grounded, false);
|
||
assert.deepEqual(answer.citations, []);
|
||
assert.match(answer.text, /not enough confirmed logs/i);
|
||
assert.match(answer.text, /2 of 3/);
|
||
});
|
||
|
||
test('a pattern question cites every confirmed log the described pattern rests on', () => {
|
||
const answer = answerFromLedger(
|
||
'What pattern do you see?',
|
||
[entry('a', 2, 1), entry('b', 5, 4), entry('c', 9, 4), entry('d', 12, 4)],
|
||
{ now: NOW },
|
||
);
|
||
|
||
assert.equal(answer.kind, 'pattern');
|
||
assert.equal(answer.grounded, true);
|
||
assert.match(answer.text, /4 confirmed logs/);
|
||
assert.match(answer.text, /Firm side \(Type 1–2\): 1 of 4/);
|
||
assert.deepEqual(answer.citations.map(citation => citation.id).sort(), ['a', 'b', 'c', 'd']);
|
||
});
|
||
|
||
test('a Hermes reply resolves the record references it cites and never shows the raw marker', () => {
|
||
const { chatRecords } = buildChatLedger([entry('a', 2, 4), entry('b', 5, 3)], { now: NOW });
|
||
|
||
const resolved = resolveAgentReply(
|
||
'Your newest Type 4 log is the one from this week [entry:L1], and the Type 3 before it [entry:L2].',
|
||
chatRecords,
|
||
);
|
||
|
||
assert.equal(resolved.text.includes('[entry:'), false);
|
||
assert.match(resolved.text, /newest Type 4 log/);
|
||
assert.deepEqual(resolved.citations.map(citation => citation.id), ['a', 'b']);
|
||
assert.equal(resolved.unverified, false);
|
||
});
|
||
|
||
test('a citation to a record that is not in the ledger is dropped instead of being shown as a source', () => {
|
||
const { chatRecords } = buildChatLedger([entry('a', 2, 4)], { now: NOW });
|
||
|
||
const resolved = resolveAgentReply('You had 9 logs last month [entry:L99].', chatRecords);
|
||
|
||
assert.equal(resolved.text.includes('[entry:'), false);
|
||
assert.deepEqual(resolved.citations, []);
|
||
assert.equal(resolved.unresolvedRefs, 1);
|
||
assert.equal(resolved.unverified, true);
|
||
});
|
||
|
||
test('an uncited claim about the journal is flagged as unverified rather than presented as fact', () => {
|
||
const { chatRecords } = buildChatLedger([entry('a', 2, 4), entry('b', 5, 3)], { now: NOW });
|
||
|
||
const resolved = resolveAgentReply('You logged 7 times this week and you are improving.', chatRecords);
|
||
|
||
assert.equal(resolved.unverified, true);
|
||
assert.deepEqual(resolved.citations, []);
|
||
assert.match(resolved.notice, /not verified against your journal/i);
|
||
});
|
||
|
||
test('a privacy question answers with the stored-data boundary instead of the journal summary', () => {
|
||
const answer = answerFromLedger('What happens to my photo?', [entry('a', 2, 4), entry('b', 5, 3), entry('c', 9, 6)], { now: NOW });
|
||
|
||
assert.equal(answer.kind, 'privacy');
|
||
assert.equal(answer.grounded, false);
|
||
assert.deepEqual(answer.citations, []);
|
||
assert.match(answer.text, /browser/i);
|
||
assert.match(answer.text, /never receives photos/i);
|
||
});
|
||
|
||
test('an unmatched question still gets a grounded answer instead of an empty bubble', () => {
|
||
const answer = answerFromLedger('hello', [entry('a', 2, 4), entry('b', 5, 3), entry('c', 9, 6)], { now: NOW });
|
||
|
||
assert.equal(answer.kind, 'summary');
|
||
assert.equal(answer.grounded, true);
|
||
assert.ok(answer.text.length > 20, 'a general question must not produce an empty reply');
|
||
assert.equal(answer.citations.length, 3);
|
||
|
||
const empty = answerFromLedger('hello', [], { now: NOW });
|
||
assert.equal(empty.grounded, false);
|
||
assert.deepEqual(empty.citations, []);
|
||
assert.match(empty.text, /no confirmed logs|not enough/i);
|
||
});
|
||
|