Changeset 0.22.6 (#148)

This commit is contained in:
2026-03-03 13:12:13 +00:00
parent 45fe965c32
commit d8e0664fa3
24 changed files with 983 additions and 2473 deletions

View File

@@ -7,6 +7,10 @@
// 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.
//
// Run: node --test src/js/__tests__/policy-gating.test.js
// ==========================================
@@ -16,6 +20,21 @@ const fs = require('fs');
const path = require('path');
const SRC = path.join(__dirname, '..');
const TEMPLATES = path.join(__dirname, '..', '..', '..', 'server', 'pages', 'templates');
// ── Helper: read all server template HTML ────
function readAllTemplates() {
const files = [];
function walk(dir) {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) walk(full);
else if (entry.name.endsWith('.html')) files.push(fs.readFileSync(full, 'utf-8'));
}
}
walk(TEMPLATES);
return files.join('\n');
}
// ── Source code audits ───────────────────────
// These tests read the actual source files and verify that required
@@ -28,7 +47,9 @@ describe('Policy wiring audit — source code', () => {
// 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');
const indexSrc = fs.readFileSync(path.join(SRC, '..', 'index.html'), 'utf-8');
// 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 ──
@@ -54,18 +75,18 @@ describe('Policy wiring audit — source code', () => {
'MISSING: allow_user_personas check in UI');
});
it('admin settings UI has adminUserPresetsToggle', () => {
assert.ok(indexSrc.includes('adminUserPresetsToggle'),
'MISSING: preset toggle in admin settings HTML');
it('admin settings template has user-personas toggle', () => {
assert.ok(templateSrc.includes('settUserPersonas'),
'MISSING: settUserPersonas toggle in admin settings template');
});
it('admin settings load reads allow_user_personas', () => {
assert.ok(uiSrc.includes("adminUserPresetsToggle"),
'MISSING: loadAdminSettings must read allow_user_personas into toggle');
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', () => {
assert.ok(appSrc.includes("allow_user_personas"),
it('admin settings save writes allow_user_personas (SPA bridge)', () => {
assert.ok(appSrc.includes('allow_user_personas'),
'MISSING: handleSaveAdminSettings must write allow_user_personas');
});
@@ -199,55 +220,101 @@ 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.
describe('Admin settings field mapping', () => {
// Maps what the frontend sends to what the backend expects
describe('Admin settings field mapping (server templates)', () => {
// New element IDs used by server-rendered admin/settings.html + pages.js
const settingsFieldMap = {
'adminRegToggle': 'allow_registration',
'adminRegDefaultState': 'default_user_active',
'adminUserProvidersToggle': 'allow_user_byok',
'adminUserPresetsToggle': 'allow_user_personas',
'adminBannerEnabled': 'banner',
'settRegEnabled': 'allow_registration',
'settRegDefaultState': 'default_user_active',
'settUserBYOK': 'allow_user_byok',
'settUserPersonas': 'allow_user_personas',
'settBannerEnabled': 'banner',
};
// Read all app-side files (handleSaveAdminSettings is in settings-handlers.js)
const appSrc = ['app.js', 'settings-handlers.js', 'admin-handlers.js']
.map(f => fs.readFileSync(path.join(SRC, f), 'utf-8')).join('\n');
const indexSrc = fs.readFileSync(path.join(SRC, '..', 'index.html'), 'utf-8');
const pagesSrc = fs.readFileSync(path.join(SRC, 'pages.js'), 'utf-8');
const templateSrc = readAllTemplates();
for (const [elementId, settingKey] of Object.entries(settingsFieldMap)) {
it(`HTML has element #${elementId}`, () => {
assert.ok(indexSrc.includes(`id="${elementId}"`),
`MISSING: #${elementId} in index.html — admin settings incomplete`);
it(`template has element #${elementId}`, () => {
assert.ok(templateSrc.includes(`id="${elementId}"`),
`MISSING: #${elementId} in server templates — admin settings incomplete`);
});
it(`frontend writes setting "${settingKey}"`, () => {
assert.ok(appSrc.includes(settingKey),
`MISSING: "${settingKey}" in handleSaveAdminSettings`);
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',
];
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`);
});
}
});
// ── HTML element existence checks ────────────
// v0.22.5: Elements now live in server templates, not index.html.
// Settings surface dynamic sections (providers, personas) have scaffold
// containers rendered by Go templates that JS then populates.
describe('Critical HTML elements exist', () => {
const indexSrc = fs.readFileSync(path.join(SRC, '..', 'index.html'), 'utf-8');
describe('Critical HTML elements exist in server templates', () => {
const templateSrc = readAllTemplates();
const requiredElements = [
'adminMemberUser', // Team member user dropdown
// Settings surface — provider section scaffold
'userPresetList', // User preset list container
'userAddPresetBtn', // New preset button (policy-gated)
'userAddPresetForm', // Preset form container
'userProvidersDisabled', // BYOK disabled notice
'providerShowAddBtn', // Add provider button (policy-gated)
'adminUserProvidersToggle', // Admin toggle for BYOK
'adminUserPresetsToggle', // Admin toggle for presets
// Admin settings — policy toggles
'settUserBYOK', // Admin toggle for BYOK (was adminUserProvidersToggle)
'settUserPersonas', // Admin toggle for presets (was adminUserPresetsToggle)
];
for (const id of requiredElements) {
it(`#${id} exists in index.html`, () => {
assert.ok(indexSrc.includes(`id="${id}"`),
it(`#${id} exists in server templates`, () => {
assert.ok(templateSrc.includes(`id="${id}"`),
`MISSING element: #${id} — UI feature will break`);
});
}
});
// ── 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.
describe('SPA bridge — dynamic element creators', () => {
const uiAdminSrc = fs.readFileSync(path.join(SRC, 'ui-admin.js'), 'utf-8');
it('ui-admin.js has loadMemberUserDropdown', () => {
assert.ok(uiAdminSrc.includes('loadMemberUserDropdown'),
'MISSING: loadMemberUserDropdown — team member add will break');
});
it('loadMemberUserDropdown references adminMemberUser', () => {
assert.ok(uiAdminSrc.includes('adminMemberUser'),
'MISSING: adminMemberUser reference in loadMemberUserDropdown');
});
});