magus-cli 7.13.0 → 7.14.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/README.md +7 -4
- package/package.json +4 -4
- package/src/__tests__/market-r2-adapters.test.ts +190 -0
- package/src/__tests__/market-r2-company-cli.test.ts +350 -0
- package/src/__tests__/market-r2-company-unit.test.ts +224 -0
- package/src/__tests__/market-r2-local-items.test.ts +197 -0
- package/src/__tests__/market-r2-plugin-add.test.ts +470 -0
- package/src/__tests__/market-r2-plugin-publish.test.ts +568 -0
- package/src/__tests__/market-r2-routing.test.ts +127 -0
- package/src/__tests__/market-r2-skill-create-plugin.test.ts +315 -0
- package/src/__tests__/market-r2b-display-path.test.ts +180 -0
- package/src/__tests__/market-r2b-fit-row.test.ts +287 -0
- package/src/__tests__/market-r2b-local-items.test.ts +253 -0
- package/src/__tests__/market-r2b-plugin-add.test.ts +130 -0
- package/src/__tests__/market-router.test.ts +4 -4
- package/src/__tests__/scope-uninstall-wrong-scope.test.ts +341 -0
- package/src/cli/market-view.ts +59 -18
- package/src/cli/market.ts +128 -44
- package/src/cli/router.ts +42 -10
- package/src/data/marketplaces.ts +1 -0
- package/src/main.tsx +1 -1
- package/src/market-tui.tsx +1 -1
- package/src/services/claude-cli.ts +13 -0
- package/src/services/local-marketplace.ts +8 -0
- package/src/services/market-change.ts +6 -6
- package/src/services/market-local.ts +93 -0
- package/src/services/market-ops-plugin.ts +366 -47
- package/src/services/market-ops-publish.ts +363 -15
- package/src/services/market-ops-repo.ts +94 -13
- package/src/services/market-ops-skill.ts +93 -11
- package/src/services/market-ops.ts +72 -8
- package/src/services/market-publish.ts +10 -1
- package/src/services/market-types.ts +23 -0
- package/src/services/update-engine.ts +97 -1
- package/src/tui-runtime.tsx +2 -2
- package/src/types/index.ts +2 -0
- package/src/ui/adapters/marketAdapter.ts +123 -4
- package/src/ui/adapters/pluginsAdapter.ts +25 -1
- package/src/ui/components/primitives/ListCategoryRow.tsx +22 -2
- package/src/ui/market/MarketplaceScreen.tsx +55 -24
- package/src/ui/market/MarketplacesDetail.snowflake.tsx +60 -4
- package/src/ui/market/MarketplacesScreen.tsx +120 -29
- package/src/ui/market/PluginDetail.snowflake.tsx +12 -1
- package/src/ui/market/marketKeymap.ts +29 -2
- package/src/ui/market/useMarketState.ts +146 -5
- package/src/utils/display-path.ts +33 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import { afterEach, expect, test } from "bun:test";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
ID,
|
|
6
|
+
MANIFEST,
|
|
7
|
+
MarketFixture,
|
|
8
|
+
PLUGIN_MANIFEST,
|
|
9
|
+
SKILL,
|
|
10
|
+
SKILL_REL,
|
|
11
|
+
write,
|
|
12
|
+
} from "./market-fixture.js";
|
|
13
|
+
|
|
14
|
+
const NEW_ID = "writing-kit@acme";
|
|
15
|
+
const NEW_MANIFEST = "plugins/writing-kit/.claude-plugin/plugin.json";
|
|
16
|
+
const NEW_SKILL = "plugins/writing-kit/skills/authoring/SKILL.md";
|
|
17
|
+
const AUTHORING =
|
|
18
|
+
"---\nname: authoring\ndescription: Writes guides.\n---\nWrite clear guides.\n";
|
|
19
|
+
|
|
20
|
+
let f: MarketFixture | undefined;
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
f?.close();
|
|
23
|
+
f = undefined;
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test.each(["project", "global"] as const)(
|
|
27
|
+
"R4.2 R4.5 TEST-34 adding a %s skill creates its plugin in the same consented commit at 0.1.0",
|
|
28
|
+
(from) => {
|
|
29
|
+
f = new MarketFixture(true);
|
|
30
|
+
const repo = f.seed();
|
|
31
|
+
f.register(repo);
|
|
32
|
+
const folder = f.folder(from, "authoring");
|
|
33
|
+
write(join(folder, "SKILL.md"), AUTHORING);
|
|
34
|
+
const old = f.head(repo);
|
|
35
|
+
const before = f.snapshot();
|
|
36
|
+
const args = [
|
|
37
|
+
"skill",
|
|
38
|
+
"add",
|
|
39
|
+
"authoring",
|
|
40
|
+
"--plugin",
|
|
41
|
+
NEW_ID,
|
|
42
|
+
"--from",
|
|
43
|
+
from,
|
|
44
|
+
"--create-plugin",
|
|
45
|
+
"--description",
|
|
46
|
+
"Writing tools",
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
const plan = f.market(args);
|
|
50
|
+
|
|
51
|
+
expect({ exit: plan.code, outcome: plan.result.outcome }).toEqual({
|
|
52
|
+
exit: 3,
|
|
53
|
+
outcome: "needs-consent",
|
|
54
|
+
});
|
|
55
|
+
expect(typeof plan.result.planId).toBe("string");
|
|
56
|
+
expect(plan.result.plan?.join("\n")).toContain("writing-kit");
|
|
57
|
+
expect(plan.result.plan?.join("\n")).toContain("authoring");
|
|
58
|
+
expect(f.snapshot()).toEqual(before);
|
|
59
|
+
expect(f.head(repo)).toBe(old);
|
|
60
|
+
|
|
61
|
+
const result = f.market([...args, `--yes=${plan.result.planId}`]);
|
|
62
|
+
|
|
63
|
+
expect({ exit: result.code, outcome: result.result.outcome }).toEqual({
|
|
64
|
+
exit: 0,
|
|
65
|
+
outcome: "ok",
|
|
66
|
+
});
|
|
67
|
+
expect(
|
|
68
|
+
f.git(
|
|
69
|
+
f.root,
|
|
70
|
+
"--git-dir",
|
|
71
|
+
repo.bare,
|
|
72
|
+
"rev-list",
|
|
73
|
+
"--count",
|
|
74
|
+
`${old}..HEAD`,
|
|
75
|
+
),
|
|
76
|
+
).toBe("1");
|
|
77
|
+
expect(
|
|
78
|
+
f
|
|
79
|
+
.git(f.root, "--git-dir", repo.bare, "diff", "--name-only", old, "HEAD")
|
|
80
|
+
.split("\n")
|
|
81
|
+
.sort(),
|
|
82
|
+
).toEqual([MANIFEST, NEW_MANIFEST, NEW_SKILL].sort());
|
|
83
|
+
expect(f.remoteJson(repo, NEW_MANIFEST)).toEqual({
|
|
84
|
+
name: "writing-kit",
|
|
85
|
+
version: "0.1.0",
|
|
86
|
+
description: "Writing tools",
|
|
87
|
+
author: { name: "acme" },
|
|
88
|
+
});
|
|
89
|
+
expect(f.remoteJson(repo, MANIFEST).plugins).toContainEqual({
|
|
90
|
+
name: "writing-kit",
|
|
91
|
+
source: "./plugins/writing-kit",
|
|
92
|
+
version: "0.1.0",
|
|
93
|
+
description: "Writing tools",
|
|
94
|
+
});
|
|
95
|
+
expect(f.remoteBytes(repo, NEW_SKILL)).toBe(AUTHORING);
|
|
96
|
+
expect(f.remoteBytes(repo, SKILL_REL)).toBe(SKILL);
|
|
97
|
+
expect(f.version(NEW_ID)).toBeNull();
|
|
98
|
+
expect(result.result.localCopy?.action).toBe("kept");
|
|
99
|
+
expect(result.result.localCopy?.scope).toBe(
|
|
100
|
+
from === "global" ? "user" : "project",
|
|
101
|
+
);
|
|
102
|
+
expect(result.result.localCopy?.reason?.length).toBeGreaterThan(0);
|
|
103
|
+
expect(readFileSync(join(folder, "SKILL.md"), "utf8")).toBe(AUTHORING);
|
|
104
|
+
expect(result.result.steps.map((step) => step.id)).toEqual([
|
|
105
|
+
"edit",
|
|
106
|
+
"copy",
|
|
107
|
+
"validate",
|
|
108
|
+
"commit",
|
|
109
|
+
"deliver",
|
|
110
|
+
"refresh",
|
|
111
|
+
"install",
|
|
112
|
+
"local-copy",
|
|
113
|
+
]);
|
|
114
|
+
|
|
115
|
+
write(
|
|
116
|
+
join(folder, "SKILL.md"),
|
|
117
|
+
`${AUTHORING}\nA tracked follow-up edit.\n`,
|
|
118
|
+
);
|
|
119
|
+
const tracked = f.market([
|
|
120
|
+
"publish",
|
|
121
|
+
"authoring",
|
|
122
|
+
"--plugin",
|
|
123
|
+
NEW_ID,
|
|
124
|
+
"--from",
|
|
125
|
+
from,
|
|
126
|
+
]);
|
|
127
|
+
expect({ exit: tracked.code, outcome: tracked.result.outcome }).toEqual({
|
|
128
|
+
exit: 3,
|
|
129
|
+
outcome: "needs-consent",
|
|
130
|
+
});
|
|
131
|
+
expect(tracked.result.error).toBeUndefined();
|
|
132
|
+
expect(f.head(repo)).toBe(result.result.commit!);
|
|
133
|
+
f.assertOwned();
|
|
134
|
+
},
|
|
135
|
+
90_000,
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
test("R4.2 TEST-35 --create-plugin reuses an existing plugin without overwriting its description", () => {
|
|
139
|
+
f = new MarketFixture();
|
|
140
|
+
const repo = f.seed();
|
|
141
|
+
f.register(repo);
|
|
142
|
+
f.install();
|
|
143
|
+
write(join(f.folder("project", "authoring"), "SKILL.md"), AUTHORING);
|
|
144
|
+
const old = f.head(repo);
|
|
145
|
+
|
|
146
|
+
const result = f.market([
|
|
147
|
+
"skill",
|
|
148
|
+
"add",
|
|
149
|
+
"authoring",
|
|
150
|
+
"--plugin",
|
|
151
|
+
ID,
|
|
152
|
+
"--create-plugin",
|
|
153
|
+
"--description",
|
|
154
|
+
"Only for a newly created plugin",
|
|
155
|
+
"--keep-local",
|
|
156
|
+
"--yes",
|
|
157
|
+
]);
|
|
158
|
+
|
|
159
|
+
expect(result.code).toBe(0);
|
|
160
|
+
expect(f.remoteJson(repo, PLUGIN_MANIFEST).version).toBe("0.3.2");
|
|
161
|
+
expect(f.remoteJson(repo, PLUGIN_MANIFEST).description).toBe(
|
|
162
|
+
"Reviews changes.",
|
|
163
|
+
);
|
|
164
|
+
expect(f.remoteJson(repo, MANIFEST).plugins).toHaveLength(1);
|
|
165
|
+
expect(
|
|
166
|
+
f.remoteBytes(repo, "plugins/review-kit/skills/authoring/SKILL.md"),
|
|
167
|
+
).toBe(AUTHORING);
|
|
168
|
+
expect(f.remoteBytes(repo, SKILL_REL)).toBe(SKILL);
|
|
169
|
+
expect(
|
|
170
|
+
f.git(
|
|
171
|
+
f.root,
|
|
172
|
+
"--git-dir",
|
|
173
|
+
repo.bare,
|
|
174
|
+
"rev-list",
|
|
175
|
+
"--count",
|
|
176
|
+
`${old}..HEAD`,
|
|
177
|
+
),
|
|
178
|
+
).toBe("1");
|
|
179
|
+
f.assertOwned();
|
|
180
|
+
}, 60_000);
|
|
181
|
+
|
|
182
|
+
test.each([
|
|
183
|
+
{
|
|
184
|
+
label: "missing plugin without flag",
|
|
185
|
+
skill: "authoring",
|
|
186
|
+
target: NEW_ID,
|
|
187
|
+
flags: [],
|
|
188
|
+
code: 2,
|
|
189
|
+
error: "not-found",
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
label: "invalid new plugin name",
|
|
193
|
+
skill: "authoring",
|
|
194
|
+
target: "../escape@acme",
|
|
195
|
+
flags: ["--create-plugin"],
|
|
196
|
+
code: 2,
|
|
197
|
+
error: "invalid-name",
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
label: "already held skill",
|
|
201
|
+
skill: "review",
|
|
202
|
+
target: ID,
|
|
203
|
+
flags: ["--create-plugin"],
|
|
204
|
+
code: 1,
|
|
205
|
+
error: "name-taken",
|
|
206
|
+
},
|
|
207
|
+
])(
|
|
208
|
+
"R4.2 TEST-36 skill add refuses $label and leaves source and remote alone",
|
|
209
|
+
({ skill, target, flags, code, error }) => {
|
|
210
|
+
f = new MarketFixture();
|
|
211
|
+
const repo = f.seed();
|
|
212
|
+
f.register(repo);
|
|
213
|
+
write(
|
|
214
|
+
join(f.folder("project", skill), "SKILL.md"),
|
|
215
|
+
skill === "review" ? SKILL : AUTHORING,
|
|
216
|
+
);
|
|
217
|
+
const before = f.snapshot();
|
|
218
|
+
const old = f.head(repo);
|
|
219
|
+
|
|
220
|
+
const result = f.market([
|
|
221
|
+
"skill",
|
|
222
|
+
"add",
|
|
223
|
+
skill,
|
|
224
|
+
"--plugin",
|
|
225
|
+
target,
|
|
226
|
+
...flags,
|
|
227
|
+
"--yes",
|
|
228
|
+
]);
|
|
229
|
+
|
|
230
|
+
expect({ exit: result.code, error: result.result.error?.code }).toEqual({
|
|
231
|
+
exit: code,
|
|
232
|
+
error,
|
|
233
|
+
});
|
|
234
|
+
expect(result.result.outcome).toBe("failed");
|
|
235
|
+
expect(f.head(repo)).toBe(old);
|
|
236
|
+
expect(f.snapshot()).toEqual(before);
|
|
237
|
+
},
|
|
238
|
+
30_000,
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
test("R4.2 TEST-37 --description without --create-plugin is a usage error with no side effects", () => {
|
|
242
|
+
f = new MarketFixture();
|
|
243
|
+
const repo = f.seed();
|
|
244
|
+
f.register(repo);
|
|
245
|
+
write(join(f.folder("project", "authoring"), "SKILL.md"), AUTHORING);
|
|
246
|
+
const before = f.snapshot();
|
|
247
|
+
const old = f.head(repo);
|
|
248
|
+
|
|
249
|
+
const result = f.market([
|
|
250
|
+
"skill",
|
|
251
|
+
"add",
|
|
252
|
+
"authoring",
|
|
253
|
+
"--plugin",
|
|
254
|
+
ID,
|
|
255
|
+
"--description",
|
|
256
|
+
"Not a create action",
|
|
257
|
+
"--yes",
|
|
258
|
+
]);
|
|
259
|
+
|
|
260
|
+
expect(result.code).toBe(2);
|
|
261
|
+
expect(f.snapshot()).toEqual(before);
|
|
262
|
+
expect(f.head(repo)).toBe(old);
|
|
263
|
+
}, 30_000);
|
|
264
|
+
|
|
265
|
+
test.each(["description", "source"] as const)(
|
|
266
|
+
"R4.5 TEST-44 changing new skill/plugin %s after consent refuses the whole atomic add",
|
|
267
|
+
(changed) => {
|
|
268
|
+
f = new MarketFixture();
|
|
269
|
+
const repo = f.seed();
|
|
270
|
+
f.register(repo);
|
|
271
|
+
const folder = f.folder("project", "authoring");
|
|
272
|
+
write(join(folder, "SKILL.md"), AUTHORING);
|
|
273
|
+
const args = [
|
|
274
|
+
"skill",
|
|
275
|
+
"add",
|
|
276
|
+
"authoring",
|
|
277
|
+
"--plugin",
|
|
278
|
+
NEW_ID,
|
|
279
|
+
"--create-plugin",
|
|
280
|
+
"--description",
|
|
281
|
+
"Writing tools",
|
|
282
|
+
];
|
|
283
|
+
const plan = f.market(args);
|
|
284
|
+
expect(plan.code).toBe(3);
|
|
285
|
+
expect(typeof plan.result.planId).toBe("string");
|
|
286
|
+
if (changed === "source")
|
|
287
|
+
write(join(folder, "SKILL.md"), `${AUTHORING}\nChanged after consent.\n`);
|
|
288
|
+
const bytes = readFileSync(join(folder, "SKILL.md"), "utf8");
|
|
289
|
+
const old = f.head(repo);
|
|
290
|
+
const changedArgs =
|
|
291
|
+
changed === "description"
|
|
292
|
+
? [...args.slice(0, -1), "Different description"]
|
|
293
|
+
: args;
|
|
294
|
+
|
|
295
|
+
const result = f.market([...changedArgs, `--yes=${plan.result.planId}`]);
|
|
296
|
+
|
|
297
|
+
expect({
|
|
298
|
+
exit: result.code,
|
|
299
|
+
outcome: result.result.outcome,
|
|
300
|
+
error: result.result.error?.code,
|
|
301
|
+
}).toEqual({
|
|
302
|
+
exit: 3,
|
|
303
|
+
outcome: "needs-consent",
|
|
304
|
+
error: "plan-changed",
|
|
305
|
+
});
|
|
306
|
+
expect(result.result.planId).not.toBe(plan.result.planId);
|
|
307
|
+
expect(f.head(repo)).toBe(old);
|
|
308
|
+
expect(f.remotePaths(repo)).not.toContain(NEW_MANIFEST);
|
|
309
|
+
expect(f.remotePaths(repo)).not.toContain(NEW_SKILL);
|
|
310
|
+
expect(readFileSync(join(folder, "SKILL.md"), "utf8")).toBe(bytes);
|
|
311
|
+
expect(f.version(NEW_ID)).toBeNull();
|
|
312
|
+
f.assertOwned();
|
|
313
|
+
},
|
|
314
|
+
60_000,
|
|
315
|
+
);
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { afterEach, expect, test } from "bun:test";
|
|
2
|
+
import { join, sep } from "node:path";
|
|
3
|
+
import { displayPath } from "../utils/display-path.js";
|
|
4
|
+
import { MarketFixture, sourceModule } from "./market-fixture.js";
|
|
5
|
+
|
|
6
|
+
const project = join(sep, "a", "project");
|
|
7
|
+
const home = join(sep, "people", "alex");
|
|
8
|
+
const nestedProject = join(home, "work", "project");
|
|
9
|
+
const skillParts = [".claude", "skills", "greet"];
|
|
10
|
+
|
|
11
|
+
const cases = [
|
|
12
|
+
{
|
|
13
|
+
label: "a project skill is relative to cwd",
|
|
14
|
+
abs: join(project, ...skillParts),
|
|
15
|
+
cwd: project,
|
|
16
|
+
home,
|
|
17
|
+
expected: join(...skillParts),
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
label: "cwd itself is a dot",
|
|
21
|
+
abs: project,
|
|
22
|
+
cwd: project,
|
|
23
|
+
home,
|
|
24
|
+
expected: ".",
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
label: "the /a/project-x sibling is not inside /a/project",
|
|
28
|
+
abs: join(sep, "a", "project-x"),
|
|
29
|
+
cwd: project,
|
|
30
|
+
home,
|
|
31
|
+
expected: join(sep, "a", "project-x"),
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
label: "a descendant of /a/project-x is not inside /a/project",
|
|
35
|
+
abs: join(sep, "a", "project-x", ...skillParts),
|
|
36
|
+
cwd: project,
|
|
37
|
+
home,
|
|
38
|
+
expected: join(sep, "a", "project-x", ...skillParts),
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
label: "an ancestor of cwd stays absolute outside home",
|
|
42
|
+
abs: join(sep, "a"),
|
|
43
|
+
cwd: project,
|
|
44
|
+
home,
|
|
45
|
+
expected: join(sep, "a"),
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
label: "a home skill outside cwd uses a tilde",
|
|
49
|
+
abs: join(home, ...skillParts),
|
|
50
|
+
cwd: project,
|
|
51
|
+
home,
|
|
52
|
+
expected: join("~", ...skillParts),
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
label: "home itself is a tilde outside cwd",
|
|
56
|
+
abs: home,
|
|
57
|
+
cwd: project,
|
|
58
|
+
home,
|
|
59
|
+
expected: "~",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
label: "a sibling sharing the home prefix stays absolute",
|
|
63
|
+
abs: join(sep, "people", "alex-extra", "tools"),
|
|
64
|
+
cwd: project,
|
|
65
|
+
home,
|
|
66
|
+
expected: join(sep, "people", "alex-extra", "tools"),
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
label: "an unrelated path is unchanged",
|
|
70
|
+
abs: join(sep, "elsewhere", "tools"),
|
|
71
|
+
cwd: project,
|
|
72
|
+
home,
|
|
73
|
+
expected: join(sep, "elsewhere", "tools"),
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
label: "cwd wins for a path inside both cwd and home",
|
|
77
|
+
abs: join(nestedProject, ...skillParts),
|
|
78
|
+
cwd: nestedProject,
|
|
79
|
+
home,
|
|
80
|
+
expected: join(...skillParts),
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
label: "cwd inside home is still a dot for itself",
|
|
84
|
+
abs: nestedProject,
|
|
85
|
+
cwd: nestedProject,
|
|
86
|
+
home,
|
|
87
|
+
expected: ".",
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
label: "home above cwd uses a tilde rather than parent traversal",
|
|
91
|
+
abs: home,
|
|
92
|
+
cwd: nestedProject,
|
|
93
|
+
home,
|
|
94
|
+
expected: "~",
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
label: "cwd equal to home wins for the directory itself",
|
|
98
|
+
abs: home,
|
|
99
|
+
cwd: home,
|
|
100
|
+
home,
|
|
101
|
+
expected: ".",
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
label: "cwd equal to home wins for a descendant",
|
|
105
|
+
abs: join(home, ...skillParts),
|
|
106
|
+
cwd: home,
|
|
107
|
+
home,
|
|
108
|
+
expected: join(...skillParts),
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
label: "home nested inside cwd is shown relative to cwd",
|
|
112
|
+
abs: join(project, "personal-home"),
|
|
113
|
+
cwd: project,
|
|
114
|
+
home: join(project, "personal-home"),
|
|
115
|
+
expected: "personal-home",
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
label: "a root cwd contains descendants",
|
|
119
|
+
abs: project,
|
|
120
|
+
cwd: sep,
|
|
121
|
+
home,
|
|
122
|
+
expected: join("a", "project"),
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
label: "a root cwd is a dot for itself",
|
|
126
|
+
abs: sep,
|
|
127
|
+
cwd: sep,
|
|
128
|
+
home,
|
|
129
|
+
expected: ".",
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
label: "a root home abbreviates paths outside cwd",
|
|
133
|
+
abs: join(sep, "elsewhere", "tools"),
|
|
134
|
+
cwd: project,
|
|
135
|
+
home: sep,
|
|
136
|
+
expected: join("~", "elsewhere", "tools"),
|
|
137
|
+
},
|
|
138
|
+
];
|
|
139
|
+
|
|
140
|
+
test.each(cases)(
|
|
141
|
+
"R6.1 REQ-4 TEST-11 $label",
|
|
142
|
+
({ abs, cwd, home: explicitHome, expected }) => {
|
|
143
|
+
const shown = displayPath(abs, cwd, explicitHome);
|
|
144
|
+
|
|
145
|
+
expect(shown).toBe(expected);
|
|
146
|
+
},
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
let world: MarketFixture | undefined;
|
|
150
|
+
afterEach(() => {
|
|
151
|
+
world?.close();
|
|
152
|
+
world = undefined;
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test.each([
|
|
156
|
+
{ label: "chosen HOME", mode: "chosen" },
|
|
157
|
+
{ label: "absent HOME", mode: "absent" },
|
|
158
|
+
{ label: "empty HOME", mode: "empty" },
|
|
159
|
+
] as const)(
|
|
160
|
+
"R6.1 REQ-4 TEST-12 uses the specified default home with $label",
|
|
161
|
+
({ mode }) => {
|
|
162
|
+
world = new MarketFixture(true);
|
|
163
|
+
const f = world;
|
|
164
|
+
const chosenHome = join(f.root, "shell-home");
|
|
165
|
+
const changeHome =
|
|
166
|
+
mode === "chosen"
|
|
167
|
+
? `process.env.HOME=${JSON.stringify(chosenHome)};`
|
|
168
|
+
: mode === "absent"
|
|
169
|
+
? "delete process.env.HOME;"
|
|
170
|
+
: 'process.env.HOME="";';
|
|
171
|
+
const homeExpression =
|
|
172
|
+
mode === "chosen" ? JSON.stringify(chosenHome) : "homedir()";
|
|
173
|
+
|
|
174
|
+
const shown = f.value<string>(
|
|
175
|
+
`import {homedir} from 'node:os';import {join} from 'node:path';${changeHome}const {displayPath}=await import(${JSON.stringify(sourceModule("utils/display-path.js"))});console.log(JSON.stringify(displayPath(join(${homeExpression},'library','team-tools'),${JSON.stringify(f.project)})));`,
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
expect(shown).toBe(join("~", "library", "team-tools"));
|
|
179
|
+
},
|
|
180
|
+
);
|