magus-cli 7.12.2 → 7.14.0
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 +21 -0
- package/package.json +4 -4
- package/src/__tests__/market-bases.test.ts +112 -0
- package/src/__tests__/market-config.test.ts +84 -0
- package/src/__tests__/market-conflicts.test.ts +175 -0
- package/src/__tests__/market-consent.test.ts +185 -0
- package/src/__tests__/market-content.test.ts +183 -0
- package/src/__tests__/market-delivery.test.ts +308 -0
- package/src/__tests__/market-fixture.ts +1167 -0
- package/src/__tests__/market-git.test.ts +130 -0
- package/src/__tests__/market-identity.test.ts +127 -0
- package/src/__tests__/market-install.test.ts +143 -0
- package/src/__tests__/market-layout.test.ts +206 -0
- package/src/__tests__/market-publish.test.ts +263 -0
- 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-readonly.test.ts +162 -0
- package/src/__tests__/market-repositories.test.ts +263 -0
- package/src/__tests__/market-results.test.ts +198 -0
- package/src/__tests__/market-router.test.ts +65 -0
- package/src/__tests__/market-safety.test.ts +357 -0
- package/src/__tests__/market-teammates.test.ts +304 -0
- package/src/cli/install.ts +64 -5
- package/src/cli/market-view.ts +329 -0
- package/src/cli/market.ts +664 -0
- package/src/cli/router.ts +69 -3
- package/src/cli/update-view.ts +56 -0
- package/src/cli/update.ts +111 -9
- package/src/data/marketplaces.ts +8 -2
- package/src/main.tsx +20 -2
- package/src/market-tui.tsx +21 -0
- package/src/services/active-closure.ts +71 -9
- package/src/services/claude-cli.ts +140 -10
- package/src/services/claude-settings.ts +19 -25
- package/src/services/doctor.ts +5 -5
- package/src/services/git-write.ts +615 -0
- package/src/services/github-cli.ts +309 -0
- package/src/services/local-marketplace.ts +112 -50
- package/src/services/market-bases.ts +191 -0
- package/src/services/market-change.ts +242 -0
- package/src/services/market-clone.ts +572 -0
- package/src/services/market-fs.ts +138 -0
- package/src/services/market-install.ts +172 -0
- package/src/services/market-layout.ts +311 -0
- package/src/services/market-local.ts +93 -0
- package/src/services/market-marker.ts +49 -0
- package/src/services/market-ops-content.ts +277 -0
- package/src/services/market-ops-plugin.ts +655 -0
- package/src/services/market-ops-publish.ts +1227 -0
- package/src/services/market-ops-repo.ts +916 -0
- package/src/services/market-ops-skill.ts +788 -0
- package/src/services/market-ops.ts +686 -0
- package/src/services/market-publish.ts +468 -0
- package/src/services/market-registry.ts +382 -0
- package/src/services/market-types.ts +268 -0
- package/src/services/marketplace-conflicts.ts +127 -0
- package/src/services/marketplace-refresh.ts +33 -3
- package/src/services/marketplace-sync.ts +253 -22
- package/src/services/plugin-dependencies.ts +17 -0
- package/src/services/plugin-manager.ts +1 -1
- package/src/services/plugin-mcp-config.ts +6 -9
- package/src/services/plugin-requires.ts +20 -8
- package/src/services/profile-absorb.ts +88 -3
- package/src/services/profile-adopt.ts +10 -1
- package/src/services/profiles.ts +64 -1
- package/src/services/resolver.ts +17 -5
- package/src/services/settings-manager.ts +2 -2
- package/src/services/skills-manager.ts +2 -2
- package/src/services/update-plan.ts +67 -8
- package/src/tui-runtime.tsx +142 -0
- package/src/tui.tsx +7 -124
- package/src/types/index.ts +7 -0
- package/src/ui/adapters/marketAdapter.ts +366 -0
- package/src/ui/adapters/pluginsAdapter.ts +45 -3
- package/src/ui/components/layout/ScreenLayout.tsx +43 -17
- package/src/ui/components/modals/ConfirmModal.tsx +145 -8
- package/src/ui/components/modals/HelpOverlay.tsx +10 -3
- package/src/ui/components/modals/ModalContainer.tsx +47 -5
- package/src/ui/components/primitives/ListCategoryRow.tsx +22 -2
- package/src/ui/components/primitives/ScopeBadge.tsx +28 -0
- package/src/ui/components/primitives/ScopeVersionLine.tsx +27 -0
- package/src/ui/components/primitives/SelectableRow.tsx +14 -1
- package/src/ui/components/primitives/ToneLine.tsx +25 -0
- package/src/ui/components/primitives/index.ts +5 -0
- package/src/ui/keymap.ts +21 -5
- package/src/ui/market/MarketApp.tsx +59 -0
- package/src/ui/market/MarketplaceScreen.tsx +280 -0
- package/src/ui/market/MarketplacesDetail.snowflake.tsx +161 -0
- package/src/ui/market/MarketplacesScreen.tsx +264 -0
- package/src/ui/market/PluginDetail.snowflake.tsx +61 -0
- package/src/ui/market/SkillDetail.snowflake.tsx +71 -0
- package/src/ui/market/marketKeymap.ts +243 -0
- package/src/ui/market/useMarketState.ts +694 -0
- package/src/ui/renderers/pluginRenderers.tsx +31 -3
- package/src/ui/renderers/pluginStatusText.ts +6 -0
- package/src/ui/screens/PluginsScreen.tsx +49 -5
- package/src/ui/screens/SkillsScreen.tsx +2 -2
- package/src/ui/state/AppContext.tsx +14 -2
- package/src/ui/state/types.ts +9 -2
- package/src/utils/config-dir.ts +29 -9
- package/src/utils/display-path.ts +33 -0
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import { afterEach, expect, test } from "bun:test";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import type { LocalMarketplace } from "../services/local-marketplace.js";
|
|
5
|
+
import type { Marketplace } from "../types/index.js";
|
|
6
|
+
import {
|
|
7
|
+
MANIFEST,
|
|
8
|
+
MarketFixture,
|
|
9
|
+
readJson,
|
|
10
|
+
sourceModule,
|
|
11
|
+
writeJson,
|
|
12
|
+
} from "./market-fixture.js";
|
|
13
|
+
|
|
14
|
+
let f: MarketFixture | undefined;
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
f?.close();
|
|
17
|
+
f = undefined;
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test.each([
|
|
21
|
+
{
|
|
22
|
+
label: "split flag",
|
|
23
|
+
flags: ["--company", " 10xlabs "],
|
|
24
|
+
company: "10xlabs",
|
|
25
|
+
},
|
|
26
|
+
{ label: "equals flag", flags: ["--company=10xlabs"], company: "10xlabs" },
|
|
27
|
+
{
|
|
28
|
+
label: "64 Unicode code points",
|
|
29
|
+
flags: [`--company=${"🏢".repeat(64)}`],
|
|
30
|
+
company: "🏢".repeat(64),
|
|
31
|
+
},
|
|
32
|
+
{ label: "GitHub owner default", flags: [], company: "different-owner" },
|
|
33
|
+
])(
|
|
34
|
+
"R1.1 R1.2 TEST-10 create writes owner.name from $label",
|
|
35
|
+
({ flags, company }) => {
|
|
36
|
+
f = new MarketFixture();
|
|
37
|
+
|
|
38
|
+
const result = f.market([
|
|
39
|
+
"create",
|
|
40
|
+
"different-owner/team-tools",
|
|
41
|
+
"--private",
|
|
42
|
+
"--name",
|
|
43
|
+
"team",
|
|
44
|
+
...flags,
|
|
45
|
+
"--yes",
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
expect({ exit: result.code, outcome: result.result.outcome }).toEqual({
|
|
49
|
+
exit: 0,
|
|
50
|
+
outcome: "ok",
|
|
51
|
+
});
|
|
52
|
+
const bare = join(f.root, "remotes", "different-owner", "team-tools.git");
|
|
53
|
+
const manifest = JSON.parse(
|
|
54
|
+
f.git(f.root, "--git-dir", bare, "show", `HEAD:${MANIFEST}`),
|
|
55
|
+
);
|
|
56
|
+
expect(manifest.owner).toEqual({ name: company });
|
|
57
|
+
expect(
|
|
58
|
+
f.market(["list"]).result.marketplaces?.find((row) => row.name === "team")
|
|
59
|
+
?.company,
|
|
60
|
+
).toBe(company);
|
|
61
|
+
f.assertOwned();
|
|
62
|
+
},
|
|
63
|
+
60_000,
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const invalidCompanies = [
|
|
67
|
+
"",
|
|
68
|
+
" ",
|
|
69
|
+
"Acme\nLabs",
|
|
70
|
+
"Acme\rLabs",
|
|
71
|
+
"Acme\tLabs",
|
|
72
|
+
"Acme\u0000Labs",
|
|
73
|
+
"Acme\u007fLabs",
|
|
74
|
+
"Acme
Labs",
|
|
75
|
+
"Acme
Labs",
|
|
76
|
+
"x".repeat(65),
|
|
77
|
+
"🏢".repeat(65),
|
|
78
|
+
];
|
|
79
|
+
// NUL cannot be represented in an OS argv; its validation is covered by TEST-2.
|
|
80
|
+
for (const command of ["create", "connect"] as const) {
|
|
81
|
+
test.each(invalidCompanies.filter((name) => !name.includes("\u0000")))(
|
|
82
|
+
`R1.3 TEST-11 ${command} rejects company %j before any GitHub work`,
|
|
83
|
+
(company) => {
|
|
84
|
+
f = new MarketFixture();
|
|
85
|
+
const before = f.snapshot();
|
|
86
|
+
const args =
|
|
87
|
+
command === "create"
|
|
88
|
+
? ["create", "acme/not-created", "--private"]
|
|
89
|
+
: ["connect", "acme/not-created"];
|
|
90
|
+
|
|
91
|
+
const result = f.market([...args, `--company=${company}`, "--yes"]);
|
|
92
|
+
|
|
93
|
+
expect(result.code).toBe(2);
|
|
94
|
+
expect(f.snapshot()).toEqual(before);
|
|
95
|
+
expect(
|
|
96
|
+
existsSync(join(f.root, "remotes", "acme", "not-created.git")),
|
|
97
|
+
).toBe(false);
|
|
98
|
+
expect(f.journal().filter((event) => event.tool === "gh")).toEqual([]);
|
|
99
|
+
},
|
|
100
|
+
30_000,
|
|
101
|
+
);
|
|
102
|
+
test(`R1.3 TEST-11 ${command} rejects a missing --company value without writes`, () => {
|
|
103
|
+
f = new MarketFixture();
|
|
104
|
+
const before = f.snapshot();
|
|
105
|
+
const args =
|
|
106
|
+
command === "create"
|
|
107
|
+
? ["create", "acme/not-created", "--private"]
|
|
108
|
+
: ["connect", "acme/not-created"];
|
|
109
|
+
|
|
110
|
+
const result = f.cli(["market", ...args, "--company"]);
|
|
111
|
+
|
|
112
|
+
expect(result.code).toBe(2);
|
|
113
|
+
expect(f.snapshot()).toEqual(before);
|
|
114
|
+
expect(f.journal()).toEqual([]);
|
|
115
|
+
}, 30_000);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
test.each(["create", "connect"] as const)(
|
|
119
|
+
"R1.4 R4.5 TEST-12 %s plan displays the trimmed company and binds consent to it",
|
|
120
|
+
(command) => {
|
|
121
|
+
f = new MarketFixture();
|
|
122
|
+
const repo = command === "connect" ? f.seed({ empty: true }) : undefined;
|
|
123
|
+
const base =
|
|
124
|
+
command === "create"
|
|
125
|
+
? ["create", "acme/new-market", "--private", "--name", "acme"]
|
|
126
|
+
: ["connect", repo!.slug, "--name", "acme"];
|
|
127
|
+
const before = f.snapshot();
|
|
128
|
+
const plan = f.market([...base, "--company", " 10xlabs "]);
|
|
129
|
+
const equivalent = f.market([...base, "--company=10xlabs"]);
|
|
130
|
+
|
|
131
|
+
expect(plan.code).toBe(3);
|
|
132
|
+
expect(plan.result.outcome).toBe("needs-consent");
|
|
133
|
+
expect(plan.result.plan).toContain(
|
|
134
|
+
'Company name: 10xlabs (magus shows the marketplace as "by 10xlabs")',
|
|
135
|
+
);
|
|
136
|
+
expect(typeof plan.result.planId).toBe("string");
|
|
137
|
+
expect(plan.result.planId?.length).toBeGreaterThan(0);
|
|
138
|
+
expect(equivalent.result.planId).toBe(plan.result.planId);
|
|
139
|
+
expect(f.snapshot()).toEqual(before);
|
|
140
|
+
|
|
141
|
+
const changed = f.market([
|
|
142
|
+
...base,
|
|
143
|
+
"--company",
|
|
144
|
+
"Other Labs",
|
|
145
|
+
`--yes=${plan.result.planId}`,
|
|
146
|
+
]);
|
|
147
|
+
|
|
148
|
+
expect({
|
|
149
|
+
exit: changed.code,
|
|
150
|
+
outcome: changed.result.outcome,
|
|
151
|
+
error: changed.result.error?.code,
|
|
152
|
+
}).toEqual({
|
|
153
|
+
exit: 3,
|
|
154
|
+
outcome: "needs-consent",
|
|
155
|
+
error: "plan-changed",
|
|
156
|
+
});
|
|
157
|
+
expect(changed.result.planId).not.toBe(plan.result.planId);
|
|
158
|
+
expect(changed.result.plan).toContain(
|
|
159
|
+
'Company name: Other Labs (magus shows the marketplace as "by Other Labs")',
|
|
160
|
+
);
|
|
161
|
+
const bare =
|
|
162
|
+
repo?.bare ?? join(f.root, "remotes", "acme", "new-market.git");
|
|
163
|
+
if (repo) {
|
|
164
|
+
expect(
|
|
165
|
+
f.git(f.root, "--git-dir", bare, "rev-list", "--all", "--count"),
|
|
166
|
+
).toBe("0");
|
|
167
|
+
} else {
|
|
168
|
+
expect(existsSync(bare)).toBe(false);
|
|
169
|
+
}
|
|
170
|
+
// A refused apply may sync its disposable clone; no repository or registration is changed.
|
|
171
|
+
expect(f.market(["list"]).result.marketplaces).toEqual([]);
|
|
172
|
+
|
|
173
|
+
const applied = f.market([
|
|
174
|
+
...base,
|
|
175
|
+
"--company=10xlabs",
|
|
176
|
+
`--yes=${plan.result.planId}`,
|
|
177
|
+
]);
|
|
178
|
+
expect(applied.code).toBe(0);
|
|
179
|
+
const manifest = JSON.parse(
|
|
180
|
+
f.git(f.root, "--git-dir", bare, "show", `HEAD:${MANIFEST}`),
|
|
181
|
+
);
|
|
182
|
+
expect(manifest.owner).toEqual({ name: "10xlabs" });
|
|
183
|
+
f.assertOwned();
|
|
184
|
+
},
|
|
185
|
+
60_000,
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
test.each([
|
|
189
|
+
{
|
|
190
|
+
empty: true,
|
|
191
|
+
companyFlag: ["--company", " 10xlabs "],
|
|
192
|
+
expected: "10xlabs",
|
|
193
|
+
},
|
|
194
|
+
{ empty: false, companyFlag: ["--company=10xlabs"], expected: "10xlabs" },
|
|
195
|
+
{ empty: true, companyFlag: [], expected: "acme" },
|
|
196
|
+
{ empty: false, companyFlag: [], expected: "acme" },
|
|
197
|
+
])(
|
|
198
|
+
"R1.1 R1.5 TEST-13 connect writes company only when making a manifest: %j",
|
|
199
|
+
({ empty, companyFlag, expected }) => {
|
|
200
|
+
f = new MarketFixture();
|
|
201
|
+
const repo = f.seed({ empty, noManifest: true });
|
|
202
|
+
const old = empty ? undefined : f.head(repo);
|
|
203
|
+
|
|
204
|
+
const result = f.market([
|
|
205
|
+
"connect",
|
|
206
|
+
repo.slug,
|
|
207
|
+
"--name",
|
|
208
|
+
"acme",
|
|
209
|
+
...companyFlag,
|
|
210
|
+
"--yes",
|
|
211
|
+
]);
|
|
212
|
+
|
|
213
|
+
expect(result.code).toBe(0);
|
|
214
|
+
expect(f.remoteJson(repo, MANIFEST).owner).toEqual({ name: expected });
|
|
215
|
+
if (old) {
|
|
216
|
+
expect(
|
|
217
|
+
f.git(
|
|
218
|
+
f.root,
|
|
219
|
+
"--git-dir",
|
|
220
|
+
repo.bare,
|
|
221
|
+
"rev-list",
|
|
222
|
+
"--count",
|
|
223
|
+
`${old}..HEAD`,
|
|
224
|
+
),
|
|
225
|
+
).toBe("1");
|
|
226
|
+
expect(f.remoteFile(repo, "README.md")).toBe(
|
|
227
|
+
"Existing repository documentation",
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
f.assertOwned();
|
|
231
|
+
},
|
|
232
|
+
60_000,
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
test.each([true, false])(
|
|
236
|
+
"R1.5 TEST-14 existing marketplace refuses --company, marker=%s",
|
|
237
|
+
(marker) => {
|
|
238
|
+
f = new MarketFixture();
|
|
239
|
+
const repo = f.seed({ marker });
|
|
240
|
+
const manifest = readJson(join(repo.author, MANIFEST));
|
|
241
|
+
manifest.owner = { name: "Manifest Company" };
|
|
242
|
+
writeJson(join(repo.author, MANIFEST), manifest);
|
|
243
|
+
f.push(repo);
|
|
244
|
+
const before = f.snapshot();
|
|
245
|
+
const head = f.head(repo);
|
|
246
|
+
|
|
247
|
+
const refused = f.market([
|
|
248
|
+
"connect",
|
|
249
|
+
repo.slug,
|
|
250
|
+
"--company",
|
|
251
|
+
"Override",
|
|
252
|
+
"--yes",
|
|
253
|
+
]);
|
|
254
|
+
|
|
255
|
+
expect(refused.code).toBe(2);
|
|
256
|
+
expect(f.head(repo)).toBe(head);
|
|
257
|
+
expect(f.remoteJson(repo, MANIFEST).owner).toEqual({
|
|
258
|
+
name: "Manifest Company",
|
|
259
|
+
});
|
|
260
|
+
expect(f.snapshot()).toEqual(before);
|
|
261
|
+
|
|
262
|
+
const connected = f.market(["connect", repo.slug, "--yes"]);
|
|
263
|
+
expect(connected.code).toBe(0);
|
|
264
|
+
expect(f.remoteJson(repo, MANIFEST).owner).toEqual({
|
|
265
|
+
name: "Manifest Company",
|
|
266
|
+
});
|
|
267
|
+
expect(f.market(["list"]).result.marketplaces?.[0]?.company).toBe(
|
|
268
|
+
"Manifest Company",
|
|
269
|
+
);
|
|
270
|
+
f.assertOwned();
|
|
271
|
+
},
|
|
272
|
+
60_000,
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
test.each([
|
|
276
|
+
{ owner: { name: " 10xlabs " }, expected: "10xlabs" },
|
|
277
|
+
{ owner: { name: " " }, expected: undefined },
|
|
278
|
+
{ owner: {}, expected: undefined },
|
|
279
|
+
])(
|
|
280
|
+
"R1.6 TEST-6 scanner and catalog carry trimmed ownerName or omit the key: %j",
|
|
281
|
+
({ owner, expected }) => {
|
|
282
|
+
f = new MarketFixture(true);
|
|
283
|
+
const repo = f.seed();
|
|
284
|
+
const manifest = readJson(join(repo.author, MANIFEST));
|
|
285
|
+
manifest.owner = owner;
|
|
286
|
+
writeJson(join(repo.author, MANIFEST), manifest);
|
|
287
|
+
f.push(repo);
|
|
288
|
+
f.register(repo);
|
|
289
|
+
const before = f.snapshot();
|
|
290
|
+
|
|
291
|
+
const data = f.value<{
|
|
292
|
+
local: LocalMarketplace;
|
|
293
|
+
catalog: Marketplace;
|
|
294
|
+
localHasName: boolean;
|
|
295
|
+
catalogHasName: boolean;
|
|
296
|
+
}>(
|
|
297
|
+
`const {scanLocalMarketplaces}=await import(${JSON.stringify(sourceModule("services/local-marketplace.js"))});const {getAllMarketplaces}=await import(${JSON.stringify(sourceModule("data/marketplaces.js"))});const locals=await scanLocalMarketplaces(); const local=locals.get('acme'); const catalog=getAllMarketplaces(locals).find(m=>m.name==='acme');console.log(JSON.stringify({local,catalog,localHasName:Object.hasOwn(local,'ownerName'),catalogHasName:Object.hasOwn(catalog,'ownerName')}));`,
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
expect(data.local.ownerName).toBe(expected);
|
|
301
|
+
expect(data.catalog.ownerName).toBe(expected);
|
|
302
|
+
expect(data.localHasName).toBe(expected !== undefined);
|
|
303
|
+
expect(data.catalogHasName).toBe(expected !== undefined);
|
|
304
|
+
expect(f.snapshot()).toEqual(before);
|
|
305
|
+
},
|
|
306
|
+
30_000,
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
test.each([
|
|
310
|
+
{ owner: { name: " 10xlabs " }, company: "10xlabs" },
|
|
311
|
+
{ owner: { name: "" }, company: "acme" },
|
|
312
|
+
])(
|
|
313
|
+
"R1.7 TEST-15 list JSON and text show company without changing state: %j",
|
|
314
|
+
({ owner, company }) => {
|
|
315
|
+
f = new MarketFixture(true);
|
|
316
|
+
const repo = f.seed();
|
|
317
|
+
const manifest = readJson(join(repo.author, MANIFEST));
|
|
318
|
+
manifest.owner = owner;
|
|
319
|
+
writeJson(join(repo.author, MANIFEST), manifest);
|
|
320
|
+
f.push(repo);
|
|
321
|
+
f.register(repo);
|
|
322
|
+
const before = f.snapshot();
|
|
323
|
+
const head = f.head(repo);
|
|
324
|
+
|
|
325
|
+
const json = f.market(["list"]);
|
|
326
|
+
const text = f.cli(["market", "list"]);
|
|
327
|
+
|
|
328
|
+
expect(json.code).toBe(0);
|
|
329
|
+
expect(
|
|
330
|
+
json.result.marketplaces?.map(
|
|
331
|
+
({ name, repo: source, company: value, scope }) => ({
|
|
332
|
+
name,
|
|
333
|
+
repo: source,
|
|
334
|
+
company: value,
|
|
335
|
+
scope,
|
|
336
|
+
}),
|
|
337
|
+
),
|
|
338
|
+
).toEqual([{ name: "acme", repo: "acme/market", company, scope: "user" }]);
|
|
339
|
+
expect(text.code).toBe(0);
|
|
340
|
+
const line = text.out
|
|
341
|
+
.split("\n")
|
|
342
|
+
.find((line) => line.includes(`by ${company}`));
|
|
343
|
+
expect(line).toBeDefined();
|
|
344
|
+
expect(line).toContain("acme/market");
|
|
345
|
+
expect(line).toContain("USER");
|
|
346
|
+
expect(f.snapshot()).toEqual(before);
|
|
347
|
+
expect(f.head(repo)).toBe(head);
|
|
348
|
+
},
|
|
349
|
+
30_000,
|
|
350
|
+
);
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
COMPANY_NAME_MAX,
|
|
4
|
+
checkCompanyName,
|
|
5
|
+
} from "../services/market-ops-repo.js";
|
|
6
|
+
import { companyOf } from "../services/market-ops.js";
|
|
7
|
+
import { MarketError } from "../services/market-types.js";
|
|
8
|
+
import type { Marketplace } from "../types/index.js";
|
|
9
|
+
import {
|
|
10
|
+
buildPluginBrowserItems,
|
|
11
|
+
companyName,
|
|
12
|
+
} from "../ui/adapters/pluginsAdapter.js";
|
|
13
|
+
|
|
14
|
+
const marketplace = (over: Partial<Marketplace> = {}): Marketplace => ({
|
|
15
|
+
name: "company-tools",
|
|
16
|
+
displayName: "Company tools",
|
|
17
|
+
description: "Team tools",
|
|
18
|
+
source: { source: "github", repo: "github-owner/tools" },
|
|
19
|
+
company: true,
|
|
20
|
+
...over,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe("round-two company identity", () => {
|
|
24
|
+
test.each([
|
|
25
|
+
{ input: " 10xlabs ", expected: "10xlabs" },
|
|
26
|
+
{ input: "A", expected: "A" },
|
|
27
|
+
{ input: " North Star Labs ", expected: "North Star Labs" },
|
|
28
|
+
{ input: "x".repeat(64), expected: "x".repeat(64) },
|
|
29
|
+
{ input: ` ${"界".repeat(64)} `, expected: "界".repeat(64) },
|
|
30
|
+
{ input: "🏢".repeat(64), expected: "🏢".repeat(64) },
|
|
31
|
+
])(
|
|
32
|
+
"R1.3 TEST-1 returns trimmed company name at valid boundaries: $input",
|
|
33
|
+
({ input, expected }) => {
|
|
34
|
+
expect(COMPANY_NAME_MAX).toBe(64);
|
|
35
|
+
expect(checkCompanyName(input)).toBe(expected);
|
|
36
|
+
},
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const invalidNames = [
|
|
40
|
+
{ input: "", message: "The company name is empty" },
|
|
41
|
+
{ input: " ", message: "The company name is empty" },
|
|
42
|
+
{
|
|
43
|
+
input: "x".repeat(65),
|
|
44
|
+
message: "The company name is longer than 64 characters",
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
input: "🏢".repeat(65),
|
|
48
|
+
message: "The company name is longer than 64 characters",
|
|
49
|
+
},
|
|
50
|
+
...[0, 1, 8, 9, 10, 11, 12, 13, 31, 127, 128, 133, 159, 0x2028, 0x2029].map(
|
|
51
|
+
(code) => ({
|
|
52
|
+
input: `Acme${String.fromCodePoint(code)}Labs`,
|
|
53
|
+
message: "The company name must be a single line",
|
|
54
|
+
}),
|
|
55
|
+
),
|
|
56
|
+
];
|
|
57
|
+
test.each(invalidNames)(
|
|
58
|
+
"R1.3 TEST-2 rejects invalid company name %j as invalid-name usage",
|
|
59
|
+
({ input, message }) => {
|
|
60
|
+
expect(() => checkCompanyName(input)).toThrow(MarketError);
|
|
61
|
+
expect(() => checkCompanyName(input)).toThrow(message);
|
|
62
|
+
let failure: unknown;
|
|
63
|
+
try {
|
|
64
|
+
checkCompanyName(input);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
failure = error;
|
|
67
|
+
}
|
|
68
|
+
expect(failure).toBeInstanceOf(MarketError);
|
|
69
|
+
if (!(failure instanceof MarketError))
|
|
70
|
+
throw new Error("Expected a company-name MarketError");
|
|
71
|
+
expect({ code: failure.code, exit: failure.exit }).toEqual({
|
|
72
|
+
code: "invalid-name",
|
|
73
|
+
exit: 2,
|
|
74
|
+
});
|
|
75
|
+
},
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
test.each([
|
|
79
|
+
{
|
|
80
|
+
manifest: { owner: { name: " 10xlabs " } },
|
|
81
|
+
source: "git-owner/market",
|
|
82
|
+
expected: "10xlabs",
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
manifest: { owner: { name: "10xlabs" } },
|
|
86
|
+
source: "https://git.example.invalid/team/market",
|
|
87
|
+
expected: "10xlabs",
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
manifest: { owner: { name: " " } },
|
|
91
|
+
source: "GitOwner/market",
|
|
92
|
+
expected: "GitOwner",
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
manifest: { owner: { name: 42 } },
|
|
96
|
+
source: "GitOwner/market",
|
|
97
|
+
expected: "GitOwner",
|
|
98
|
+
},
|
|
99
|
+
{ manifest: {}, source: "GitOwner/market", expected: "GitOwner" },
|
|
100
|
+
{ manifest: null, source: "GitOwner/market", expected: "GitOwner" },
|
|
101
|
+
{ manifest: { owner: null }, source: "", expected: undefined },
|
|
102
|
+
{
|
|
103
|
+
manifest: { owner: { name: "" } },
|
|
104
|
+
source: "https://git.example.invalid/team/market",
|
|
105
|
+
expected: undefined,
|
|
106
|
+
},
|
|
107
|
+
{ manifest: {}, source: "file:///company-market", expected: undefined },
|
|
108
|
+
])(
|
|
109
|
+
"R1.6 TEST-3 companyOf prefers manifest owner and falls back only to GitHub source: %j",
|
|
110
|
+
({ manifest, source, expected }) => {
|
|
111
|
+
expect(companyOf(manifest, source)).toBe(expected);
|
|
112
|
+
},
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
test.each([
|
|
116
|
+
{ mp: marketplace({ ownerName: "10xlabs" }), expected: "10xlabs" },
|
|
117
|
+
{ mp: marketplace(), expected: "github-owner" },
|
|
118
|
+
{
|
|
119
|
+
mp: marketplace({ source: { source: "github", repo: "" } }),
|
|
120
|
+
expected: undefined,
|
|
121
|
+
},
|
|
122
|
+
])(
|
|
123
|
+
"R1.6 TEST-4 companyName uses manifest, GitHub owner, then no name: %j",
|
|
124
|
+
({ mp, expected }) => {
|
|
125
|
+
expect(companyName(mp)).toBe(expected);
|
|
126
|
+
},
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe("round-two company badges", () => {
|
|
131
|
+
test.each([
|
|
132
|
+
{ mp: marketplace({ ownerName: "10xlabs" }), badge: "by 10xlabs" },
|
|
133
|
+
{ mp: marketplace(), badge: "by github-owner" },
|
|
134
|
+
{
|
|
135
|
+
mp: marketplace({ source: { source: "github", repo: "" } }),
|
|
136
|
+
badge: "◆ Company",
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
mp: marketplace({
|
|
140
|
+
source: { source: "github", repo: "MadAppGang/team-tools" },
|
|
141
|
+
ownerName: "Different Company",
|
|
142
|
+
}),
|
|
143
|
+
badge: "by Different Company",
|
|
144
|
+
},
|
|
145
|
+
])(
|
|
146
|
+
"R1.6 TEST-5 empty company marketplace has the real byline and info tone: $badge",
|
|
147
|
+
({ mp, badge }) => {
|
|
148
|
+
const rows = buildPluginBrowserItems({
|
|
149
|
+
marketplaces: [mp],
|
|
150
|
+
plugins: [],
|
|
151
|
+
collapsedMarketplaces: new Set(),
|
|
152
|
+
});
|
|
153
|
+
const row = rows.find((item) => item.kind === "category");
|
|
154
|
+
expect(row?.kind).toBe("category");
|
|
155
|
+
if (row?.kind !== "category") throw new Error("Company category missing");
|
|
156
|
+
expect({ tone: row.tone, badge: row.badge }).toEqual({
|
|
157
|
+
tone: "info",
|
|
158
|
+
badge,
|
|
159
|
+
});
|
|
160
|
+
},
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
test.each([
|
|
164
|
+
{ name: "A".repeat(24), expected: `by ${"A".repeat(24)}` },
|
|
165
|
+
{ name: "A".repeat(25), expected: `by ${"A".repeat(23)}…` },
|
|
166
|
+
])(
|
|
167
|
+
"R1.6 TEST-5 truncates the company name to 24 columns including ellipsis: $name",
|
|
168
|
+
({ name, expected }) => {
|
|
169
|
+
const row = buildPluginBrowserItems({
|
|
170
|
+
marketplaces: [marketplace({ ownerName: name })],
|
|
171
|
+
plugins: [],
|
|
172
|
+
collapsedMarketplaces: new Set(["company-tools"]),
|
|
173
|
+
}).find((item) => item.kind === "category");
|
|
174
|
+
expect(row?.kind === "category" ? row.badge : undefined).toBe(expected);
|
|
175
|
+
},
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
test("R1.6 TEST-5 wide company characters also fit the 24-column name budget", () => {
|
|
179
|
+
const row = buildPluginBrowserItems({
|
|
180
|
+
marketplaces: [marketplace({ ownerName: "界".repeat(20) })],
|
|
181
|
+
plugins: [],
|
|
182
|
+
collapsedMarketplaces: new Set(),
|
|
183
|
+
}).find((item) => item.kind === "category");
|
|
184
|
+
if (row?.kind !== "category" || !row.badge)
|
|
185
|
+
throw new Error("Company badge missing");
|
|
186
|
+
expect(row.badge.startsWith("by ")).toBe(true);
|
|
187
|
+
expect(row.badge.endsWith("…")).toBe(true);
|
|
188
|
+
expect(Bun.stringWidth(row.badge.slice(3))).toBeLessThanOrEqual(24);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("R1.6 TEST-5 company marketplaces sort before ordinary and MadAppGang marketplaces", () => {
|
|
192
|
+
const rows = buildPluginBrowserItems({
|
|
193
|
+
marketplaces: [
|
|
194
|
+
marketplace({
|
|
195
|
+
name: "ordinary",
|
|
196
|
+
company: false,
|
|
197
|
+
source: { source: "github", repo: "other/ordinary" },
|
|
198
|
+
}),
|
|
199
|
+
marketplace({
|
|
200
|
+
name: "magus",
|
|
201
|
+
company: false,
|
|
202
|
+
source: { source: "github", repo: "MadAppGang/magus" },
|
|
203
|
+
}),
|
|
204
|
+
marketplace({ name: "company-tools", ownerName: "10xlabs" }),
|
|
205
|
+
],
|
|
206
|
+
plugins: ["ordinary", "magus", "company-tools"].map((name) => ({
|
|
207
|
+
id: `demo@${name}`,
|
|
208
|
+
name: "demo",
|
|
209
|
+
version: "1.0.0",
|
|
210
|
+
description: "Demonstration plugin",
|
|
211
|
+
marketplace: name,
|
|
212
|
+
marketplaceDisplay: name,
|
|
213
|
+
author: { name: "Publisher" },
|
|
214
|
+
})),
|
|
215
|
+
collapsedMarketplaces: new Set(),
|
|
216
|
+
}).filter((item) => item.kind === "category");
|
|
217
|
+
expect(rows.map((row) => row.marketplace.name).sort()).toEqual([
|
|
218
|
+
"company-tools",
|
|
219
|
+
"magus",
|
|
220
|
+
"ordinary",
|
|
221
|
+
]);
|
|
222
|
+
expect(rows[0]?.marketplace.name).toBe("company-tools");
|
|
223
|
+
});
|
|
224
|
+
});
|