pediatric-ai-scribe-v3/test/model-roster-pruning.test.js
Daniel 24c8d71b7e
Some checks failed
Forgejo Android APK / Root app tests (push) Successful in 52s
Forgejo Docker Build / Root app tests (push) Successful in 47s
Forgejo Android APK / Build signed APK (push) Successful in 1m56s
Forgejo Docker Build / Build Docker image (push) Successful in 11s
Forgejo Docker Build / Deploy to the host (push) Failing after 0s
feat: send a resource to your own Nextcloud, as the rendered file
A .pptx landing in someone's own storage is worth having; a markdown blob is
not, and it is not what they would have downloaded. So this renders through
exactly the path the download uses — stored deck, its figures, the chosen
theme — and PUTs the bytes. The file never travels through the browser.

Offered only when a Nextcloud is connected: an action that always fails is
worse than one that is not offered. An article offers Word, a deck PowerPoint,
and asking for slides from an article is refused with the reason.

Putting a file in Nextcloud now lives in src/utils/nextcloudFiles.js. Two
callers want it and neither should grow its own copy of the WebDAV dance — make
the dated folder a segment at a time, PUT, migrate a legacy plaintext token —
because it reaches into storage that is not ours and a second slightly
different copy is how the two drift. It also replaces a route importing another
route.

Also: a model that leaves the roster now leaves every list that names it.
clinical_assistant.allowed_models and the image roster are advisory copies of
the roster, and a stale id there was invisible until someone asked a clinical
question and the request failed at the gateway. Removing or disabling a model
prunes it; clearing the roster clears them. Re-enabling deliberately does not
re-allow it — that is a separate decision.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
2026-09-12 20:34:58 +02:00

46 lines
2.4 KiB
JavaScript

// A model that leaves the roster has to leave every list that names it. The
// allowed lists are advisory copies of the roster; the roster is the fact. A
// stale id there is invisible until someone asks a clinical question and the
// request fails at the gateway.
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const route = fs.readFileSync(path.join(__dirname, '..', 'src/routes/adminConfig.js'), 'utf8');
test('one helper owns forgetting a model, and covers every list that names one', () => {
assert.match(route, /async function forgetModelEverywhere\(ids\)/);
for (const key of ['clinical_assistant.allowed_models',
'clinical_assistant.allowed_image_models',
'clinical_assistant.image_model_roster']) {
assert.ok(route.includes("'" + key + "'"), key + ' is not pruned');
}
// And a default pointing at a model that has gone.
assert.match(route, /gone\.indexOf\(current\) !== -1\) await db\.setSetting\('models\.default', ''\)/);
});
test('removing a custom model prunes it', () => {
const handler = route.slice(route.indexOf("router.delete('/config/models/custom"));
assert.match(handler.slice(0, 900), /await forgetModelEverywhere\(modelId\)/);
});
test('disabling a model prunes it too — it stops being selectable either way', () => {
const handler = route.slice(route.indexOf("router.put('/config/models/toggle"));
assert.match(handler.slice(0, 1600), /if \(!enabled\) await forgetModelEverywhere\(modelId\)/);
// Re-enabling must NOT silently re-allow it: that is a separate decision.
assert.doesNotMatch(handler.slice(0, 1600), /if \(enabled\) await forgetModelEverywhere/);
});
test('clearing every model clears every allowed list', () => {
const handler = route.slice(route.indexOf("router.post('/config/models/clear-all"));
assert.match(handler.slice(0, 900), /await db\.setSetting\(key, ''\)/);
assert.match(handler.slice(0, 900), /Nothing is on the roster any more/);
});
test('a list is only written when it actually changed', () => {
// Avoids a settings write, an updated_at bump and a cache refresh per removal
// when nothing referenced the model.
const fn = route.slice(route.indexOf('async function forgetModelEverywhere'));
assert.match(fn.slice(0, 1200), /if \(kept\.join\(','\) !== current\)/);
});