fix: drop the dead admin jump links; stop a test flake at its source

Admin links
- The [data-admin-jump] click handler had been pasted inside
  discoverModels(), so it only registered once someone pressed Search in AI
  Model Management, and re-registered on every later search. Normally
  nothing intercepted the click, so href="#" did what it says: jump to the
  top and leave "#" in the URL.
- This is an app, so the pointers are plain text naming the sections rather
  than links, and the handler is gone.

Test flake
- admin-clinical-assistant-wiring failed about 1 run in 4 with "Unable to
  deserialize cloned data due to invalid or unsupported version": node:test
  reads a test file's results back over the child's stdout, and app.js's own
  console.log landed inside a serialized frame.
- The page's console is now forwarded to stderr (jsdom 29: forwardTo).
  Verified: child stdout clean, 0 failures in 32 stress runs (was 6 in 24),
  and three full-suite runs at 668/668.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
This commit is contained in:
Daniel 2026-09-10 15:16:47 +02:00
parent adcea2a0ca
commit 609305538a
3 changed files with 12 additions and 16 deletions

View file

@ -296,7 +296,7 @@
<strong class="admin-row-label">Model availability</strong>
<div style="flex:1;display:flex;flex-direction:column;gap:8px;min-width:0;">
<p style="margin:0;font-size:12px;color:var(--g500);">Models users may select in the Clinical Assistant. Leave a list empty to keep only the configured model.</p>
<p style="margin:0;font-size:12px;color:var(--g500);">To make a new model appear here, add it from the gateway: chat models in <a href="#" data-admin-jump="admin-model-search" style="color:var(--blue);font-weight:600;">AI Model Management</a> above, image models in <a href="#" data-admin-jump="admin-image-search" style="color:var(--blue);font-weight:600;">Image Generation</a> below — search, press <strong>+ Add</strong>, and it shows up in this list.</p>
<p style="margin:0;font-size:12px;color:var(--g500);">To make a new model appear here, add it from the gateway: chat models under <strong>AI Model Management</strong> above, image models under <strong>Image Generation</strong> below — search there, press <strong>+ Add</strong>, and it shows up in this list.</p>
<div>
<div style="font-size:12px;font-weight:600;color:var(--g600);margin-bottom:2px;">Chat models</div>
<div id="assistant-allowed-chat-models" aria-label="Chat models available to the Clinical Assistant"></div>

View file

@ -909,19 +909,6 @@ initImageSettings();
container.innerHTML = '<p style="color:var(--g400);font-size:13px;"><i class="fas fa-spinner fa-spin"></i> Querying provider API...</p>';
if (hint) hint.style.display = 'none';
// The Clinical Assistant lists models but cannot add them; adding happens in
// AI Model Management, one section up. Without this the two sections look
// unrelated and there is no visible way to get a new model into the list.
document.addEventListener('click', function(event) {
var jump = event.target.closest && event.target.closest('[data-admin-jump]');
if (!jump) return;
event.preventDefault();
var target = document.getElementById(jump.getAttribute('data-admin-jump'));
if (!target) return;
target.scrollIntoView({ behavior: 'smooth', block: 'center' });
target.focus({ preventScroll: true });
});
fetch('/api/admin/config/models/discover?q=' + encodeURIComponent(search), { headers: getAuthHeaders() })
.then(function(r) { return r.json(); })

View file

@ -3,10 +3,19 @@ const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const { pathToFileURL } = require('node:url');
const { JSDOM } = require('jsdom');
const { JSDOM, VirtualConsole } = require('jsdom');
const { Console } = require('node:console');
const root = path.join(__dirname, '..');
const read = file => fs.readFileSync(path.join(root, file), 'utf8');
const tick = () => new Promise(resolve => setImmediate(resolve));
// The page's console goes to stderr, never stdout. node:test reads each test
// file's results back over stdout as serialized frames; app.js's own log line
// ("✅ App.js loaded") written there could land inside a frame and fail the
// whole file with "Unable to deserialize cloned data" — about 1 run in 4.
function pageConsole() {
// jsdom 29 renamed sendTo() to forwardTo().
return new VirtualConsole().forwardTo(new Console({ stdout: process.stderr, stderr: process.stderr }));
}
function browserGlobals(t, dom, fetch, toasts) {
const values = { window: dom.window, document: dom.window.document, fetch, getAuthHeaders: () => ({ 'X-Test': 'synthetic' }), showToast: (...args) => toasts.push(args) };
@ -20,7 +29,7 @@ function browserGlobals(t, dom, fetch, toasts) {
}
test('native admin initializer preserves lazy navigation, assistant actions and read-only ENV budget metadata', async t => {
const dom = new JSDOM('<button class="tab-btn active" data-tab="home">Home</button><button class="tab-btn" data-tab="admin">Admin</button><div id="home-tab" class="tab-content" data-component="home" data-loaded="1"></div><div id="admin-tab" class="tab-content" data-component="admin"></div>', { runScripts: 'outside-only', url: 'https://app.example' });
const dom = new JSDOM('<button class="tab-btn active" data-tab="home">Home</button><button class="tab-btn" data-tab="admin">Admin</button><div id="home-tab" class="tab-content" data-component="home" data-loaded="1"></div><div id="admin-tab" class="tab-content" data-component="admin"></div>', { runScripts: 'outside-only', url: 'https://app.example', virtualConsole: pageConsole() });
const calls = []; const toasts = [];
const fetch = async (url, options = {}) => {
calls.push({ url, options });