@robota-sdk/agent-command 3.0.0-beta.79 → 3.0.0-beta.82
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -2
- package/dist/node/index.cjs +77 -38
- package/dist/node/index.d.cts +946 -0
- package/dist/node/index.d.cts.map +1 -0
- package/dist/node/index.d.ts +564 -51
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.js +77 -38
- package/dist/node/index.js.map +1 -1
- package/package.json +39 -16
- package/src/advisor/advisor-command-module.ts +47 -0
- package/src/advisor/advisor-command.test.ts +102 -0
- package/src/advisor/advisor-command.ts +55 -0
- package/src/advisor/advisor-cost-functional.test.ts +92 -0
- package/src/advisor/index.ts +6 -0
- package/src/agent/__tests__/agent-command.test.ts +39 -8
- package/src/agent/agent-command-module.ts +14 -7
- package/src/agent/agent-command-parser.ts +1 -1
- package/src/agent/agent-command.ts +3 -3
- package/src/background/__tests__/background-command-module.test.ts +59 -52
- package/src/background/background-command-module.ts +2 -1
- package/src/background/background-command.ts +3 -3
- package/src/command-module-utils.ts +33 -0
- package/src/compact/__tests__/compact-command-module.test.ts +31 -23
- package/src/compact/compact-command-module.ts +10 -1
- package/src/compact/compact-command.ts +6 -3
- package/src/context/__tests__/context-command-module.test.ts +214 -63
- package/src/context/auto-compact-format.ts +25 -0
- package/src/context/context-breakdown.ts +295 -0
- package/src/context/context-command-module.ts +45 -14
- package/src/context/context-command.ts +34 -257
- package/src/default/__tests__/__snapshots__/model-exposure.test.ts.snap +110 -0
- package/src/default/__tests__/default-command-modules.test.ts +59 -7
- package/src/default/__tests__/model-exposure.test.ts +241 -0
- package/src/default/__tests__/org-policy-forwarding.test.ts +98 -0
- package/src/default/default-command-modules.ts +130 -18
- package/src/default/index.ts +4 -1
- package/src/devices/__tests__/devices-command-module.test.ts +212 -0
- package/src/devices/devices-command-module.ts +224 -0
- package/src/devices/devices-command-port.ts +82 -0
- package/src/devices/index.ts +11 -0
- package/src/doctor/__tests__/doctor-command-module.test.ts +145 -0
- package/src/doctor/__tests__/doctor-display-vocabulary.test.ts +38 -0
- package/src/doctor/__tests__/doctor-fixture.ts +145 -0
- package/src/doctor/__tests__/doctor-redaction.test.ts +29 -0
- package/src/doctor/__tests__/doctor-runner.test.ts +464 -0
- package/src/doctor/doctor-command-module.ts +132 -0
- package/src/doctor/doctor-extensions-probe.ts +299 -0
- package/src/doctor/doctor-node-deps.ts +89 -0
- package/src/doctor/doctor-redaction.ts +99 -0
- package/src/doctor/doctor-render.ts +70 -0
- package/src/doctor/doctor-repair.ts +136 -0
- package/src/doctor/doctor-runner.ts +122 -0
- package/src/doctor/doctor-settings-probe.ts +239 -0
- package/src/doctor/doctor-storage-probe.ts +187 -0
- package/src/doctor/doctor-types.ts +109 -0
- package/src/doctor/index.ts +32 -0
- package/src/editor/__tests__/editor-command-functional.test.ts +49 -16
- package/src/editor/editor-command-module.ts +6 -5
- package/src/editor/editor-command.ts +10 -5
- package/src/effort/effort-command-module.ts +51 -0
- package/src/effort/effort-command.test.ts +140 -0
- package/src/effort/effort-command.ts +122 -0
- package/src/effort/index.ts +6 -0
- package/src/exit/__tests__/exit-command-module.test.ts +16 -9
- package/src/exit/exit-command-module.ts +2 -1
- package/src/exit/exit-command.ts +6 -6
- package/src/fork/__tests__/fork-command.test.ts +184 -0
- package/src/fork/fork-command-module.ts +61 -0
- package/src/fork/fork-command.ts +162 -0
- package/src/fork/index.ts +8 -0
- package/src/git/__tests__/fake-git-port.ts +28 -0
- package/src/git/__tests__/git-command-module.test.ts +103 -0
- package/src/git/__tests__/git-commit.test.ts +185 -0
- package/src/git/__tests__/git-diff.test.ts +125 -0
- package/src/git/__tests__/git-process.test.ts +221 -0
- package/src/git/__tests__/git-status.test.ts +117 -0
- package/src/git/git-command-module.ts +139 -0
- package/src/git/git-commit.ts +251 -0
- package/src/git/git-diff.ts +129 -0
- package/src/git/git-process.ts +169 -0
- package/src/git/git-status.ts +134 -0
- package/src/git/index.ts +17 -0
- package/src/goal/__tests__/goal-command.test.ts +9 -4
- package/src/goal/goal-command-module.ts +2 -1
- package/src/goal/goal-command.ts +6 -9
- package/src/handoff/__tests__/handoff-command-module.test.ts +24 -0
- package/src/handoff/__tests__/handoff-command.test.ts +218 -0
- package/src/handoff/handoff-command-module.ts +49 -0
- package/src/handoff/handoff-command.ts +187 -0
- package/src/handoff/index.ts +6 -0
- package/src/help/__tests__/help-command-module.test.ts +63 -40
- package/src/help/__tests__/help-command.test.ts +46 -37
- package/src/help/help-command-module.ts +2 -1
- package/src/help/help-command.ts +3 -3
- package/src/index.ts +124 -2
- package/src/keybindings/__tests__/keybindings-command-module.test.ts +57 -0
- package/src/keybindings/index.ts +6 -0
- package/src/keybindings/keybindings-command-module.ts +74 -0
- package/src/language/__tests__/language-command-module.test.ts +33 -28
- package/src/language/language-command-module.ts +2 -1
- package/src/language/language-command.ts +8 -6
- package/src/mcp-activation/__tests__/mcp-activation-command.test.ts +630 -0
- package/src/mcp-activation/__tests__/mcp-model-view.test.ts +211 -0
- package/src/mcp-activation/index.ts +12 -0
- package/src/mcp-activation/mcp-activation-command-module.ts +92 -0
- package/src/mcp-activation/mcp-activation-command.ts +448 -0
- package/src/mcp-activation/mcp-model-notice.ts +85 -0
- package/src/mcp-activation/mcp-model-status.ts +99 -0
- package/src/memory/__tests__/memory-command-module.test.ts +167 -74
- package/src/memory/memory-command-module.ts +18 -1
- package/src/memory/memory-command.ts +38 -38
- package/src/mode/__tests__/mode-command-module.test.ts +51 -38
- package/src/mode/mode-command-module.ts +2 -1
- package/src/mode/mode-command.ts +15 -7
- package/src/output-style/__tests__/output-style-command.test.ts +96 -0
- package/src/output-style/index.ts +6 -0
- package/src/output-style/output-style-command-module.ts +143 -0
- package/src/peers/__tests__/peers-command.test.ts +321 -0
- package/src/peers/index.ts +9 -0
- package/src/peers/peers-command-module.ts +49 -0
- package/src/peers/peers-command.ts +191 -0
- package/src/permissions/__tests__/permissions-command-module.test.ts +149 -37
- package/src/permissions/permissions-command-module.ts +2 -1
- package/src/permissions/permissions-command.ts +46 -4
- package/src/plan/__tests__/plan-command.test.ts +129 -0
- package/src/plan/index.ts +9 -0
- package/src/plan/plan-command-module.ts +49 -0
- package/src/plan/plan-command.ts +92 -0
- package/src/plugin/__tests__/plugin-command-module.test.ts +46 -40
- package/src/plugin/__tests__/plugin-host-boundary.test.ts +11 -0
- package/src/plugin/plugin-command-module.ts +3 -1
- package/src/plugin/plugin-command.ts +17 -16
- package/src/preset/__tests__/host-supplied-preset-registry.test.ts +98 -0
- package/src/preset/__tests__/preset-command-module.test.ts +102 -56
- package/src/preset/preset-command-module.ts +26 -8
- package/src/preset/preset-command.ts +85 -27
- package/src/provider/__tests__/org-policy.test.ts +4 -5
- package/src/provider/__tests__/provider-command-module.test.ts +11 -14
- package/src/provider/__tests__/provider-startup-host-settings.test.ts +16 -0
- package/src/provider/__tests__/scripted-interaction.ts +7 -5
- package/src/provider/provider-command-execution.ts +4 -4
- package/src/provider/provider-command-module.ts +2 -1
- package/src/provider/provider-command-profile-lifecycle.ts +4 -4
- package/src/provider/provider-command-profile-operations.ts +3 -3
- package/src/provider/provider-command-profile.ts +1 -1
- package/src/provider/provider-command-setup.ts +4 -4
- package/src/provider/provider-startup.ts +62 -35
- package/src/remote-control/__tests__/remote-control-command-module.test.ts +36 -0
- package/src/remote-control/__tests__/remote-control-command.test.ts +216 -0
- package/src/remote-control/index.ts +8 -0
- package/src/remote-control/remote-control-command-module.ts +61 -0
- package/src/remote-control/remote-control-command.ts +138 -0
- package/src/reset/__tests__/reset-command-module.test.ts +5 -5
- package/src/reset/reset-command-module.ts +2 -1
- package/src/reset/reset-command.ts +3 -4
- package/src/rewind/__tests__/rewind-command-module.test.ts +40 -0
- package/src/rewind/rewind-command-module.ts +2 -1
- package/src/rewind/rewind-command.ts +73 -6
- package/src/sandbox/__tests__/sandbox-command.test.ts +80 -0
- package/src/sandbox/index.ts +2 -0
- package/src/sandbox/sandbox-command-module.ts +56 -0
- package/src/sandbox/sandbox-command.ts +94 -0
- package/src/schedule/__tests__/loop-command-functional.test.ts +386 -0
- package/src/schedule/__tests__/loop-command.test.ts +360 -0
- package/src/schedule/__tests__/loop-jitter.test.ts +25 -0
- package/src/schedule/__tests__/monitor-model-permission-functional.test.ts +126 -0
- package/src/schedule/__tests__/schedule-command.test.ts +143 -3
- package/src/schedule/__tests__/schedule-redos.test.ts +81 -0
- package/src/schedule/index.ts +3 -0
- package/src/schedule/loop-command.ts +333 -0
- package/src/schedule/loop-jitter.ts +41 -0
- package/src/schedule/schedule-command-module.ts +92 -14
- package/src/schedule/schedule-command.ts +98 -9
- package/src/schedule/schedule-spec-parser.ts +5 -1
- package/src/session/__tests__/rename-host-persistence.test.ts +72 -0
- package/src/session/__tests__/session-command-module.test.ts +210 -45
- package/src/session/session-command-module.ts +68 -37
- package/src/session/session-command.ts +147 -80
- package/src/settings/settings-command-module.ts +3 -2
- package/src/shell/__tests__/resolve-shell-host.test.ts +11 -0
- package/src/shell/__tests__/shell-command-functional.test.ts +23 -1
- package/src/shell/resolve-shell.ts +2 -2
- package/src/shell/shell-command-module.ts +6 -5
- package/src/shell/shell-command.ts +9 -5
- package/src/skills/__tests__/skills-command-module.test.ts +69 -28
- package/src/skills/index.ts +1 -0
- package/src/skills/skills-command-module.ts +22 -5
- package/src/skills/skills-command.ts +14 -13
- package/src/statusline/__tests__/statusline-command-module.test.ts +5 -4
- package/src/statusline/statusline-command-module.ts +2 -1
- package/src/statusline/statusline-command.ts +4 -4
- package/src/theme/__tests__/theme-command.test.ts +204 -0
- package/src/theme/index.ts +12 -0
- package/src/theme/theme-command-module.ts +211 -0
- package/src/user-local/__tests__/user-local-command.test.ts +145 -110
- package/src/user-local/user-local-command-constants.ts +1 -1
- package/src/user-local/user-local-command-module.ts +6 -5
- package/src/user-local/user-local-command.ts +12 -6
- package/src/user-local/user-local-memory-command.ts +17 -7
- package/src/plugins/default-plugin-command-adapter.ts +0 -164
- package/src/plugins/default-plugin-command-source-loader.ts +0 -31
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { renderDoctorReport } from '../doctor-render.js';
|
|
4
|
+
|
|
5
|
+
import type { IDoctorReport } from '../doctor-types.js';
|
|
6
|
+
|
|
7
|
+
const REPORT: IDoctorReport = {
|
|
8
|
+
checks: [{ id: 'storage.user', label: 'Storage', status: 'warn', repair: 'storage.user' }],
|
|
9
|
+
failCount: 0,
|
|
10
|
+
warnCount: 1,
|
|
11
|
+
repairable: ['storage.user'],
|
|
12
|
+
exitCode: 0,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
describe('doctor display vocabulary', () => {
|
|
16
|
+
it('uses neutral report and repair text without a host vocabulary', () => {
|
|
17
|
+
const text = renderDoctorReport(REPORT).join('\n');
|
|
18
|
+
expect(text).not.toMatch(/robota/i);
|
|
19
|
+
expect(text).not.toContain('--repair');
|
|
20
|
+
expect(text).toContain('storage.user');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('uses the host-selected title, product name, and repair command', () => {
|
|
24
|
+
const text = renderDoctorReport(REPORT, 'Atlas doctor', {
|
|
25
|
+
productName: 'Atlas',
|
|
26
|
+
formatRepairCommand: (id) => `atlas doctor --repair ${id}`,
|
|
27
|
+
repairOffer: 'run atlas doctor --repair <check-id>',
|
|
28
|
+
}).join('\n');
|
|
29
|
+
expect(text).toContain('Atlas doctor');
|
|
30
|
+
expect(text).toContain('atlas doctor --repair storage.user');
|
|
31
|
+
expect(text).toContain('Atlas may work');
|
|
32
|
+
expect(text).toContain('run atlas doctor --repair <check-id>');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('preserves the public string title argument', () => {
|
|
36
|
+
expect(renderDoctorReport(REPORT, 'Atlas doctor')).toContain('Atlas doctor');
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
createNodeHostContributionSource,
|
|
7
|
+
createNodeHostSettingsSource,
|
|
8
|
+
createRestrictedWorkspaceProjectAccess,
|
|
9
|
+
} from '@robota-sdk/agent-framework';
|
|
10
|
+
|
|
11
|
+
import { inspectPathFacts } from '../doctor-node-deps.js';
|
|
12
|
+
|
|
13
|
+
import type { IDoctorDeps, IDoctorInputs } from '../doctor-types.js';
|
|
14
|
+
import type { IProviderDefinition } from '@robota-sdk/agent-core';
|
|
15
|
+
|
|
16
|
+
export const MARKERS = {
|
|
17
|
+
env: 'sk-doctor-marker-env-1a2b3c',
|
|
18
|
+
profile: 'sk-doctor-marker-profile-4d5e6f',
|
|
19
|
+
mcp: 'sk-doctor-marker-mcp-9f8e7d',
|
|
20
|
+
adjacent: 'sk-doctor-marker-adjacent-7c8d9e',
|
|
21
|
+
} as const;
|
|
22
|
+
|
|
23
|
+
export interface IDoctorFixture {
|
|
24
|
+
readonly home: string;
|
|
25
|
+
readonly inputs: IDoctorInputs;
|
|
26
|
+
readonly deps: IDoctorDeps;
|
|
27
|
+
readonly probes: { host: string; port: number }[];
|
|
28
|
+
write(relative: string, content: string): string;
|
|
29
|
+
mkdir(relative: string, mode?: number): string;
|
|
30
|
+
cleanup(): void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** A provider definition whose SDK-embedded host is declared through `endpoint`, like anthropic. */
|
|
34
|
+
export function fixtureProviderDefinition(
|
|
35
|
+
overrides: Partial<IProviderDefinition> = {},
|
|
36
|
+
): IProviderDefinition {
|
|
37
|
+
return {
|
|
38
|
+
type: 'fixture',
|
|
39
|
+
displayName: 'Fixture',
|
|
40
|
+
defaults: { model: 'fixture-model', apiKey: '$ENV:ROBOTA_DOCTOR_FIXTURE_KEY' },
|
|
41
|
+
endpoint: { host: 'api.fixture.example', port: 443 },
|
|
42
|
+
createProvider: () => {
|
|
43
|
+
throw new Error('the doctor must never construct a provider');
|
|
44
|
+
},
|
|
45
|
+
...overrides,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface IDoctorFixtureOptions {
|
|
50
|
+
env?: Record<string, string | undefined>;
|
|
51
|
+
providerDefinitions?: readonly IProviderDefinition[];
|
|
52
|
+
reachable?: boolean;
|
|
53
|
+
resolvable?: readonly string[];
|
|
54
|
+
guarantee?: 'posix-mode' | 'windows-acl';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function fixtureDeps(
|
|
58
|
+
options: IDoctorFixtureOptions,
|
|
59
|
+
probes: { host: string; port: number }[],
|
|
60
|
+
): IDoctorDeps {
|
|
61
|
+
const resolvable = new Set(options.resolvable ?? []);
|
|
62
|
+
return {
|
|
63
|
+
probeEndpoint: async (host, port) => {
|
|
64
|
+
probes.push({ host, port });
|
|
65
|
+
return options.reachable === false
|
|
66
|
+
? { reachable: false, error: 'connect ECONNREFUSED' }
|
|
67
|
+
: { reachable: true, elapsedMs: 1 };
|
|
68
|
+
},
|
|
69
|
+
// Real filesystem facts, so the storage probe is exercised against the fixture HOME.
|
|
70
|
+
inspectPath: inspectPathFacts,
|
|
71
|
+
resolveCommand: (command) => resolvable.has(command.split(/\s+/)[0] ?? command),
|
|
72
|
+
ownerOnlyGuarantee: () => options.guarantee ?? 'posix-mode',
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function createDoctorFixture(options: IDoctorFixtureOptions = {}): IDoctorFixture {
|
|
77
|
+
const home = mkdtempSync(join(tmpdir(), 'robota-doctor-fixture-'));
|
|
78
|
+
const write = (relative: string, content: string): string => {
|
|
79
|
+
const path = join(home, relative);
|
|
80
|
+
mkdirSync(join(path, '..'), { recursive: true });
|
|
81
|
+
writeFileSync(path, content, 'utf8');
|
|
82
|
+
return path;
|
|
83
|
+
};
|
|
84
|
+
const mkdir = (relative: string, mode = 0o700): string => {
|
|
85
|
+
const path = join(home, relative);
|
|
86
|
+
mkdirSync(path, { recursive: true });
|
|
87
|
+
chmodSync(path, mode);
|
|
88
|
+
return path;
|
|
89
|
+
};
|
|
90
|
+
const probes: { host: string; port: number }[] = [];
|
|
91
|
+
const env = options.env ?? {};
|
|
92
|
+
const inputs: IDoctorInputs = {
|
|
93
|
+
cwd: home,
|
|
94
|
+
userHome: home,
|
|
95
|
+
userSettingsPath: join(home, '.robota', 'settings.json'),
|
|
96
|
+
userStorage: { root: join(home, '.robota'), sessions: join(home, '.robota', 'sessions') },
|
|
97
|
+
settingsSources: [
|
|
98
|
+
createNodeHostSettingsSource('user', join(home, '.robota', 'settings.json')),
|
|
99
|
+
createNodeHostSettingsSource('user', join(home, '.claude', 'settings.json')),
|
|
100
|
+
],
|
|
101
|
+
projectAccess: createRestrictedWorkspaceProjectAccess('untrusted', home),
|
|
102
|
+
providerDefinitions: options.providerDefinitions ?? [fixtureProviderDefinition()],
|
|
103
|
+
env,
|
|
104
|
+
contributionSources: [createNodeHostContributionSource(home)],
|
|
105
|
+
skillRoots: [{ root: join('.robota', 'skills'), kind: 'skills' }],
|
|
106
|
+
pluginsDirs: [join(home, '.robota', 'plugins')],
|
|
107
|
+
hostChecks: [
|
|
108
|
+
{ id: 'host.fixture', label: 'Fixture host check', status: 'ok', cause: 'present' },
|
|
109
|
+
],
|
|
110
|
+
};
|
|
111
|
+
const deps = fixtureDeps(options, probes);
|
|
112
|
+
return {
|
|
113
|
+
home,
|
|
114
|
+
inputs,
|
|
115
|
+
deps,
|
|
116
|
+
probes,
|
|
117
|
+
write,
|
|
118
|
+
mkdir,
|
|
119
|
+
cleanup: () => rmSync(home, { recursive: true, force: true }),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** The "broken HOME" of the built-CLI scenario, as a unit fixture. */
|
|
124
|
+
export function installBrokenHome(fixture: IDoctorFixture): void {
|
|
125
|
+
fixture.mkdir('.robota');
|
|
126
|
+
fixture.mkdir('.robota/sessions');
|
|
127
|
+
fixture.write('.robota/settings.json', '');
|
|
128
|
+
fixture.write('.claude/settings.json', '{"defaultTrustLevel":42}');
|
|
129
|
+
fixture.write(
|
|
130
|
+
'.robota/plugins/cache/fixture-market/broken-plugin/1.0.0/.claude-plugin/plugin.json',
|
|
131
|
+
'{',
|
|
132
|
+
);
|
|
133
|
+
fixture.write(
|
|
134
|
+
'.robota/plugins/cache/fixture-market/mcp-plugin/1.0.0/.claude-plugin/plugin.json',
|
|
135
|
+
JSON.stringify({ name: 'mcp-plugin', version: '1.0.0', description: 'doctor fixture' }),
|
|
136
|
+
);
|
|
137
|
+
fixture.write(
|
|
138
|
+
'.robota/plugins/cache/fixture-market/mcp-plugin/1.0.0/.mcp.json',
|
|
139
|
+
JSON.stringify({
|
|
140
|
+
mcpServers: {
|
|
141
|
+
ghost: { command: 'robota-doctor-missing-binary', env: { GHOST_TOKEN: MARKERS.mcp } },
|
|
142
|
+
},
|
|
143
|
+
}),
|
|
144
|
+
);
|
|
145
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { redactDiagnosticText } from '../doctor-redaction.js';
|
|
4
|
+
|
|
5
|
+
describe('doctor redaction', () => {
|
|
6
|
+
it('still masks URL userinfo', () => {
|
|
7
|
+
expect(redactDiagnosticText('see https://user:hunter22@example.com/x')).toBe(
|
|
8
|
+
'see https://[redacted]@example.com/x',
|
|
9
|
+
);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('stays linear on long word runs that never reach a URL scheme', () => {
|
|
13
|
+
const timeFor = (n: number): number => {
|
|
14
|
+
const text = `x-${'a-'.repeat(n / 2)}`;
|
|
15
|
+
let best = Number.POSITIVE_INFINITY;
|
|
16
|
+
for (let i = 0; i < 3; i += 1) {
|
|
17
|
+
const started = performance.now();
|
|
18
|
+
redactDiagnosticText(text);
|
|
19
|
+
best = Math.min(best, performance.now() - started);
|
|
20
|
+
}
|
|
21
|
+
return best;
|
|
22
|
+
};
|
|
23
|
+
const small = timeFor(16 * 1024);
|
|
24
|
+
const large = timeFor(64 * 1024);
|
|
25
|
+
// Linear growth is about 4x for 4x input; the old unbounded scheme run grew about 16x.
|
|
26
|
+
if (large >= 50) expect(large / Math.max(small, 1)).toBeLessThan(10);
|
|
27
|
+
expect(large).toBeLessThan(500);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
createNodeHostSettingsSource,
|
|
6
|
+
createRestrictedWorkspaceProjectAccess,
|
|
7
|
+
} from '@robota-sdk/agent-framework';
|
|
8
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
9
|
+
|
|
10
|
+
import { renderDoctorReport } from '../doctor-render.js';
|
|
11
|
+
import { applyDoctorRepair, planDoctorRepair } from '../doctor-repair.js';
|
|
12
|
+
import { runDoctor } from '../doctor-runner.js';
|
|
13
|
+
import {
|
|
14
|
+
MARKERS,
|
|
15
|
+
createDoctorFixture,
|
|
16
|
+
fixtureProviderDefinition,
|
|
17
|
+
installBrokenHome,
|
|
18
|
+
} from './doctor-fixture.js';
|
|
19
|
+
|
|
20
|
+
import type { IDoctorFixture } from './doctor-fixture.js';
|
|
21
|
+
import type { IDoctorReport } from '../doctor-types.js';
|
|
22
|
+
|
|
23
|
+
const fixtures: IDoctorFixture[] = [];
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
for (const fixture of fixtures.splice(0)) fixture.cleanup();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
function fixture(...args: Parameters<typeof createDoctorFixture>): IDoctorFixture {
|
|
29
|
+
const created = createDoctorFixture(...args);
|
|
30
|
+
fixtures.push(created);
|
|
31
|
+
return created;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function byId(report: IDoctorReport, id: string) {
|
|
35
|
+
const check = report.checks.find((candidate) => candidate.id === id);
|
|
36
|
+
if (check === undefined)
|
|
37
|
+
throw new Error(`no check ${id}; have ${report.checks.map((c) => c.id).join(', ')}`);
|
|
38
|
+
return check;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const CLEAN_SETTINGS = {
|
|
42
|
+
currentProvider: 'doctor-env',
|
|
43
|
+
providers: {
|
|
44
|
+
'doctor-env': { type: 'fixture', model: 'fixture-model', apiKey: '$ENV:ROBOTA_DOCTOR_MARKER' },
|
|
45
|
+
'doctor-inactive': { type: 'fixture', model: 'fixture-model', apiKey: MARKERS.profile },
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
describe('runDoctor (OBSERVABILITY-1991)', () => {
|
|
50
|
+
it('uses neutral provider and trust guidance unless the host supplies its own commands', async () => {
|
|
51
|
+
const f = fixture({ env: {} });
|
|
52
|
+
const neutral = await runDoctor(f.inputs, f.deps);
|
|
53
|
+
expect(byId(neutral, 'provider.resolution').detail?.join(' ')).not.toMatch(/robota/i);
|
|
54
|
+
expect(byId(neutral, 'workspace.trust').detail?.join(' ')).not.toMatch(/robota/i);
|
|
55
|
+
|
|
56
|
+
const hosted = await runDoctor({
|
|
57
|
+
...f.inputs,
|
|
58
|
+
diagnosticGuidance: {
|
|
59
|
+
providerResolution: 'Run atlas configure.',
|
|
60
|
+
projectTrust: 'Run atlas trust.',
|
|
61
|
+
},
|
|
62
|
+
}, f.deps);
|
|
63
|
+
expect(byId(hosted, 'provider.resolution').detail).toEqual(['Run atlas configure.']);
|
|
64
|
+
expect(byId(hosted, 'workspace.trust').detail).toEqual(['Run atlas trust.']);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('shows each effective settings hook source without embedding payloads in provenance', async () => {
|
|
68
|
+
const f = fixture({ env: {} });
|
|
69
|
+
const userPath = f.write(
|
|
70
|
+
'.robota/settings.json',
|
|
71
|
+
JSON.stringify({
|
|
72
|
+
disabledHooks: ['project-muted'],
|
|
73
|
+
hooks: {
|
|
74
|
+
PreToolUse: [{ matcher: '*', hooks: [{ type: 'command', command: 'user-private' }] }],
|
|
75
|
+
},
|
|
76
|
+
}),
|
|
77
|
+
);
|
|
78
|
+
const laterPath = f.write(
|
|
79
|
+
'.claude/settings.json',
|
|
80
|
+
JSON.stringify({
|
|
81
|
+
hooks: {
|
|
82
|
+
PreToolUse: [
|
|
83
|
+
{ matcher: '*', hooks: [{ type: 'command', command: 'project-private' }] },
|
|
84
|
+
{
|
|
85
|
+
id: 'project-muted',
|
|
86
|
+
matcher: '*',
|
|
87
|
+
hooks: [{ type: 'prompt', prompt: 'muted-private' }],
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
},
|
|
91
|
+
}),
|
|
92
|
+
);
|
|
93
|
+
const inputs = {
|
|
94
|
+
...f.inputs,
|
|
95
|
+
settingsSources: [
|
|
96
|
+
createNodeHostSettingsSource('user', userPath),
|
|
97
|
+
createNodeHostSettingsSource('user', laterPath),
|
|
98
|
+
],
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const report = await runDoctor(inputs, f.deps);
|
|
102
|
+
const output = renderDoctorReport(report).join('\n');
|
|
103
|
+
const mergeDetail = byId(report, 'settings.merge').detail?.join('\n') ?? '';
|
|
104
|
+
|
|
105
|
+
expect(byId(report, 'settings.merge').detail).toContain(
|
|
106
|
+
`settings hook 1: PreToolUse/command ← ${userPath}`,
|
|
107
|
+
);
|
|
108
|
+
expect(byId(report, 'settings.merge').detail).toContain(
|
|
109
|
+
`settings hook 2: PreToolUse/command ← ${laterPath}`,
|
|
110
|
+
);
|
|
111
|
+
expect(output).toContain(`settings hook 1: PreToolUse/command ← ${userPath}`);
|
|
112
|
+
expect(output).toContain(`settings hook 2: PreToolUse/command ← ${laterPath}`);
|
|
113
|
+
expect(mergeDetail).not.toMatch(/private/);
|
|
114
|
+
expect(output).not.toContain('PreToolUse/prompt');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('TC-05/TC-02: names every failing path and cause in the broken HOME and exits 1', async () => {
|
|
118
|
+
const f = fixture({ env: { ROBOTA_DOCTOR_MARKER: MARKERS.env } });
|
|
119
|
+
installBrokenHome(f);
|
|
120
|
+
const report = await runDoctor(f.inputs, f.deps);
|
|
121
|
+
|
|
122
|
+
expect(report.exitCode).toBe(1);
|
|
123
|
+
expect(byId(report, 'host.fixture').status).toBe('ok');
|
|
124
|
+
const user = byId(report, 'settings.user.robota');
|
|
125
|
+
expect(user).toMatchObject({
|
|
126
|
+
status: 'fail',
|
|
127
|
+
cause: 'empty',
|
|
128
|
+
path: join(f.home, '.robota', 'settings.json'),
|
|
129
|
+
repair: 'settings.user.robota',
|
|
130
|
+
});
|
|
131
|
+
const claude = byId(report, 'settings.user.claude');
|
|
132
|
+
expect(claude.status).toBe('fail');
|
|
133
|
+
expect(claude.path).toBe(join(f.home, '.claude', 'settings.json'));
|
|
134
|
+
expect(claude.cause).toContain('schema-invalid');
|
|
135
|
+
expect(claude.cause).toContain('defaultTrustLevel');
|
|
136
|
+
expect(claude.cause).not.toContain('42');
|
|
137
|
+
expect(claude.repair).toBeUndefined();
|
|
138
|
+
expect(byId(report, 'settings.merge').status).toBe('warn');
|
|
139
|
+
|
|
140
|
+
const broken = byId(report, 'plugin.broken-plugin@fixture-market');
|
|
141
|
+
expect(broken.status).toBe('fail');
|
|
142
|
+
expect(broken.path).toContain(join('broken-plugin', '1.0.0', '.claude-plugin', 'plugin.json'));
|
|
143
|
+
const ghost = byId(report, 'mcp.plugin.mcp-plugin@fixture-market.ghost');
|
|
144
|
+
expect(ghost).toMatchObject({ status: 'warn' });
|
|
145
|
+
expect(ghost.cause).toContain('robota-doctor-missing-binary');
|
|
146
|
+
expect(byId(report, 'mcp.activation').status).toBe('not-configured');
|
|
147
|
+
expect(byId(report, 'mcp.connection').status).toBe('not-probed');
|
|
148
|
+
expect(byId(report, 'hooks.execution').status).toBe('not-probed');
|
|
149
|
+
expect(byId(report, 'storage.user').status).toBe('ok');
|
|
150
|
+
expect(byId(report, 'storage.project').status).toBe('not-configured');
|
|
151
|
+
expect(byId(report, 'workspace.trust').status).toBe('warn');
|
|
152
|
+
expect(report.repairable).toEqual(['settings.user.robota']);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('TC-03: no rendered line carries a marker secret from any input', async () => {
|
|
156
|
+
const f = fixture({ env: { ROBOTA_DOCTOR_MARKER: MARKERS.env } });
|
|
157
|
+
installBrokenHome(f);
|
|
158
|
+
// Overwrite the schema-invalid layer with a syntax error beside a secret (the parser-snippet case).
|
|
159
|
+
f.write('.claude/settings.json', `{"providers":{"x":{"apiKey": ${MARKERS.adjacent}}}}`);
|
|
160
|
+
const report = await runDoctor(f.inputs, f.deps);
|
|
161
|
+
const text = renderDoctorReport(report).join('\n');
|
|
162
|
+
for (const marker of Object.values(MARKERS)) expect(text).not.toContain(marker);
|
|
163
|
+
expect(byId(report, 'settings.user.claude').cause).toContain('invalid-json');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('TC-03: masks the resolved credential and inactive-profile literals in a clean configuration', async () => {
|
|
167
|
+
const f = fixture({ env: { ROBOTA_DOCTOR_MARKER: MARKERS.env } });
|
|
168
|
+
f.mkdir('.robota');
|
|
169
|
+
f.mkdir('.robota/sessions');
|
|
170
|
+
f.write('.robota/settings.json', JSON.stringify(CLEAN_SETTINGS));
|
|
171
|
+
const report = await runDoctor(f.inputs, f.deps);
|
|
172
|
+
const text = renderDoctorReport(report).join('\n');
|
|
173
|
+
expect(report.exitCode).toBe(0);
|
|
174
|
+
expect(text).not.toContain(MARKERS.env);
|
|
175
|
+
expect(text).not.toContain(MARKERS.profile);
|
|
176
|
+
expect(byId(report, 'provider.resolution').cause).toContain('fixture (fixture-model)');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('TC-03: a parse error in a settings layer never renders the file excerpt around the fault', async () => {
|
|
180
|
+
const f = fixture({ env: {} });
|
|
181
|
+
f.mkdir('.robota');
|
|
182
|
+
f.mkdir('.robota/sessions');
|
|
183
|
+
// Unquoted value: JSON.parse's message quotes the bytes around the fault, i.e. the credential.
|
|
184
|
+
f.write(
|
|
185
|
+
'.robota/settings.json',
|
|
186
|
+
`{"currentProvider":"p","providers":{"p":{"apiKey": ${MARKERS.adjacent}}}}`,
|
|
187
|
+
);
|
|
188
|
+
const report = await runDoctor(f.inputs, f.deps);
|
|
189
|
+
const text = renderDoctorReport(report, 'robota doctor').join('\n');
|
|
190
|
+
expect(text).not.toContain(MARKERS.adjacent);
|
|
191
|
+
expect(text).not.toContain(MARKERS.adjacent.slice(0, 10));
|
|
192
|
+
expect(text).not.toContain('..."');
|
|
193
|
+
expect(byId(report, 'provider.resolution')).toMatchObject({
|
|
194
|
+
status: 'fail',
|
|
195
|
+
cause: expect.stringContaining('SettingsParseError'),
|
|
196
|
+
});
|
|
197
|
+
expect(byId(report, 'settings.user.robota')).toMatchObject({ status: 'fail' });
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('TC-04: derives the reachability host from profile baseURL, then defaults.baseURL, then endpoint', async () => {
|
|
201
|
+
const profile = fixture({ env: {} });
|
|
202
|
+
profile.write(
|
|
203
|
+
'.robota/settings.json',
|
|
204
|
+
JSON.stringify({
|
|
205
|
+
currentProvider: 'p',
|
|
206
|
+
providers: {
|
|
207
|
+
p: { type: 'fixture', model: 'm', apiKey: 'k1234', baseURL: 'http://127.0.0.1:9' },
|
|
208
|
+
},
|
|
209
|
+
}),
|
|
210
|
+
);
|
|
211
|
+
await runDoctor(profile.inputs, profile.deps);
|
|
212
|
+
expect(profile.probes).toEqual([{ host: '127.0.0.1', port: 9 }]);
|
|
213
|
+
|
|
214
|
+
const defaults = fixture({
|
|
215
|
+
env: {},
|
|
216
|
+
providerDefinitions: [
|
|
217
|
+
fixtureProviderDefinition({
|
|
218
|
+
defaults: { model: 'm', baseURL: 'https://compat.example/v1' },
|
|
219
|
+
endpoint: undefined,
|
|
220
|
+
}),
|
|
221
|
+
],
|
|
222
|
+
});
|
|
223
|
+
defaults.write(
|
|
224
|
+
'.robota/settings.json',
|
|
225
|
+
JSON.stringify({
|
|
226
|
+
currentProvider: 'p',
|
|
227
|
+
providers: { p: { type: 'fixture', model: 'm', apiKey: 'k1234' } },
|
|
228
|
+
}),
|
|
229
|
+
);
|
|
230
|
+
await runDoctor(defaults.inputs, defaults.deps);
|
|
231
|
+
expect(defaults.probes).toEqual([{ host: 'compat.example', port: 443 }]);
|
|
232
|
+
|
|
233
|
+
const endpoint = fixture({ env: {} });
|
|
234
|
+
endpoint.write(
|
|
235
|
+
'.robota/settings.json',
|
|
236
|
+
JSON.stringify({
|
|
237
|
+
currentProvider: 'p',
|
|
238
|
+
providers: { p: { type: 'fixture', model: 'm', apiKey: 'k1234' } },
|
|
239
|
+
}),
|
|
240
|
+
);
|
|
241
|
+
const report = await runDoctor(endpoint.inputs, endpoint.deps);
|
|
242
|
+
expect(endpoint.probes).toEqual([{ host: 'api.fixture.example', port: 443 }]);
|
|
243
|
+
expect(byId(report, 'provider.reachability').cause).toContain('definition endpoint');
|
|
244
|
+
|
|
245
|
+
const none = fixture({
|
|
246
|
+
env: {},
|
|
247
|
+
providerDefinitions: [fixtureProviderDefinition({ endpoint: undefined })],
|
|
248
|
+
});
|
|
249
|
+
none.write(
|
|
250
|
+
'.robota/settings.json',
|
|
251
|
+
JSON.stringify({
|
|
252
|
+
currentProvider: 'p',
|
|
253
|
+
providers: { p: { type: 'fixture', model: 'm', apiKey: 'k1234' } },
|
|
254
|
+
}),
|
|
255
|
+
);
|
|
256
|
+
const noneReport = await runDoctor(none.inputs, none.deps);
|
|
257
|
+
expect(none.probes).toEqual([]);
|
|
258
|
+
expect(byId(noneReport, 'provider.reachability')).toMatchObject({ status: 'warn' });
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('TC-04: an unreachable host is warn, not fail, so an offline doctor still exits 0', async () => {
|
|
262
|
+
const f = fixture({ env: {}, reachable: false });
|
|
263
|
+
f.mkdir('.robota');
|
|
264
|
+
f.mkdir('.robota/sessions');
|
|
265
|
+
f.write(
|
|
266
|
+
'.robota/settings.json',
|
|
267
|
+
JSON.stringify({
|
|
268
|
+
currentProvider: 'p',
|
|
269
|
+
providers: { p: { type: 'fixture', model: 'm', apiKey: 'k1234' } },
|
|
270
|
+
}),
|
|
271
|
+
);
|
|
272
|
+
const report = await runDoctor(f.inputs, f.deps);
|
|
273
|
+
expect(byId(report, 'provider.reachability').status).toBe('warn');
|
|
274
|
+
expect(report.exitCode).toBe(0);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('TC-05: a missing user storage root is warn and repairable; non-posix mode is not-probed', async () => {
|
|
278
|
+
const missing = fixture({ env: {} });
|
|
279
|
+
const report = await runDoctor(missing.inputs, missing.deps);
|
|
280
|
+
expect(byId(report, 'storage.user')).toMatchObject({ status: 'warn', repair: 'storage.user' });
|
|
281
|
+
expect(byId(report, 'provider.resolution').status).toBe('fail');
|
|
282
|
+
|
|
283
|
+
const win = fixture({ env: {}, guarantee: 'windows-acl' });
|
|
284
|
+
win.mkdir('.robota');
|
|
285
|
+
win.mkdir('.robota/sessions');
|
|
286
|
+
expect(byId(await runDoctor(win.inputs, win.deps), 'storage.user').status).toBe('not-probed');
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('TC-05: a too-open storage directory is warn and repairable', async () => {
|
|
290
|
+
const f = fixture({ env: {} });
|
|
291
|
+
f.mkdir('.robota', 0o755);
|
|
292
|
+
f.mkdir('.robota/sessions', 0o755);
|
|
293
|
+
const report = await runDoctor(f.inputs, f.deps);
|
|
294
|
+
expect(byId(report, 'storage.user')).toMatchObject({ status: 'warn', repair: 'storage.user' });
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('TC-07: repair rewrites only an empty user settings file, re-reads before writing, and is idempotent', async () => {
|
|
298
|
+
const f = fixture({ env: {} });
|
|
299
|
+
installBrokenHome(f);
|
|
300
|
+
const path = join(f.home, '.robota', 'settings.json');
|
|
301
|
+
|
|
302
|
+
expect(planDoctorRepair('nope', f.inputs, f.deps)).toMatchObject({ ok: false });
|
|
303
|
+
expect(planDoctorRepair('settings.user.claude', f.inputs, f.deps)).toMatchObject({ ok: false });
|
|
304
|
+
|
|
305
|
+
const refused = await applyDoctorRepair(
|
|
306
|
+
'settings.user.robota',
|
|
307
|
+
f.inputs,
|
|
308
|
+
f.deps,
|
|
309
|
+
async () => false,
|
|
310
|
+
);
|
|
311
|
+
expect(refused.applied).toBe(false);
|
|
312
|
+
expect(readFileSync(path, 'utf8')).toBe('');
|
|
313
|
+
|
|
314
|
+
// The state changes between confirmation and write: refused, nothing overwritten.
|
|
315
|
+
const raced = await applyDoctorRepair('settings.user.robota', f.inputs, f.deps, async () => {
|
|
316
|
+
f.write('.robota/settings.json', '{"language":"ko"}');
|
|
317
|
+
return true;
|
|
318
|
+
});
|
|
319
|
+
expect(raced).toMatchObject({ applied: false });
|
|
320
|
+
expect(readFileSync(path, 'utf8')).toBe('{"language":"ko"}');
|
|
321
|
+
|
|
322
|
+
f.write('.robota/settings.json', '');
|
|
323
|
+
const applied = await applyDoctorRepair(
|
|
324
|
+
'settings.user.robota',
|
|
325
|
+
f.inputs,
|
|
326
|
+
f.deps,
|
|
327
|
+
async () => true,
|
|
328
|
+
);
|
|
329
|
+
expect(applied.applied).toBe(true);
|
|
330
|
+
expect(JSON.parse(readFileSync(path, 'utf8'))).toEqual({});
|
|
331
|
+
expect(byId(await runDoctor(f.inputs, f.deps), 'settings.user.robota').status).toBe('ok');
|
|
332
|
+
|
|
333
|
+
const again = await applyDoctorRepair(
|
|
334
|
+
'settings.user.robota',
|
|
335
|
+
f.inputs,
|
|
336
|
+
f.deps,
|
|
337
|
+
async () => true,
|
|
338
|
+
);
|
|
339
|
+
expect(again).toMatchObject({ applied: false });
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it('TC-07: storage repair creates owner-only directories and is refused once clean', async () => {
|
|
343
|
+
const f = fixture({ env: {} });
|
|
344
|
+
const applied = await applyDoctorRepair('storage.user', f.inputs, f.deps, async () => true);
|
|
345
|
+
expect(applied.applied).toBe(true);
|
|
346
|
+
expect(statSync(join(f.home, '.robota', 'sessions')).mode & 0o077).toBe(0);
|
|
347
|
+
expect(byId(await runDoctor(f.inputs, f.deps), 'storage.user').status).toBe('ok');
|
|
348
|
+
expect(
|
|
349
|
+
await applyDoctorRepair('storage.user', f.inputs, f.deps, async () => true),
|
|
350
|
+
).toMatchObject({ applied: false });
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('TC-05 (skills): a skill definition session discovery throws on is a fail check, not a warning', async () => {
|
|
354
|
+
const f = fixture({ env: {} });
|
|
355
|
+
f.mkdir('.robota');
|
|
356
|
+
f.mkdir('.robota/sessions');
|
|
357
|
+
f.write('.robota/settings.json', JSON.stringify(CLEAN_SETTINGS));
|
|
358
|
+
f.write('.robota/skills/bad-effort/SKILL.md', '---\nname: bad\neffort: extreme\n---\nbody\n');
|
|
359
|
+
f.write('.robota/skills/no-frontmatter/SKILL.md', 'just text\n');
|
|
360
|
+
const report = await runDoctor(f.inputs, f.deps);
|
|
361
|
+
expect(
|
|
362
|
+
byId(report, `skill.${join('.robota', 'skills', 'bad-effort', 'SKILL.md')}`),
|
|
363
|
+
).toMatchObject({
|
|
364
|
+
status: 'fail',
|
|
365
|
+
cause: 'frontmatter-invalid',
|
|
366
|
+
detail: [expect.stringContaining('effort: expected one of')],
|
|
367
|
+
});
|
|
368
|
+
expect(JSON.stringify(report)).not.toContain('received "extreme"');
|
|
369
|
+
expect(
|
|
370
|
+
byId(report, `skill.${join('.robota', 'skills', 'no-frontmatter', 'SKILL.md')}`).status,
|
|
371
|
+
).toBe('warn');
|
|
372
|
+
expect(report.exitCode).toBe(1);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it('TC-05 (workspace.trust): a trust service that swallowed an error is a fail check naming the cause', async () => {
|
|
376
|
+
const f = fixture({ env: {} });
|
|
377
|
+
const report = await runDoctor(
|
|
378
|
+
{
|
|
379
|
+
...f.inputs,
|
|
380
|
+
projectAccess: createRestrictedWorkspaceProjectAccess(
|
|
381
|
+
'store-unavailable',
|
|
382
|
+
f.home,
|
|
383
|
+
new RangeError('trust store is corrupt'),
|
|
384
|
+
),
|
|
385
|
+
},
|
|
386
|
+
f.deps,
|
|
387
|
+
);
|
|
388
|
+
const trust = byId(report, 'workspace.trust');
|
|
389
|
+
expect(trust.status).toBe('fail');
|
|
390
|
+
expect(trust.cause).toBe('store-unavailable: RangeError: trust store is corrupt');
|
|
391
|
+
expect(trust.path).toBe(f.home);
|
|
392
|
+
expect(report.exitCode).toBe(1);
|
|
393
|
+
|
|
394
|
+
const untrusted = byId(await runDoctor(f.inputs, f.deps), 'workspace.trust');
|
|
395
|
+
expect(untrusted).toMatchObject({ status: 'warn', cause: 'untrusted' });
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it('TC-05: reports the quarantined provider endpoint without printing either credential', async () => {
|
|
399
|
+
const f = fixture({ env: {} });
|
|
400
|
+
f.mkdir('.robota');
|
|
401
|
+
f.mkdir('.robota/sessions');
|
|
402
|
+
f.write(
|
|
403
|
+
'.robota/settings.json',
|
|
404
|
+
JSON.stringify({
|
|
405
|
+
currentProvider: 'q',
|
|
406
|
+
providers: {
|
|
407
|
+
q: {
|
|
408
|
+
type: 'fixture',
|
|
409
|
+
model: 'm',
|
|
410
|
+
apiKey: MARKERS.profile,
|
|
411
|
+
baseURL: 'http://127.0.0.1:9',
|
|
412
|
+
},
|
|
413
|
+
},
|
|
414
|
+
}),
|
|
415
|
+
);
|
|
416
|
+
f.write(
|
|
417
|
+
'.claude/settings.json',
|
|
418
|
+
JSON.stringify({ providers: { q: { baseURL: 'http://127.0.0.1:10' } } }),
|
|
419
|
+
);
|
|
420
|
+
const report = await runDoctor(f.inputs, f.deps);
|
|
421
|
+
expect(byId(report, 'provider.security')).toMatchObject({
|
|
422
|
+
status: 'warn',
|
|
423
|
+
cause: expect.stringContaining('quarantined'),
|
|
424
|
+
});
|
|
425
|
+
const text = renderDoctorReport(report, 'robota doctor').join('\n');
|
|
426
|
+
expect(text).not.toContain(MARKERS.profile);
|
|
427
|
+
expect(text).toContain('[provider.security] warn');
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it('a throwing endpoint probe is a provider fail check, not a rejected run', async () => {
|
|
431
|
+
const f = fixture({ env: {} });
|
|
432
|
+
f.mkdir('.robota');
|
|
433
|
+
f.mkdir('.robota/sessions');
|
|
434
|
+
f.write('.robota/settings.json', JSON.stringify(CLEAN_SETTINGS));
|
|
435
|
+
const deps = {
|
|
436
|
+
...f.deps,
|
|
437
|
+
probeEndpoint: () => Promise.reject(new RangeError('socket table exhausted')),
|
|
438
|
+
};
|
|
439
|
+
const report = await runDoctor(f.inputs, deps);
|
|
440
|
+
expect(byId(report, 'provider')).toMatchObject({
|
|
441
|
+
status: 'fail',
|
|
442
|
+
cause: expect.stringContaining('RangeError'),
|
|
443
|
+
});
|
|
444
|
+
expect(report.exitCode).toBe(1);
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
it('renders a composition failure as a fail check instead of crashing', async () => {
|
|
448
|
+
const f = fixture({ env: {} });
|
|
449
|
+
const report = await runDoctor(
|
|
450
|
+
{
|
|
451
|
+
...f.inputs,
|
|
452
|
+
compositionFailure: {
|
|
453
|
+
id: 'workspace.composition',
|
|
454
|
+
label: 'Workspace composition',
|
|
455
|
+
status: 'fail',
|
|
456
|
+
cause: 'WorkspaceAuthorityRequiredError: boom',
|
|
457
|
+
},
|
|
458
|
+
},
|
|
459
|
+
f.deps,
|
|
460
|
+
);
|
|
461
|
+
expect(byId(report, 'workspace.composition').status).toBe('fail');
|
|
462
|
+
expect(report.exitCode).toBe(1);
|
|
463
|
+
});
|
|
464
|
+
});
|