Extract dashboard settings routes (#71)

* review: harden readonly git checks and lint verification

* test: satisfy readonly reviewer sandbox typing

* test: fix readonly reviewer sandbox assertions

* test: remove impossible readonly sandbox guards

* test: relax readonly sandbox assertions

* test: cast readonly sandbox expectation shape

* test: cast readonly sandbox expectation through unknown

* Phase 0 — STEP_DONE/TASK_DONE split + owner-follow-up integration smoke

* Add STEP_DONE guards, verdict storage, and stale delivery suppression

* style: format paired stepdone telemetry files

* Route STEP_DONE through reviewer

* Add structured Discord attachments

* style: format structured attachment test

* Fix room thread bot output parity

* Remove duplicate work item attachment migration from owner branch

* Remove legacy dashboard rooms renderer

* Extract dashboard parsed body renderer

* Extract dashboard redaction helpers

* Extract dashboard RoomCardV2 component

* Include RoomCardV2 test in vitest suite

* Extract dashboard RoomBoardV2 component

* Extract dashboard EmptyState component

* Extract dashboard InboxPanel component

* Split dashboard InboxPanel card renderer

* Extract dashboard TaskPanel component

* Extract dashboard UsagePanel component

* Fix dashboard room thread chunk rendering

* Extract dashboard ServicePanel component

* Render dashboard live progress markdown

* Extract dashboard SettingsPanel component

* Fix structured attachment rendering

* Extract dashboard simple route table

* Extract dashboard settings routes
This commit is contained in:
Eyejoker
2026-04-28 18:09:45 +09:00
committed by GitHub
parent fb79aa307b
commit d1008546f8
3 changed files with 476 additions and 179 deletions

View File

@@ -0,0 +1,184 @@
import { describe, expect, it } from 'vitest';
import {
handleSettingsRoute,
type SettingsRouteDependencies,
} from './web-dashboard-settings-routes.js';
import type {
ClaudeAccountSummary,
CodexAccountSummary,
FastModeSnapshot,
ModelConfigSnapshot,
} from './settings-store.js';
function jsonResponse(value: unknown, init?: ResponseInit): Response {
return new Response(JSON.stringify(value), {
...init,
headers: {
'content-type': 'application/json; charset=utf-8',
...(init?.headers as Record<string, string> | undefined),
},
});
}
function request(
pathname: string,
method = 'GET',
body?: Record<string, unknown>,
): Request {
return new Request(`http://localhost${pathname}`, {
method,
body: body === undefined ? undefined : JSON.stringify(body),
headers:
body === undefined ? undefined : { 'content-type': 'application/json' },
});
}
const modelConfig: ModelConfigSnapshot = {
owner: { model: 'gpt-5.4', effort: 'medium' },
reviewer: { model: 'claude-sonnet', effort: 'high' },
arbiter: { model: 'gpt-5.4', effort: 'high' },
};
const fastMode: FastModeSnapshot = { codex: true, claude: false };
function makeClaudeAccount(): ClaudeAccountSummary {
return {
index: 0,
expiresAt: 1777348544000,
scopes: ['openid'],
exists: true,
};
}
function makeCodexAccount(): CodexAccountSummary {
return {
index: 1,
accountId: 'acct_1',
email: 'codex@example.com',
planType: 'pro',
subscriptionUntil: null,
subscriptionLastChecked: '2026-04-28T08:00:00.000Z',
exists: true,
};
}
function makeDeps(
overrides: Partial<SettingsRouteDependencies> = {},
): SettingsRouteDependencies {
return {
addClaudeAccountFromToken: () => ({ index: 2, accountId: null }),
getActiveCodexSettingsIndex: () => 1,
getFastMode: () => fastMode,
getModelConfig: () => modelConfig,
listClaudeAccounts: () => [makeClaudeAccount()],
listCodexAccounts: () => [makeCodexAccount()],
refreshAllCodexAccounts: async () => ({ refreshed: [1], failed: [] }),
refreshCodexAccount: async () => makeCodexAccount(),
removeAccountDirectory: () => undefined,
setActiveCodexSettingsIndex: () => undefined,
updateFastMode: () => fastMode,
updateModelConfig: () => modelConfig,
...overrides,
};
}
async function route(
pathname: string,
method = 'GET',
body?: Record<string, unknown>,
deps: SettingsRouteDependencies = makeDeps(),
): Promise<Response | null> {
return handleSettingsRoute({
url: new URL(`http://localhost${pathname}`),
request: request(pathname, method, body),
jsonResponse,
deps,
});
}
describe('web dashboard settings routes', () => {
it('serves settings snapshots without touching the server router', async () => {
const accounts = await route('/api/settings/accounts');
expect(accounts?.status).toBe(200);
await expect(accounts?.json()).resolves.toMatchObject({
claude: [{ index: 0, exists: true }],
codex: [{ index: 1, email: 'codex@example.com' }],
codexCurrentIndex: 1,
});
const models = await route('/api/settings/models');
expect(models?.status).toBe(200);
await expect(models?.json()).resolves.toEqual(modelConfig);
const mode = await route('/api/settings/fast-mode');
expect(mode?.status).toBe(200);
await expect(mode?.json()).resolves.toEqual(fastMode);
const unmatched = await route('/api/overview');
expect(unmatched).toBeNull();
});
it('handles settings mutations through injected dependencies', async () => {
const calls: string[] = [];
const deps = makeDeps({
addClaudeAccountFromToken: (token) => {
calls.push(`add:${token}`);
return { index: 3, accountId: null };
},
removeAccountDirectory: (provider, index) => {
calls.push(`delete:${provider}:${index}`);
},
setActiveCodexSettingsIndex: (index) => {
calls.push(`current:${index}`);
},
updateModelConfig: (input) => {
calls.push(`models:${JSON.stringify(input)}`);
return modelConfig;
},
});
const modelUpdate = await route(
'/api/settings/models',
'PATCH',
{ owner: { model: 'gpt-5.5' } },
deps,
);
expect(modelUpdate?.status).toBe(200);
const add = await route(
'/api/settings/accounts/claude',
'POST',
{ token: ' claude-token ' },
deps,
);
expect(add?.status).toBe(200);
await expect(add?.json()).resolves.toMatchObject({ ok: true, index: 3 });
const del = await route(
'/api/settings/accounts/codex/4',
'DELETE',
undefined,
deps,
);
expect(del?.status).toBe(200);
const current = await route(
'/api/settings/accounts/codex/current',
'PUT',
{ index: 4 },
deps,
);
expect(current?.status).toBe(200);
await expect(current?.json()).resolves.toMatchObject({
ok: true,
codexCurrentIndex: 1,
});
expect(calls).toEqual([
'models:{"owner":{"model":"gpt-5.5"}}',
'add:claude-token',
'delete:codex:4',
'current:4',
]);
});
});