pediatric-ai-scribe-v3/test/assistant-preview.test.js
Daniel db83255c58 feat: display-only sources toggle, signed-out preview, and a composer that carries the toolbar
Sources (correcting what I built earlier)
The previous toggle branched the SYSTEM PROMPT, so the same question could get a
different answer depending on a display setting — the bias this was meant to
avoid. The prompt is now unconditional: buildSystemPrompt takes no display
argument and is byte-identical either way. Hiding sources happens on the way out
— the server omits them and strips the now-orphaned [n] markers from the copy it
sends. The answer is generated, stored and exported with citations intact, so
turning the setting back on restores them without re-asking anything. Renamed to
clinical_assistant.show_sources; the old key is still honoured.

Signed-out preview (admin opt-in, default off)
A visitor may try the assistant; reaching for the workspace asks them to sign in.
Deliberately narrow:
- Reachable paths are an exact allow-list, not a pattern, so a new endpoint is
  private unless someone adds it on purpose.
- A preview visitor gets no identity at all (id: null), so nothing can be owned,
  saved, billed or addressed to them.
- The image tool is withheld rather than left to fail on a null owner, and no
  audit rows are written.
- A caller presenting a token is authenticated normally, so preview can never
  downgrade a real session; if the setting cannot be read, authentication is
  required.
- Actions needing an account are hidden rather than offered and refused.

Composer
The bar above the transcript is gone. Patient take home, Export PDF, Download
transcript and Attach images moved into a + menu in the composer, and the model
selector moved beside send — shown only when there is more than one model, as
before. Both views now start at the same top edge, so switching modes cannot
nudge the page up or down. On an empty transcript the tiled ground runs behind
and below the composer, which floats on it above centre.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GmpYHPSLGmXGZMyLpn2Lbe
2026-09-10 04:12:36 +02:00

93 lines
5.3 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, '..');
const read = file => fs.readFileSync(path.join(root, file), 'utf8');
// Signed-out preview is an admin opt-in that widens who can reach the assistant,
// so the boundaries matter more than the feature.
test('preview is off unless an admin turns it on', () => {
const route = read('src/routes/clinicalAssistant.js');
assert.match(route, /getSetting\('clinical_assistant\.preview_enabled', 'false'\)/,
'default is off');
assert.match(route, /=== 'true'/, 'and only the exact string enables it');
const admin = read('src/routes/adminConfig.js');
assert.match(admin, /clinical_assistant\.preview_enabled' && !\['true', 'false'\]/,
'the server refuses a non-boolean');
});
test('preview reaches an allow-list of paths, never a pattern', () => {
const route = read('src/routes/clinicalAssistant.js');
const block = route.slice(route.indexOf('var PREVIEW_PATHS'), route.indexOf('var PREVIEW_USER'));
// A pattern would silently include future routes; an exact list means a new
// endpoint is private until someone adds it here deliberately.
for (const allowed of ['/clinical-assistant/status', '/clinical-assistant/examples',
'/clinical-assistant/chat', '/clinical-assistant/chat/stream']) {
assert.ok(block.includes("'" + allowed + "'"), allowed + ' is previewable');
}
for (const denied of ['/clinical-assistant/chats', '/clinical-assistant/image',
'/clinical-assistant/patient-takehome', '/clinical-assistant/translate']) {
assert.ok(!block.includes("'" + denied + "'"), denied + ' must stay private');
}
assert.match(route, /PREVIEW_PATHS\.has\(req\.path\)/, 'matched exactly, not by prefix');
});
test('preview never downgrades a real session, and a lookup failure closes the door', () => {
const route = read('src/routes/clinicalAssistant.js');
const mw = route.slice(route.indexOf('router.use(async function(req, res, next)'), route.indexOf('router.use(authMiddleware);'));
assert.match(mw, /if \(req\.user\) return next\(\);/, 'an established identity is untouched');
assert.match(mw, /hasCredential[\s\S]*?return authMiddleware/,
'a caller presenting a token is authenticated normally, not previewed');
assert.match(mw, /catch \(_\) \{\s*\n\s*return authMiddleware/,
'if the setting cannot be read, authentication is required');
});
test('a preview visitor has no identity, so nothing can be owned or billed', () => {
const route = read('src/routes/clinicalAssistant.js');
assert.match(route, /PREVIEW_USER = Object\.freeze\(\{ id: null, preview: true/,
'no user id at all');
// Image generation is owned, stored and paid for, so the tool is withheld
// rather than left to fail on a null owner downstream.
assert.match(route, /tools: req\.user\.preview \? undefined : imageTool\.tools/);
// Each dispatch must be reachable only behind a preview check — either inline
// on the same statement, or inside an enclosing `if (!req.user.preview) {`.
const lines = route.split('\n');
lines.forEach((line, i) => {
if (!line.includes('imageTool.dispatch(')) return;
const guardedInline = line.includes('!req.user.preview');
const guardedByBlock = lines.slice(Math.max(0, i - 3), i)
.some(l => l.includes('if (!req.user.preview) {'));
assert.ok(guardedInline || guardedByBlock,
'unguarded imageTool.dispatch at line ' + (i + 1) + ': ' + line.trim().slice(0, 60));
});
assert.equal(lines.filter(l => l.includes('imageTool.dispatch(')).length, 2,
'both chat paths are covered');
assert.match(route, /if \(!req\.user\.preview\) logger\.audit/,
'audit rows are not written against a null user');
});
test('preview is entered by declining the login overlay, not by faking a session', () => {
const auth = read('public/js/auth.js');
const fn = auth.slice(auth.indexOf('function showAuthScreen()'), auth.indexOf('// ── Check for SSO redirect'));
assert.match(fn, /pathname === '\/assistant'/, 'only the assistant page previews');
assert.match(fn, /data\.success && data\.preview/, 'and only when the server says so');
assert.match(fn, /authScreen\.style\.display = 'flex';/, 'anything else raises the login screen');
// No token, no user object, no stored credential — the overlay is simply not
// shown, so nothing downstream can mistake a visitor for a signed-in user.
assert.doesNotMatch(fn, /CURRENT_USER =|SecureStorage\.set/, 'preview never mints an identity');
});
test('reaching for the workspace is where a preview visitor is asked to sign in', () => {
const app = read('public/js/app.js');
const handler = app.slice(app.indexOf('// Assistant / Workspace switch'), app.indexOf('window.activateTab = activateTab;'));
assert.match(handler, /assistant-preview'\) && !wantsAssistant/);
assert.match(handler, /screen\.style\.display = 'flex'/, 'the login screen is raised');
// Actions that need an account are hidden rather than offered and refused.
const css = read('public/css/styles.css');
for (const hidden of ['.assistant-plus', '.assistant-history .card', '.assistant-rail-actions']) {
assert.ok(css.includes('body.assistant-preview ' + hidden), hidden + ' is hidden in preview');
}
assert.match(read('public/components/assistant.html'), /assistant-preview-note/, 'and the state is stated plainly');
});