pediatric-ai-scribe-v3/test/assistant-preview.test.js
Daniel 004fb80a60
Some checks failed
Forgejo Android APK / Root app tests (push) Successful in 49s
Forgejo Docker Build / Root app tests (push) Successful in 49s
Forgejo Android APK / Build signed APK (push) Successful in 1m51s
Forgejo Docker Build / Build Docker image (push) Successful in 18s
Forgejo Docker Build / Deploy to the host (push) Failing after 0s
fix: the signed-out preview is reachable from the page, from any path
The server side has worked since this morning, but no browser could reach it.
authFetch rejects every /api request that has no account before it is sent,
and the four preview endpoints were not on its short list — so the status call
that decides whether to show the login screen never left the browser, and the
screen was always shown. The list now mirrors the server's own allow-list
exactly: status, examples, chat, chat/stream, and nothing else.

Preview now begins from any path. A visitor landing on the root met the login
wall while /assistant did not, which read as "preview doesn't work"; both now
enter the assistant, and the URL follows.

Reaching for anything that needs an account raises the sign-in screen through
one hook in authFetch rather than a check on every control — but only for
something the visitor did. The page also fetches saved chats and config in the
background on load, and the first version raised the screen for those too,
burying the assistant before a word was typed. The hook is gated on
navigator.userActivation.

The HIPAA notice is hidden on that screen in preview: it is an invitation to
sign in, not the compliance notice a clinician sees on first login.

Verified in a browser: landing on / and on /assistant both show the assistant
with no login wall and no HIPAA text; clicking Workspace raises sign-in.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
2026-09-11 04:40:40 +02:00

119 lines
7.2 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'));
// Any path previews, not only /assistant: a visitor landing on the root should
// meet the assistant, not a wall. enterPreview moves them onto that tab.
assert.doesNotMatch(fn, /pathname === '\/assistant'/, 'the root previews too');
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');
// The prompt a preview visitor meets is an invitation, not the compliance notice.
assert.ok(css.includes('body.assistant-preview .hipaa-notice { display:none; }'), 'no HIPAA notice in preview');
});
test('the browser lets exactly the preview endpoints out without an account, and nothing else', () => {
// Every /api fetch without an account is rejected before it is sent. Until the
// preview paths were allowed here, the status call never reached the server,
// so the server-side preview was unreachable from the page.
const af = read('public/js/authFetch.js');
const list = af.slice(af.indexOf('var previewPaths'), af.indexOf('function inPreview'));
for (const p of ['/api/clinical-assistant/status', '/api/clinical-assistant/examples',
'/api/clinical-assistant/chat', '/api/clinical-assistant/chat/stream']) {
assert.ok(list.includes("'" + p + "'"), p + ' is allowed');
}
assert.doesNotMatch(list, /saved-chats|\/config|\/images|\/admin/, 'nothing that needs an account');
assert.match(af, /if \(previewPaths\.has\(url\.pathname\)\) return rawFetch/, 'they go out with no ticket');
// Anything else a preview visitor reaches for raises sign-in via one hook,
// rather than a check on every button.
// ...and only for something the visitor did. The page fetches saved chats and
// config in the background on load; raising sign-in for those buried the
// assistant before a word was typed.
assert.match(af, /navigator\.userActivation \? navigator\.userActivation\.isActive : false/);
assert.match(af, /if \(inPreview\(\) && gesture\) document\.dispatchEvent\(new CustomEvent\('preview-needs-account'/);
assert.match(read('public/js/auth.js'), /addEventListener\('preview-needs-account'/, 'and auth.js raises the screen');
});