From 4152ee10159c197799152a909c6f3dbbb040218e Mon Sep 17 00:00:00 2001 From: Devvyn <22340871+opticon454@users.noreply.github.com> Date: Mon, 5 Oct 2026 09:56:10 +0800 Subject: [PATCH] test(doctor): minimal-PATH searchDirs regression and the non-admin gate Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01JrzFKEdBLwVfu6ev2ZscJS --- test/doctor-cli-json.test.ts | 43 +++++++++++++++++++++++++++- test/doctor-settings.browser.test.ts | 12 ++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/test/doctor-cli-json.test.ts b/test/doctor-cli-json.test.ts index 72d6aa78..64c04881 100644 --- a/test/doctor-cli-json.test.ts +++ b/test/doctor-cli-json.test.ts @@ -3,7 +3,9 @@ // `doctor --json`, prints a parseable DependencyReportJson on stdout, even when it exits // non-zero because something required is missing. -import { execFile } from 'node:child_process'; +import { execFile, execFileSync } from 'node:child_process'; +import { chmodSync, mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { describe, expect, it } from 'vitest'; @@ -33,4 +35,43 @@ describe('codeman doctor --json', () => { expect(node?.status).toBe('ok'); expect(report.tools.every((t: { category: string }) => t.category === 'core')).toBe(true); }, 90_000); + + // The report an operator got wrong in production: under systemd the PATH is minimal, so a CLI + // installed in ~/.local/bin read `missing` while the Run menu (which also searches the registry's + // searchDirs) found it. The PATH here holds nothing but `which`. + it('finds a CLI that lives only in a registry searchDirs entry when the PATH is minimal', async () => { + const home = mkdtempSync(join(tmpdir(), 'doctor-home-')); + const bare = mkdtempSync(join(tmpdir(), 'doctor-path-')); + try { + mkdirSync(join(home, '.local/bin'), { recursive: true }); + const fake = join(home, '.local/bin/claude'); + writeFileSync(fake, '#!/bin/sh\necho "2.1.0 (Claude Code)"\n'); + chmodSync(fake, 0o755); + symlinkSync(execFileSyncWhich(), join(bare, 'which')); + const stdout = await new Promise((resolve, reject) => { + execFile( + process.execPath, + [ + join(ROOT, 'node_modules/tsx/dist/cli.mjs'), + join(ROOT, 'src/index.ts'), + 'doctor', + '--json', + '--category', + 'core', + ], + { timeout: 60_000, cwd: ROOT, env: { ...process.env, HOME: home, PATH: bare } }, + (err, out) => (out ? resolve(out) : reject(err ?? new Error('no output'))) + ); + }); + const claude = JSON.parse(stdout).tools.find((t: { id: string }) => t.id === 'claude'); + expect(claude).toMatchObject({ status: 'ok', path: fake }); + } finally { + rmSync(home, { recursive: true, force: true }); + rmSync(bare, { recursive: true, force: true }); + } + }, 90_000); }); + +function execFileSyncWhich(): string { + return execFileSync('sh', ['-c', 'command -v which'], { encoding: 'utf-8' }).trim(); +} diff --git a/test/doctor-settings.browser.test.ts b/test/doctor-settings.browser.test.ts index df7d6e6e..e1dc8ff8 100644 --- a/test/doctor-settings.browser.test.ts +++ b/test/doctor-settings.browser.test.ts @@ -93,4 +93,16 @@ describe('Diagnostics panel in a real browser', () => { 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) => + 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); + }); });