Changeset 0.37.10 (#222)

Co-authored-by: gobha <jasafpro@gmail.com>
Co-committed-by: gobha <jasafpro@gmail.com>
This commit is contained in:
2026-03-21 23:39:23 +00:00
committed by xcaliber
parent 37b639c9c8
commit 8d8a118232
52 changed files with 3209 additions and 13811 deletions

View File

@@ -1,8 +1,15 @@
const { describe, it } = require('node:test');
const assert = require('node:assert');
const { createBrowserContext, loadSource } = require('./helpers');
const fs = require('fs');
const path = require('path');
const { createBrowserContext, loadSource, SRC } = require('./helpers');
const vm = require('vm');
// v0.37.10: extensions.js deleted — extension system moves to Preact in v0.37.12.
// Skip all tests if the source file doesn't exist.
const EXTENSIONS_EXISTS = fs.existsSync(path.join(SRC, 'extensions.js'));
const maybeDescribe = EXTENSIONS_EXISTS ? describe : describe.skip;
/**
* Load Events + Extensions into a browser context.
*/
@@ -23,7 +30,7 @@ function loadExtensions(overrides = {}) {
// KaTeX Renderer Extension
// ═══════════════════════════════════════════
describe('KaTeX renderer extension', () => {
maybeDescribe('KaTeX renderer extension', () => {
function loadWithKaTeX() {
const ctx = loadExtensions();
ctx.Extensions.register({
@@ -133,7 +140,7 @@ describe('KaTeX renderer extension', () => {
// CSV Table Extension
// ═══════════════════════════════════════════
describe('CSV Table extension', () => {
maybeDescribe('CSV Table extension', () => {
function loadWithCSV() {
const ctx = loadExtensions();
ctx.Extensions.register({
@@ -296,7 +303,7 @@ describe('CSV Table extension', () => {
// Diff Viewer Extension
// ═══════════════════════════════════════════
describe('Diff Viewer extension', () => {
maybeDescribe('Diff Viewer extension', () => {
function loadWithDiff() {
const ctx = loadExtensions();
ctx.Extensions.register({
@@ -427,7 +434,7 @@ describe('Diff Viewer extension', () => {
// JS Sandbox Extension (Tool)
// ═══════════════════════════════════════════
describe('JS Sandbox extension', () => {
maybeDescribe('JS Sandbox extension', () => {
it('registers js_eval tool handler', async () => {
const ctx = loadExtensions();
ctx.Extensions.register({
@@ -484,7 +491,7 @@ describe('JS Sandbox extension', () => {
// Regex Tester Extension (Tool)
// ═══════════════════════════════════════════
describe('Regex Tester extension', () => {
maybeDescribe('Regex Tester extension', () => {
function loadWithRegex() {
const ctx = loadExtensions();
ctx.Extensions.register({

View File

@@ -1,8 +1,15 @@
const { describe, it } = require('node:test');
const assert = require('node:assert');
const { createBrowserContext, loadSource } = require('./helpers');
const fs = require('fs');
const path = require('path');
const { createBrowserContext, loadSource, SRC } = require('./helpers');
const vm = require('vm');
// v0.37.10: extensions.js deleted — extension system moves to Preact in v0.37.12.
// Skip all tests if the source file doesn't exist.
const EXTENSIONS_EXISTS = fs.existsSync(path.join(SRC, 'extensions.js'));
const maybeDescribe = EXTENSIONS_EXISTS ? describe : describe.skip;
/**
* Load Events + Extensions into a browser context.
* Returns an object with Events, Extensions, and the sandbox.
@@ -25,7 +32,7 @@ function loadExtensions(overrides = {}) {
// ── Registration ─────────────────────────────
describe('Extensions.register', () => {
maybeDescribe('Extensions.register', () => {
it('registers an extension by id', () => {
const ctx = loadExtensions();
ctx.Extensions.register({ id: 'test-ext', init() {} });
@@ -48,7 +55,7 @@ describe('Extensions.register', () => {
// ── Lifecycle ────────────────────────────────
describe('Extensions.initAll', () => {
maybeDescribe('Extensions.initAll', () => {
it('calls init on registered extensions', async () => {
const ctx = loadExtensions();
let initialized = false;
@@ -84,7 +91,7 @@ describe('Extensions.initAll', () => {
});
});
describe('Extensions.destroyAll', () => {
maybeDescribe('Extensions.destroyAll', () => {
it('calls destroy and deactivates extensions', async () => {
const ctx = loadExtensions();
let destroyed = false;
@@ -102,7 +109,7 @@ describe('Extensions.destroyAll', () => {
// ── Context Object ──────────────────────────
describe('Extension context', () => {
maybeDescribe('Extension context', () => {
it('provides scoped events', async () => {
const ctx = loadExtensions();
let receivedPayload = null;
@@ -159,7 +166,7 @@ describe('Extension context', () => {
// ── Tool Registration ───────────────────────
describe('ctx.tools.handle', () => {
maybeDescribe('ctx.tools.handle', () => {
it('registers a tool handler', async () => {
const ctx = loadExtensions();
ctx.Extensions.register({
@@ -177,7 +184,7 @@ describe('ctx.tools.handle', () => {
// ── Tool Bridge ─────────────────────────────
describe('Tool bridge', () => {
maybeDescribe('Tool bridge', () => {
it('routes tool.call.* to registered handler and emits result', async () => {
const ctx = loadExtensions();
let resultEmitted = null;
@@ -269,7 +276,7 @@ describe('Tool bridge', () => {
// ── Renderer Pipeline ───────────────────────
describe('Renderer pipeline', () => {
maybeDescribe('Renderer pipeline', () => {
it('registers and runs block renderers', async () => {
const ctx = loadExtensions();
let rendered = false;
@@ -356,7 +363,7 @@ describe('Renderer pipeline', () => {
// ── Debug ────────────────────────────────────
describe('Extensions.debug', () => {
maybeDescribe('Extensions.debug', () => {
it('returns extension info and renderer count', async () => {
const ctx = loadExtensions();
ctx.Extensions.register({
@@ -376,7 +383,7 @@ describe('Extensions.debug', () => {
// ── Mermaid Extension ─────────────────────────────
describe('Mermaid renderer extension', () => {
maybeDescribe('Mermaid renderer extension', () => {
function loadWithMermaid() {
const ctx = loadExtensions();
// Load the mermaid extension script into the same VM context

View File

@@ -4,6 +4,8 @@
// Loads the vanilla JS source files into a
// simulated browser environment so tests run
// against the ACTUAL frontend code.
//
// v0.37.10: Removed loadAppModules (api.js + app.js deleted).
// ==========================================
const fs = require('fs');
@@ -104,23 +106,24 @@ function createBrowserContext(overrides = {}) {
* Returns the sandbox for inspection.
*/
function loadSource(sandbox, filename) {
const code = fs.readFileSync(path.join(SRC, filename), 'utf-8');
const filepath = path.join(SRC, filename);
if (!fs.existsSync(filepath)) {
throw new Error(`Source file not found: ${filename} (deleted in v0.37.10?)`);
}
const code = fs.readFileSync(filepath, 'utf-8');
const ctx = vm.createContext(sandbox);
vm.runInContext(code, ctx, { filename });
return sandbox;
}
/**
* Loads API + App modules into a fresh context.
* Returns { API, App, sandbox }.
* Safely read a source file, returning empty string if deleted.
* For tests that audit source code content.
*/
function loadAppModules(overrides = {}) {
const sandbox = createBrowserContext(overrides);
loadSource(sandbox, 'sb.js');
loadSource(sandbox, 'app-state.js');
loadSource(sandbox, 'api.js');
loadSource(sandbox, 'app.js');
return { API: sandbox.API, App: sandbox.App, sandbox };
function readSourceSafe(filename) {
const filepath = path.join(SRC, filename);
if (!fs.existsSync(filepath)) return '';
return fs.readFileSync(filepath, 'utf-8');
}
/**
@@ -158,7 +161,7 @@ function processModelsResponse(data, hiddenModels = new Set()) {
module.exports = {
createBrowserContext,
loadSource,
loadAppModules,
readSourceSafe,
processModelsResponse,
SRC,
};

View File

@@ -7,9 +7,12 @@
// policy exists but the frontend doesn't
// check it.
//
// v0.22.5: Updated for server-rendered Go
// templates. HTML is now in server/pages/
// templates/ — not in src/index.html.
// v0.22.5: Updated for server-rendered Go templates.
// v0.37.5: Settings surface moved to Preact — legacy SPA tests
// replaced with component source audits.
// v0.37.10: Legacy SPA source audit removed (ui-core.js, app.js,
// pages.js, settings-handlers.js, ui-admin.js all deleted).
// Policy gating now verified via Preact surfaces + admin templates.
//
// Run: node --test src/js/__tests__/policy-gating.test.js
// ==========================================
@@ -36,94 +39,6 @@ function readAllTemplates() {
return files.join('\n');
}
// ── Source code audits ───────────────────────
// These tests read the actual source files and verify that required
// wiring exists. If someone removes a policy check, CI breaks.
describe('Policy wiring audit — source code', () => {
// Read all UI files (ui-core.js + ui-settings.js + ui-admin.js replace old ui.js)
const uiSrc = ['ui-core.js', 'ui-settings.js', 'ui-admin.js', 'ui-format.js']
.map(f => fs.readFileSync(path.join(SRC, f), 'utf-8')).join('\n');
// Read all app-side files (app.js + extracted handler files replace old monolith app.js)
const appSrc = ['app.js', 'settings-handlers.js', 'admin-handlers.js', 'chat.js', 'tokens.js', 'notes.js']
.map(f => fs.readFileSync(path.join(SRC, f), 'utf-8')).join('\n');
// Pages.js — server-rendered page handlers (v0.22.5+)
const pagesSrc = fs.readFileSync(path.join(SRC, 'pages.js'), 'utf-8');
const templateSrc = readAllTemplates();
// ── allow_user_byok ──
it('UI has checkUserProvidersAllowed function', () => {
assert.ok(uiSrc.includes('checkUserProvidersAllowed'),
'MISSING: checkUserProvidersAllowed — provider tab will show when policy is off');
});
it('checkUserProvidersAllowed reads App.policies.allow_user_byok', () => {
assert.ok(uiSrc.includes('allow_user_byok'),
'MISSING: allow_user_byok check in UI');
});
// ── allow_user_personas ──
it('UI has checkUserPersonasAllowed function', () => {
assert.ok(uiSrc.includes('checkUserPersonasAllowed'),
'MISSING: checkUserPersonasAllowed — persona button will show when policy is off');
});
it('checkUserPersonasAllowed reads App.policies.allow_user_personas', () => {
assert.ok(uiSrc.includes('allow_user_personas'),
'MISSING: allow_user_personas check in UI');
});
it('admin settings template has user-personas toggle', () => {
assert.ok(templateSrc.includes('settUserPersonas'),
'MISSING: settUserPersonas toggle in admin settings template');
});
it('Pages.saveSettings writes allow_user_personas', () => {
assert.ok(pagesSrc.includes('allow_user_personas'),
'MISSING: allow_user_personas in Pages.saveSettings');
});
it('admin settings save writes allow_user_personas (SPA bridge)', () => {
assert.ok(appSrc.includes('allow_user_personas'),
'MISSING: handleSaveAdminSettings must write allow_user_personas');
});
// ── Settings live-apply ──
it('admin save calls initBanners for policy refresh', () => {
assert.ok(appSrc.includes('await initBanners()'),
'MISSING: initBanners call after admin save — policies won\'t refresh');
});
it('admin save calls checkUserProvidersAllowed', () => {
// Find in handleSaveAdminSettings
const saveFunc = appSrc.slice(appSrc.indexOf('handleSaveAdminSettings'));
assert.ok(saveFunc.includes('checkUserProvidersAllowed'),
'MISSING: checkUserProvidersAllowed call after admin save');
});
it('admin save calls checkUserPersonasAllowed', () => {
const saveFunc = appSrc.slice(appSrc.indexOf('handleSaveAdminSettings'));
assert.ok(saveFunc.includes('checkUserPersonasAllowed'),
'MISSING: checkUserPersonasAllowed call after admin save');
});
it('admin save calls fetchModels to refresh model list', () => {
const saveFunc = appSrc.slice(appSrc.indexOf('handleSaveAdminSettings'));
assert.ok(saveFunc.includes('fetchModels'),
'MISSING: fetchModels call after admin save — model list stale after settings change');
});
// ── Models tab calls preset check ──
it('models tab switch calls checkUserPersonasAllowed', () => {
assert.ok(uiSrc.includes('checkUserPersonasAllowed'),
'MISSING: checkUserPersonasAllowed call on models tab switch');
});
});
// ── Policy evaluation logic ──────────────────
describe('Policy evaluation logic', () => {
@@ -158,7 +73,6 @@ describe('Policy evaluation logic', () => {
});
// ── Team member dropdown logic ───────────────
// Bug: Empty dropdown because resp.data used instead of resp.users
describe('Team member dropdown population', () => {
function getAvailableUsers(usersResp, membersResp) {
@@ -221,72 +135,37 @@ describe('Team member dropdown population', () => {
// ── Admin settings field mapping ─────────────
// v0.22.5: Server-rendered admin settings template uses new element IDs.
// Pages.saveSettings() in pages.js is the primary handler.
// v0.37.10: pages.js deleted — admin settings save now handled by
// Preact admin surface. Template elements still required.
describe('Admin settings field mapping (server templates)', () => {
// New element IDs used by server-rendered admin/settings.html + pages.js
const settingsFieldMap = {
'settRegEnabled': 'allow_registration',
'settRegDefaultState': 'default_user_active',
'settUserBYOK': 'allow_user_byok',
'settUserPersonas': 'allow_user_personas',
'settBannerEnabled': 'banner',
};
const pagesSrc = fs.readFileSync(path.join(SRC, 'pages.js'), 'utf-8');
const templateSrc = readAllTemplates();
for (const [elementId, settingKey] of Object.entries(settingsFieldMap)) {
it(`template has element #${elementId}`, () => {
assert.ok(templateSrc.includes(`id="${elementId}"`),
`MISSING: #${elementId} in server templates — admin settings incomplete`);
});
it(`Pages.saveSettings writes setting "${settingKey}"`, () => {
assert.ok(pagesSrc.includes(settingKey),
`MISSING: "${settingKey}" in Pages.saveSettings`);
});
}
});
// ── SPA bridge field mapping (backward compat) ──
// The SPA chat surface still loads settings-handlers.js + ui-admin.js.
// These use legacy element IDs for handleSaveAdminSettings().
// Verified at the JS level (elements are SPA-modal DOM, not templates).
describe('SPA bridge — admin settings handler references policy keys', () => {
const appSrc = ['settings-handlers.js', 'admin-handlers.js']
.map(f => fs.readFileSync(path.join(SRC, f), 'utf-8')).join('\n');
const requiredPolicies = [
'allow_registration',
'default_user_active',
'allow_user_byok',
'allow_user_personas',
const requiredElements = [
'settRegEnabled',
'settRegDefaultState',
'settUserBYOK',
'settUserPersonas',
'settBannerEnabled',
];
for (const key of requiredPolicies) {
it(`SPA bridge writes policy "${key}"`, () => {
assert.ok(appSrc.includes(key),
`MISSING: "${key}" in SPA bridge handler — policy not saved`);
for (const id of requiredElements) {
it(`template has element #${id}`, () => {
assert.ok(templateSrc.includes(`id="${id}"`),
`MISSING: #${id} in server templates — admin settings incomplete`);
});
}
});
// ── HTML element existence checks ────────────
// v0.22.5: Elements now live in server templates, not index.html.
// v0.37.5: Settings surface elements moved to Preact components
// (src/js/sw/surfaces/settings/). Only admin template elements
// are checked here. Settings surface policy gating is handled by
// the Preact SettingsSurface component (nav gating + section routing).
// ── Critical HTML elements in templates ───────
describe('Critical HTML elements exist in server templates', () => {
const templateSrc = readAllTemplates();
const requiredElements = [
// Admin settings — policy toggles (still in Go templates)
'settUserBYOK', // Admin toggle for BYOK (was adminUserProvidersToggle)
'settUserPersonas', // Admin toggle for presets (was adminUserPresetsToggle)
'settUserBYOK', // Admin toggle for BYOK
'settUserPersonas', // Admin toggle for personas
];
for (const id of requiredElements) {
@@ -297,6 +176,34 @@ describe('Critical HTML elements exist in server templates', () => {
}
});
// ── Chat surface template ────────────────────
// v0.37.10: Chat surface is now Preact-rendered. Verify the mount
// point exists and the old SPA scaffold is gone.
describe('Chat surface template (v0.37.10)', () => {
const templateSrc = readAllTemplates();
it('chat-mount div exists', () => {
assert.ok(templateSrc.includes('id="chat-mount"'),
'MISSING: #chat-mount — Preact chat surface cannot render');
});
it('old appContainer div is gone', () => {
assert.ok(!templateSrc.includes('id="appContainer"'),
'STALE: #appContainer still in templates — old SPA scaffold not removed');
});
it('chat template loads Preact surface module', () => {
assert.ok(templateSrc.includes('sw/surfaces/chat/index.js'),
'MISSING: chat surface module import in template');
});
it('chat template loads SDK boot', () => {
assert.ok(templateSrc.includes('sw/sdk/index.js'),
'MISSING: SDK boot import in chat template');
});
});
// ── Preact surface policy gating ─────────────
// v0.37.5: Settings surface elements are now rendered by Preact
// components. Verify the components handle policy gating.
@@ -326,20 +233,26 @@ describe('Settings Preact surface handles policy gating', () => {
});
});
// ── SPA bridge — dynamic DOM elements ────────
// adminMemberUser is created by ui-admin.js loadMemberUserDropdown()
// which runs inside the SPA chat surface. Verify the JS function exists.
// ── Admin Preact surface ─────────────────────
// v0.37.6: Admin surface handles settings save via Preact.
// Verify the admin surface has policy key handling.
describe('SPA bridge — dynamic element creators', () => {
const uiAdminSrc = fs.readFileSync(path.join(SRC, 'ui-admin.js'), 'utf-8');
describe('Admin Preact surface handles settings', () => {
const SURFACES = path.join(SRC, 'sw', 'surfaces', 'admin');
const settingsPath = path.join(SURFACES, 'settings.js');
it('ui-admin.js has loadMemberUserDropdown', () => {
assert.ok(uiAdminSrc.includes('loadMemberUserDropdown'),
'MISSING: loadMemberUserDropdown — team member add will break');
});
// Only test if the admin settings component exists
if (fs.existsSync(settingsPath)) {
const settingsSrc = fs.readFileSync(settingsPath, 'utf-8');
it('loadMemberUserDropdown references adminMemberUser', () => {
assert.ok(uiAdminSrc.includes('adminMemberUser'),
'MISSING: adminMemberUser reference in loadMemberUserDropdown');
});
it('admin settings component references allow_user_byok', () => {
assert.ok(settingsSrc.includes('allow_user_byok'),
'MISSING: allow_user_byok in admin settings component');
});
it('admin settings component references allow_user_personas', () => {
assert.ok(settingsSrc.includes('allow_user_personas'),
'MISSING: allow_user_personas in admin settings component');
});
}
});