The pool was cases — "A 6-week-old with 3 days of projectile vomiting… which fluid do you start?" — and the user, reading them on the assistant's front page: "Vague nonsense clinical scenarios. I want concrete things people learn from and pathophysiology if possible, all related to peds. No more clinical scenarios. Use deepseek with its reasoning off." So the brief is inverted. A question asks one definite thing with a definite answer — a mechanism, a distinction between two look-alikes, the number that decides, the reason behind a rule of practice — mechanism first, naming its disease, drug or finding, and staying paediatric. The filter refuses a vignette opener and a bare definition instead of demanding a number; the prompt version moves to 3 so every deployment rebuilds the pool once; the model defaults to DeepSeek flash with thinking disabled, which the assistant already knows how to ask for. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016fZGJNyDvERbMgS2Uc2msP
128 lines
7.4 KiB
JavaScript
128 lines
7.4 KiB
JavaScript
const { test } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
|
|
const root = path.join(__dirname, '..');
|
|
|
|
function read(relativePath) {
|
|
return fs.readFileSync(path.join(root, relativePath), 'utf8');
|
|
}
|
|
|
|
test('clinical assistant starter prompts are Redis or indexed-source backed', () => {
|
|
const route = read('src/routes/clinicalAssistant.js');
|
|
const pool = read('src/utils/clinicalPromptPool.js');
|
|
assert.doesNotMatch(route, /EXAMPLE_CANDIDATES|TOPIC_SUGGESTIONS|topic_suggestions|buildSuggestionAnswer/);
|
|
assert.match(route, /createClinicalPromptPool\(\{[\s\S]*redisCache: redisCache/);
|
|
assert.match(route, /isUsefulIndexedTopicExample/);
|
|
assert.match(pool, /clinical-assistant:prompt-pool:v2/);
|
|
assert.match(pool, /redisBaseKey \+ ':all'/);
|
|
assert.match(pool, /redisBaseKey \+ ':last-good'/);
|
|
assert.match(pool, /redisBaseKey \+ ':category:' \+ category/);
|
|
assert.match(pool, /redisBaseKey \+ ':age:' \+ ageBand/);
|
|
assert.match(pool, /redisBaseKey \+ ':intent:' \+ intent/);
|
|
assert.match(pool, /CLINICAL_ASSISTANT_PROMPT_POOL_TARGET, 1000/);
|
|
// Built once, then only from the admin button: no timer unless the environment asks for one.
|
|
assert.match(pool, /CLINICAL_ASSISTANT_PROMPT_POOL_REFRESH_MS, 0\)/);
|
|
assert.match(pool, /process\.env\.CLINICAL_ASSISTANT_PROMPT_MODEL \|\| 'ds-deepseek-v4\.1-flash'/);
|
|
assert.match(route, /loadStoredPromptPool: loadLatestPromptPoolSnapshot/);
|
|
assert.match(pool, /PEDIATRIC_TAXONOMY/);
|
|
assert.match(pool, /taxonomyWithQuotas\(target\)/);
|
|
assert.match(pool, /age_band: ageBand/);
|
|
assert.match(pool, /function hasPediatricSignal\(prompt, item\)/);
|
|
assert.match(pool, /var examples = await fallbackIndexedExamples\(\)/);
|
|
assert.doesNotMatch(pool, /taxonomyFallbackExamples|taxonomySupplementExamples/);
|
|
assert.doesNotMatch(pool, /return fallbackIndexedExamples\(\)/);
|
|
});
|
|
|
|
test('clinical assistant prompt pool can be regenerated by admins', () => {
|
|
const admin = read('src/routes/adminConfig.js');
|
|
const page = read('public/components/admin.html');
|
|
const js = read('public/js/admin/clinicalAssistant.js');
|
|
assert.match(admin, /\/clinical-assistant\/prompt-pool\/regenerate/);
|
|
assert.match(admin, /\/clinical-assistant\/prompt-pool\/restore/);
|
|
assert.match(page, /btn-regenerate-assistant-prompt-pool/);
|
|
assert.match(page, /btn-restore-assistant-prompt-pool/);
|
|
assert.match(js, /regenerateAssistantPromptPool/);
|
|
assert.match(js, /restoreAssistantPromptPool/);
|
|
});
|
|
|
|
test('clinical assistant prompt forbids relabeling other sources as named sources', () => {
|
|
const answer = read('src/utils/clinicalAnswer.js');
|
|
assert.match(answer, /If the user names a specific source, textbook, guideline, or table/);
|
|
assert.match(answer, /do not claim that another source is from the named source/);
|
|
assert.match(answer, /If the named source is absent from the retrieved sources, say that explicitly/);
|
|
});
|
|
|
|
test('clinical assistant prompt forbids LaTeX-escaped citations', () => {
|
|
const answer = read('src/utils/clinicalAnswer.js');
|
|
assert.match(answer, /Never escape citation brackets/);
|
|
assert.ok(answer.includes(String.raw`write [1], not \\[1\\]`));
|
|
});
|
|
|
|
test('clinical assistant prompt requires bracketed citations in table source columns', () => {
|
|
const answer = read('src/utils/clinicalAnswer.js');
|
|
assert.match(answer, /Source, Source\(s\), Citation, or Citation\(s\) column/);
|
|
assert.match(answer, /must use bracketed citation tokens like \[1\] or \[1, 3\]/);
|
|
assert.match(answer, /never bare numbers like 1 or 1, 3/);
|
|
});
|
|
|
|
// ---- what a starter question has to be ----------------------------------------
|
|
|
|
function fakePool(overrides) {
|
|
const { createClinicalPromptPool } = require('../src/utils/clinicalPromptPool');
|
|
const store = new Map();
|
|
const redisCache = {
|
|
getJson: async k => store.has(k) ? store.get(k) : null,
|
|
setJson: async (k, v) => { store.set(k, v); }
|
|
};
|
|
return { store, pool: createClinicalPromptPool(Object.assign({
|
|
redisCache, getSetting: async () => '', semanticSearch: async () => ({}), dedupeSources: x => x,
|
|
normalizeMcpSearchResponse: () => [], callAI: async () => ({ content: '{"questions":[]}' })
|
|
}, overrides || {})) };
|
|
}
|
|
|
|
test('a starter question is one concrete thing to learn, not a scenario', () => {
|
|
const { pool } = fakePool();
|
|
const ok = q => pool.isUsefulQuestion(q, { category: 'respiratory', intent: 'management', age_band: 'infant' });
|
|
// What the user refused: "vague nonsense clinical scenarios".
|
|
assert.equal(ok('A 6-week-old with 3 days of projectile non-bilious vomiting, weight down 8% and a chloride of 88: which fluid do you start, and what corrects before theatre is safe?'), false, 'a vignette');
|
|
assert.equal(ok('A 14-year-old on the ward with asthma, SpO2 91% after two salbutamol nebulisers: at what point do you add magnesium?'), false, 'a vignette');
|
|
assert.equal(ok('An infant presents with bilious emesis and a scaphoid abdomen: which imaging comes first?'), false, 'a vignette without an age');
|
|
assert.equal(ok('What is bronchiolitis?'), false, 'a bare definition');
|
|
// What it should hold: mechanisms, distinctions, the numbers that matter.
|
|
assert.equal(ok('Why does pyloric stenosis produce a hypochloraemic metabolic alkalosis in infants, and why is the urine paradoxically acidic?'), true);
|
|
assert.equal(ok('How does a ductal-dependent heart lesion stay silent in a newborn until the duct closes, and what does prostaglandin E1 do?'), true);
|
|
assert.equal(ok('Why are infants under 3 months given broader empiric antibiotic cover for fever than older children?'), true);
|
|
assert.equal(ok('What separates croup from epiglottitis in a child, and why does one respond to dexamethasone?'), true);
|
|
});
|
|
|
|
test('a pool built by an older prompt is served, then rebuilt in the background', async () => {
|
|
let generated = 0;
|
|
const { store, pool } = fakePool({
|
|
callAI: async () => { generated++; return { content: '{"questions":[]}' }; }
|
|
});
|
|
// A fresh pool from the previous prompt version, one day old.
|
|
const old = { generatedAt: Date.now() - 86400000, promptVersion: pool.PROMPT_VERSION - 1, target: 10,
|
|
examples: [{ label: 'Old', prompt: 'Old question?', category: 'respiratory', age_band: 'infant', intent: 'management' }] };
|
|
await pool.writePromptPool(old, { skipStore: true });
|
|
const served = await pool.getAvailableExamples();
|
|
assert.equal(served[0].prompt, 'Old question?', 'the old pool is what the reader gets meanwhile');
|
|
await new Promise(r => setTimeout(r, 20));
|
|
assert.ok(generated > 0 || store.size >= 1, 'a rebuild was started');
|
|
// The same pool stamped with the current version is not stale.
|
|
const fresh = Object.assign({}, old, { promptVersion: pool.PROMPT_VERSION });
|
|
await pool.writePromptPool(fresh, { skipStore: true });
|
|
const again = await pool.refreshIfNeeded(false);
|
|
assert.equal(again[0].prompt, 'Old question?');
|
|
});
|
|
|
|
test('a reply cut off mid-list still yields the questions that finished', () => {
|
|
const { pool } = fakePool();
|
|
// Reach the parser through the public surface it feeds: a refresh with a
|
|
// callAI that answers with truncated JSON.
|
|
const src = require('node:fs').readFileSync(require('node:path').join(__dirname, '..', 'src/utils/clinicalPromptPool.js'), 'utf8');
|
|
assert.match(src, /objects\.forEach\(function \(chunk\)/, 'the salvage exists');
|
|
assert.match(src, /maxTokens: 7000/, 'and the ceiling makes room for cases');
|
|
assert.ok(pool);
|
|
});
|