fix(doctor): resolve CLIs via searchDirs, single-flight runs, admin-gate the group (#536 review)

- doctor probes each CLI's discovery.searchDirs when which misses and runs --version on the resolved path, so a service with a minimal PATH no longer reports installed CLIs as missing
- GET /api/doctor shares one in-flight run per category
- Diagnostics group hidden from non-admins in multi-user mode (_applyDoctorAdminGate)
- 500 uses INTERNAL_ERROR; a killed child reports 'timed out after 30 s'
- browser test blocks service workers so page.route() is reliable
- wiki: Diagnostics sentence

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JrzFKEdBLwVfu6ev2ZscJS
This commit is contained in:
Devvyn
2026-10-05 09:11:10 +08:00
co-authored by Claude Sonnet 5.5
parent 1b89d7a387
commit db9a39405b
8 changed files with 143 additions and 9 deletions
+48 -1
View File
@@ -1,4 +1,4 @@
import { describe, it, expect } from 'vitest';
import { describe, it, expect, vi } from 'vitest';
import { dependencyRegistry } from '../src/config/dependency-registry.js';
import {
detectEnvironment,
@@ -259,6 +259,53 @@ describe('checkTool with requireVersionMatch (generic binary names)', () => {
});
});
describe('checkTool with searchDirs (service PATH is minimal)', () => {
const claudeLike: ToolDependency = {
...tmuxTool,
id: 'claude',
label: 'Claude CLI',
resolvers: [
{
match: ['linux'],
resolver: { kind: 'path', bins: ['claude'], searchDirs: ['/home/u/.local/bin', '/opt/npm/bin/'] },
},
],
};
it('finds a CLI that only lives in a searchDirs entry and runs --version on the absolute path', () => {
const runVersion = vi.fn(() => 'claude 2.1.0');
const host = fakeHost('linux', { fileExists: (p) => p === '/opt/npm/bin/claude', runVersion });
expect(checkTool(claudeLike, host)).toMatchObject({
status: 'ok',
path: '/opt/npm/bin/claude',
version: '2.1.0',
});
expect(runVersion).toHaveBeenCalledWith('/opt/npm/bin/claude', ['--version']);
});
it('still reports missing when neither PATH nor any search dir has it', () => {
expect(checkTool(claudeLike, fakeHost('linux'))).toMatchObject({ status: 'missing' });
});
it('prefers the PATH hit over a search dir', () => {
const host = fakeHost('linux', {
which: () => '/usr/bin/claude',
fileExists: () => true,
runVersion: () => '1.0.0',
});
expect(checkTool(claudeLike, host)).toMatchObject({ path: '/usr/bin/claude' });
});
it('carries each enabled CLI’s expanded discovery.searchDirs onto its registry row', () => {
const rows = dependencyRegistry().flatMap((t) => t.resolvers.map((r) => r.resolver));
const withDirs = rows.filter((r) => r.kind === 'path' && r.searchDirs?.length);
expect(withDirs.length).toBeGreaterThan(0);
for (const r of withDirs) {
if (r.kind === 'path') for (const d of r.searchDirs ?? []) expect(d.startsWith('~')).toBe(false);
}
});
});
describe('checkAll', () => {
it('maps every tool to a result', () => {
const results = checkAll([tmuxTool, msTool], fakeHost('linux'));