pediatric-ai-scribe-v3/test/assistant-image-intent.test.js
Daniel 022869f01d fix: mobile layout regression, workspace menu in the assistant rail, prompt as principles
Mobile
body.assistant-workspace .assistant-layout is both later in the file and more
specific than the .assistant-layout rule inside @media (max-width:640px), so
phones were inheriting height: calc(100vh - 64px) — an offset for the app header
that workspace mode already hides — plus grid rules on a flex container. The
topbar then overlapped the content. Added a matching-specificity mobile override
restoring 100dvh and the flex layout.

Workspace menu in the rail
The saved-chats rail gains a collapsible Workspace section listing the app's own
tabs, so the assistant sidebar carries both its chats and the workspace
navigation. It is built from the real .tab-btn elements rather than a second copy
of the menu, so tabs added, renamed or hidden in index.html follow automatically
and admin-only tabs stay hidden.

Prompt
Rewritten as principles instead of an enumerated rulebook: "respond to the user's
latest message, not to an earlier one", "if it carries no question, ask what they
would like you to look up", "never repeat a previous answer", and for the tool
"use it when the latest message asks for a picture, or for a change to one you
just made, and not otherwise". No example words in any language remain — the
model reads the message as written. 1032 -> 788 characters.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BkfrkQwA4YGrGw9LZSpeAq
2026-09-09 22:24:10 +02:00

58 lines
3.2 KiB
JavaScript

const { test } = require('node:test');
const assert = require('node:assert/strict');
const path = require('node:path');
const { pathToFileURL } = require('node:url');
async function imagesModule() {
return import(pathToFileURL(path.join(__dirname, '..', 'public/js/assistant/images.js')).href);
}
test('assistant image intent does not intercept table retrieval requests', async () => {
const { isImageRequest } = await imagesModule();
assert.equal(isImageRequest('can you show me the table?'), false);
assert.equal(isImageRequest('show me Table 13.1'), false);
});
test('assistant image intent still handles explicit visual requests', async () => {
const { isImageRequest } = await imagesModule();
assert.equal(isImageRequest('show me the diagram'), true);
assert.equal(isImageRequest('create an infographic for asthma'), true);
});
// A run of image turns used to make every acknowledgement produce another image:
// the model saw its own "I'll generate an educational image…" in the history and
// replayed it verbatim for "Окей" and "Nice". The fix is not a word list in the
// route — the model already has the tool, the prompt and the conversation, so the
// policy lives where the model reads it.
test('the prompt states principles, not an enumerated list of cases', () => {
const { DEFAULT_BEHAVIOR } = require('../src/utils/clinicalPrompts');
assert.match(DEFAULT_BEHAVIOR, /Respond to the user's latest message, not to an earlier one/);
assert.match(DEFAULT_BEHAVIOR, /If it carries no question/, 'covers acknowledgements without naming any');
assert.match(DEFAULT_BEHAVIOR, /Never repeat a previous answer/);
assert.match(DEFAULT_BEHAVIOR, /do not answer from your own knowledge/, 'grounded in retrieved context');
// No word list anywhere: the model reads the message in whatever language it
// arrives in, so no vocabulary needs maintaining here.
for (const word of ['ok', 'nice', 'thanks', 'okay']) {
assert.doesNotMatch(DEFAULT_BEHAVIOR, new RegExp('"' + word + '"', 'i'), 'no example word list');
}
});
test('the tool description says when to use it and when not', () => {
const { tools } = require('../src/utils/imageTool');
const description = tools[0].function.description;
assert.match(description, /asks for a picture, or for a change to one you just made/);
assert.match(description, /and not otherwise/);
assert.match(description, /grounded in the current clinical content/);
});
test('no hand-maintained acknowledgement or language list is left in the route', () => {
const fs = require('node:fs');
const src = fs.readFileSync(path.join(__dirname, '..', 'src/routes/clinicalAssistant.js'), 'utf8');
assert.doesNotMatch(src, /GREETING_RE/, 'greeting word list removed — the model decides');
assert.doesNotMatch(src, /IMAGE_NOUN_NON_LATIN|IMAGE_REPEAT_NON_LATIN/, 'no multilingual vocabulary lists');
assert.doesNotMatch(src, /suppressRepeatedImage/, 'no answer-inspection backstop');
// The one surviving pattern is the compatibility path for models that write
// the prompt as text; it reads the USER's message only.
const fallback = src.slice(src.indexOf('async function dispatchImageRequestFallback'));
assert.match(fallback, /isExplicitImageRequest\(prepared\.message\)/, 'gated on the user, never on the answer');
});