test/dashboard-devin-panel-routes.test.js
Rama master · réplica de lectura del repositorio original.
// Route contract for the DevIn panel:
// GET /devin/panel
//
// The panel inherits the dashboard auth gate (no new exemption), reads only
// in-process state (no upstream call), and must never put a credential or an
// email in the body. It also pins the shared switch-row helper that
// /runtime-env-status and the panel now both use.
import { afterEach, describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { config } from '../src/config.js';
import {
addAccountByKey,
configureBindHost,
recordAccountRequest,
removeAccount,
__resetAccountRequestsForTest,
_resetLockoutForTests,
} from '../src/auth.js';
import { accountRef } from '../src/log-safety.js';
import { handleDashboardApi } from '../src/dashboard/api.js';
import { _resetRuntimeConfigForTests } from '../src/runtime-config.js';
const originalDashboardPassword = config.dashboardPassword;
const originalApiKey = config.apiKey;
const created = new Set();
function fakeRes() {
return {
statusCode: 0,
headers: {},
body: '',
writeHead(status, headers) { this.statusCode = status; this.headers = headers || {}; },
end(chunk) { this.body += chunk ? String(chunk) : ''; },
json() { return this.body ? JSON.parse(this.body) : null; },
};
}
function req(path, headers = {}) {
return { url: `/dashboard/api${path}`, headers, socket: { remoteAddress: '127.0.0.1' } };
}
function authed(path, extra = {}) {
return req(path, { 'x-dashboard-password': 'dash-secret', ...extra });
}
function addAccount(label) {
const key = `panel-key-${label}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
const account = addAccountByKey(key, label);
created.add(account.id);
return { account, key };
}
function gateOn() {
_resetRuntimeConfigForTests();
config.dashboardPassword = 'dash-secret';
config.apiKey = '';
configureBindHost('0.0.0.0');
}
afterEach(() => {
for (const id of created) removeAccount(id);
created.clear();
__resetAccountRequestsForTest();
_resetRuntimeConfigForTests();
_resetLockoutForTests();
configureBindHost('0.0.0.0');
config.dashboardPassword = originalDashboardPassword;
config.apiKey = originalApiKey;
});
describe('GET /devin/panel', () => {
it('fails closed without dashboard auth', async () => {
gateOn();
const res = fakeRes();
await handleDashboardApi('GET', '/devin/panel', {}, req('/devin/panel'), res);
assert.equal(res.statusCode, 401);
});
it('returns the four sections with ok:true', async () => {
gateOn();
const res = fakeRes();
await handleDashboardApi('GET', '/devin/panel', {}, authed('/devin/panel'), res);
assert.equal(res.statusCode, 200);
assert.equal(res.headers['Cache-Control'], 'no-store');
const body = res.json();
assert.equal(body.ok, true);
assert.equal(typeof body.generatedAt, 'number');
assert.ok(Array.isArray(body.runtime.switches));
assert.equal(body.runtime.switches.length, 6);
assert.ok(body.runtime.langServer && typeof body.runtime.langServer.running === 'boolean');
assert.ok(body.runtime.detach && typeof body.runtime.detach.enabled === 'boolean');
assert.ok(Array.isArray(body.sessions.sources));
assert.equal(body.sessions.turns.available, true);
assert.equal(typeof body.sessions.turns.totalAccounts, 'number');
assert.equal(typeof body.sessions.turns.capped, 'boolean');
assert.ok(Array.isArray(body.quota.knobs));
assert.equal(body.quota.knobs.length, 5);
assert.ok(body.quota.drought && typeof body.quota.drought.active === 'boolean');
});
it('merges a recorded billed turn under the hashed account ref, never the key or email', async () => {
gateOn();
const { account, key } = addAccount('panel-turn@example.test');
recordAccountRequest(account, {
model: 'swe-2',
usage: { prompt_tokens: 7, completion_tokens: 3, total_tokens: 10 },
creditCost: 1.5,
acuCost: 0.25,
now: Date.now(),
});
const res = fakeRes();
await handleDashboardApi('GET', '/devin/panel', {}, authed('/devin/panel'), res);
assert.equal(res.statusCode, 200);
const body = res.json();
const turns = body.sessions.turns;
assert.equal(turns.retained, 1);
assert.equal(turns.items.length, 1);
const row = turns.items[0];
assert.equal(row.accountRef, accountRef(account.id));
assert.equal(row.model, 'swe-2');
assert.equal(row.totalTokens, 10);
assert.equal(row.credit, 1.5);
assert.equal(row.acu, 0.25);
assert.ok(!res.body.includes(key), 'the raw account key must not reach the panel body');
assert.ok(!res.body.includes('panel-turn@example.test'), 'the account email must not reach the panel body');
});
it('serves the snapshot without touching the network', async () => {
gateOn();
const original = globalThis.fetch;
let calls = 0;
globalThis.fetch = (...args) => { calls++; return original(...args); };
try {
const res = fakeRes();
await handleDashboardApi('GET', '/devin/panel', {}, authed('/devin/panel'), res);
assert.equal(res.statusCode, 200);
} finally {
globalThis.fetch = original;
}
assert.equal(calls, 0, 'opening the panel must not call upstream');
});
it('declares the unreadable sources instead of returning empty lists', async () => {
gateOn();
const res = fakeRes();
await handleDashboardApi('GET', '/devin/panel', {}, authed('/devin/panel'), res);
const sources = res.json().sessions.sources;
const byId = Object.fromEntries(sources.map((s) => [s.id, s]));
assert.equal(byId.proxyTurns.available, true);
assert.equal(byId.cascadeTrajectories.available, false);
assert.equal(byId.cascadeTrajectories.reason, 'ls_only');
assert.equal(byId.acpSessions.available, false);
assert.equal(byId.acpSessions.reason, 'not_tracked');
});
it('reports the effective switch values and their source', async () => {
gateOn();
const res = fakeRes();
await handleDashboardApi('GET', '/devin/panel', {}, authed('/devin/panel'), res);
const switches = res.json().runtime.switches;
for (const row of switches) {
assert.equal(typeof row.key, 'string');
assert.ok('value' in row);
assert.ok(row.source === 'env' || row.source === 'config');
assert.equal(typeof row.overridden, 'boolean');
}
const cred = res.json().runtime.credentials;
assert.equal(cred.writable, false);
});
it('does not answer for a non-GET method', async () => {
gateOn();
const res = fakeRes();
await handleDashboardApi('POST', '/devin/panel', {}, authed('/devin/panel'), res);
assert.notEqual(res.statusCode, 200);
});
});
describe('GET /runtime-env-status (shared switch rows)', () => {
it('still returns flat effective values plus per-switch detail', async () => {
gateOn();
const res = fakeRes();
await handleDashboardApi('GET', '/runtime-env-status', {}, authed('/runtime-env-status'), res);
assert.equal(res.statusCode, 200);
const body = res.json();
assert.equal(body.devinConnect, body.switches.devinConnect.value);
assert.equal(body.devinCliMode, body.switches.devinCliMode.value);
for (const key of ['devinConnect', 'devinOnly', 'devinCliMode', 'allowClientTools', 'loginHostFallback', 'autoRelogin']) {
assert.ok(body.switches[key], `${key} must be present`);
assert.ok('value' in body.switches[key]);
assert.ok('override' in body.switches[key]);
assert.ok('source' in body.switches[key]);
}
});
it('reports config after an override and env after clearing it', async () => {
gateOn();
const put = fakeRes();
await handleDashboardApi('PUT', '/runtime-env-status', { devinConnect: true }, authed('/runtime-env-status'), put);
assert.equal(put.statusCode, 200);
assert.equal(put.json().switches.devinConnect.source, 'config');
assert.equal(put.json().switches.devinConnect.value, true);
const clear = fakeRes();
await handleDashboardApi('PUT', '/runtime-env-status', { devinConnect: null }, authed('/runtime-env-status'), clear);
assert.equal(clear.statusCode, 200);
assert.equal(clear.json().switches.devinConnect.source, 'env');
assert.equal(clear.json().switches.devinConnect.override, null);
});
});