// #257 close-out: a failed dashboard request must be distinguishable from a // payload that merely lacks a field — on the wire AND in the panel. // // Before this file, `success === false` was an unreachable branch for every // HTTP error: 53 non-2xx `json()` envelopes in src/dashboard/api.js carried no // `success` key, and src/dashboard/index.html's `_apiRaw` handed the raw error // body to the loaders. The loaders that did check `success === false` never // fired on a 404/500, so they painted an empty panel over the last good state. // // Four layers are pinned here, each of which fails if the corresponding fix is // reverted: // 1. `json()` stamps `success:false` on non-2xx envelopes only — 2xx bodies // keep their exact shape. // 2. The same property end-to-end through `handleDashboardApi` on real routes. // 3. Both skins' fetch wrappers force `success:false` on a non-2xx response, // including a non-JSON error body. // 4. The shipped loaders, run for real against a failing `api()`, must leave // the DOM/state they own untouched — and must still render a successful // payload, so a loader that "passes" by doing nothing cannot hide here. import { afterEach, describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import { config } from '../src/config.js'; import { configureBindHost, _resetLockoutForTests } from '../src/auth.js'; import { handleDashboardApi } from '../src/dashboard/api.js'; import { _resetRuntimeConfigForTests } from '../src/runtime-config.js'; const read = (rel) => readFileSync(new URL(`../${rel}`, import.meta.url), 'utf8'); const API_SRC = read('src/dashboard/api.js'); const MAIN_HTML = read('src/dashboard/index.html'); const SKETCH_HTML = read('src/dashboard/index-sketch.html'); const originalDashboardPassword = config.dashboardPassword; const originalApiKey = config.apiKey; const originalAllowNoAuth = process.env.DASHBOARD_ALLOW_NO_AUTH; afterEach(() => { _resetRuntimeConfigForTests(); _resetLockoutForTests(); config.dashboardPassword = originalDashboardPassword; config.apiKey = originalApiKey; if (originalAllowNoAuth === undefined) delete process.env.DASHBOARD_ALLOW_NO_AUTH; else process.env.DASHBOARD_ALLOW_NO_AUTH = originalAllowNoAuth; configureBindHost('0.0.0.0'); }); const FAILED = { success: false, error: 'Dashboard API: GET /x not found' }; // ── source extraction ─────────────────────────────────────────────────────── // Pull a method body out of a dashboard source file so the SHIPPED code runs. // Brace/quote aware: template literals and comments inside these bodies contain // braces of their own, and a naive counter stops in the wrong place. function extractMethod(src, signature) { const at = src.indexOf(`\n ${signature}`); assert.notEqual(at, -1, `missing \`${signature}\` in the dashboard source`); const open = src.indexOf('{', at + signature.length - 1); assert.notEqual(open, -1, `no body for ${signature}`); let depth = 0; for (let i = open; i < src.length; i++) { const c = src[i]; if (c === '/' && src[i + 1] === '/') { i = src.indexOf('\n', i); if (i < 0) break; continue; } if (c === '/' && src[i + 1] === '*') { i = src.indexOf('*/', i); if (i < 0) break; i += 1; continue; } if (c === "'" || c === '"') { i += 1; while (i < src.length && src[i] !== c) { if (src[i] === '\\') i += 1; i += 1; } continue; } if (c === '`') { i += 1; while (i < src.length && src[i] !== '`') { if (src[i] === '\\') { i += 2; continue; } if (src[i] === '$' && src[i + 1] === '{') { // Balanced `${ … }` inside a template is a nested expression. let d = 0; i += 1; while (i < src.length) { if (src[i] === '{') d += 1; else if (src[i] === '}') { d -= 1; if (d === 0) { i += 1; break; } } i += 1; } continue; } i += 1; } continue; } if (c === '{') depth += 1; else if (c === '}' && --depth === 0) return src.slice(open, i + 1); } throw new Error(`could not delimit the body of ${signature}`); } function extractTopLevelFunction(src, signature) { const at = src.indexOf(`function ${signature}(`); assert.notEqual(at, -1, `missing \`function ${signature}(\` in api.js`); const open = src.indexOf('{', at + signature.length); let depth = 0; for (let i = open; i < src.length; i++) { if (src[i] === '{') depth += 1; else if (src[i] === '}' && --depth === 0) return src.slice(at, i + 1); } throw new Error(`could not delimit function ${signature}`); } const I18nStub = { t: (key, params) => (params ? `${key}:${JSON.stringify(params)}` : key) }; const localStorageStub = { getItem: () => null, setItem() {}, removeItem() {} }; function instantiate(src, signature, doc) { const body = extractMethod(src, signature); const stripped = signature.replace(/^async\s+/, ''); const name = stripped.slice(0, stripped.indexOf('(')); const asyncKw = /^async\s/.test(signature) ? 'async ' : ''; // eslint-disable-next-line no-new-func -- executing the shipped source is the point return new Function('document', 'I18n', 'localStorage', `return ${asyncKw}function ${name}() ${body};`)( doc, I18nStub, localStorageStub, ); } // ── DOM / App stubs ───────────────────────────────────────────────────────── function makeEl(id) { const target = { id, __writes: [], style: {}, dataset: {}, classList: { add() {}, remove() {}, toggle() {}, contains: () => true }, appendChild() {}, click() {}, remove() {}, }; return new Proxy(target, { set(t, key, value) { if (key !== '__writes') t.__writes.push(String(key)); t[key] = value; return true; }, }); } function makeDom(existingIds = null) { const els = new Map(); const getElementById = (id) => { if (existingIds && !existingIds.includes(id)) return null; if (!els.has(id)) els.set(id, makeEl(id)); return els.get(id); }; return { els, el: getElementById, doc: { cookie: '', getElementById, querySelector: () => makeEl('query'), createElement: (tag) => makeEl(tag), body: { appendChild() {} }, }, }; } function appStub(apiResult, extra = {}) { const self = { api: async () => apiResult, toasts: [], toast(message, type) { self.toasts.push([message, type]); }, esc: (s) => String(s === null || s === undefined ? '' : s).replace(/[&<>"']/g, (c) => c), escJsAttr: (s) => String(s === null || s === undefined ? '' : s), escAttr: (s) => String(s === null || s === undefined ? '' : s), systemPromptDomId: (k) => `sp-${encodeURIComponent(String(k)).replace(/%/g, '_')}`, _helpTip: () => '', metric: () => '', _animateNumber: () => {}, enhanceControls: () => {}, accountsListUrl: () => '/accounts', _dropStaleSelection: () => false, renderTablePagination: () => {}, downloadAccountsJson: () => {}, poll: () => {}, ...extra, }; return self; } async function runLoader(src, signature, self, dom) { const fn = instantiate(src, signature, dom.doc); return fn.call(self); } // ── 1. json() stamps non-2xx envelopes only ───────────────────────────────── describe('#257 the dashboard json() helper stamps every non-2xx envelope', () => { function runJson(status, body) { const jsonFn = new Function(`return (${extractTopLevelFunction(API_SRC, 'json')});`)(); let sent = null; const res = { _dashboardCorsOrigin: '', writeHead() {}, end(chunk) { sent = chunk; } }; jsonFn(res, status, body); return sent === null ? undefined : JSON.parse(sent); } it('adds success:false for every error status class the dashboard uses', () => { for (const status of [400, 401, 403, 404, 409, 429, 500, 503]) { const out = runJson(status, { error: 'ERR_X' }); assert.equal(out.success, false, `status ${status} must carry success:false`); assert.equal(out.error, 'ERR_X', `status ${status} must keep the original error field`); } }); it('leaves 2xx bodies byte-identical — including bodies with no success key', () => { assert.deepEqual(runJson(200, { flags: { nativeToolCall: true } }), { flags: { nativeToolCall: true } }); assert.deepEqual(runJson(200, { success: true, cleaned: 2 }), { success: true, cleaned: 2 }); assert.equal(runJson(204, ''), ''); // A non-2xx body that already declares success keeps its own value. assert.deepEqual(runJson(409, { success: true, odd: 1 }), { success: true, odd: 1 }); }); it('never turns a non-object body into an envelope', () => { assert.equal(runJson(500, 'boom'), 'boom'); assert.deepEqual(runJson(500, [1, 2]), [1, 2]); assert.equal(runJson(500, null), null); }); }); // ── 2. the property survives the real handler ─────────────────────────────── describe('#257 real dashboard error routes answer with success:false', () => { function fakeRes() { return { statusCode: 0, body: '', writeHead(status) { this.statusCode = status; }, end(chunk) { this.body += chunk ? String(chunk) : ''; }, json() { return this.body ? JSON.parse(this.body) : null; }, }; } const localReq = (path, headers = {}) => ({ url: `/dashboard/api${path}`, headers, socket: { remoteAddress: '127.0.0.1' }, }); const authed = { 'x-dashboard-password': 'dash-secret' }; it('401 gate, 400 validation, 403 loopback-only and the 404 fallback all carry it', async () => { _resetRuntimeConfigForTests(); config.dashboardPassword = 'dash-secret'; config.apiKey = ''; configureBindHost('0.0.0.0'); const cases = [ ['401', 'GET', '/cache', {}, {}], ['400', 'POST', '/accounts', {}, authed], ['403', 'GET', '/accounts/import-local', {}, authed], ['404', 'GET', '/definitely-not-a-dashboard-route', {}, authed], ]; for (const [label, method, path, body, headers] of cases) { const res = fakeRes(); await handleDashboardApi(method, path, body, localReq(path, headers), res); assert.equal(res.statusCode, Number(label), `${method} ${path} should answer ${label}`); const payload = res.json(); assert.equal(payload.success, false, `${method} ${path} (${label}) must carry success:false`); assert.ok(payload.error, `${method} ${path} (${label}) must keep its error field`); } }); it('a 200 route that never had a success key still does not gain one', async () => { _resetRuntimeConfigForTests(); config.dashboardPassword = 'dash-secret'; config.apiKey = ''; configureBindHost('0.0.0.0'); const res = fakeRes(); await handleDashboardApi('GET', '/experimental', {}, localReq('/experimental', authed), res); assert.equal(res.statusCode, 200); const payload = res.json(); assert.ok(!Object.prototype.hasOwnProperty.call(payload, 'success'), '2xx bodies must not be reshaped by the error-envelope fix'); assert.ok(payload.flags && payload.tunables, 'and must still carry their real payload'); }); }); // ── 3. both skins synthesize the envelope for non-2xx ─────────────────────── describe('#257 the client fetch wrappers force success:false on non-2xx', () => { function withFetch(status, body) { const r = { status, ok: status >= 200 && status < 300, json: async () => body, }; return { fetch: async () => r }; } async function runApiRaw(html, signature, fetchImpl, extraSelf = {}) { const body = extractMethod(html, signature); const stripped = signature.replace(/^async\s+/, ''); const name = stripped.slice(0, stripped.indexOf('(')); const dom = makeDom(['login-overlay', 'login-password', 'login-btn']); // eslint-disable-next-line no-new-func -- executing the shipped source is the point const fn = new Function('document', 'I18n', 'localStorage', 'fetch', `return async function ${name}(method, path, body) ${body};`)(dom.doc, I18nStub, localStorageStub, fetchImpl); // The shipped _apiRaw routes the toast text through translateError so an // unmapped ERR_* code never reaches the user; the stub keeps the raw text // so the assertions below still see the server's message. const self = { _authEpoch: 0, toasts: [], toast(m, t) { self.toasts.push([m, t]); }, password: null, translateError(code, fallback) { return code || fallback; }, ...extraSelf }; return { out: await fn.call(self, 'GET', '/x', null), self }; } for (const [label, html, signature] of [ ['index.html', MAIN_HTML, 'async _apiRaw(method, path, body)'], ['index-sketch.html', SKETCH_HTML, 'async api(method, path, body)'], ]) { it(`${label}: a 404 body gains success:false and keeps its error`, async () => { const { out } = await runApiRaw(html, signature, async () => ({ status: 404, ok: false, json: async () => ({ error: 'Dashboard API: GET /x not found' }), })); assert.equal(out.success, false, `${label} must force success:false on a 404`); assert.equal(out.error, 'Dashboard API: GET /x not found', `${label} must keep the server error`); }); it(`${label}: a non-JSON error body still yields an envelope`, async () => { const { out } = await runApiRaw(html, signature, async () => ({ status: 500, ok: false, json: async () => { throw new Error('not json'); }, })); assert.equal(out.success, false, `${label} must synthesize success:false when the body is unparseable`); }); it(`${label}: a 200 body passes through untouched`, async () => { const { out } = await runApiRaw(html, signature, async () => ({ status: 200, ok: true, json: async () => ({ flags: { nativeToolCall: true } }), })); assert.deepEqual(out, { flags: { nativeToolCall: true } }, `${label} must not reshape 2xx bodies`); }); } }); // ── 4. loaders keep the last good panel when the read fails ───────────────── describe('#257 panel loaders keep the last good state on a failed read', () => { it('loadAccounts no longer repaints over a valid table on a failed read', async () => { let painted = 0; const stub = (result) => appStub(result, { loadDrought: () => {}, renderAccountsSkeleton: () => {}, setAccountsLoadStatus: () => {}, renderAccountsPagination: () => {}, renderAccountsKpis: () => {}, renderAccountsFilterState: () => {}, renderAccountsTable: () => { painted += 1; }, applyAccountsSortState: () => {}, applyAccountsDensity: () => {}, startAccountsClock: () => {}, }); await runLoader(MAIN_HTML, 'async loadAccounts()', stub(FAILED), makeDom()); assert.equal(painted, 0, 'a failed read must not paint an empty table over a valid pool'); await runLoader(MAIN_HTML, 'async loadAccounts()', stub({ accounts: [], stats: {}, total: 0 }), makeDom()); assert.equal(painted, 1, 'control: a successful read must still paint, or this test proves nothing'); }); it('loadModels keeps the previous catalog and mode on failure', async () => { const self = appStub(FAILED); self.allModels = ['LAST-MODEL']; self.modelAccessConfig = { mode: 'allowlist', list: ['m'], defaultModel: 'm' }; const dom = makeDom([]); await runLoader(MAIN_HTML, 'async loadModels()', self, dom); assert.deepEqual(self.allModels, ['LAST-MODEL'], 'a failed read must not empty the model catalog'); assert.equal(self.modelAccessConfig.mode, 'allowlist', 'and must not reset the access mode to "all"'); }); const cases = [ { name: 'loadExperimental', signature: 'async loadExperimental()', existing: ['exp-cascade-reuse', 'exp-pool-cards', 'skin-select', 'exp-drought-threshold'], seed: (dom, self) => { dom.el('exp-pool-cards').innerHTML = 'LAST-GOOD'; dom.el('exp-cascade-reuse').checked = true; }, read: (dom) => [dom.el('exp-pool-cards').innerHTML, dom.el('exp-cascade-reuse').checked], expect: ['LAST-GOOD', true], ok: { flags: { cascadeConversationReuse: true }, conversationPool: { hits: 3 }, tunables: {} }, readOk: (dom) => dom.el('exp-pool-cards').innerHTML, extraSelf: { loadClineCompat: () => {}, loadCcCompat: () => {} }, }, { name: 'loadSystemPrompts', signature: 'async loadSystemPrompts()', existing: ['system-prompts-editor'], seed: (dom, self) => { dom.el('system-prompts-editor').innerHTML = 'LAST-GOOD'; self._systemPromptOriginals = { keep: 'me' }; }, read: (dom, self) => [dom.el('system-prompts-editor').innerHTML, JSON.stringify(self._systemPromptOriginals)], expect: ['LAST-GOOD', '{"keep":"me"}'], ok: { prompts: { toolReinforcement: 'body text' } }, readOk: (dom) => dom.el('system-prompts-editor').innerHTML, }, { name: 'renderRuntimeEnvStatus', signature: 'async renderRuntimeEnvStatus()', existing: ['set-runtime-env-body'], seed: (dom) => { dom.el('set-runtime-env-body').innerHTML = 'LAST-GOOD'; }, read: (dom) => [dom.el('set-runtime-env-body').innerHTML], expect: ['LAST-GOOD'], ok: { switches: { devinConnect: { value: true, source: 'config' } } }, readOk: (dom) => dom.el('set-runtime-env-body').innerHTML, }, { name: 'renderSecuritySettings', signature: 'async renderSecuritySettings()', existing: ['set-security-body'], seed: (dom) => { dom.el('set-security-body').innerHTML = 'LAST-GOOD'; }, read: (dom) => [dom.el('set-security-body').innerHTML], expect: ['LAST-GOOD'], ok: { tunables: { emailLockThreshold: 4 } }, readOk: (dom) => dom.el('set-security-body').innerHTML, }, { name: 'renderBreakerSettings', signature: 'async renderBreakerSettings()', existing: ['set-breaker-body'], seed: (dom) => { dom.el('set-breaker-body').innerHTML = 'LAST-GOOD'; }, read: (dom) => [dom.el('set-breaker-body').innerHTML], expect: ['LAST-GOOD'], ok: { knobs: { errorStreakThreshold: { value: 4 } } }, readOk: (dom) => dom.el('set-breaker-body').innerHTML, }, { name: 'loadCredentials', signature: 'async loadCredentials()', existing: ['credentials-apikey-source', 'credentials-apikey-masked', 'credentials-dashboardpw-source', 'credentials-dashboardpw-status'], seed: (dom) => { for (const id of ['credentials-apikey-source', 'credentials-apikey-masked', 'credentials-dashboardpw-source', 'credentials-dashboardpw-status']) { dom.el(id).textContent = 'LAST-GOOD'; } }, read: (dom) => ['credentials-apikey-source', 'credentials-apikey-masked', 'credentials-dashboardpw-source', 'credentials-dashboardpw-status'] .map((id) => dom.el(id).textContent), expect: ['LAST-GOOD', 'LAST-GOOD', 'LAST-GOOD', 'LAST-GOOD'], ok: { apiKeySource: 'env', apiKey_masked: 'sk-****', dashboardPasswordSource: 'env', dashboardPasswordSet: true }, readOk: (dom) => dom.el('credentials-apikey-source').textContent, }, { name: 'loadClineCompat', signature: 'async loadClineCompat()', existing: ['cline-compat-badge', 'cline-compat-repairs'], seed: (dom) => { dom.el('cline-compat-badge').style.display = 'inline'; }, read: (dom) => [dom.el('cline-compat-badge').style.display], expect: ['inline'], ok: { enabled: true, stats: { argRepairs: 3 } }, readOk: (dom) => dom.el('cline-compat-badge').style.display, }, { name: 'loadCcCompat', signature: 'async loadCcCompat()', existing: ['cc-compat-badge', 'cc-compat-schema'], seed: (dom) => { dom.el('cc-compat-badge').style.display = 'inline'; }, read: (dom) => [dom.el('cc-compat-badge').style.display], expect: ['inline'], ok: { enabled: true, stats: { schemaNormalized: 3 } }, readOk: (dom) => dom.el('cc-compat-badge').style.display, }, ]; for (const spec of cases) { it(`${spec.name} leaves its panel alone when the read fails`, async () => { const dom = makeDom(spec.existing); const self = appStub(FAILED, spec.extraSelf || {}); spec.seed(dom, self); await runLoader(MAIN_HTML, spec.signature, self, dom); assert.deepEqual(spec.read(dom, self), spec.expect, `${spec.name} repainted on a failed read`); // Control: the same loader must still write when the read succeeds. const okDom = makeDom(spec.existing); const okSelf = appStub(spec.ok, spec.extraSelf || {}); if (spec.seed) spec.seed(okDom, okSelf); await runLoader(MAIN_HTML, spec.signature, okSelf, okDom); assert.notDeepEqual(spec.readOk(okDom, okSelf), spec.expect[0], `${spec.name} no longer renders a successful payload — this guard would be vacuous`); }); } it('loadSettings keeps the last prefs on failure but still drives its sub-panels', async () => { const dom = makeDom(['set-oauth-skip-chooser']); dom.el('set-oauth-skip-chooser').checked = true; const called = []; const self = appStub(FAILED, { renderSecuritySettings: () => called.push('security'), renderRuntimeEnvStatus: () => called.push('runtimeEnv'), loadCredentials: () => called.push('credentials'), loadSystemPrompts: () => called.push('prompts'), renderBreakerSettings: () => called.push('breaker'), }); self._prefs = { keep: true }; await runLoader(MAIN_HTML, 'async loadSettings()', self, dom); assert.deepEqual(self._prefs, { keep: true }, 'a failed prefs read must not reset the toggles'); assert.equal(dom.el('set-oauth-skip-chooser').checked, true, 'nor repaint the checkbox'); assert.deepEqual(called.sort(), ['breaker', 'credentials', 'prompts', 'runtimeEnv', 'security'], 'the sub-panels own their own failure handling and must still be driven'); }); it('exportAccounts does not download an empty pool on a failed read', async () => { const dom = makeDom([]); const self = appStub(FAILED); await runLoader(MAIN_HTML, 'async exportAccounts()', self, dom); assert.deepEqual(self.toasts, [], 'a failed export must not toast "exported 0 accounts" as success'); const okDom = makeDom([]); const okSelf = appStub({ accounts: [] }); await runLoader(MAIN_HTML, 'async exportAccounts()', okSelf, okDom); assert.ok(okSelf.toasts.some(([, type]) => type === 'success'), 'control: a successful export still reports success'); }); }); describe('#257 the sketch skin keeps its panels too', () => { const cases = [ { name: 'loadExperimental', signature: 'async loadExperimental()', existing: ['exp-cascade-reuse', 'exp-pool-cards', 'skin-select'], seed: (dom) => { dom.el('exp-pool-cards').innerHTML = 'LAST-GOOD'; dom.el('exp-cascade-reuse').checked = true; }, read: (dom) => [dom.el('exp-pool-cards').innerHTML, dom.el('exp-cascade-reuse').checked], expect: ['LAST-GOOD', true], ok: { flags: { cascadeConversationReuse: true }, conversationPool: { hits: 1 } }, readOk: (dom) => dom.el('exp-pool-cards').innerHTML, extraSelf: { loadCredentials: () => {}, loadSystemPrompts: () => {} }, }, { name: 'loadSystemPrompts', signature: 'async loadSystemPrompts()', existing: ['system-prompts-editor'], seed: (dom) => { dom.el('system-prompts-editor').innerHTML = 'LAST-GOOD'; }, read: (dom) => [dom.el('system-prompts-editor').innerHTML], expect: ['LAST-GOOD'], ok: { prompts: { toolReinforcement: 'body text' } }, readOk: (dom) => dom.el('system-prompts-editor').innerHTML, }, { name: 'loadCredentials', signature: 'async loadCredentials()', existing: ['credentials-apikey-source', 'credentials-apikey-masked', 'credentials-dashboardpw-source', 'credentials-dashboardpw-status'], seed: (dom) => { dom.el('credentials-apikey-source').textContent = 'LAST-GOOD'; }, read: (dom) => [dom.el('credentials-apikey-source').textContent], expect: ['LAST-GOOD'], ok: { apiKeySource: 'env' }, readOk: (dom) => dom.el('credentials-apikey-source').textContent, }, ]; for (const spec of cases) { it(`sketch ${spec.name} leaves its panel alone when the read fails`, async () => { const dom = makeDom(spec.existing); const self = appStub(FAILED, spec.extraSelf || {}); spec.seed(dom, self); await runLoader(SKETCH_HTML, spec.signature, self, dom); assert.deepEqual(spec.read(dom, self), spec.expect, `sketch ${spec.name} repainted on a failed read`); const okDom = makeDom(spec.existing); const okSelf = appStub(spec.ok, spec.extraSelf || {}); spec.seed(okDom, okSelf); await runLoader(SKETCH_HTML, spec.signature, okSelf, okDom); assert.notDeepEqual(spec.readOk(okDom, okSelf), spec.expect[0], `sketch ${spec.name} no longer renders a successful payload`); }); } });