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,350 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blind tests — contract C2 (requirements R2.1, R2.2, R2.3).
|
|
3
|
+
*
|
|
4
|
+
* Written from requirements.md and architecture.md only.
|
|
5
|
+
*
|
|
6
|
+
* NOTHING from claude-settings runs in this process. Under Bun, a runtime
|
|
7
|
+
* HOME change does not move `os.homedir()` (architecture.md C2 correction),
|
|
8
|
+
* and this module can resolve `~/.claude` — so every call runs in a child
|
|
9
|
+
* `bun` spawned with a temp HOME (see ./child-run.ts). The child reports only
|
|
10
|
+
* booleans and names: never a value read from a file, never an error
|
|
11
|
+
* message, so no failure can print a real user file.
|
|
12
|
+
*
|
|
13
|
+
* Valid-file cases write a per-test random token into the file; the child
|
|
14
|
+
* reports whether the value it read contains that token, which proves the
|
|
15
|
+
* temp file — and not some other file — was the one read.
|
|
16
|
+
*/
|
|
17
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
18
|
+
import { randomUUID } from "node:crypto";
|
|
19
|
+
import {
|
|
20
|
+
mkdirSync,
|
|
21
|
+
mkdtempSync,
|
|
22
|
+
readFileSync,
|
|
23
|
+
rmSync,
|
|
24
|
+
writeFileSync,
|
|
25
|
+
} from "node:fs";
|
|
26
|
+
import os from "node:os";
|
|
27
|
+
import path from "node:path";
|
|
28
|
+
import { type ChildResult, runInChild } from "./child-run.js";
|
|
29
|
+
|
|
30
|
+
const MODULE = path.resolve(
|
|
31
|
+
import.meta.dir,
|
|
32
|
+
"../../services/claude-settings.ts",
|
|
33
|
+
);
|
|
34
|
+
const T = 30_000; // per-test timeout: each test spawns one child bun
|
|
35
|
+
|
|
36
|
+
const dirs: string[] = [];
|
|
37
|
+
afterEach(() => {
|
|
38
|
+
for (const dir of dirs.splice(0))
|
|
39
|
+
rmSync(dir, { recursive: true, force: true });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
function tempDir(tag: string): string {
|
|
43
|
+
const dir = mkdtempSync(path.join(os.tmpdir(), `magus-blind-c2-${tag}-`));
|
|
44
|
+
dirs.push(dir);
|
|
45
|
+
return dir;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function write(file: string, content: string): void {
|
|
49
|
+
mkdirSync(path.dirname(file), { recursive: true });
|
|
50
|
+
writeFileSync(file, content);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Three distinct ways a file can exist and not be JSON. */
|
|
54
|
+
const INVALID: Record<string, string> = {
|
|
55
|
+
truncated: '{\n "enabledPlugins": {\n "a@b": true,\n',
|
|
56
|
+
"plain text": "this is not json at all\n",
|
|
57
|
+
"trailing comma": '{ "enabledPlugins": { "a@b": true, }, }\n',
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Child body: call `mod[job.call](...job.args)` and report a summary.
|
|
62
|
+
* Reports whether a value is `{}` and whether it contains `job.expect`
|
|
63
|
+
* (subset match) — never the value itself. On a throw, reports the error's
|
|
64
|
+
* class and whether it carries/names `job.file` — never its message.
|
|
65
|
+
*/
|
|
66
|
+
const CALL_BODY = `
|
|
67
|
+
const fs = await import("node:fs");
|
|
68
|
+
const real = (p) => { try { return fs.realpathSync(p); } catch { return null; } };
|
|
69
|
+
const subset = (a, e) => {
|
|
70
|
+
if (e === null || typeof e !== "object") return Object.is(a, e);
|
|
71
|
+
if (a === null || typeof a !== "object") return false;
|
|
72
|
+
if (Array.isArray(e)) return Array.isArray(a) && a.length === e.length && e.every((v, i) => subset(a[i], v));
|
|
73
|
+
return Object.keys(e).every((k) => subset(a[k], e[k]));
|
|
74
|
+
};
|
|
75
|
+
try {
|
|
76
|
+
const value = await mod[job.call](...job.args);
|
|
77
|
+
const isObj = value !== null && typeof value === "object" && !Array.isArray(value);
|
|
78
|
+
await out({
|
|
79
|
+
stage: "done",
|
|
80
|
+
ok: true,
|
|
81
|
+
isEmptyObject: isObj && Object.keys(value).length === 0,
|
|
82
|
+
matchesExpected: job.expect === undefined ? null : subset(value, job.expect),
|
|
83
|
+
});
|
|
84
|
+
} catch (e) {
|
|
85
|
+
const isErr = e instanceof Error;
|
|
86
|
+
const P = mod.SettingsParseError;
|
|
87
|
+
const want = job.file ? real(job.file) : null;
|
|
88
|
+
const msg = isErr && typeof e.message === "string" ? e.message : "";
|
|
89
|
+
const p = e && typeof e.path === "string" ? e.path : null;
|
|
90
|
+
await out({
|
|
91
|
+
stage: "done",
|
|
92
|
+
ok: false,
|
|
93
|
+
isError: isErr,
|
|
94
|
+
name: isErr ? e.name : typeof e,
|
|
95
|
+
isSettingsParseError: typeof P === "function" && e instanceof P,
|
|
96
|
+
pathIsString: p !== null,
|
|
97
|
+
pathMatchesFile: !!want && p !== null && real(p) === want,
|
|
98
|
+
messageNamesFile: !!job.file && (msg.includes(job.file) || (!!want && msg.includes(want))),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
`;
|
|
102
|
+
|
|
103
|
+
interface Case {
|
|
104
|
+
home: string;
|
|
105
|
+
project: string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function newCase(): Case {
|
|
109
|
+
return { home: tempDir("home"), project: tempDir("proj") };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function call(
|
|
113
|
+
c: Case,
|
|
114
|
+
fn: string,
|
|
115
|
+
args: unknown[],
|
|
116
|
+
extra: { file?: string; expect?: unknown } = {},
|
|
117
|
+
): ChildResult {
|
|
118
|
+
return runInChild({
|
|
119
|
+
home: c.home,
|
|
120
|
+
cwd: c.project,
|
|
121
|
+
module: MODULE,
|
|
122
|
+
job: { call: fn, args, ...extra },
|
|
123
|
+
body: CALL_BODY,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
interface Reader {
|
|
128
|
+
name: string;
|
|
129
|
+
call: string;
|
|
130
|
+
args: (c: Case) => unknown[];
|
|
131
|
+
file: (c: Case) => string;
|
|
132
|
+
/** Valid content carrying `token`, and the subset the reader must return. */
|
|
133
|
+
valid: (token: string) => Record<string, unknown>;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const READERS: Reader[] = [
|
|
137
|
+
{
|
|
138
|
+
name: "readSettings (<project>/.claude/settings.json)",
|
|
139
|
+
call: "readSettings",
|
|
140
|
+
args: (c) => [c.project],
|
|
141
|
+
file: (c) => path.join(c.project, ".claude", "settings.json"),
|
|
142
|
+
valid: (t) => ({ enabledPlugins: { [`${t}@blind`]: true } }),
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
name: "readLocalSettings (<project>/.claude/settings.local.json)",
|
|
146
|
+
call: "readLocalSettings",
|
|
147
|
+
args: (c) => [c.project],
|
|
148
|
+
file: (c) => path.join(c.project, ".claude", "settings.local.json"),
|
|
149
|
+
valid: (t) => ({ enabledPlugins: { [`${t}@blind`]: false } }),
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
name: "readMcpConfig (<project>/.mcp.json)",
|
|
153
|
+
call: "readMcpConfig",
|
|
154
|
+
args: (c) => [c.project],
|
|
155
|
+
file: (c) => path.join(c.project, ".mcp.json"),
|
|
156
|
+
valid: (t) => ({ mcpServers: { [t]: { command: "echo", args: ["hi"] } } }),
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
name: "readGlobalSettings (<HOME>/.claude/settings.json, child process with temp HOME)",
|
|
160
|
+
call: "readGlobalSettings",
|
|
161
|
+
args: () => [],
|
|
162
|
+
file: (c) => path.join(c.home, ".claude", "settings.json"),
|
|
163
|
+
valid: (t) => ({ enabledPlugins: { [`${t}@global-blind`]: true } }),
|
|
164
|
+
},
|
|
165
|
+
];
|
|
166
|
+
|
|
167
|
+
describe("C2 — child-process harness", () => {
|
|
168
|
+
test(
|
|
169
|
+
"R2 control: the child's os.homedir() is the temp HOME",
|
|
170
|
+
() => {
|
|
171
|
+
const c = newCase();
|
|
172
|
+
const r = runInChild({
|
|
173
|
+
home: c.home,
|
|
174
|
+
cwd: c.project,
|
|
175
|
+
body: 'await out({ stage: "done", ok: true });',
|
|
176
|
+
});
|
|
177
|
+
expect(r).toEqual({ stage: "done", ok: true });
|
|
178
|
+
},
|
|
179
|
+
T,
|
|
180
|
+
);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
for (const reader of READERS) {
|
|
184
|
+
describe(`C2 — ${reader.name}`, () => {
|
|
185
|
+
test(
|
|
186
|
+
"R2.1 a missing file reads as an empty object",
|
|
187
|
+
() => {
|
|
188
|
+
const c = newCase();
|
|
189
|
+
expect(call(c, reader.call, reader.args(c))).toEqual({
|
|
190
|
+
stage: "done",
|
|
191
|
+
ok: true,
|
|
192
|
+
isEmptyObject: true,
|
|
193
|
+
matchesExpected: null,
|
|
194
|
+
});
|
|
195
|
+
},
|
|
196
|
+
T,
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
test(
|
|
200
|
+
"R2.1 control: a valid file reads as its content (the temp file, identified by a random token)",
|
|
201
|
+
() => {
|
|
202
|
+
const c = newCase();
|
|
203
|
+
const content = reader.valid(`blind-${randomUUID()}`);
|
|
204
|
+
write(reader.file(c), JSON.stringify(content, null, 2));
|
|
205
|
+
expect(
|
|
206
|
+
call(c, reader.call, reader.args(c), { expect: content }),
|
|
207
|
+
).toMatchObject({
|
|
208
|
+
stage: "done",
|
|
209
|
+
ok: true,
|
|
210
|
+
matchesExpected: true,
|
|
211
|
+
});
|
|
212
|
+
},
|
|
213
|
+
T,
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
for (const [kind, content] of Object.entries(INVALID)) {
|
|
217
|
+
test(
|
|
218
|
+
`R2.2 an existing file that is not valid JSON (${kind}) throws SettingsParseError`,
|
|
219
|
+
() => {
|
|
220
|
+
const c = newCase();
|
|
221
|
+
write(reader.file(c), content);
|
|
222
|
+
expect(call(c, reader.call, reader.args(c))).toMatchObject({
|
|
223
|
+
stage: "done",
|
|
224
|
+
ok: false,
|
|
225
|
+
isError: true,
|
|
226
|
+
isSettingsParseError: true,
|
|
227
|
+
});
|
|
228
|
+
},
|
|
229
|
+
T,
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
test(
|
|
234
|
+
"R2.2 the SettingsParseError carries the path of the file that failed",
|
|
235
|
+
() => {
|
|
236
|
+
const c = newCase();
|
|
237
|
+
const file = reader.file(c);
|
|
238
|
+
write(file, INVALID.truncated as string);
|
|
239
|
+
// pathMatchesFile compares real paths: macOS tmpdir is /var -> /private/var.
|
|
240
|
+
expect(call(c, reader.call, reader.args(c), { file })).toMatchObject({
|
|
241
|
+
stage: "done",
|
|
242
|
+
ok: false,
|
|
243
|
+
isSettingsParseError: true,
|
|
244
|
+
pathIsString: true,
|
|
245
|
+
pathMatchesFile: true,
|
|
246
|
+
});
|
|
247
|
+
},
|
|
248
|
+
T,
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
test(
|
|
252
|
+
"R2.2 an existing unparseable file never reads as {}",
|
|
253
|
+
() => {
|
|
254
|
+
const c = newCase();
|
|
255
|
+
write(reader.file(c), INVALID["plain text"] as string);
|
|
256
|
+
const r = call(c, reader.call, reader.args(c));
|
|
257
|
+
expect(r.stage).toBe("done");
|
|
258
|
+
expect(r.ok).toBe(false);
|
|
259
|
+
},
|
|
260
|
+
T,
|
|
261
|
+
);
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
describe("C2 — enablePlugin read-modify-write", () => {
|
|
266
|
+
const PLUGIN = "demo@magus-blind";
|
|
267
|
+
const settingsFile = (c: Case) =>
|
|
268
|
+
path.join(c.project, ".claude", "settings.json");
|
|
269
|
+
|
|
270
|
+
test(
|
|
271
|
+
"R2.3 control: enablePlugin on a valid settings.json sets enabledPlugins[id] and keeps other keys",
|
|
272
|
+
() => {
|
|
273
|
+
const c = newCase();
|
|
274
|
+
const file = settingsFile(c);
|
|
275
|
+
write(
|
|
276
|
+
file,
|
|
277
|
+
JSON.stringify(
|
|
278
|
+
{
|
|
279
|
+
enabledPlugins: { "other@x": true },
|
|
280
|
+
permissions: { allow: ["Read"] },
|
|
281
|
+
},
|
|
282
|
+
null,
|
|
283
|
+
2,
|
|
284
|
+
),
|
|
285
|
+
);
|
|
286
|
+
expect(call(c, "enablePlugin", [PLUGIN, true, c.project])).toMatchObject({
|
|
287
|
+
stage: "done",
|
|
288
|
+
ok: true,
|
|
289
|
+
});
|
|
290
|
+
// The file is this test's temp testdata, so reading it here is safe.
|
|
291
|
+
const after = JSON.parse(readFileSync(file, "utf8"));
|
|
292
|
+
expect(after.enabledPlugins[PLUGIN]).toBe(true);
|
|
293
|
+
expect(after.enabledPlugins["other@x"]).toBe(true);
|
|
294
|
+
expect(after.permissions).toEqual({ allow: ["Read"] });
|
|
295
|
+
},
|
|
296
|
+
T,
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
for (const enabled of [true, false]) {
|
|
300
|
+
for (const [kind, content] of Object.entries(INVALID)) {
|
|
301
|
+
test(
|
|
302
|
+
`R2.3 enablePlugin(…, ${enabled}) fails on an unparseable settings.json (${kind})`,
|
|
303
|
+
() => {
|
|
304
|
+
const c = newCase();
|
|
305
|
+
write(settingsFile(c), content);
|
|
306
|
+
expect(
|
|
307
|
+
call(c, "enablePlugin", [PLUGIN, enabled, c.project]),
|
|
308
|
+
).toMatchObject({
|
|
309
|
+
stage: "done",
|
|
310
|
+
ok: false,
|
|
311
|
+
isError: true,
|
|
312
|
+
});
|
|
313
|
+
},
|
|
314
|
+
T,
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
test(
|
|
318
|
+
`R2.3 enablePlugin(…, ${enabled}) leaves an unparseable settings.json (${kind}) byte-for-byte unchanged`,
|
|
319
|
+
() => {
|
|
320
|
+
const c = newCase();
|
|
321
|
+
const file = settingsFile(c);
|
|
322
|
+
write(file, content);
|
|
323
|
+
const before = readFileSync(file);
|
|
324
|
+
const r = call(c, "enablePlugin", [PLUGIN, enabled, c.project]);
|
|
325
|
+
expect(r.stage).toBe("done");
|
|
326
|
+
expect(readFileSync(file).equals(before)).toBe(true);
|
|
327
|
+
},
|
|
328
|
+
T,
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
test(
|
|
334
|
+
"R2.3 the enablePlugin failure identifies the unparseable file",
|
|
335
|
+
() => {
|
|
336
|
+
// R2.4 needs the TUI to name the file, so the error that reaches it must
|
|
337
|
+
// carry the path: as a SettingsParseError, or in its message.
|
|
338
|
+
const c = newCase();
|
|
339
|
+
const file = settingsFile(c);
|
|
340
|
+
write(file, INVALID.truncated as string);
|
|
341
|
+
const r = call(c, "enablePlugin", [PLUGIN, true, c.project], { file });
|
|
342
|
+
expect(r).toMatchObject({ stage: "done", ok: false, isError: true });
|
|
343
|
+
const namesFile =
|
|
344
|
+
(r.isSettingsParseError === true && r.pathMatchesFile === true) ||
|
|
345
|
+
r.messageNamesFile === true;
|
|
346
|
+
expect(namesFile).toBe(true);
|
|
347
|
+
},
|
|
348
|
+
T,
|
|
349
|
+
);
|
|
350
|
+
});
|