Files
Codeman/test/doctor-settings.browser.test.ts
T
Codeman maintainer cf26853390 fix(doctor): Diagnostics landing fixes (#536)
- The doctor now judges candidates like the run mode's resolver: the PATH
  hit, then each search dir, each one version-checked on its own and
  skipped on a mismatch (a wrong `pi`/`grok` on the PATH no longer hides
  the real one in a search dir). A search-dir candidate must be an
  absolute path to an executable regular file, so a relative dir or a
  file without the x bit reads as missing, as it does in the Run menu.
  `isExecutableRegularFile` is exported from cli-executable-resolver.ts
  and reused rather than copied.
- Every doctor probe passes killSignal: 'SIGKILL'; a --version that
  ignores SIGTERM held the probe for its full runtime (15 s vs 5 s
  measured with a TERM-trapping script).
- README no longer claims parity with the Run menu or nvm prefixes.
- The Diagnostics panel marks a missing optional tool with ○, a missing
  required one with ✗, as the terminal doctor does.
- expandSearchDir names its twin, expandHome() in cli-resolver.ts.
- test/doctor-cli-json.test.ts is hermetic: temp HOME, a PATH of only
  `which` and `node`, and a clis.json that drops the registry's absolute
  search dirs, so it never runs the machine's installed agent CLIs.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 19:51:59 +02:00

111 lines
4.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/** @fileoverview Settings → System → Diagnostics in a real browser, with GET /api/doctor stubbed at the network layer. */
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { chromium, type Browser, type Page } from 'playwright';
import { WebServer } from '../src/web/server.js';
const PORT = 3196;
const REPORT = {
platform: { environment: 'linux' },
summary: { ok: 1, requiredMissing: 1, optionalMissing: 0, exitCode: 1 },
tools: [
{
id: 'node',
label: 'Node.js',
category: 'core',
required: true,
usedBy: [],
status: 'ok',
version: '22.1.0',
path: '/usr/bin/node',
},
{
id: 'tmux',
label: 'tmux',
category: 'core',
required: true,
usedBy: [],
status: 'missing',
installHint: 'apt install tmux',
},
// Host-supplied strings must be rendered as text, never as markup.
{
id: 'x',
label: '<img src=x onerror=window.__pwned=1>',
category: 'other',
required: false,
usedBy: [],
status: 'missing',
},
],
};
describe('Diagnostics panel in a real browser', () => {
let server: WebServer;
let browser: Browser;
let page: Page;
beforeAll(async () => {
server = new WebServer(PORT, false, true);
await server.start();
browser = await chromium.launch({ headless: true });
// A controlling service worker can swallow requests before page.route() sees them, letting the
// real /api/doctor (a forked Node process) answer instead; block it so the stub is reliable.
page = await (await browser.newContext({ serviceWorkers: 'block' })).newPage();
await page.goto(`http://localhost:${PORT}`, { waitUntil: 'domcontentloaded' });
await page.waitForFunction(() => (window as any).app?.terminal, null, { timeout: 30000 });
await page.evaluate(() => (window as any).app.openAppSettings());
}, 90000);
afterAll(async () => {
if (browser) await browser.close();
if (server) await server.stop();
}, 60000);
it('lists each tool with status, version, path and install hint, and renders host strings as text', async () => {
await page.route('**/api/doctor', (route) =>
route.fulfill({ contentType: 'application/json', body: JSON.stringify({ success: true, data: REPORT }) })
);
await page.click('#doctorRunBtn');
await page.waitForFunction(() => /1 ok/.test(document.getElementById('doctorResult')?.textContent ?? ''));
const text = await page.textContent('#doctorResult');
expect(text).toContain('1 ok · 1 required missing · 0 optional missing (linux)');
expect(text).toContain('✓ Node.js ok · 22.1.0');
expect(text).toContain('/usr/bin/node');
expect(text).toContain('✗ tmux missing · required');
// A missing OPTIONAL tool is not an error: ○, as the terminal doctor marks it.
expect(text).toContain('○ <img src=x onerror=window.__pwned=1> missing · optional');
expect(text).toContain('Install: apt install tmux');
expect(text).toContain('<img src=x onerror=window.__pwned=1>'); // shown literally
expect(await page.evaluate(() => (window as any).__pwned)).toBeUndefined();
expect(await page.$('#doctorResult img')).toBeNull();
expect(await page.isDisabled('#doctorRunBtn')).toBe(false);
});
it('shows the server’s message when the check fails, and re-enables the button', async () => {
await page.unroute('**/api/doctor');
await page.route('**/api/doctor', (route) =>
route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({ success: false, errorCode: 'OPERATION_FAILED', error: 'doctor failed: boom' }),
})
);
await page.click('#doctorRunBtn');
await page.waitForFunction(() => /boom/.test(document.getElementById('doctorResult')?.textContent ?? ''));
expect(await page.isDisabled('#doctorRunBtn')).toBe(false);
});
it('hides the Diagnostics group from a non-admin in multi-user mode and shows it to an admin', async () => {
const visible = (user: Record<string, unknown>) =>
page.evaluate((u) => {
(window as any).__codemanUser = u;
document.dispatchEvent(new CustomEvent('codeman:me'));
return getComputedStyle(document.getElementById('doctorGroup')!).display !== 'none';
}, user);
expect(await visible({ multiUser: true, role: 'user' })).toBe(false);
expect(await visible({ multiUser: true, role: 'admin' })).toBe(true);
expect(await visible({ multiUser: false })).toBe(true);
});
});