mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
Three of Ark0N's four "will take at merge" items, applied instead since they were straightforward to do properly: 1. test/frontend-cli-no-id-branching.test.ts's ALLOWED_BRANCHES keyed on <file>::<expression> (fixed last round) closed the line-shift problem but opened a new one: every stock id was already allowlisted for session-ui.js in the `mode === '<id>'` form, so a BRAND NEW branch reusing that exact expression anywhere in the file passed unnoticed. Reproduced live (`if (this.mode === 'codex')` injected into runOpenCode()) — stayed green under the old version. Each allowlist entry now carries the exact count of approved call sites, and a new test asserts actual-vs-declared count for every key; a mismatch in either direction is real (higher = new unreviewed branch riding in on an existing approval, lower = a reviewed site was removed and the entry is now stale). Reproduced again against the fix: same injection now fails with an exact diagnostic (expected 2, found 3). 2. Added test/run-mode-launch-table-drift.test.ts. RUN_MODE_LAUNCH restates four things stock.ts already owns (label, install command, supportsCustomModel, the external-mode key set), and they agree today with nothing enforcing it. supportsCustomModel is the dangerous one: the Run-menu picker's rows come from the server-injected window.__codemanCustomModelClis (built from capabilities.customModelInjection.kind), so a CLI gaining a real injection recipe later would be OFFERED in the picker while _runCliMode silently drops the customModel field for it — the session launches on the vendor's cloud while the UI claims the local endpoint. Drives the real session-ui.js via JSDOM and compares RUN_MODE_LAUNCH against STOCK_CLIS on all four axes. 3. Inlined the "Open Question 7 in PR-B2.md" references in the allowlist reasons — PR-B2.md is a local planning doc, never part of the committed tree, so the reference was dead on arrival for anyone reading the repo. Points at the PR #458 review thread instead. 4. Added a sentence to docs/cli-registry.md naming the new frontend guard alongside the backend one it mirrors. Full gate: 406 files / 7721 tests / 0 failures, typecheck/lint/format/ check:frontend-syntax all clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GuHtuPiHXdykq9T6rKQJ9n
92 lines
4.6 KiB
TypeScript
92 lines
4.6 KiB
TypeScript
/**
|
|
* @fileoverview `RUN_MODE_LAUNCH` (session-ui.js, PR B2) restates four things
|
|
* `stock.ts` already owns: label, an install command, whether the CLI
|
|
* supports a custom-model launch, and the external-mode key set itself.
|
|
* They agree today, but nothing enforced it — the dangerous drift is
|
|
* `supportsCustomModel`: the Run menu's "CLI (endpoint)" rows come from the
|
|
* server-injected `window.__codemanCustomModelClis` (built from
|
|
* `capabilities.customModelInjection.kind !== 'unsupported'`), so a CLI that
|
|
* gains a real injection recipe later would be OFFERED in that menu while
|
|
* `_runCliMode` still drops the `customModel` field for it — the session
|
|
* launches on the vendor's cloud while the UI claims the local endpoint.
|
|
*
|
|
* Drives the REAL session-ui.js via JSDOM (`runScripts: 'dangerously'`, same
|
|
* approach as test/custom-model-one-shot-launch.test.ts), extracting the
|
|
* module-level `RUN_MODE_LAUNCH` const by appending one assignment line to
|
|
* the SAME source string before the one `eval()` call — it is not attached
|
|
* to `window` on its own (top-level `const` lives in the script's own
|
|
* lexical scope, not the global object), and a SEPARATE later `eval()` call
|
|
* cannot see an earlier call's top-level bindings either (measured: each
|
|
* `window.eval()` invocation gets its own top-level lexical environment in
|
|
* jsdom), so the assignment has to ride in the same evaluated string.
|
|
*
|
|
* Port: none.
|
|
*/
|
|
import { readFileSync } from 'node:fs';
|
|
import { JSDOM } from 'jsdom';
|
|
import { describe, expect, it } from 'vitest';
|
|
import { STOCK_CLIS } from '../src/config/cli-registry/stock.js';
|
|
|
|
const SESSION_UI_JS = readFileSync(new URL('../src/web/public/session-ui.js', import.meta.url), 'utf-8');
|
|
|
|
interface RunModeLaunchEntry {
|
|
label: string;
|
|
installHint: string;
|
|
supportsCustomModel: boolean;
|
|
buildConfig: (globalSettings: Record<string, unknown>) => Record<string, unknown> | null;
|
|
}
|
|
|
|
function loadRunModeLaunch(): Record<string, RunModeLaunchEntry> {
|
|
const dom = new JSDOM('<!doctype html><body></body>', { url: 'http://localhost/', runScripts: 'dangerously' });
|
|
const win = dom.window as unknown as Window & typeof globalThis & { CodemanApp: new () => unknown };
|
|
(win as unknown as { eval: (s: string) => void }).eval('window.CodemanApp = function CodemanApp() {};');
|
|
// The assignment MUST be part of the same evaluated string as
|
|
// SESSION_UI_JS — RUN_MODE_LAUNCH is a bare top-level `const`, so it only
|
|
// exists in the lexical scope of THIS eval call.
|
|
(win as unknown as { eval: (s: string) => void }).eval(
|
|
`${SESSION_UI_JS}\nwindow.__TEST_RUN_MODE_LAUNCH = RUN_MODE_LAUNCH;`
|
|
);
|
|
return (win as unknown as { __TEST_RUN_MODE_LAUNCH: Record<string, RunModeLaunchEntry> }).__TEST_RUN_MODE_LAUNCH;
|
|
}
|
|
|
|
describe('RUN_MODE_LAUNCH (session-ui.js) stays in step with stock.ts', () => {
|
|
const runModeLaunch = loadRunModeLaunch();
|
|
const byId = new Map(STOCK_CLIS.map((e) => [e.id as string, e]));
|
|
|
|
it('covers exactly the non-claude, non-shell stock CLIs — no more, no fewer', () => {
|
|
const expectedIds = STOCK_CLIS.map((e) => e.id as string)
|
|
.filter((id) => id !== 'claude' && id !== 'shell')
|
|
.sort();
|
|
expect(Object.keys(runModeLaunch).sort()).toEqual(expectedIds);
|
|
});
|
|
|
|
it('label matches CliEntry.label for every entry', () => {
|
|
for (const [id, entry] of Object.entries(runModeLaunch)) {
|
|
const stockEntry = byId.get(id);
|
|
expect(stockEntry, `no stock entry for ${id}`).toBeTruthy();
|
|
expect(entry.label, `${id} label drifted from stock.ts`).toBe(stockEntry!.label);
|
|
}
|
|
});
|
|
|
|
it('installHint embeds the real linux install command', () => {
|
|
for (const [id, entry] of Object.entries(runModeLaunch)) {
|
|
const command = byId.get(id)!.discovery.install.command?.linux;
|
|
if (!command) continue; // shell-less entries (none today) carry no command to check
|
|
expect(entry.installHint, `${id} installHint no longer matches stock.ts's linux install command`).toContain(
|
|
command
|
|
);
|
|
}
|
|
});
|
|
|
|
it('supportsCustomModel matches capabilities.customModelInjection.kind !== "unsupported"', () => {
|
|
// This is the one that fails SILENTLY if it drifts (see file header):
|
|
// window.__codemanCustomModelClis (server.ts) is built from this same
|
|
// stock.ts field, so a mismatch here means the Run-menu picker and the
|
|
// actual launch body disagree about which CLIs are custom-model-capable.
|
|
for (const [id, entry] of Object.entries(runModeLaunch)) {
|
|
const supported = byId.get(id)!.capabilities.customModelInjection.kind !== 'unsupported';
|
|
expect(entry.supportsCustomModel, `${id}.supportsCustomModel drifted from stock.ts's capability`).toBe(supported);
|
|
}
|
|
});
|
|
});
|