timmy-talking-turd/tests/domain.test.js
Timmy 3ae2a72b67
All checks were successful
Quality gates / quality (pull_request) Successful in 1m19s
fix: recognize past-tense vomiting in chat safety
2026-08-20 17:04:10 +00:00

119 lines
4.2 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import {
bucketForBristolType,
buildTimmySummary,
detectUrgentFlags,
detectUrgentText,
exportLedger,
hasUrgentLedgerContext,
photoQualityMessage,
sanitizeEntry,
} from '../src/domain.js';
test('maps Bristol types to clinically grounded buckets', () => {
assert.equal(bucketForBristolType(1), 'constipation');
assert.equal(bucketForBristolType(2), 'constipation');
assert.equal(bucketForBristolType(3), 'typical');
assert.equal(bucketForBristolType(4), 'typical');
assert.equal(bucketForBristolType(5), 'loose');
assert.equal(bucketForBristolType(7), 'loose');
assert.equal(bucketForBristolType(0), 'unknown');
});
test('escalates reported blood, black stool, severe pain, vomiting, fever, or inability to pass gas', () => {
const result = detectUrgentFlags({
blood: true,
blackOrDarkRed: false,
severePain: true,
vomiting: false,
fever: false,
cannotPassGas: false,
});
assert.equal(result.urgent, true);
assert.deepEqual(result.flags, ['blood', 'severePain']);
assert.match(result.message, /medical care/i);
});
test('does not invent reassurance when no urgent flags are reported', () => {
const result = detectUrgentFlags({});
assert.equal(result.urgent, false);
assert.deepEqual(result.flags, []);
assert.match(result.message, /not a diagnosis/i);
});
test('detects common urgent symptom language without matching unrelated blood wording', () => {
for (const message of [
'I have rectal bleeding',
'There is blood in my stool',
'My stool is black',
'I have severe stomach pain',
'I am throwing up and have a fever',
'I threw up',
'I puked twice',
'I am barfing',
'I barfed',
'I hurled',
'I am upchucking',
'I spewed',
'I tossed my cookies',
'I lost my lunch',
'I have emesis',
'I am unable to pass gas',
]) assert.equal(detectUrgentText(message).urgent, true, message);
assert.equal(detectUrgentText('My blood pressure was checked').urgent, false);
});
test('detects urgent flags or language in confirmed ledger context', () => {
assert.equal(hasUrgentLedgerContext([{ symptoms: { cannotPassGas: true }, note: '' }]), true);
assert.equal(hasUrgentLedgerContext([{ symptoms: {}, note: 'I have rectal bleeding' }]), true);
assert.equal(hasUrgentLedgerContext([{ symptoms: {}, note: 'ordinary entry' }]), false);
});
test('Timmy summary reports patterns without clearing food or diagnosing disease', () => {
const entries = [
{ bristolType: 3, occurredAt: '2026-08-15T08:00:00.000Z' },
{ bristolType: 4, occurredAt: '2026-08-16T08:00:00.000Z' },
{ bristolType: 6, occurredAt: '2026-08-17T08:00:00.000Z' },
];
const summary = buildTimmySummary(entries);
assert.match(summary, /3 logs/);
assert.match(summary, /2 typical/);
assert.doesNotMatch(summary, /safe|diagnos|Taco Bell|clear/i);
});
test('sanitizes a user entry to the MVP data contract', () => {
const entry = sanitizeEntry({
id: 'abc',
occurredAt: '2026-08-17T12:00:00.000Z',
bristolType: 4,
color: 'brown',
urgency: 2,
discomfort: 1,
note: 'After lunch',
photoDataUrl: 'data:image/jpeg;base64,abc',
unexpected: 'drop me',
});
assert.deepEqual(Object.keys(entry).sort(), [
'bristolType', 'color', 'discomfort', 'id', 'note', 'occurredAt',
'photoDataUrl', 'symptoms', 'urgency'
].sort());
assert.equal(entry.unexpected, undefined);
});
test('photo quality guidance is deterministic and does not claim visual diagnosis', () => {
assert.match(photoQualityMessage({ width: 300, height: 300, brightness: 0.5 }), /closer/i);
assert.match(photoQualityMessage({ width: 1200, height: 900, brightness: 0.02 }), /light/i);
assert.match(photoQualityMessage({ width: 1200, height: 900, brightness: 0.5 }), /review/i);
assert.doesNotMatch(photoQualityMessage({ width: 1200, height: 900, brightness: 0.5 }), /type [1-7]|disease|diagnos/i);
});
test('export ledger is portable JSON with version and entries', () => {
const text = exportLedger([{ id: 'a', bristolType: 4 }], '2026-08-18T00:00:00.000Z');
const parsed = JSON.parse(text);
assert.equal(parsed.schemaVersion, 1);
assert.equal(parsed.exportedAt, '2026-08-18T00:00:00.000Z');
assert.equal(parsed.entries.length, 1);
});