magus-cli 7.3.1 → 7.4.1
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/package.json +4 -4
- package/src/__tests__/blind/c1-npm-package.test.ts +206 -0
- package/src/__tests__/blind/c10-colour-roles.test.ts +265 -0
- package/src/__tests__/blind/c11-cli-parse-refusal.test.ts +454 -0
- package/src/__tests__/blind/c12-skill-frontmatter.test.ts +323 -0
- package/src/__tests__/blind/c13-wrong-shape.test.ts +603 -0
- package/src/__tests__/blind/c14-cli-row-label.test.ts +259 -0
- package/src/__tests__/blind/c15-probe-ownership.test.ts +533 -0
- package/src/__tests__/blind/c16-nested-wrong-shape.test.ts +297 -0
- package/src/__tests__/blind/c17-enter-label.test.ts +625 -0
- package/src/__tests__/blind/c18-hook-shape.test.ts +354 -0
- package/src/__tests__/blind/c19-paste-routing.test.ts +252 -0
- package/src/__tests__/blind/c2-settings-parse.test.ts +350 -0
- package/src/__tests__/blind/c20-registry-and-file-safety.test.ts +560 -0
- package/src/__tests__/blind/c4-key-dispatch.test.ts +397 -0
- package/src/__tests__/blind/c5-keymap.test.ts +461 -0
- package/src/__tests__/blind/c6-request-gate.test.ts +133 -0
- package/src/__tests__/blind/c7-tab-tiers.test.ts +320 -0
- package/src/__tests__/blind/c8-text-width.test.ts +137 -0
- package/src/__tests__/blind/c9-cli-tool-version.test.ts +239 -0
- package/src/__tests__/blind/child-run.ts +114 -0
- package/src/__tests__/cli-tools-adapter.test.ts +5 -2
- package/src/__tests__/component-badge.test.ts +20 -36
- package/src/__tests__/footer-hints.test.ts +1 -1
- package/src/__tests__/manifest.test.ts +25 -17
- package/src/__tests__/marketplace-badge.test.ts +4 -4
- package/src/__tests__/models-screen-state.test.ts +36 -13
- package/src/__tests__/scope-squares.test.tsx +28 -114
- package/src/__tests__/shutdown.test.ts +578 -0
- package/src/__tests__/stale-load-repair.test.ts +5 -4
- package/src/__tests__/styles-manager.test.ts +4 -4
- package/src/__tests__/styles-screen-state.test.ts +26 -12
- package/src/__tests__/tabbar-layout.test.ts +44 -32
- package/src/__tests__/theme-adaptive-colors.test.ts +6 -59
- package/src/__tests__/tool-probe.test.ts +47 -22
- package/src/__tests__/uppercase-keybindings.test.ts +25 -12
- package/src/cli/ansi.ts +2 -2
- package/src/cli/bootstrap.ts +33 -2
- package/src/cli/doctor.ts +120 -15
- package/src/cli/install.ts +35 -4
- package/src/cli/profile.ts +18 -1
- package/src/cli/router.ts +31 -18
- package/src/cli/update-view.ts +1 -1
- package/src/data/cli-tools.ts +9 -1
- package/src/services/claude-settings.ts +434 -198
- package/src/services/cli-tool-commands.ts +45 -5
- package/src/services/cli-tool-version.ts +101 -0
- package/src/services/gitignore-service.ts +39 -22
- package/src/services/hook-registration.ts +23 -6
- package/src/services/manifest.ts +117 -52
- package/src/services/plugin-manager.ts +19 -5
- package/src/services/profile-adopt.ts +7 -2
- package/src/services/profile-startup.ts +87 -7
- package/src/services/profiles.ts +36 -23
- package/src/services/settings-file.ts +164 -4
- package/src/services/settings-manager.ts +19 -5
- package/src/services/skills-manager.ts +46 -0
- package/src/services/tool-probe.ts +316 -44
- package/src/services/toolchain.ts +39 -0
- package/src/services/update-engine.ts +5 -0
- package/src/services/update-plan.ts +9 -0
- package/src/tui.tsx +48 -21
- package/src/ui/App.tsx +58 -197
- package/src/ui/adapters/cliToolsAdapter.ts +141 -12
- package/src/ui/adapters/pluginsAdapter.ts +10 -7
- package/src/ui/adapters/settingsAdapter.ts +0 -21
- package/src/ui/adapters/skillsAdapter.ts +27 -8
- package/src/ui/adapters/stylesAdapter.ts +15 -11
- package/src/ui/components/CategoryHeader.tsx +7 -1
- package/src/ui/components/EmptyFilterState.tsx +2 -2
- package/src/ui/components/FlagDetailEditor.tsx +0 -0
- package/src/ui/components/ScreenErrorBoundary.tsx +105 -0
- package/src/ui/components/TabBar.tsx +93 -21
- package/src/ui/components/layout/FooterHints.tsx +6 -27
- package/src/ui/components/layout/ScreenLayout.tsx +68 -42
- package/src/ui/components/layout/index.ts +1 -5
- package/src/ui/components/modals/ConfirmModal.tsx +2 -19
- package/src/ui/components/modals/HelpOverlay.tsx +186 -0
- package/src/ui/components/modals/MessageModal.tsx +3 -15
- package/src/ui/components/modals/ModalContainer.tsx +107 -60
- package/src/ui/components/primitives/ErrorNotice.tsx +38 -0
- package/src/ui/components/primitives/ListCategoryRow.tsx +18 -7
- package/src/ui/components/primitives/MetaText.tsx +1 -1
- package/src/ui/components/primitives/ScopeDetail.tsx +31 -26
- package/src/ui/components/primitives/ScopeSquares.tsx +23 -28
- package/src/ui/components/primitives/SelectableRow.tsx +24 -1
- package/src/ui/components/primitives/index.ts +4 -3
- package/src/ui/hooks/useProfileStartupModal.ts +15 -3
- package/src/ui/input/KeyRouter.tsx +304 -0
- package/src/ui/input/key-dispatch.ts +197 -0
- package/src/ui/keymap.ts +905 -0
- package/src/ui/renderers/cliToolRenderers.tsx +192 -92
- package/src/ui/renderers/gitignoreRenderers.tsx +41 -43
- package/src/ui/renderers/markdownRenderer.tsx +2 -2
- package/src/ui/renderers/mcpRenderers.tsx +28 -14
- package/src/ui/renderers/modelRenderers.tsx +12 -13
- package/src/ui/renderers/modelVisuals.tsx +0 -2
- package/src/ui/renderers/pluginRenderers.tsx +47 -54
- package/src/ui/renderers/profileRenderers.tsx +32 -37
- package/src/ui/renderers/settingsRenderers.tsx +84 -55
- package/src/ui/renderers/skillRenderers.tsx +237 -190
- package/src/ui/renderers/styleRenderers.tsx +13 -2
- package/src/ui/runtime/shutdown.ts +268 -0
- package/src/ui/screens/AliasScreen.tsx +118 -100
- package/src/ui/screens/CliToolsScreen.tsx +173 -101
- package/src/ui/screens/EnvVarsScreen.tsx +44 -42
- package/src/ui/screens/GitignoreScreen.tsx +50 -55
- package/src/ui/screens/McpRegistryScreen.tsx +52 -86
- package/src/ui/screens/McpScreen.tsx +47 -33
- package/src/ui/screens/ModelsScreen.tsx +59 -104
- package/src/ui/screens/PluginsScreen.tsx +129 -129
- package/src/ui/screens/ProfilesScreen.tsx +49 -46
- package/src/ui/screens/SkillsScreen.tsx +184 -204
- package/src/ui/screens/StylesScreen.tsx +104 -157
- package/src/ui/state/AppContext.tsx +6 -0
- package/src/ui/state/DimensionsContext.tsx +1 -10
- package/src/ui/state/reducer.ts +5 -8
- package/src/ui/state/types.ts +11 -9
- package/src/ui/text-width.ts +341 -0
- package/src/ui/theme-mode.ts +34 -71
- package/src/ui/theme.ts +50 -28
- package/src/utils/request-gate.ts +37 -0
- package/src/ui/components/ScopeIndicator.tsx +0 -63
- package/src/ui/components/SearchInput.tsx +0 -57
- package/src/ui/hooks/index.ts +0 -2
- package/src/ui/hooks/useKeyboard.ts +0 -24
- package/src/ui/hooks/useKeyboardHandler.ts +0 -63
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blind tests — contract C11 (requirement R2.7, with R2.6 for `doctor`).
|
|
3
|
+
*
|
|
4
|
+
* Written from requirements.md and architecture.md only.
|
|
5
|
+
*
|
|
6
|
+
* Every magus run is a child `bun --no-env-file src/main.tsx …` launched with
|
|
7
|
+
* HOME set to a temp dir and cwd set to a temp project (C11, and the C2
|
|
8
|
+
* correction: HOME must be set at launch, never changed at runtime). Before
|
|
9
|
+
* any magus run, ./child-run.ts proves the same env really redirects
|
|
10
|
+
* `os.homedir()`; if it does not, magus is never launched.
|
|
11
|
+
*
|
|
12
|
+
* Only side-effect-free invocations from C11 are used: `install --check`,
|
|
13
|
+
* `profile list`, `doctor` (no `--fix`). Never `upgrade`.
|
|
14
|
+
*
|
|
15
|
+
* Nothing the child prints is surfaced verbatim. Assertions compare summary
|
|
16
|
+
* objects (booleans, exit code, redacted `.json` path tokens), so no failure
|
|
17
|
+
* can echo the contents of a user file.
|
|
18
|
+
*/
|
|
19
|
+
import { afterAll, describe, expect, test } from "bun:test";
|
|
20
|
+
import {
|
|
21
|
+
existsSync,
|
|
22
|
+
mkdirSync,
|
|
23
|
+
mkdtempSync,
|
|
24
|
+
readFileSync,
|
|
25
|
+
realpathSync,
|
|
26
|
+
rmSync,
|
|
27
|
+
writeFileSync,
|
|
28
|
+
} from "node:fs";
|
|
29
|
+
import os from "node:os";
|
|
30
|
+
import path from "node:path";
|
|
31
|
+
import { runInChild } from "./child-run.js";
|
|
32
|
+
|
|
33
|
+
const MAIN = path.resolve(import.meta.dir, "../../main.tsx");
|
|
34
|
+
const SPAWN_TIMEOUT = 45_000; // a child that runs longer is killed and reported as timedOut
|
|
35
|
+
const T = 120_000; // per-test timeout: the first test in a block pays for guard + magus run
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// temp dirs
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
const dirs: string[] = [];
|
|
42
|
+
afterAll(() => {
|
|
43
|
+
for (const dir of dirs.splice(0))
|
|
44
|
+
rmSync(dir, { recursive: true, force: true });
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
function tempDir(tag: string): string {
|
|
48
|
+
const dir = mkdtempSync(path.join(os.tmpdir(), `magus-blind-c11-${tag}-`));
|
|
49
|
+
dirs.push(dir);
|
|
50
|
+
return dir;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function write(file: string, content: string): void {
|
|
54
|
+
mkdirSync(path.dirname(file), { recursive: true });
|
|
55
|
+
writeFileSync(file, content);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface Case {
|
|
59
|
+
home: string;
|
|
60
|
+
project: string;
|
|
61
|
+
/** Both spellings of each temp dir: as created, and resolved (macOS /var -> /private/var). */
|
|
62
|
+
homeForms: string[];
|
|
63
|
+
projectForms: string[];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function newCase(): Case {
|
|
67
|
+
const home = tempDir("home");
|
|
68
|
+
const project = tempDir("proj");
|
|
69
|
+
const forms = (d: string) => [...new Set([d, realpathSync(d)])];
|
|
70
|
+
return {
|
|
71
|
+
home,
|
|
72
|
+
project,
|
|
73
|
+
homeForms: forms(home),
|
|
74
|
+
projectForms: forms(project),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// testdata
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
/** Cut off mid-object: present on disk, not valid JSON. */
|
|
83
|
+
const TRUNCATED_SETTINGS = '{\n "enabledPlugins": {\n "a@b": true,\n';
|
|
84
|
+
const TRUNCATED_PROFILES =
|
|
85
|
+
'{\n "profiles": {\n "blind": {\n "plugins": [\n';
|
|
86
|
+
const VALID_SETTINGS = '{\n "enabledPlugins": {}\n}\n';
|
|
87
|
+
/** Minimal valid JSON. The profiles.json schema is not in the contract, so the control only needs JSON validity. */
|
|
88
|
+
const VALID_PROFILES = "{}\n";
|
|
89
|
+
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
// output classification
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: ESC is the thing being matched.
|
|
95
|
+
const ANSI = /\x1b\[[0-9;?]*[A-Za-z]/g;
|
|
96
|
+
const ERROR_STARTING = /Error starting magus/i;
|
|
97
|
+
/** Either wording the requirements use for the refusal (R2.4 "not valid JSON", R2.7 "fix the JSON by hand"). */
|
|
98
|
+
const NOT_VALID_JSON = /not valid JSON|invalid JSON|fix the JSON by hand/i;
|
|
99
|
+
/** R2.7 quotes this note. */
|
|
100
|
+
const FIX_BY_HAND = /fix the JSON by hand/i;
|
|
101
|
+
/** Bun's source dump: `12 | const x = …`. */
|
|
102
|
+
const LINE_NUMBER_PIPE = /^\s*\d+\s*\|/m;
|
|
103
|
+
/** A stack frame: ` at fn (file:1:2)`. */
|
|
104
|
+
const AT_FRAME = /^[ \t]{2,}at \S/m;
|
|
105
|
+
/** Bun's uncaught-error banner: `Bun v1.4.0 (…)`. */
|
|
106
|
+
const BUN_CRASH_BANNER = /^Bun v\d+\.\d+\.\d+/m;
|
|
107
|
+
|
|
108
|
+
interface CliRun {
|
|
109
|
+
exitCode: number | null;
|
|
110
|
+
signal: string | null;
|
|
111
|
+
timedOut: boolean;
|
|
112
|
+
stdout: string;
|
|
113
|
+
stderr: string;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function combined(r: CliRun): string {
|
|
117
|
+
return `${r.stdout}\n${r.stderr}`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function escapeRe(s: string): string {
|
|
121
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** `.json` path tokens in the output, with temp dirs replaced by labels. Paths only — never file contents. */
|
|
125
|
+
function jsonPathsSeen(text: string, c: Case): string[] {
|
|
126
|
+
const tokens = text.match(/[~\w./-]*\.json\b/g) ?? [];
|
|
127
|
+
const redact = (t: string): string => {
|
|
128
|
+
for (const d of c.homeForms)
|
|
129
|
+
if (t.startsWith(d)) return `<HOME>${t.slice(d.length)}`;
|
|
130
|
+
for (const d of c.projectForms)
|
|
131
|
+
if (t.startsWith(d)) return `<PROJECT>${t.slice(d.length)}`;
|
|
132
|
+
if (t.startsWith("/")) return `<outside-temp>/${path.basename(t)}`;
|
|
133
|
+
return t;
|
|
134
|
+
};
|
|
135
|
+
return [...new Set(tokens.map(redact))];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
// the damaged file, and every spelling that counts as naming it
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
|
|
142
|
+
type Damaged = "project-settings" | "profiles" | "global-settings";
|
|
143
|
+
|
|
144
|
+
interface Target {
|
|
145
|
+
file: (c: Case) => string;
|
|
146
|
+
label: string;
|
|
147
|
+
truncated: string;
|
|
148
|
+
valid: string;
|
|
149
|
+
/** True when `text` names this file by an absolute path, `~/…`, or a project-relative path. */
|
|
150
|
+
named: (text: string, c: Case) => boolean;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function projectFile(rel: string, truncated: string, valid: string): Target {
|
|
154
|
+
// A bare relative `.claude/x.json` counts, but not when it is the tail of
|
|
155
|
+
// `~/.claude/x.json` or of another absolute path.
|
|
156
|
+
const relRe = new RegExp(`(^|[^\\w~/.-])(\\./)?${escapeRe(rel)}`, "m");
|
|
157
|
+
return {
|
|
158
|
+
file: (c) => path.join(c.project, rel),
|
|
159
|
+
label: rel,
|
|
160
|
+
truncated,
|
|
161
|
+
valid,
|
|
162
|
+
named: (text, c) =>
|
|
163
|
+
c.projectForms.some((d) => text.includes(path.join(d, rel))) ||
|
|
164
|
+
relRe.test(text),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const TARGETS: Record<Damaged, Target> = {
|
|
169
|
+
"project-settings": projectFile(
|
|
170
|
+
".claude/settings.json",
|
|
171
|
+
TRUNCATED_SETTINGS,
|
|
172
|
+
VALID_SETTINGS,
|
|
173
|
+
),
|
|
174
|
+
profiles: projectFile(
|
|
175
|
+
".claude/profiles.json",
|
|
176
|
+
TRUNCATED_PROFILES,
|
|
177
|
+
VALID_PROFILES,
|
|
178
|
+
),
|
|
179
|
+
"global-settings": {
|
|
180
|
+
file: (c) => path.join(c.home, ".claude", "settings.json"),
|
|
181
|
+
label: "~/.claude/settings.json",
|
|
182
|
+
truncated: TRUNCATED_SETTINGS,
|
|
183
|
+
valid: VALID_SETTINGS,
|
|
184
|
+
named: (text, c) =>
|
|
185
|
+
text.includes("~/.claude/settings.json") ||
|
|
186
|
+
c.homeForms.some((d) =>
|
|
187
|
+
text.includes(path.join(d, ".claude", "settings.json")),
|
|
188
|
+
),
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
// running magus
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
|
|
196
|
+
function cliEnv(home: string): Record<string, string> {
|
|
197
|
+
const env: Record<string, string> = {};
|
|
198
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
199
|
+
if (v === undefined) continue;
|
|
200
|
+
// Nothing inherited may point magus at real config or a real project.
|
|
201
|
+
if (
|
|
202
|
+
k === "CLAUDE_CONFIG_DIR" ||
|
|
203
|
+
k === "CLAUDE_PROJECT_DIR" ||
|
|
204
|
+
k === "FORCE_COLOR"
|
|
205
|
+
)
|
|
206
|
+
continue;
|
|
207
|
+
if (k.startsWith("MAGUS_")) continue;
|
|
208
|
+
env[k] = v;
|
|
209
|
+
}
|
|
210
|
+
env.HOME = home;
|
|
211
|
+
env.CLAUDE_CONFIG_DIR = path.join(home, ".claude");
|
|
212
|
+
env.XDG_CONFIG_HOME = path.join(home, ".config");
|
|
213
|
+
env.NO_COLOR = "1";
|
|
214
|
+
return env;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Proves HOME set at launch redirects `os.homedir()` in a child bun for this case. Throws if not. */
|
|
218
|
+
function guardHome(c: Case): void {
|
|
219
|
+
const r = runInChild({
|
|
220
|
+
home: c.home,
|
|
221
|
+
cwd: c.project,
|
|
222
|
+
body: 'await out({ stage: "done", ok: true });',
|
|
223
|
+
});
|
|
224
|
+
if (r.stage !== "done") {
|
|
225
|
+
throw new Error(
|
|
226
|
+
`HOME guard failed (stage ${r.stage}): magus was not launched`,
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function runMagus(c: Case, args: string[]): CliRun {
|
|
232
|
+
guardHome(c);
|
|
233
|
+
const proc = Bun.spawnSync(
|
|
234
|
+
[process.execPath, "--no-env-file", MAIN, ...args],
|
|
235
|
+
{
|
|
236
|
+
cwd: c.project,
|
|
237
|
+
env: cliEnv(c.home),
|
|
238
|
+
stdin: "ignore",
|
|
239
|
+
stdout: "pipe",
|
|
240
|
+
stderr: "pipe",
|
|
241
|
+
timeout: SPAWN_TIMEOUT,
|
|
242
|
+
},
|
|
243
|
+
);
|
|
244
|
+
const timedOut =
|
|
245
|
+
(proc as { exitedDueToTimeout?: boolean }).exitedDueToTimeout === true ||
|
|
246
|
+
proc.exitCode === null;
|
|
247
|
+
return {
|
|
248
|
+
exitCode: proc.exitCode,
|
|
249
|
+
signal: (proc as { signalCode?: string | null }).signalCode ?? null,
|
|
250
|
+
timedOut,
|
|
251
|
+
stdout: proc.stdout.toString().replace(ANSI, ""),
|
|
252
|
+
stderr: proc.stderr.toString().replace(ANSI, ""),
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function once<V>(fn: () => V): () => V {
|
|
257
|
+
let memo: { v: V } | undefined;
|
|
258
|
+
return () => {
|
|
259
|
+
memo ??= { v: fn() };
|
|
260
|
+
return memo.v;
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ---------------------------------------------------------------------------
|
|
265
|
+
// scenarios
|
|
266
|
+
// ---------------------------------------------------------------------------
|
|
267
|
+
|
|
268
|
+
interface Scenario {
|
|
269
|
+
name: string;
|
|
270
|
+
args: string[];
|
|
271
|
+
damaged: Damaged;
|
|
272
|
+
/** R2.6: doctor reports the file and exits normally; exit 1 is not asserted for it. */
|
|
273
|
+
isDoctor: boolean;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const SCENARIOS: Scenario[] = [
|
|
277
|
+
{
|
|
278
|
+
name: "(a) no .claude/profiles.json, truncated .claude/settings.json — `install --check`",
|
|
279
|
+
args: ["install", "--check"],
|
|
280
|
+
damaged: "project-settings",
|
|
281
|
+
isDoctor: false,
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
name: "(b) truncated .claude/profiles.json — `profile list`",
|
|
285
|
+
args: ["profile", "list"],
|
|
286
|
+
damaged: "profiles",
|
|
287
|
+
isDoctor: false,
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
name: "(b) truncated .claude/profiles.json — `install --check`",
|
|
291
|
+
args: ["install", "--check"],
|
|
292
|
+
damaged: "profiles",
|
|
293
|
+
isDoctor: false,
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
name: "(c) truncated global ~/.claude/settings.json (temp HOME) — `doctor`",
|
|
297
|
+
args: ["doctor"],
|
|
298
|
+
damaged: "global-settings",
|
|
299
|
+
isDoctor: true,
|
|
300
|
+
},
|
|
301
|
+
];
|
|
302
|
+
|
|
303
|
+
describe("C11 — child-process harness", () => {
|
|
304
|
+
test(
|
|
305
|
+
"R2.7 control: a child bun launched with the test env reports os.homedir() as the temp HOME",
|
|
306
|
+
() => {
|
|
307
|
+
const c = newCase();
|
|
308
|
+
const r = runInChild({
|
|
309
|
+
home: c.home,
|
|
310
|
+
cwd: c.project,
|
|
311
|
+
body: 'await out({ stage: "done", ok: true });',
|
|
312
|
+
});
|
|
313
|
+
expect(r).toEqual({ stage: "done", ok: true });
|
|
314
|
+
},
|
|
315
|
+
T,
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
test("R2.7 control: the CLI entry point named by C11 exists", () => {
|
|
319
|
+
expect(existsSync(MAIN)).toBe(true);
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
for (const s of SCENARIOS) {
|
|
324
|
+
const target = TARGETS[s.damaged];
|
|
325
|
+
const cmd = `magus ${s.args.join(" ")}`;
|
|
326
|
+
|
|
327
|
+
describe(`C11 — ${s.name}`, () => {
|
|
328
|
+
const damaged = once(() => {
|
|
329
|
+
const c = newCase();
|
|
330
|
+
const file = target.file(c);
|
|
331
|
+
write(file, target.truncated);
|
|
332
|
+
const before = readFileSync(file);
|
|
333
|
+
const run = runMagus(c, s.args);
|
|
334
|
+
const after = existsSync(file) ? readFileSync(file) : null;
|
|
335
|
+
return { c, run, before, after };
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
if (s.isDoctor) {
|
|
339
|
+
test(
|
|
340
|
+
`R2.7 ${cmd} with ${target.label} truncated does not crash with "Error starting magus"`,
|
|
341
|
+
() => {
|
|
342
|
+
const { run } = damaged();
|
|
343
|
+
expect({
|
|
344
|
+
timedOut: run.timedOut,
|
|
345
|
+
exitedOnItsOwn: run.exitCode !== null && run.signal === null,
|
|
346
|
+
printsErrorStartingMagus: ERROR_STARTING.test(combined(run)),
|
|
347
|
+
}).toEqual({
|
|
348
|
+
timedOut: false,
|
|
349
|
+
exitedOnItsOwn: true,
|
|
350
|
+
printsErrorStartingMagus: false,
|
|
351
|
+
});
|
|
352
|
+
},
|
|
353
|
+
T,
|
|
354
|
+
);
|
|
355
|
+
} else {
|
|
356
|
+
test(
|
|
357
|
+
`R2.7 ${cmd} with ${target.label} truncated exits 1`,
|
|
358
|
+
() => {
|
|
359
|
+
const { run } = damaged();
|
|
360
|
+
expect({ exitCode: run.exitCode, timedOut: run.timedOut }).toEqual({
|
|
361
|
+
exitCode: 1,
|
|
362
|
+
timedOut: false,
|
|
363
|
+
});
|
|
364
|
+
},
|
|
365
|
+
T,
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
test(
|
|
370
|
+
`R2.7 ${cmd} with ${target.label} truncated names the damaged file's path`,
|
|
371
|
+
() => {
|
|
372
|
+
const { c, run } = damaged();
|
|
373
|
+
const text = combined(run);
|
|
374
|
+
expect({
|
|
375
|
+
namesDamagedFile: target.named(text, c),
|
|
376
|
+
jsonPathsSeen: jsonPathsSeen(text, c),
|
|
377
|
+
}).toMatchObject({ namesDamagedFile: true });
|
|
378
|
+
},
|
|
379
|
+
T,
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
test(
|
|
383
|
+
`R2.7 ${cmd} with ${target.label} truncated prints no "Error starting magus" and no stack or source dump`,
|
|
384
|
+
() => {
|
|
385
|
+
const { run } = damaged();
|
|
386
|
+
const text = combined(run);
|
|
387
|
+
expect({
|
|
388
|
+
printsErrorStartingMagus: ERROR_STARTING.test(text),
|
|
389
|
+
lineNumberPipeSourceDump: LINE_NUMBER_PIPE.test(text),
|
|
390
|
+
stackFrameAtLine: AT_FRAME.test(text),
|
|
391
|
+
bunCrashBanner: BUN_CRASH_BANNER.test(text),
|
|
392
|
+
}).toEqual({
|
|
393
|
+
printsErrorStartingMagus: false,
|
|
394
|
+
lineNumberPipeSourceDump: false,
|
|
395
|
+
stackFrameAtLine: false,
|
|
396
|
+
bunCrashBanner: false,
|
|
397
|
+
});
|
|
398
|
+
},
|
|
399
|
+
T,
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
test(
|
|
403
|
+
`R2.7 ${cmd} leaves the truncated ${target.label} byte-identical`,
|
|
404
|
+
() => {
|
|
405
|
+
const { before, after } = damaged();
|
|
406
|
+
expect({
|
|
407
|
+
stillExists: after !== null,
|
|
408
|
+
byteIdentical: after?.equals(before) === true,
|
|
409
|
+
}).toEqual({ stillExists: true, byteIdentical: true });
|
|
410
|
+
},
|
|
411
|
+
T,
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
if (s.isDoctor) {
|
|
415
|
+
test(
|
|
416
|
+
`R2.7 ${cmd} with ${target.label} truncated says the file is not valid JSON`,
|
|
417
|
+
() => {
|
|
418
|
+
const { run } = damaged();
|
|
419
|
+
expect({
|
|
420
|
+
saysNotValidJson: NOT_VALID_JSON.test(combined(run)),
|
|
421
|
+
}).toEqual({ saysNotValidJson: true });
|
|
422
|
+
},
|
|
423
|
+
T,
|
|
424
|
+
);
|
|
425
|
+
} else {
|
|
426
|
+
test(
|
|
427
|
+
`R2.7 ${cmd} with ${target.label} truncated prints the "fix the JSON by hand" note on stderr`,
|
|
428
|
+
() => {
|
|
429
|
+
const { run } = damaged();
|
|
430
|
+
expect({
|
|
431
|
+
fixByHandNoteOnStderr: FIX_BY_HAND.test(run.stderr),
|
|
432
|
+
}).toEqual({
|
|
433
|
+
fixByHandNoteOnStderr: true,
|
|
434
|
+
});
|
|
435
|
+
},
|
|
436
|
+
T,
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
test(
|
|
441
|
+
`R2.7 control: ${cmd} with a valid ${target.label} does not print the not-valid-JSON message`,
|
|
442
|
+
() => {
|
|
443
|
+
const c = newCase();
|
|
444
|
+
write(target.file(c), target.valid);
|
|
445
|
+
const run = runMagus(c, s.args);
|
|
446
|
+
expect({
|
|
447
|
+
timedOut: run.timedOut,
|
|
448
|
+
printsNotValidJson: NOT_VALID_JSON.test(combined(run)),
|
|
449
|
+
}).toEqual({ timedOut: false, printsNotValidJson: false });
|
|
450
|
+
},
|
|
451
|
+
T,
|
|
452
|
+
);
|
|
453
|
+
});
|
|
454
|
+
}
|