fix: correct a false Settings claim; make every test child's stdout pure TAP
Feature audit - Settings claimed you could reference a template by saying "use my normal physical exam" in dictation. No phrase handling exists anywhere, and the prompt says the opposite: "Never copy clinical content from a template — only formatting and structure." So a template can never supply findings. The text now says what happens, and keeps the true privacy statement that only template categories go to the AI (Custom is filtered out in /memories/context by AI_CONTEXT_CATEGORIES). - Templates themselves are real: CRUD plus /memories/context, injected as style hints by hpi, soap, sickVisit, wellVisit, edEncounters and hospitalCourse, behind the `memories` feature flag. Docs - docs/CLINICAL_ASSISTANT.md listed six settings and offered `deepl`, which no longer exists in the code. The table now covers all seventeen keys the server reads, with their fallbacks, plus how a model reaches a user. Testing - Every test file's stdout is now pure TAP, which is the stream node:test parses results from. Three sources: a leftover debug console.log dumping 600 characters of HTML, page modules logging into a JSDOM without a virtual console, and the server startup banners. The banners are guarded by NODE_TEST_CONTEXT, set only inside node:test children, so production and `node server.js` output is unchanged (verified both ways). - Three consecutive full-suite runs at 671/671. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
This commit is contained in:
parent
cf7a3161a8
commit
31abddb6e6
6 changed files with 55 additions and 12 deletions
|
|
@ -95,14 +95,45 @@ Every message offers Translate with a target-language picker and a provider choi
|
|||
|
||||
Important settings include:
|
||||
|
||||
All are stored in `settings` and edited under Admin → Clinical Assistant /
|
||||
Learning, except the image roster, which is written by the Image Generation
|
||||
card. Every one is read through `getSetting`, so an unset key falls back to the
|
||||
default in the right-hand column.
|
||||
|
||||
| Setting | Purpose |
|
||||
|---|---|
|
||||
| `clinical_assistant.chat_model` | Chat model used for answers |
|
||||
| `clinical_assistant.image_model` | Image model used for explicit image generation |
|
||||
| `clinical_assistant.chat_model` | Chat model for answers; falls back to `models.default` |
|
||||
| `clinical_assistant.image_model` | Image model for explicit image generation; falls back to `CLINICAL_ASSISTANT_IMAGE_MODEL`, then `openai-gpt-image-1` |
|
||||
| `clinical_assistant.fallback_image_model` | Single retry target when the image model fails |
|
||||
| `clinical_assistant.allowed_models` | Comma-separated chat models a user may pick. Empty means no choice: the configured model is used. A non-empty list always includes the configured model; anything else is rejected with 400 `model_not_allowed` |
|
||||
| `clinical_assistant.allowed_image_models` | The same, for image models |
|
||||
| `clinical_assistant.image_model_roster` | Image models an admin added from Admin → Image Generation (**+ Add**). This is the pool the Image models tick-list offers; it is not itself an allowlist. Validated as up to 100 ids |
|
||||
| `clinical_assistant.search_limit` | Number of MCP results requested |
|
||||
| `clinical_assistant.context_chars` | Context characters requested from MCP |
|
||||
| `clinical_assistant.conversation_chars` | Input budget in UTF-16 code units. Empty means use `CLINICAL_ASSISTANT_CONVERSATION_CHARS`; a value must be 1000-1000000 |
|
||||
| `clinical_assistant.show_sources` | `true`/`false`. Display only: hides the Sources panel and the citation markers. The prompt, the retrieval and the stored answer are byte-for-byte identical either way, so it cannot bias an answer; turning it back on restores the citations |
|
||||
| `clinical_assistant.preview_enabled` | `true`/`false`. Lets signed-out visitors try the assistant read-only; anything needing an account asks them to sign in |
|
||||
| `clinical_assistant.system_behavior` | Admin-editable assistant behavior guidance |
|
||||
| `clinical_assistant.translate_provider` | Default translation provider: `libretranslate` or `deepl` |
|
||||
| `clinical_assistant.image_behavior` | Guidance for the `generate_image` tool |
|
||||
| `clinical_assistant.patient_takehome_behavior` | Guidance for patient take-home text |
|
||||
| `clinical_assistant.prompt_model` | Model that generates the starter prompt pool |
|
||||
| `clinical_assistant.translate_provider` | Translation provider. `libretranslate` is the only value the server accepts |
|
||||
| `clinical_assistant.citations_enabled` | Legacy key, read only as a fallback for `show_sources` |
|
||||
|
||||
## Choosing a model
|
||||
|
||||
The composer shows a **Model** button rather than the model id, which can be as
|
||||
long as `openrouter-gemini-3.1-flash-image-preview`; clicking it opens the list.
|
||||
The button is a face for `#assistant-chat-model-select`, which stays in the DOM
|
||||
as the state holder — so a choice made in the popup is saved by the same
|
||||
delegated `change` listener as before, under an account-scoped storage key. The
|
||||
whole control is hidden unless the allowlist offers more than one model.
|
||||
|
||||
For an image model to reach a user, an admin does two things: **+ Add** it under
|
||||
Admin → Image Generation (which puts it in `image_model_roster`), then tick it
|
||||
in the Clinical Assistant's Image models list (which puts it in
|
||||
`allowed_image_models`). Discovery lists what the gateway advertises with mode
|
||||
`image_generation`; it never adds anything on its own.
|
||||
|
||||
## Testing Priorities
|
||||
|
||||
|
|
|
|||
|
|
@ -151,7 +151,7 @@
|
|||
<!-- My Templates / Memories -->
|
||||
<div class="settings-section card" data-feature="memories">
|
||||
<h3><i class="fas fa-book-medical"></i> My Templates</h3>
|
||||
<p style="font-size:13px;color:var(--g600);">Save reusable templates for physical exam, ROS, encounter format, etc. Only template categories are sent to AI when generating notes. You can reference them by saying "use my normal physical exam" in dictation.</p>
|
||||
<p style="font-size:13px;color:var(--g600);">Save reusable templates for physical exam, ROS, encounter format, etc. Only template categories are sent to AI when generating notes — anything saved as <strong>Custom</strong> is never sent. Templates go out with Encounter HPI, Dictation HPI, SOAP, Sick Visit, Well Visit, ED Encounter and Hospital Course, and shape how a note is laid out and worded. The AI is told never to copy clinical content out of a template, so a template cannot supply findings you did not dictate: say them instead, for example “normal cardiac exam”, and they are documented.</p>
|
||||
<div style="margin-bottom:10px;display:flex;gap:8px;flex-wrap:wrap;align-items:center;">
|
||||
<select id="mem-category" style="font-size:13px;padding:5px 8px;border:1px solid var(--g300);border-radius:6px;">
|
||||
<option value="physical_exam">Physical Exam Template</option>
|
||||
|
|
|
|||
|
|
@ -166,5 +166,9 @@ var logger = {
|
|||
}
|
||||
};
|
||||
|
||||
console.log('✅ Logger initialized');
|
||||
// node:test reads a test file's results back over the child's stdout, so a
|
||||
// banner written there can land inside a serialized frame and fail the whole
|
||||
// file. NODE_TEST_CONTEXT is set only inside those children, so production and
|
||||
// `node server.js` output is unchanged.
|
||||
if (!process.env.NODE_TEST_CONTEXT) console.log('✅ Logger initialized');
|
||||
module.exports = logger;
|
||||
|
|
|
|||
|
|
@ -169,9 +169,15 @@ var AVAILABLE_MODELS = getAvailableModels();
|
|||
var DEFAULT_MODEL = getDefaultModel();
|
||||
var FALLBACK_MODEL = getFallbackModel();
|
||||
|
||||
console.log('🤖 Provider:', activeProvider);
|
||||
console.log('🤖 Default model:', DEFAULT_MODEL);
|
||||
console.log('🤖 Models available:', AVAILABLE_MODELS.length);
|
||||
// node:test reads a test file's results back over the child's stdout, so a
|
||||
// banner written there can land inside a serialized frame and fail the whole
|
||||
// file. NODE_TEST_CONTEXT is set only inside those children, so production and
|
||||
// `node server.js` output is unchanged.
|
||||
if (!process.env.NODE_TEST_CONTEXT) {
|
||||
console.log('🤖 Provider:', activeProvider);
|
||||
console.log('🤖 Default model:', DEFAULT_MODEL);
|
||||
console.log('🤖 Models available:', AVAILABLE_MODELS.length);
|
||||
}
|
||||
|
||||
// DB-aware model list (used by /api/models endpoint)
|
||||
async function getAvailableModelsWithOverrides(db) {
|
||||
|
|
|
|||
|
|
@ -160,7 +160,6 @@ test('actual CMS presentation image controls are visible and insertion/save/reop
|
|||
const markdown='---\nmarp: true\n---\n# Original [3]\n| Dose | Page |\n| 5 mg | 19 [3] |\n';
|
||||
doc.getElementById('lh-marp-editor').value=markdown;doc.getElementById('lh-cms-edit-title').value='Presentation';
|
||||
ui.context.learningImages.show([{jobId:id}]);await new Promise(r=>setImmediate(r));
|
||||
console.log('CMSPANEL', panel.innerHTML.slice(-600));
|
||||
const insert=[...panel.querySelectorAll('button')].find(b=>b.textContent==='Insert image at end of content');assert.ok(insert);insert.click();
|
||||
const inserted=doc.getElementById('lh-marp-editor').value;assert.ok(inserted.startsWith(markdown));assert.ok(inserted.includes(']('+src+')'));
|
||||
ui.context.saveContent();await new Promise(r=>setImmediate(r));assert.equal(saved.body,inserted);assert.equal(saved.content_type,'presentation');
|
||||
|
|
|
|||
|
|
@ -4,7 +4,10 @@ const fs = require('node:fs');
|
|||
const path = require('node:path');
|
||||
const vm = require('node:vm');
|
||||
const { pathToFileURL } = require('node:url');
|
||||
const { JSDOM } = require('jsdom');
|
||||
const { JSDOM, VirtualConsole } = require('jsdom');
|
||||
const { Console } = require('node:console');
|
||||
// Page console goes to stderr: node:test parses the child's stdout.
|
||||
const pageConsole = () => new VirtualConsole().forwardTo(new Console({ stdout: process.stderr, stderr: process.stderr }));
|
||||
const express = require('express');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const sessions = require('../src/utils/sessions');
|
||||
|
|
@ -131,7 +134,7 @@ test('actual quiz write endpoints reject invalid types before all mutations and
|
|||
});
|
||||
|
||||
test('actual quiz renderer contains malicious legacy metadata and preserves quiz input semantics', async () => {
|
||||
const dom = new JSDOM('<div id="root"></div>');
|
||||
const dom = new JSDOM('<div id="root"></div>', { virtualConsole: pageConsole() });
|
||||
global.window = dom.window;
|
||||
global.document = dom.window.document;
|
||||
window.DOMPurify = require('dompurify')(window);
|
||||
|
|
@ -197,7 +200,7 @@ function edBrowser(server, storage = {}) {
|
|||
const dom = new JSDOM('<input id="ed-label"><div id="ed-transcript"></div>' +
|
||||
'<div id="ed-stages-container"></div><button id="btn-ed-finalize"></button>' +
|
||||
'<button id="btn-ed-save"></button><button id="btn-ed-new"></button>', {
|
||||
url: 'https://isolated.invalid', runScripts: 'outside-only'
|
||||
url: 'https://isolated.invalid', runScripts: 'outside-only', virtualConsole: pageConsole()
|
||||
});
|
||||
const w = dom.window;
|
||||
for (const [key, value] of Object.entries(storage.session || {})) w.sessionStorage.setItem(key, value);
|
||||
|
|
|
|||
Loading…
Reference in a new issue