magus-cli 7.10.4 → 7.11.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 (38) hide show
  1. package/package.json +4 -4
  2. package/src/__tests__/blind/plugin-deps/doctor.test.ts +2 -2
  3. package/src/__tests__/blind/plugin-deps/hook.test.ts +2 -2
  4. package/src/__tests__/blind/plugin-deps/read-only.test.ts +1 -1
  5. package/src/__tests__/blind/plugin-deps/schema.test.ts +6 -6
  6. package/src/__tests__/blind/requires-min-version/TEST-PLAN.md +54 -0
  7. package/src/__tests__/blind/requires-min-version/manifests.test.ts +68 -0
  8. package/src/__tests__/blind/requires-min-version/parse.test.ts +266 -0
  9. package/src/__tests__/blind/requires-min-version/probe.test.ts +359 -0
  10. package/src/__tests__/blind/requires-min-version/render.test.ts +252 -0
  11. package/src/__tests__/blind/requires-min-version/support.ts +83 -0
  12. package/src/__tests__/blind/requires-min-version/surfaces.test.ts +231 -0
  13. package/src/__tests__/blind/review-followups/bun-install-path.test.ts +1 -1
  14. package/src/__tests__/blind/review-followups/ledger-permissions.test.ts +1 -1
  15. package/src/__tests__/blind/review-followups/ledger-pruning.test.ts +1 -1
  16. package/src/__tests__/blind/review-followups/path-advice.test.ts +1 -1
  17. package/src/__tests__/deps-check-hook.test.ts +9 -9
  18. package/src/__tests__/deps-plan.test.ts +1 -1
  19. package/src/__tests__/deps-report.test.ts +5 -5
  20. package/src/__tests__/doctor-json.test.ts +2 -2
  21. package/src/__tests__/marketplace-refresh.test.ts +1 -1
  22. package/src/__tests__/plugin-requires.test.ts +2 -2
  23. package/src/__tests__/profile-worktree-include.test.ts +212 -0
  24. package/src/__tests__/requires-probe.test.ts +2 -2
  25. package/src/cli/deps-view.ts +5 -3
  26. package/src/services/deps-plan.ts +38 -14
  27. package/src/services/deps-report.ts +24 -7
  28. package/src/services/doctor-bins.ts +40 -12
  29. package/src/services/profile-activate.ts +9 -0
  30. package/src/services/profile-live.ts +26 -0
  31. package/src/services/profile-skills.ts +14 -4
  32. package/src/services/profile-state.ts +18 -0
  33. package/src/services/resolver.ts +1 -0
  34. package/src/shared/plugin/hooks/deps-check.ts +7 -3
  35. package/src/shared/plugin/hooks/lib/requires-probe.ts +257 -33
  36. package/src/ui/adapters/cliToolsAdapter.ts +6 -1
  37. package/src/ui/renderers/cliToolRenderers.tsx +7 -1
  38. package/src/ui/screens/CliToolsScreen.tsx +8 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "magus-cli",
3
- "version": "7.10.4",
3
+ "version": "7.11.0",
4
4
  "description": "TUI tool for managing Claude Code plugins, MCPs, and configuration",
5
5
  "type": "module",
6
6
  "main": "src/main.tsx",
@@ -65,8 +65,8 @@
65
65
  "typescript": "^5.6.3"
66
66
  },
67
67
  "optionalDependencies": {
68
- "magus-cli-darwin-arm64": "7.10.4",
69
- "magus-cli-darwin-x64": "7.10.4",
70
- "magus-cli-linux-x64": "7.10.4"
68
+ "magus-cli-darwin-arm64": "7.11.0",
69
+ "magus-cli-darwin-x64": "7.11.0",
70
+ "magus-cli-linux-x64": "7.11.0"
71
71
  }
72
72
  }
@@ -25,7 +25,7 @@ function requirement(
25
25
  }
26
26
 
27
27
  function requires(bin: Record<string, unknown>[]): Record<string, unknown> {
28
- return { bin, schema: 1 };
28
+ return { bin, schema: 2 };
29
29
  }
30
30
 
31
31
  async function installPlugin(
@@ -157,7 +157,7 @@ describe("magus doctor black-box contract", () => {
157
157
  magusOnPath: false,
158
158
  pathAdvice: [],
159
159
  plugins_needing_update: [],
160
- schema: 1,
160
+ schema: 2,
161
161
  unfixable: [],
162
162
  });
163
163
  expect(report.platform).toEqual({
@@ -27,7 +27,7 @@ function requirement(
27
27
  }
28
28
 
29
29
  function requires(bin: Record<string, unknown>[]): Record<string, unknown> {
30
- return { bin, schema: 1 };
30
+ return { bin, schema: 2 };
31
31
  }
32
32
 
33
33
  describe("registered SessionStart dependency hook", () => {
@@ -292,7 +292,7 @@ describe("registered SessionStart dependency hook", () => {
292
292
  const plugin = await makeHookPlugin(world, "invalid", {
293
293
  bin: [],
294
294
  mystery: true,
295
- schema: 1,
295
+ schema: 2,
296
296
  });
297
297
  await linkExecutable(world.bin, "bun", process.execPath);
298
298
  const input = sessionInput("invalid-session");
@@ -30,7 +30,7 @@ test("all doctor modes leave Claude Code marketplace state byte-identical", asyn
30
30
  name: "present-tool",
31
31
  },
32
32
  ],
33
- schema: 1,
33
+ schema: 2,
34
34
  },
35
35
  join(clone, "plugins"),
36
36
  );
@@ -13,7 +13,7 @@ function requirement(
13
13
  }
14
14
 
15
15
  function manifest(bin: Record<string, unknown>[]): Record<string, unknown> {
16
- return { requires: { bin, schema: 1 } };
16
+ return { requires: { bin, schema: 2 } };
17
17
  }
18
18
 
19
19
  function invalidText(value: unknown): string {
@@ -26,7 +26,7 @@ function invalidText(value: unknown): string {
26
26
  return result.errors.join("\n");
27
27
  }
28
28
 
29
- describe("requires schema 1", () => {
29
+ describe("requires schema 2", () => {
30
30
  // Spec: AS-BUILT.md §1, “brew takes EXACTLY ONE ...” through the
31
31
  // renderInstall table define the seven manifest installer kinds.
32
32
  test("accepts a complete declaration of every manifest via", () => {
@@ -78,7 +78,7 @@ describe("requires schema 1", () => {
78
78
  // level, a requirement, an option) ... error naming the key.”
79
79
  test("rejects and locates unknown keys at all three schema levels", () => {
80
80
  const top = invalidText({
81
- requires: { bin: [], mysteryTop: true, schema: 1 },
81
+ requires: { bin: [], mysteryTop: true, schema: 2 },
82
82
  });
83
83
  const req = invalidText(
84
84
  manifest([
@@ -102,7 +102,7 @@ describe("requires schema 1", () => {
102
102
  expect(option).toContain("mysteryOption");
103
103
  });
104
104
 
105
- // Spec: AS-BUILT.md §1, “schema greater than 1 ... smaller ...;
105
+ // Spec: AS-BUILT.md §1, “schema greater than 2 ... smaller ...;
106
106
  // missing/non-number → invalid” and “no requires key ... empty.”
107
107
  test("classifies schema versions and treats no requires as empty", () => {
108
108
  expect(parseRequires({})).toEqual({ ok: true, requirements: [] });
@@ -112,11 +112,11 @@ describe("requires schema 1", () => {
112
112
  ok: false,
113
113
  });
114
114
  }
115
- expect(parseRequires({ requires: { bin: [], schema: 2 } })).toMatchObject({
115
+ expect(parseRequires({ requires: { bin: [], schema: 3 } })).toMatchObject({
116
116
  kind: "newer-schema",
117
117
  ok: false,
118
118
  });
119
- expect(parseRequires({ requires: { bin: [], schema: 0 } })).toMatchObject({
119
+ expect(parseRequires({ requires: { bin: [], schema: 1 } })).toMatchObject({
120
120
  kind: "older-schema",
121
121
  ok: false,
122
122
  });
@@ -0,0 +1,54 @@
1
+ # Test Plan: requires schema 2 — minVersion per dependency
2
+
3
+ - **Spec**: docs/plans/2026-10-20-requires-min-version.md
4
+ - **Contract**: ai-docs/plugin-deps-contract/contract/requires-probe.d.ts, contract/doctor-json.d.ts (schema 1; schema-2 additions taken from the spec)
5
+ - **Stack**: bun **Framework**: bun:test **Scenarios**: 109 tests (parameterised)
6
+
7
+ Fake executables are `#!/bin/sh` scripts in a temp dir that print a chosen version and exit
8
+ with a chosen code. The hook tests link the test runner's own bun only to execute the hook's
9
+ TypeScript, as the schema-1 blind suite does; no test reads the real claudish or bun version.
10
+
11
+ ## Scenarios (grouped)
12
+
13
+ | Group | Type | Priority | Given / When / Then | REQ |
14
+ |---|---|---|---|---|
15
+ | parse.test.ts schema | unit | critical | REQUIRES_SCHEMA is 2; schema 1 → older-schema; 3 → newer-schema; no requires → empty | 1, 2 |
16
+ | parse.test.ts field | unit | critical | minVersion X.Y.Z ok and kept on the requirement; v10.7.0, 10.7, 10.7.0-beta, 10.7.0.1, 10, "", " 10.7.0", 10.x.0, number → invalid; unknown key invalid and named | 3, 4, 5 |
17
+ | parse.test.ts pins | unit | high | release tag (with/without v) below floor invalid, at/above valid; numeric; non-X.Y.Z tag invalid; vv invalid; no floor → not compared; exact bun version below floor invalid, at/above valid; ranges/tags/no version not compared | 6, 7 |
18
+ | probe.test.ts × {presence, full} | integration | critical | >= floor present; < floor outdated {found,minVersion}; no version → unknown `no version in "tool --version" output`; stderr read; stdout wins; first match; suffix ignored; numeric; non-zero exit/absent → missing; slow → unknown; dependent of outdated is probed; order kept | 8–18 |
19
+ | probe.test.ts no floor | integration | high | presence depth: unfloored self-named check is PATH-only (exit-1 fake present, not run); same fake with a floor runs → missing; full depth unchanged | 12 |
20
+ | render.test.ts renderInstall | unit | critical | bun missing/outdated (@latest, @version), brew formula upgrade, brew cask upgrade --cask; other vias identical across reasons; describeStep of outdated bun | 19–22 |
21
+ | render.test.ts renderFix | unit | high | outdated → banner with `outdated <name> <found> (needs <min>)` and the upgrade line; healthy → null | 23, 24 |
22
+ | manifests.test.ts | unit | critical | every shipped plugin.json with requires is schema 2 and parses; claudish's claudish has minVersion 10.7.0; no other floors | 25, 26 |
23
+ | surfaces.test.ts doctor | e2e (CLI) | high | --json schema 2, outdated dep with found/minVersion, fix text `bun add -g widget@latest`; text doctor exit non-zero; --fix leaving old binary → failed, remedy names found, floor, onPath | 27, 28, 29 |
24
+ | surfaces.test.ts hook | e2e (hook) | high | outdated dep reported once per session with the head text; satisfied floor silent | 30 |
25
+
26
+ ## Coverage map
27
+
28
+ | Requirement | Tests | Covered |
29
+ |---|---|---|
30
+ | REQ-1 REQUIRES_SCHEMA = 2 | parse | yes |
31
+ | REQ-2 one schema only (1 → older, 3 → newer) | parse | yes |
32
+ | REQ-3 minVersion shape | parse | yes |
33
+ | REQ-4 optional field | parse, probe | yes |
34
+ | REQ-5 allowed keys | parse | yes |
35
+ | REQ-6 release tag vs floor | parse | yes |
36
+ | REQ-7 exact bun version vs floor | parse | yes |
37
+ | REQ-8..11, 13, 14 version reading and comparison | probe (both depths) | yes |
38
+ | REQ-12 floored requirement always runs its check | probe | yes |
39
+ | REQ-15, 16 non-zero/timeout as schema 1 | probe | yes |
40
+ | REQ-17 blockedBy unchanged for outdated | probe | yes |
41
+ | REQ-19..22 renderInstall reason | render | yes |
42
+ | REQ-23, 24 renderFix | render | yes |
43
+ | REQ-25, 26 shipped declarations | manifests | yes |
44
+ | REQ-27 doctor --json schema 2 | surfaces | yes |
45
+ | REQ-28 doctor text exit code | surfaces | yes |
46
+ | REQ-29 executor: still outdated → failed + remedy | surfaces | yes |
47
+ | REQ-30 hook ledger | surfaces | yes |
48
+
49
+ ## Known gaps
50
+
51
+ - TUI (`cliToolsAdapter`, `depsAdapter`): no public contract was supplied for these surfaces.
52
+ - Repo gate `scripts/check-plugin-requires.ts --self-test`: outside the contract given.
53
+ - Hook-copy byte identity (`check:hook-copies`): a repo gate, not behaviour of the contract.
54
+ - Rollout messages ("update magus-cli" / "update the plugin"): only the parse kinds are asserted, not doctor's wording.
@@ -0,0 +1,68 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
3
+ import { join } from "node:path";
4
+
5
+ import { parseRequires } from "../../../shared/plugin/hooks/lib/requires-probe.js";
6
+ import { REPO_ROOT } from "./support";
7
+
8
+ const PLUGINS = join(REPO_ROOT, "plugins");
9
+
10
+ function manifestPath(plugin: string): string {
11
+ return join(PLUGINS, plugin, ".claude-plugin", "plugin.json");
12
+ }
13
+
14
+ const declaring = readdirSync(PLUGINS)
15
+ .filter((name) => existsSync(manifestPath(name)))
16
+ .map((name) => ({
17
+ json: JSON.parse(readFileSync(manifestPath(name), "utf8")) as Record<
18
+ string,
19
+ unknown
20
+ >,
21
+ name,
22
+ }))
23
+ .filter(({ json }) => json.requires !== undefined);
24
+
25
+ describe("shipped plugin manifests under schema 2", () => {
26
+ test("REQ-25 at least one shipped manifest declares requires (the scan is not empty)", () => {
27
+ expect(declaring.map(({ name }) => name)).toContain("claudish");
28
+ });
29
+
30
+ // REQ-25: "Every plugin.json with requires moves to schema: 2."
31
+ for (const { name, json } of declaring) {
32
+ test(`REQ-25 ${name}: requires declares schema 2 and parses ok`, () => {
33
+ expect((json.requires as Record<string, unknown>).schema).toBe(2);
34
+ const result = parseRequires(json);
35
+ if (!result.ok) {
36
+ throw new Error(`${name}: ${result.kind}: ${result.errors.join("; ")}`);
37
+ }
38
+ expect(result.ok).toBe(true);
39
+ });
40
+ }
41
+
42
+ // REQ-26: claudish gets minVersion 10.7.0; no other requirement gets a floor.
43
+ test("REQ-26 claudish's claudish requirement has minVersion 10.7.0", () => {
44
+ const claudish = declaring.find(({ name }) => name === "claudish");
45
+ if (!claudish) throw new Error("plugins/claudish declares no requires");
46
+ const result = parseRequires(claudish.json);
47
+ if (!result.ok)
48
+ throw new Error(`${result.kind}: ${result.errors.join("; ")}`);
49
+ const req = result.requirements.find((r) => r.name === "claudish") as
50
+ | (Record<string, unknown> & { name: string })
51
+ | undefined;
52
+ expect(req).toBeDefined();
53
+ expect(req?.minVersion).toBe("10.7.0");
54
+ });
55
+
56
+ test("REQ-26 no other shipped requirement declares a floor", () => {
57
+ const floored: string[] = [];
58
+ for (const { name, json } of declaring) {
59
+ const bin = ((json.requires as Record<string, unknown>).bin ??
60
+ []) as Array<Record<string, unknown>>;
61
+ for (const r of bin) {
62
+ if (r.minVersion !== undefined)
63
+ floored.push(`${name}:${String(r.name)}`);
64
+ }
65
+ }
66
+ expect(floored).toEqual(["claudish:claudish"]);
67
+ });
68
+ });
@@ -0,0 +1,266 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import {
4
+ REQUIRES_SCHEMA,
5
+ parseRequires,
6
+ } from "../../../shared/plugin/hooks/lib/requires-probe.js";
7
+ import { manifest, releaseOption, requirement } from "./support";
8
+
9
+ const brew = [{ formula: "tool", via: "brew" }];
10
+ // A via:bun option needs "bun" declared in the same plugin (schema-1 rule),
11
+ // so every bun-option testdata carries this, and an invalid result can only
12
+ // come from the floor comparison.
13
+ const bunDecl = requirement("bun", [{ tool: "bun", via: "bootstrap" }]);
14
+
15
+ function okRequirements(value: unknown) {
16
+ const result = parseRequires(value);
17
+ if (!result.ok) {
18
+ throw new Error(
19
+ `expected ok, got ${result.kind}: ${result.errors.join("; ")}`,
20
+ );
21
+ }
22
+ return result.requirements;
23
+ }
24
+
25
+ function expectInvalid(value: unknown): string {
26
+ const result = parseRequires(value);
27
+ expect(result.ok).toBe(false);
28
+ if (result.ok) throw new Error("testdata unexpectedly parsed");
29
+ expect(result.kind).toBe("invalid");
30
+ expect(result.errors.length).toBeGreaterThan(0);
31
+ return result.errors.join("\n");
32
+ }
33
+
34
+ describe("requires schema 2: version and schema number", () => {
35
+ // REQ-1: "any shape change bumps requires.schema, so this is schema 2"
36
+ test("REQ-1 REQUIRES_SCHEMA is 2", () => {
37
+ expect(REQUIRES_SCHEMA).toBe(2);
38
+ });
39
+
40
+ // REQ-2: "A reader reads exactly one schema: there is no schema-1 reading path"
41
+ test("REQ-2 a schema-1 requires block is older-schema, not parsed", () => {
42
+ expect(
43
+ parseRequires(manifest([requirement("tool", brew)], 1)),
44
+ ).toMatchObject({ kind: "older-schema", ok: false });
45
+ });
46
+
47
+ test("REQ-2 a schema-3 requires block is newer-schema", () => {
48
+ expect(
49
+ parseRequires(manifest([requirement("tool", brew)], 3)),
50
+ ).toMatchObject({ kind: "newer-schema", ok: false });
51
+ });
52
+
53
+ test("REQ-2 a manifest without requires still parses as empty", () => {
54
+ expect(parseRequires({ name: "p" })).toEqual({
55
+ ok: true,
56
+ requirements: [],
57
+ });
58
+ });
59
+ });
60
+
61
+ describe("requires schema 2: the minVersion field", () => {
62
+ // REQ-3: Shape ^\d+\.\d+\.\d+$
63
+ test("REQ-3 accepts minVersion X.Y.Z and keeps it on the requirement", () => {
64
+ const reqs = okRequirements(
65
+ manifest([requirement("tool", brew, { minVersion: "10.7.0" })]),
66
+ );
67
+ expect(reqs).toHaveLength(1);
68
+ expect(reqs[0]).toMatchObject({ minVersion: "10.7.0", name: "tool" });
69
+ });
70
+
71
+ // REQ-4: Optional — a requirement without it behaves as in schema 1.
72
+ test("REQ-4 a requirement without minVersion parses ok", () => {
73
+ const reqs = okRequirements(manifest([requirement("tool", brew)]));
74
+ expect(reqs).toHaveLength(1);
75
+ expect(reqs[0].name).toBe("tool");
76
+ });
77
+
78
+ // REQ-3: "three numeric parts, no v, no prerelease. Anything else is invalid."
79
+ for (const bad of [
80
+ "v10.7.0",
81
+ "10.7",
82
+ "10.7.0-beta",
83
+ "10.7.0.1",
84
+ "10",
85
+ "",
86
+ " 10.7.0",
87
+ "10.x.0",
88
+ ]) {
89
+ test(`REQ-3 rejects minVersion ${JSON.stringify(bad)} as invalid`, () => {
90
+ expectInvalid(manifest([requirement("tool", brew, { minVersion: bad })]));
91
+ });
92
+ }
93
+
94
+ test("REQ-3 rejects a numeric (non-string) minVersion as invalid", () => {
95
+ expectInvalid(manifest([requirement("tool", brew, { minVersion: 10.7 })]));
96
+ });
97
+
98
+ // REQ-5: allowed keys are name, check, os, minVersion, install.
99
+ test("REQ-5 an unknown requirement key is still invalid and named", () => {
100
+ const errors = expectInvalid(
101
+ manifest([
102
+ requirement("tool", brew, {
103
+ minVersion: "1.0.0",
104
+ minimumVersion: "1.0.0",
105
+ }),
106
+ ]),
107
+ );
108
+ expect(errors).toContain("minimumVersion");
109
+ });
110
+
111
+ test("REQ-5 os and minVersion together are allowed", () => {
112
+ const reqs = okRequirements(
113
+ manifest([
114
+ requirement(
115
+ "tool",
116
+ [{ formula: "tool", os: ["darwin"], via: "brew" }],
117
+ {
118
+ minVersion: "1.0.0",
119
+ os: ["darwin"],
120
+ },
121
+ ),
122
+ ]),
123
+ );
124
+ expect(reqs).toHaveLength(1);
125
+ });
126
+ });
127
+
128
+ describe("requires schema 2: pinned installs against the floor", () => {
129
+ // REQ-6: release tag, one leading v removed, must parse X.Y.Z and be >= minVersion.
130
+ test("REQ-6 a release tag below the floor is invalid", () => {
131
+ expectInvalid(
132
+ manifest([
133
+ requirement("tool", [releaseOption("v10.6.9")], {
134
+ minVersion: "10.7.0",
135
+ }),
136
+ ]),
137
+ );
138
+ });
139
+
140
+ test("REQ-6 a release tag below the floor without a v is invalid", () => {
141
+ expectInvalid(
142
+ manifest([
143
+ requirement("tool", [releaseOption("10.6.9")], {
144
+ minVersion: "10.7.0",
145
+ }),
146
+ ]),
147
+ );
148
+ });
149
+
150
+ for (const tag of ["v10.7.0", "10.7.0", "v10.7.1", "10.8.0", "v11.0.0"]) {
151
+ test(`REQ-6 a release tag ${tag} at or above floor 10.7.0 is valid`, () => {
152
+ expect(
153
+ okRequirements(
154
+ manifest([
155
+ requirement("tool", [releaseOption(tag)], { minVersion: "10.7.0" }),
156
+ ]),
157
+ ),
158
+ ).toHaveLength(1);
159
+ });
160
+ }
161
+
162
+ test("REQ-6 tag comparison is numeric: v10.10.0 satisfies 10.9.0", () => {
163
+ expect(
164
+ okRequirements(
165
+ manifest([
166
+ requirement("tool", [releaseOption("v10.10.0")], {
167
+ minVersion: "10.9.0",
168
+ }),
169
+ ]),
170
+ ),
171
+ ).toHaveLength(1);
172
+ });
173
+
174
+ test("REQ-6 tag comparison is numeric: v10.9.0 is below 10.10.0", () => {
175
+ expectInvalid(
176
+ manifest([
177
+ requirement("tool", [releaseOption("v10.9.0")], {
178
+ minVersion: "10.10.0",
179
+ }),
180
+ ]),
181
+ );
182
+ });
183
+
184
+ test("REQ-6 a release tag that is not X.Y.Z is invalid when a floor is set", () => {
185
+ expectInvalid(
186
+ manifest([
187
+ requirement("tool", [releaseOption("v10")], { minVersion: "1.0.0" }),
188
+ ]),
189
+ );
190
+ });
191
+
192
+ test("REQ-6 only one leading v is removed: vv10.7.0 is invalid", () => {
193
+ expectInvalid(
194
+ manifest([
195
+ requirement("tool", [releaseOption("vv10.7.0")], {
196
+ minVersion: "10.7.0",
197
+ }),
198
+ ]),
199
+ );
200
+ });
201
+
202
+ test("REQ-4 without a floor a release tag is not compared (v1 stays valid)", () => {
203
+ expect(
204
+ okRequirements(manifest([requirement("tool", [releaseOption("v1")])])),
205
+ ).toHaveLength(1);
206
+ });
207
+
208
+ // REQ-7: exact bun version must be >= minVersion; ranges and tags not compared.
209
+ test("REQ-7 an exact bun version below the floor is invalid", () => {
210
+ expectInvalid(
211
+ manifest([
212
+ bunDecl,
213
+ requirement(
214
+ "tool",
215
+ [{ package: "tool", version: "10.6.0", via: "bun" }],
216
+ {
217
+ minVersion: "10.7.0",
218
+ },
219
+ ),
220
+ ]),
221
+ );
222
+ });
223
+
224
+ for (const version of ["10.7.0", "10.12.3"]) {
225
+ test(`REQ-7 an exact bun version ${version} at or above the floor is valid`, () => {
226
+ expect(
227
+ okRequirements(
228
+ manifest([
229
+ bunDecl,
230
+ requirement("tool", [{ package: "tool", version, via: "bun" }], {
231
+ minVersion: "10.7.0",
232
+ }),
233
+ ]),
234
+ ),
235
+ ).toHaveLength(2);
236
+ });
237
+ }
238
+
239
+ for (const version of ["^1.2.0", "latest", "~9.0.0"]) {
240
+ test(`REQ-7 a bun range or tag ${version} is not compared to the floor`, () => {
241
+ expect(
242
+ okRequirements(
243
+ manifest([
244
+ bunDecl,
245
+ requirement("tool", [{ package: "tool", version, via: "bun" }], {
246
+ minVersion: "10.7.0",
247
+ }),
248
+ ]),
249
+ ),
250
+ ).toHaveLength(2);
251
+ });
252
+ }
253
+
254
+ test("REQ-7 a bun option with no version is valid under a floor", () => {
255
+ expect(
256
+ okRequirements(
257
+ manifest([
258
+ bunDecl,
259
+ requirement("tool", [{ package: "tool", via: "bun" }], {
260
+ minVersion: "10.7.0",
261
+ }),
262
+ ]),
263
+ ),
264
+ ).toHaveLength(2);
265
+ });
266
+ });