Security - nodemailer 9.0.1 -> 9.1.1, clearing four high advisories, two of which are delivery bugs that matter for an app that sends mail: recipient-domain validation bypass via RFC 5322 comments, and an IDN/punycode allow-list bypass, both of which can route mail to an attacker-controlled domain. Live transcription - The Settings picker was a hardcoded list of six ids (local-whisper-*, local-parakeet-v3, gemini-*). None of them resolve on this gateway, and /api/transcribe prefers the user's choice over the admin default, so picking one broke every recording with "Invalid model name". Verified against the live gateway: local-whisper-large-v3-turbo -> 400. - The picker now lists what /model/info advertises as audio_transcription, cached for five minutes, with the built-in list kept only as a fallback and the admin default marked. - The pipeline itself is healthy: local-kokoro-tts produced 92KB of speech and mistral-voxtral-mini-transcribe returned the sentence back verbatim. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
95 lines
4.9 KiB
JavaScript
95 lines
4.9 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, '..');
|
|
|
|
function read(relativePath) {
|
|
return fs.readFileSync(path.join(root, relativePath), 'utf8');
|
|
}
|
|
|
|
test('AI memory context is limited to saved template categories', () => {
|
|
const route = read('src/routes/memories.js');
|
|
assert.match(route, /var AI_CONTEXT_CATEGORIES = \[/);
|
|
assert.match(route, /'physical_exam'/);
|
|
assert.match(route, /'template_ed'/);
|
|
assert.match(route, /rows = rows\.filter\(function\(r\) \{ return AI_CONTEXT_CATEGORIES\.indexOf\(r\.category\) !== -1; \}\)/);
|
|
assert.doesNotMatch(route.match(/var AI_CONTEXT_CATEGORIES = \[[\s\S]*?\];/)[0], /'custom'/);
|
|
});
|
|
|
|
test('template settings do not offer new custom AI memories', () => {
|
|
const settings = read('public/components/settings.html');
|
|
const memories = read('public/js/memories.js');
|
|
assert.doesNotMatch(settings, /<option value="custom">/);
|
|
assert.match(settings, /Only template categories are sent to AI/);
|
|
assert.match(memories, /custom: 'Custom \(not used by AI\)'/);
|
|
assert.match(memories, /window\.getUserMemoryContext = function\(\)/);
|
|
});
|
|
|
|
test('note refine corrections still call AI without storing learning memories', () => {
|
|
const app = read('public/js/app.js');
|
|
const refine = read('src/routes/refine.js');
|
|
assert.match(app, /function refineDocument\(outputElementId, inputElementId\)/);
|
|
assert.match(app, /fetch\('\/api\/refine'/);
|
|
assert.match(app, /currentDocument: docText, instructions: instructions/);
|
|
assert.match(refine, /router\.post\('\/refine'/);
|
|
assert.match(refine, /PROMPTS\.refine \+ INJECTION_GUARD/);
|
|
assert.match(refine, /sourceContext/);
|
|
assert.doesNotMatch(refine, /INSERT INTO user_memories|correction_/);
|
|
});
|
|
|
|
test('browser Whisper is removed from public runtime and user settings', () => {
|
|
assert.equal(fs.existsSync(path.join(root, 'public/js/browserWhisper.js')), false);
|
|
assert.equal(fs.existsSync(path.join(root, 'public/js/whisperWorker.js')), false);
|
|
assert.equal(fs.existsSync(path.join(root, 'public/js/whisperWorkerV2.js')), false);
|
|
assert.equal(fs.existsSync(path.join(root, 'docs/browser-whisper-setup.md')), false);
|
|
assert.equal(fs.existsSync(path.join(root, 'docs/browser-whisper-troubleshooting.md')), false);
|
|
assert.equal(fs.existsSync(path.join(root, 'scripts/download-whisper-models.sh')), false);
|
|
const publicRuntimeFiles = [
|
|
'public/index.html',
|
|
'public/js/app.js',
|
|
'public/js/transcriptionSettings.js',
|
|
'public/components/settings.html',
|
|
'public/components/faq.html'
|
|
];
|
|
publicRuntimeFiles.forEach((file) => {
|
|
assert.doesNotMatch(read(file), /BrowserWhisper|browser-whisper|Browser Whisper|Xenova\/whisper|transformers\.min\.js/, file);
|
|
});
|
|
const dockerfile = read('Dockerfile');
|
|
assert.doesNotMatch(dockerfile, /Xenova\/whisper|transformers\.min\.js|Browser Whisper/);
|
|
const server = read('server.js');
|
|
assert.doesNotMatch(server, /wasm-unsafe-eval|unsafe-eval|huggingface\.co|cdn-lfs|transformers/);
|
|
});
|
|
|
|
test('browser speech recognition is gated by explicit user setting', () => {
|
|
const app = read('public/js/app.js');
|
|
const speechFactory = app.match(/function createSpeechRecognition\(\) \{[\s\S]*?\n\}/)[0];
|
|
assert.match(speechFactory, /window\.WebSpeechRecognition && !window\.WebSpeechRecognition\.isEnabled\(\)/);
|
|
});
|
|
|
|
test('audio backup settings render without dynamic HTML templates', () => {
|
|
const audioBackup = read('public/js/audioBackup.js');
|
|
const renderer = audioBackup.match(/window\.renderAudioBackups = function\(\) \{[\s\S]*?\n \};/)[0];
|
|
assert.doesNotMatch(renderer, /innerHTML/);
|
|
assert.match(renderer, /document\.createElement\('button'\)/);
|
|
assert.match(renderer, /textContent =/);
|
|
});
|
|
|
|
// The Settings picker offered six hardcoded ids. On this gateway none of them
|
|
// resolve, and /api/transcribe prefers the user's choice over the admin
|
|
// default — so choosing one broke every recording with "Invalid model name".
|
|
test('the STT picker offers what the gateway has, not a hardcoded list', () => {
|
|
const stt = read('src/utils/sttProvider.js');
|
|
const prefs = read('src/routes/userPreferences.js');
|
|
|
|
assert.match(stt, /async function discoverSTTModels\(options\)/);
|
|
assert.match(stt, /getLiteLLMSTTModels\(resp\.data && resp\.data\.data\)/, 'filtered by audio_transcription mode');
|
|
assert.match(stt, /STT_DISCOVERY_TTL_MS = 5 \* 60 \* 1000;/, 'cached, so a user-facing page does not hit the gateway every load');
|
|
assert.match(stt, /module\.exports = \{[\s\S]{0,80}discoverSTTModels,/);
|
|
|
|
assert.match(prefs, /var sttIds = await discoverSTTModels\(\);/);
|
|
assert.match(prefs, /if \(!sttIds\.length\) sttIds = getSTTModelLists\(\)\.litellm\.slice\(\);/,
|
|
'the built-in list survives only as a fallback');
|
|
assert.match(prefs, /model === adminSttModel \? ' \(default\)' : ''/, 'the admin default is marked');
|
|
});
|