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.
Files changed (111) hide show
  1. package/README.md +21 -0
  2. package/package.json +4 -4
  3. package/src/__tests__/market-bases.test.ts +112 -0
  4. package/src/__tests__/market-config.test.ts +84 -0
  5. package/src/__tests__/market-conflicts.test.ts +175 -0
  6. package/src/__tests__/market-consent.test.ts +185 -0
  7. package/src/__tests__/market-content.test.ts +183 -0
  8. package/src/__tests__/market-delivery.test.ts +308 -0
  9. package/src/__tests__/market-fixture.ts +1167 -0
  10. package/src/__tests__/market-git.test.ts +130 -0
  11. package/src/__tests__/market-identity.test.ts +127 -0
  12. package/src/__tests__/market-install.test.ts +143 -0
  13. package/src/__tests__/market-layout.test.ts +206 -0
  14. package/src/__tests__/market-publish.test.ts +263 -0
  15. package/src/__tests__/market-r2-adapters.test.ts +190 -0
  16. package/src/__tests__/market-r2-company-cli.test.ts +350 -0
  17. package/src/__tests__/market-r2-company-unit.test.ts +224 -0
  18. package/src/__tests__/market-r2-local-items.test.ts +197 -0
  19. package/src/__tests__/market-r2-plugin-add.test.ts +470 -0
  20. package/src/__tests__/market-r2-plugin-publish.test.ts +568 -0
  21. package/src/__tests__/market-r2-routing.test.ts +127 -0
  22. package/src/__tests__/market-r2-skill-create-plugin.test.ts +315 -0
  23. package/src/__tests__/market-r2b-display-path.test.ts +180 -0
  24. package/src/__tests__/market-r2b-fit-row.test.ts +287 -0
  25. package/src/__tests__/market-r2b-local-items.test.ts +253 -0
  26. package/src/__tests__/market-r2b-plugin-add.test.ts +130 -0
  27. package/src/__tests__/market-readonly.test.ts +162 -0
  28. package/src/__tests__/market-repositories.test.ts +263 -0
  29. package/src/__tests__/market-results.test.ts +198 -0
  30. package/src/__tests__/market-router.test.ts +65 -0
  31. package/src/__tests__/market-safety.test.ts +357 -0
  32. package/src/__tests__/market-teammates.test.ts +304 -0
  33. package/src/cli/install.ts +64 -5
  34. package/src/cli/market-view.ts +329 -0
  35. package/src/cli/market.ts +664 -0
  36. package/src/cli/router.ts +69 -3
  37. package/src/cli/update-view.ts +56 -0
  38. package/src/cli/update.ts +111 -9
  39. package/src/data/marketplaces.ts +8 -2
  40. package/src/main.tsx +20 -2
  41. package/src/market-tui.tsx +21 -0
  42. package/src/services/active-closure.ts +71 -9
  43. package/src/services/claude-cli.ts +140 -10
  44. package/src/services/claude-settings.ts +19 -25
  45. package/src/services/doctor.ts +5 -5
  46. package/src/services/git-write.ts +615 -0
  47. package/src/services/github-cli.ts +309 -0
  48. package/src/services/local-marketplace.ts +112 -50
  49. package/src/services/market-bases.ts +191 -0
  50. package/src/services/market-change.ts +242 -0
  51. package/src/services/market-clone.ts +572 -0
  52. package/src/services/market-fs.ts +138 -0
  53. package/src/services/market-install.ts +172 -0
  54. package/src/services/market-layout.ts +311 -0
  55. package/src/services/market-local.ts +93 -0
  56. package/src/services/market-marker.ts +49 -0
  57. package/src/services/market-ops-content.ts +277 -0
  58. package/src/services/market-ops-plugin.ts +655 -0
  59. package/src/services/market-ops-publish.ts +1227 -0
  60. package/src/services/market-ops-repo.ts +916 -0
  61. package/src/services/market-ops-skill.ts +788 -0
  62. package/src/services/market-ops.ts +686 -0
  63. package/src/services/market-publish.ts +468 -0
  64. package/src/services/market-registry.ts +382 -0
  65. package/src/services/market-types.ts +268 -0
  66. package/src/services/marketplace-conflicts.ts +127 -0
  67. package/src/services/marketplace-refresh.ts +33 -3
  68. package/src/services/marketplace-sync.ts +253 -22
  69. package/src/services/plugin-dependencies.ts +17 -0
  70. package/src/services/plugin-manager.ts +1 -1
  71. package/src/services/plugin-mcp-config.ts +6 -9
  72. package/src/services/plugin-requires.ts +20 -8
  73. package/src/services/profile-absorb.ts +88 -3
  74. package/src/services/profile-adopt.ts +10 -1
  75. package/src/services/profiles.ts +64 -1
  76. package/src/services/resolver.ts +17 -5
  77. package/src/services/settings-manager.ts +2 -2
  78. package/src/services/skills-manager.ts +2 -2
  79. package/src/services/update-plan.ts +67 -8
  80. package/src/tui-runtime.tsx +142 -0
  81. package/src/tui.tsx +7 -124
  82. package/src/types/index.ts +7 -0
  83. package/src/ui/adapters/marketAdapter.ts +366 -0
  84. package/src/ui/adapters/pluginsAdapter.ts +45 -3
  85. package/src/ui/components/layout/ScreenLayout.tsx +43 -17
  86. package/src/ui/components/modals/ConfirmModal.tsx +145 -8
  87. package/src/ui/components/modals/HelpOverlay.tsx +10 -3
  88. package/src/ui/components/modals/ModalContainer.tsx +47 -5
  89. package/src/ui/components/primitives/ListCategoryRow.tsx +22 -2
  90. package/src/ui/components/primitives/ScopeBadge.tsx +28 -0
  91. package/src/ui/components/primitives/ScopeVersionLine.tsx +27 -0
  92. package/src/ui/components/primitives/SelectableRow.tsx +14 -1
  93. package/src/ui/components/primitives/ToneLine.tsx +25 -0
  94. package/src/ui/components/primitives/index.ts +5 -0
  95. package/src/ui/keymap.ts +21 -5
  96. package/src/ui/market/MarketApp.tsx +59 -0
  97. package/src/ui/market/MarketplaceScreen.tsx +280 -0
  98. package/src/ui/market/MarketplacesDetail.snowflake.tsx +161 -0
  99. package/src/ui/market/MarketplacesScreen.tsx +264 -0
  100. package/src/ui/market/PluginDetail.snowflake.tsx +61 -0
  101. package/src/ui/market/SkillDetail.snowflake.tsx +71 -0
  102. package/src/ui/market/marketKeymap.ts +243 -0
  103. package/src/ui/market/useMarketState.ts +694 -0
  104. package/src/ui/renderers/pluginRenderers.tsx +31 -3
  105. package/src/ui/renderers/pluginStatusText.ts +6 -0
  106. package/src/ui/screens/PluginsScreen.tsx +49 -5
  107. package/src/ui/screens/SkillsScreen.tsx +2 -2
  108. package/src/ui/state/AppContext.tsx +14 -2
  109. package/src/ui/state/types.ts +9 -2
  110. package/src/utils/config-dir.ts +29 -9
  111. package/src/utils/display-path.ts +33 -0
@@ -0,0 +1,263 @@
1
+ import { afterEach, expect, test } from "bun:test";
2
+ import { existsSync, readFileSync, rmSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import {
5
+ EDITED,
6
+ ID,
7
+ MANIFEST,
8
+ MarketFixture,
9
+ PLUGIN_MANIFEST,
10
+ REMOTE_EDIT,
11
+ SKILL,
12
+ SKILL_REL,
13
+ expectPublished,
14
+ readJson,
15
+ sourceModule,
16
+ write,
17
+ writeJson,
18
+ } from "./market-fixture.js";
19
+
20
+ let f: MarketFixture | undefined;
21
+ afterEach(() => {
22
+ f?.close();
23
+ f = undefined;
24
+ });
25
+ function baseAt(folder: string) {
26
+ return f!.value(
27
+ `const b=await import(${JSON.stringify(sourceModule("services/market-bases.js"))}); console.log(JSON.stringify(b.readBase(${JSON.stringify(folder)})));`,
28
+ );
29
+ }
30
+
31
+ test.each([false, true])(
32
+ "TEST-23 REQ-10 REQ-29 two edit/publish cycles need no force; keep-local=%s",
33
+ async (keep) => {
34
+ f = new MarketFixture();
35
+ const repo = f.seed();
36
+ f.register(repo);
37
+ f.install();
38
+ const initial = f.head(repo);
39
+ f.edit();
40
+ expect(f.head(repo)).toBe(initial);
41
+ expect(readFileSync(join(f.folder(), "SKILL.md"), "utf8")).toBe(SKILL);
42
+ const firstToken = readFileSync(join(f.folder(), ".magus-origin"), "utf8");
43
+ write(join(f.folder(), "SKILL.md"), EDITED);
44
+
45
+ const first = f.market([
46
+ "publish",
47
+ "review",
48
+ "--plugin",
49
+ ID,
50
+ "--yes",
51
+ ...(keep ? ["--keep-local"] : []),
52
+ ]);
53
+
54
+ expect(first.code).toBe(0);
55
+ expectPublished(f, repo);
56
+ expect(f.version()).toBe("0.3.2");
57
+ expect(first.result.localCopy?.action).toBe(keep ? "kept" : "removed");
58
+ if (keep) {
59
+ expect(readFileSync(join(f.folder(), ".magus-origin"), "utf8")).not.toBe(
60
+ firstToken,
61
+ );
62
+ expect(baseAt(f.folder())).not.toBeNull();
63
+ } else {
64
+ expect(existsSync(f.folder())).toBe(false);
65
+ expect(baseAt(f.folder())).toBeNull();
66
+ f.edit();
67
+ }
68
+ const next = `${EDITED}\nReview retries too.\n`;
69
+ write(join(f.folder(), "SKILL.md"), next);
70
+
71
+ const second = f.market([
72
+ "publish",
73
+ "review",
74
+ "--plugin",
75
+ ID,
76
+ "--yes",
77
+ ...(keep ? ["--keep-local"] : []),
78
+ ]);
79
+
80
+ expect(second.code).toBe(0);
81
+ expectPublished(f, repo, "0.3.3", next);
82
+ expect(f.version()).toBe("0.3.3");
83
+ expect(second.result.localCopy?.action).toBe(keep ? "kept" : "removed");
84
+ f.assertOwned();
85
+ },
86
+ 60_000,
87
+ );
88
+
89
+ test("TEST-24 REQ-11 identical local content is unchanged even without a base", () => {
90
+ f = new MarketFixture();
91
+ const repo = f.seed();
92
+ f.register(repo);
93
+ write(join(f.folder(), "SKILL.md"), SKILL);
94
+ const before = f.snapshot();
95
+ const head = f.head(repo);
96
+
97
+ const r = f.market(["publish", "review", "--plugin", ID]);
98
+
99
+ expect({ code: r.code, outcome: r.result.outcome }).toEqual({
100
+ code: 0,
101
+ outcome: "unchanged",
102
+ });
103
+ expect(f.head(repo)).toBe(head);
104
+ expect(f.snapshot()).toEqual(before);
105
+ }, 30_000);
106
+
107
+ test("TEST-24 REQ-11 an unchanged old copy reports plugin-newer and writes nothing", () => {
108
+ f = new MarketFixture();
109
+ const repo = f.seed();
110
+ f.register(repo);
111
+ f.install();
112
+ f.edit();
113
+ write(join(repo.author, SKILL_REL), REMOTE_EDIT);
114
+ const plugin = readJson(join(repo.author, PLUGIN_MANIFEST));
115
+ plugin.version = "0.3.2";
116
+ writeJson(join(repo.author, PLUGIN_MANIFEST), plugin);
117
+ f.push(repo);
118
+ f.ok(["fetch"]);
119
+ const before = f.snapshot();
120
+ const head = f.head(repo);
121
+
122
+ const r = f.market(["publish", "review", "--plugin", ID]);
123
+
124
+ expect({ code: r.code, outcome: r.result.outcome }).toEqual({
125
+ code: 0,
126
+ outcome: "plugin-newer",
127
+ });
128
+ expect(f.snapshot()).toEqual(before);
129
+ expect(f.head(repo)).toBe(head);
130
+ }, 30_000);
131
+
132
+ test.each(["no-base", "conflict"] as const)(
133
+ "TEST-24 REQ-11 %s refuses first; explicit force has its own consent and replacement",
134
+ async (state) => {
135
+ f = new MarketFixture();
136
+ const repo = state === "conflict" ? await f.prepared() : f.seed();
137
+ if (state === "no-base") {
138
+ f.register(repo);
139
+ f.install();
140
+ write(join(f.folder(), "SKILL.md"), EDITED);
141
+ } else {
142
+ write(join(repo.author, SKILL_REL), REMOTE_EDIT);
143
+ const manifest = readJson(join(repo.author, MANIFEST));
144
+ manifest.plugins[0].version = "0.3.2";
145
+ writeJson(join(repo.author, MANIFEST), manifest);
146
+ writeJson(join(repo.author, PLUGIN_MANIFEST), {
147
+ name: "review-kit",
148
+ version: "0.3.2",
149
+ });
150
+ f.push(repo);
151
+ f.ok(["fetch"]);
152
+ }
153
+ const before = f.snapshot();
154
+ const head = f.head(repo);
155
+
156
+ const refused = f.market(["publish", "review", "--plugin", ID]);
157
+
158
+ expect(refused.code).toBe(1);
159
+ expect(refused.result.error?.code).toBe(state);
160
+ expect(
161
+ refused.result.files?.some((file) => file.path.endsWith("SKILL.md")),
162
+ ).toBe(true);
163
+ expect(refused.result.plan?.length).toBeGreaterThan(0);
164
+ expect(f.head(repo)).toBe(head);
165
+ expect(f.snapshot()).toEqual(before);
166
+
167
+ const consent = f.market(["publish", "review", "--plugin", ID, "--force"]);
168
+ expect(consent.code).toBe(3);
169
+ expect(typeof consent.result.planId).toBe("string");
170
+ expect(f.head(repo)).toBe(head);
171
+ const applied = f.market([
172
+ "publish",
173
+ "review",
174
+ "--plugin",
175
+ ID,
176
+ "--force",
177
+ `--yes=${consent.result.planId}`,
178
+ ]);
179
+ expect(applied.code).toBe(0);
180
+ expectPublished(f, repo, state === "conflict" ? "0.3.3" : "0.3.2");
181
+ f.assertOwned();
182
+ },
183
+ 60_000,
184
+ );
185
+
186
+ test.each([
187
+ { keep: false, restoreToken: false },
188
+ { keep: false, restoreToken: true },
189
+ { keep: true, restoreToken: true },
190
+ ])(
191
+ "TEST-25 REQ-29 old copy restoration does not reuse path provenance: %j",
192
+ async ({ keep, restoreToken }) => {
193
+ f = new MarketFixture();
194
+ const repo = await f.prepared();
195
+ const oldToken = readFileSync(join(f.folder(), ".magus-origin"), "utf8");
196
+ f.ok([
197
+ "publish",
198
+ "review",
199
+ "--plugin",
200
+ ID,
201
+ "--yes",
202
+ ...(keep ? ["--keep-local"] : []),
203
+ ]);
204
+ rmSync(f.folder(), { recursive: true, force: true });
205
+ write(
206
+ join(f.folder(), "SKILL.md"),
207
+ `${SKILL}\nAn edit restored from before the push.\n`,
208
+ );
209
+ if (restoreToken) write(join(f.folder(), ".magus-origin"), oldToken);
210
+ const head = f.head(repo);
211
+ const before = f.snapshot();
212
+
213
+ const r = f.market(["publish", "review", "--plugin", ID, "--yes"]);
214
+
215
+ expect(r.code).toBe(1);
216
+ expect(r.result.error?.code).toBe("no-base");
217
+ expect(f.head(repo)).toBe(head);
218
+ expect(f.snapshot()).toEqual(before);
219
+ f.assertOwned();
220
+ },
221
+ 60_000,
222
+ );
223
+
224
+ test.each([false, true])(
225
+ "TEST-26 REQ-12 unversioned plugin bumps to 0.0.1; no plugin manifest=%s",
226
+ (noPluginJson) => {
227
+ f = new MarketFixture();
228
+ const repo = f.seed({
229
+ entryVersion: null,
230
+ pluginVersion: null,
231
+ noPluginJson,
232
+ });
233
+ f.register(repo);
234
+ f.install();
235
+ f.edit();
236
+ write(join(f.folder(), "SKILL.md"), EDITED);
237
+
238
+ const r = f.market(["publish", "review", "--plugin", ID, "--yes"]);
239
+
240
+ expect(r.code).toBe(0);
241
+ expect(f.remoteJson(repo, MANIFEST).plugins[0].version).toBe("0.0.1");
242
+ if (noPluginJson)
243
+ expect(f.remotePaths(repo)).not.toContain(PLUGIN_MANIFEST);
244
+ else expect(f.remoteJson(repo, PLUGIN_MANIFEST).version).toBe("0.0.1");
245
+ f.assertOwned();
246
+ },
247
+ 60_000,
248
+ );
249
+
250
+ test("TEST-26 REQ-12 a divergent catalog entry cannot override plugin.json's patch base", () => {
251
+ f = new MarketFixture();
252
+ const repo = f.seed({ entryVersion: "9.9.9", pluginVersion: "0.3.1" });
253
+ f.register(repo);
254
+ f.install();
255
+ f.edit();
256
+ write(join(f.folder(), "SKILL.md"), EDITED);
257
+
258
+ const r = f.market(["publish", "review", "--plugin", ID, "--yes"]);
259
+
260
+ expect(r.code).toBe(0);
261
+ expectPublished(f, repo, "0.3.2");
262
+ f.assertOwned();
263
+ }, 60_000);
@@ -0,0 +1,190 @@
1
+ import { expect, test } from "bun:test";
2
+ import type { MarketOverviewEntry } from "../services/market-ops.js";
3
+ import { companySkillNames } from "../services/market-ops.js";
4
+ import type { LocalItem, SkillStatusRow } from "../services/market-types.js";
5
+ import {
6
+ editedSkills,
7
+ marketplaceItems,
8
+ marketplacesItems,
9
+ nextSelectable,
10
+ pluginsToPush,
11
+ selectable,
12
+ } from "../ui/adapters/marketAdapter.js";
13
+
14
+ function status(
15
+ skill: string,
16
+ state: SkillStatusRow["state"],
17
+ owner: string | null = "review-kit@acme",
18
+ from: SkillStatusRow["from"] = "project",
19
+ ): SkillStatusRow {
20
+ return { skill, state, owner, from, path: [from, skill].join("/") };
21
+ }
22
+ const overview = (): MarketOverviewEntry => ({
23
+ name: "acme",
24
+ repo: "acme/market",
25
+ company: "10xlabs",
26
+ scope: "user",
27
+ freshness: "fresh",
28
+ reason: "",
29
+ fix: "",
30
+ commit: "a".repeat(40),
31
+ plugins: [
32
+ {
33
+ name: "review-kit",
34
+ version: "0.3.1",
35
+ skills: ["review", "lint", "unchanged"],
36
+ },
37
+ { name: "deploy-kit", version: "1.0.0", skills: ["deploy"] },
38
+ ],
39
+ });
40
+
41
+ test("R3.1 TEST-7 editedSkills counts each owned edited skill once and excludes every other state", () => {
42
+ const rows: SkillStatusRow[] = [
43
+ status("review", "edited"),
44
+ status("review", "edited", "review-kit@acme", "user"),
45
+ status("lint", "edited"),
46
+ status("unknown-origin", "no-base"),
47
+ status("both-changed", "conflict"),
48
+ status("unchanged", "same"),
49
+ status("remote-newer", "plugin-newer"),
50
+ status("offline", "not-checked"),
51
+ status("ambiguous", "ambiguous", null),
52
+ status("unowned", "not-owned", null),
53
+ status("elsewhere", "edited", "review-kit@beta"),
54
+ ];
55
+ const before = JSON.stringify(rows);
56
+
57
+ expect(editedSkills(rows, "review-kit@acme").sort()).toEqual([
58
+ "lint",
59
+ "review",
60
+ ]);
61
+ expect(editedSkills(rows, "missing@acme")).toEqual([]);
62
+ expect(JSON.stringify(rows)).toBe(before);
63
+ });
64
+
65
+ test("R3.1 TEST-7 pluginsToPush counts plugins rather than skills and isolates the marketplace", () => {
66
+ const rows = [
67
+ status("review", "edited"),
68
+ status("lint", "edited"),
69
+ status("review", "edited", "review-kit@acme", "user"),
70
+ status("deploy", "edited", "deploy-kit@acme"),
71
+ status("blocked", "conflict", "blocked-kit@acme"),
72
+ status("foreign", "edited", "review-kit@beta"),
73
+ ];
74
+
75
+ expect(pluginsToPush("acme", rows).sort()).toEqual([
76
+ "deploy-kit",
77
+ "review-kit",
78
+ ]);
79
+ expect(pluginsToPush("missing", rows)).toEqual([]);
80
+ });
81
+
82
+ test("R3.1 TEST-8 marketplace and plugin rows carry edited plugin and skill counts without expansion", () => {
83
+ const entry = overview();
84
+ const states = [
85
+ status("review", "edited"),
86
+ status("lint", "edited"),
87
+ status("unchanged", "same"),
88
+ status("deploy", "no-base", "deploy-kit@acme"),
89
+ ];
90
+ const before = JSON.stringify({ entry, states });
91
+
92
+ const first = marketplacesItems([entry], states);
93
+ const marketplace = first.find((item) => item.kind === "marketplace");
94
+ expect(marketplace?.kind).toBe("marketplace");
95
+ if (marketplace?.kind !== "marketplace")
96
+ throw new Error("Marketplace row missing");
97
+ expect(marketplace.toPush).toEqual(["review-kit"]);
98
+
99
+ const plugins = marketplaceItems(entry, {}, states, new Set());
100
+ expect(plugins.map((item) => item.kind)).toEqual(["plugin", "plugin"]);
101
+ const review = plugins.find(
102
+ (item) => item.kind === "plugin" && item.ref === "review-kit@acme",
103
+ );
104
+ const deploy = plugins.find(
105
+ (item) => item.kind === "plugin" && item.ref === "deploy-kit@acme",
106
+ );
107
+ expect(
108
+ review?.kind === "plugin" ? [...review.edited].sort() : undefined,
109
+ ).toEqual(["lint", "review"]);
110
+ expect(deploy?.kind === "plugin" ? deploy.edited : undefined).toEqual([]);
111
+ expect(JSON.stringify({ entry, states })).toBe(before);
112
+ });
113
+
114
+ test("R4.1 TEST-9 local heading and items follow company marketplaces and add actions", () => {
115
+ const local: LocalItem[] = [
116
+ {
117
+ kind: "skill",
118
+ name: "authoring",
119
+ scope: "project",
120
+ path: "project/authoring",
121
+ },
122
+ { kind: "plugin", name: "my-kit", scope: "user", path: "user/my-kit" },
123
+ ];
124
+ const rows = marketplacesItems([overview()], [], local);
125
+
126
+ expect(rows.map((item) => item.kind)).toEqual([
127
+ "marketplace",
128
+ "create",
129
+ "connect",
130
+ "local-heading",
131
+ "local",
132
+ "local",
133
+ ]);
134
+ expect(rows[3]).toEqual({ kind: "local-heading", count: 2 });
135
+ expect(rows.slice(4)).toEqual(local.map((item) => ({ kind: "local", item })));
136
+ expect(selectable(rows[3])).toBe(false);
137
+ expect(selectable(rows[4])).toBe(true);
138
+ expect(nextSelectable(rows, 2, 1)).toBe(4);
139
+ expect(nextSelectable(rows, 4, -1)).toBe(2);
140
+ expect(nextSelectable(rows, 5, 1)).toBe(5);
141
+ });
142
+
143
+ test("R4.1 R4.4 TEST-9 local-only rows remain selectable without any marketplaces; no empty heading", () => {
144
+ const item: LocalItem = {
145
+ kind: "skill",
146
+ name: "solo",
147
+ scope: "project",
148
+ path: "project/solo",
149
+ };
150
+ const rows = marketplacesItems([], [], [item]);
151
+
152
+ expect(rows).toEqual([
153
+ { kind: "create" },
154
+ { kind: "connect" },
155
+ { kind: "local-heading", count: 1 },
156
+ { kind: "local", item },
157
+ ]);
158
+ expect(nextSelectable(rows, 1, 1)).toBe(3);
159
+ expect(marketplacesItems([], [], [])).toEqual([
160
+ { kind: "create" },
161
+ { kind: "connect" },
162
+ ]);
163
+ });
164
+
165
+ test("R4.1 TEST-30 companySkillNames unions every plugin's skill names without duplicates", () => {
166
+ const marketplaces = [
167
+ { plugins: overview().plugins },
168
+ {
169
+ plugins: [
170
+ {
171
+ name: "other-kit",
172
+ version: "1.0.0",
173
+ skills: ["review", "authoring"],
174
+ },
175
+ ],
176
+ },
177
+ { plugins: [] },
178
+ ];
179
+ const before = JSON.stringify(marketplaces);
180
+
181
+ expect([...companySkillNames(marketplaces)].sort()).toEqual([
182
+ "authoring",
183
+ "deploy",
184
+ "lint",
185
+ "review",
186
+ "unchanged",
187
+ ]);
188
+ expect([...companySkillNames([])]).toEqual([]);
189
+ expect(JSON.stringify(marketplaces)).toBe(before);
190
+ });