magus-cli 7.7.0 → 7.9.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 (140) hide show
  1. package/README.md +1 -1
  2. package/package.json +4 -4
  3. package/src/__tests__/blackbox-parity.test.ts +45 -26
  4. package/src/__tests__/blackbox-plugin-state.test.ts +25 -6
  5. package/src/__tests__/blind/plugin-deps/doctor.test.ts +622 -0
  6. package/src/__tests__/blind/plugin-deps/helpers.ts +340 -0
  7. package/src/__tests__/blind/plugin-deps/hook.test.ts +328 -0
  8. package/src/__tests__/blind/plugin-deps/probe.test.ts +694 -0
  9. package/src/__tests__/blind/plugin-deps/read-only.test.ts +96 -0
  10. package/src/__tests__/blind/plugin-deps/schema.test.ts +390 -0
  11. package/src/__tests__/blind-780-cli-tools.test.ts +796 -0
  12. package/src/__tests__/blind-780-deprecated.test.ts +631 -0
  13. package/src/__tests__/blind-780-failure-reasons.test.ts +1004 -0
  14. package/src/__tests__/blind-780-plugin-resolution.test.ts +431 -0
  15. package/src/__tests__/blind-780-review2.test.ts +1178 -0
  16. package/src/__tests__/cli-apply-seams.test.ts +2 -67
  17. package/src/__tests__/cli-tool-commands.test.ts +131 -6
  18. package/src/__tests__/cli-tools-adapter.test.ts +52 -2
  19. package/src/__tests__/cli-tools-plugin-managed.test.tsx +83 -0
  20. package/src/__tests__/cli-update-view.test.ts +3 -3
  21. package/src/__tests__/deprecated-remove.test.ts +827 -0
  22. package/src/__tests__/deps-adapter.test.ts +191 -0
  23. package/src/__tests__/deps-check-hook.test.ts +220 -0
  24. package/src/__tests__/deps-plan.test.ts +896 -0
  25. package/src/__tests__/deps-report.test.ts +197 -0
  26. package/src/__tests__/doctor-bins.test.ts +134 -36
  27. package/src/__tests__/doctor-json.test.ts +261 -0
  28. package/src/__tests__/doctor.test.ts +159 -1
  29. package/src/__tests__/failure-popup-review2.test.tsx +896 -0
  30. package/src/__tests__/failure-popup.test.tsx +419 -0
  31. package/src/__tests__/final-fixes-780.test.tsx +740 -0
  32. package/src/__tests__/fixtures/bins.ts +55 -0
  33. package/src/__tests__/format-update-outcome.test.ts +6 -32
  34. package/src/__tests__/gitignore-profile.test.ts +1 -9
  35. package/src/__tests__/install-plan.test.ts +15 -1
  36. package/src/__tests__/marketplace-refresh.test.ts +9 -3
  37. package/src/__tests__/models-screen-state.test.ts +18 -40
  38. package/src/__tests__/no-shell-spawn.test.ts +77 -0
  39. package/src/__tests__/npm-package-name.test.ts +6 -9
  40. package/src/__tests__/plugin-requires.test.ts +138 -153
  41. package/src/__tests__/plugin-resolution.test.ts +52 -13
  42. package/src/__tests__/plugin-row-text.test.ts +30 -37
  43. package/src/__tests__/profile-dependencies.test.ts +7 -0
  44. package/src/__tests__/profile-startup-old-format.test.ts +0 -3
  45. package/src/__tests__/profile-startup.test.ts +40 -12
  46. package/src/__tests__/requires-probe.test.ts +858 -0
  47. package/src/__tests__/resolver.test.ts +129 -52
  48. package/src/__tests__/run.test.ts +108 -0
  49. package/src/__tests__/stale-load-repair.test.ts +31 -23
  50. package/src/__tests__/tool-probe.test.ts +1 -1
  51. package/src/__tests__/toolchain.test.ts +123 -52
  52. package/src/__tests__/update-apply.test.ts +13 -29
  53. package/src/__tests__/update-plan.test.ts +24 -18
  54. package/src/__tests__/useStaleLoadModal.test.ts +1 -57
  55. package/src/__tests__/version-unknown-state.test.ts +15 -17
  56. package/src/cli/ansi.ts +2 -7
  57. package/src/cli/deps-view.ts +213 -0
  58. package/src/cli/doctor.ts +320 -54
  59. package/src/cli/install.ts +55 -71
  60. package/src/cli/profile.ts +20 -2
  61. package/src/cli/router.ts +6 -2
  62. package/src/cli/update-view.ts +58 -15
  63. package/src/cli/update.ts +55 -12
  64. package/src/data/cli-tools.ts +24 -9
  65. package/src/services/active-closure.ts +3 -3
  66. package/src/services/catalog-cache-store.ts +8 -6
  67. package/src/services/claude-cli.ts +106 -19
  68. package/src/services/claude-settings.ts +88 -384
  69. package/src/services/cli-tool-commands.ts +104 -3
  70. package/src/services/deps-plan.ts +962 -0
  71. package/src/services/deps-report.ts +265 -0
  72. package/src/services/doctor-bins.ts +115 -43
  73. package/src/services/install-plan.ts +11 -1
  74. package/src/services/local-marketplace.ts +12 -206
  75. package/src/services/marketplace-refresh.ts +4 -4
  76. package/src/services/plugin-env-needs.ts +69 -0
  77. package/src/services/plugin-manager.ts +35 -37
  78. package/src/services/plugin-requires.ts +93 -162
  79. package/src/services/plugin-resolution.ts +34 -42
  80. package/src/services/profile-activate.ts +1 -2
  81. package/src/services/profile-startup.ts +18 -43
  82. package/src/services/profile-state.ts +72 -2
  83. package/src/services/resolver.ts +263 -59
  84. package/src/services/tool-probe.ts +18 -25
  85. package/src/services/toolchain.ts +117 -111
  86. package/src/services/update-engine.ts +345 -45
  87. package/src/services/update-plan.ts +296 -36
  88. package/src/shared/plugin/hooks/bun-missing.sh +39 -0
  89. package/src/shared/plugin/hooks/deps-check.ts +224 -0
  90. package/src/shared/plugin/hooks/lib/requires-probe.ts +1183 -0
  91. package/src/types/index.ts +60 -16
  92. package/src/ui/App.tsx +81 -171
  93. package/src/ui/adapters/cliToolsAdapter.ts +252 -12
  94. package/src/ui/adapters/depsAdapter.ts +129 -0
  95. package/src/ui/adapters/failureReport.ts +137 -0
  96. package/src/ui/adapters/pluginsAdapter.ts +297 -45
  97. package/src/ui/adapters/startupReports.ts +260 -0
  98. package/src/ui/components/modals/FailureListModal.tsx +231 -0
  99. package/src/ui/components/modals/MessageModal.tsx +7 -12
  100. package/src/ui/components/modals/ModalContainer.tsx +10 -5
  101. package/src/ui/components/modals/index.ts +1 -0
  102. package/src/ui/hooks/startupQueue.ts +56 -0
  103. package/src/ui/hooks/useGitignoreModal.ts +30 -52
  104. package/src/ui/hooks/useProfileStartupModal.ts +119 -142
  105. package/src/ui/hooks/useStaleLoadModal.ts +35 -65
  106. package/src/ui/keymap.ts +14 -1
  107. package/src/ui/renderers/cliToolRenderers.tsx +135 -2
  108. package/src/ui/renderers/mcpRenderers.tsx +110 -3
  109. package/src/ui/renderers/pluginRenderers.tsx +77 -6
  110. package/src/ui/renderers/pluginRowText.ts +25 -25
  111. package/src/ui/renderers/pluginStatusText.ts +18 -1
  112. package/src/ui/screens/AliasScreen.tsx +38 -45
  113. package/src/ui/screens/CliToolsScreen.tsx +199 -98
  114. package/src/ui/screens/EnvVarsScreen.tsx +17 -5
  115. package/src/ui/screens/GitignoreScreen.tsx +48 -67
  116. package/src/ui/screens/McpRegistryScreen.tsx +67 -38
  117. package/src/ui/screens/McpScreen.tsx +46 -19
  118. package/src/ui/screens/ModelsScreen.tsx +46 -86
  119. package/src/ui/screens/PluginsScreen.tsx +315 -259
  120. package/src/ui/screens/ProfilesScreen.tsx +79 -40
  121. package/src/ui/screens/SkillsScreen.tsx +83 -80
  122. package/src/ui/screens/StylesScreen.tsx +211 -275
  123. package/src/ui/state/AppContext.tsx +66 -44
  124. package/src/ui/state/DimensionsContext.tsx +3 -22
  125. package/src/ui/state/reducer.ts +12 -100
  126. package/src/ui/state/types.ts +48 -69
  127. package/src/utils/command-utils.ts +4 -5
  128. package/src/utils/run.ts +123 -52
  129. package/src/utils/string-utils.ts +9 -0
  130. package/src/utils/user-path.ts +60 -0
  131. package/src/__tests__/go-module.test.ts +0 -43
  132. package/src/__tests__/notices-rules.test.ts +0 -476
  133. package/src/__tests__/notices.test.tsx +0 -604
  134. package/src/__tests__/plugin-setup.test.ts +0 -1016
  135. package/src/__tests__/shell-script-callers.test.ts +0 -92
  136. package/src/__tests__/styles-status-line.test.ts +0 -72
  137. package/src/__tests__/version-snapshot.test.ts +0 -212
  138. package/src/services/plugin-setup.ts +0 -965
  139. package/src/services/version-snapshot.ts +0 -189
  140. package/src/utils/go-module.ts +0 -36
@@ -1,92 +0,0 @@
1
- /**
2
- * `runShellScript` has exactly one legitimate caller, and this pins it.
3
- *
4
- * The whole of item 3 is the split between two ways of running a child process:
5
- * argv (`runCommand`), which no shell ever sees, and a script (`runShellScript`),
6
- * which `/bin/sh` parses. Every installer command belongs to the first, because
7
- * it is assembled from package names that arrive in `.claude/profiles.json` and
8
- * plugin `requires.bin` — hand-authored, committed by teammates, fetched from
9
- * marketplaces, validated by nothing. The second exists only for the two
10
- * `TOOLCHAIN_BOOTSTRAP` values, which are constants in this repo copied from
11
- * bun.sh and brew.sh and which genuinely contain a pipe and a command
12
- * substitution.
13
- *
14
- * Nothing in the type system distinguishes them — both are strings — so after
15
- * the commit lands, the design decision is enforced by this test or by nothing.
16
- * A branded `ShellScript` type would enforce it at compile time and was
17
- * considered; for one map, two entries and one call site it is more machinery
18
- * than the rule is worth, and unlike a brand this is greppable.
19
- *
20
- * Source-scanning precedent in this suite: `uppercase-keybindings.test.ts`.
21
- */
22
-
23
- import { describe, expect, test } from "bun:test";
24
- import path from "node:path";
25
- import fs from "fs-extra";
26
-
27
- const SRC_DIR = path.join(import.meta.dir, "..");
28
-
29
- /** Files permitted to call `runShellScript`, relative to `src/`. */
30
- const ALLOWED = ["cli/install.ts"];
31
-
32
- /** The definition itself, plus this test, are not callers. */
33
- const NOT_CALLERS = ["utils/run.ts", "__tests__/shell-script-callers.test.ts"];
34
-
35
- async function sourceFiles(dir: string): Promise<string[]> {
36
- const out: string[] = [];
37
- for (const entry of await fs.readdir(dir, { withFileTypes: true })) {
38
- const full = path.join(dir, entry.name);
39
- if (entry.isDirectory()) out.push(...(await sourceFiles(full)));
40
- else if (/\.tsx?$/.test(entry.name)) out.push(full);
41
- }
42
- return out;
43
- }
44
-
45
- const isComment = (line: string) => {
46
- const t = line.trim();
47
- return t.startsWith("//") || t.startsWith("*") || t.startsWith("/*");
48
- };
49
-
50
- async function callersOf(pattern: RegExp): Promise<string[]> {
51
- const found = new Set<string>();
52
- for (const file of await sourceFiles(SRC_DIR)) {
53
- const rel = path.relative(SRC_DIR, file);
54
- if (NOT_CALLERS.includes(rel)) continue;
55
- const text = await fs.readFile(file, "utf8");
56
- for (const line of text.split("\n")) {
57
- if (isComment(line)) continue;
58
- if (pattern.test(line)) found.add(rel);
59
- }
60
- }
61
- return [...found].sort();
62
- }
63
-
64
- describe("only the toolchain bootstrap goes through a shell", () => {
65
- test("runShellScript is called from exactly the allowed files", async () => {
66
- expect(await callersOf(/\brunShellScript\(/)).toEqual(ALLOWED);
67
- });
68
-
69
- test("nothing spawns with a shell outside utils/run.ts", async () => {
70
- // Catches someone reaching for `spawn`/`exec` directly rather than
71
- // through the seam — a second shell path that never names
72
- // `runShellScript`.
73
- //
74
- // Both spellings: `shell: true` and a named shell (`shell: "/bin/bash"`).
75
- // The CLI Tools screen used the second for its version probe (`which -a`
76
- // pipelines, a string-form `exec` of each check command); that probe now
77
- // lives in services/tool-probe.ts and runs argv only. `shell: false` and
78
- // the alias writer's `shell: ShellKind` values are not spawn options and
79
- // do not match.
80
- expect(await callersOf(/shell:\s*(true|["'`])/)).toEqual([]);
81
- });
82
-
83
- test("the scan can actually fail", async () => {
84
- // Negative control. The two assertions above pass trivially if the walk
85
- // finds nothing or the pattern never matches, and a check that cannot
86
- // fail is not evidence. `runCommand(` is known to be called from several
87
- // files, so a working scan must find them.
88
- const runCommandCallers = await callersOf(/\brunCommand\(/);
89
- expect(runCommandCallers.length).toBeGreaterThan(0);
90
- expect(runCommandCallers).toContain("cli/install.ts");
91
- });
92
- });
@@ -1,72 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { appReducer, initialState } from "../ui/state/reducer.js";
3
- import type { AppAction, AppState } from "../ui/state/types.js";
4
-
5
- function run(actions: AppAction[], from: AppState = initialState): AppState {
6
- return actions.reduce(appReducer, from);
7
- }
8
-
9
- /**
10
- * The status line lives in app state, not in the screen.
11
- *
12
- * Two bugs drove this, and both were invisible to every test we had:
13
- *
14
- * 1. It was cleared on a 5-second wall clock, so the message describing what
15
- * you just did was usually gone before you looked at it — and unobservable
16
- * to anything sampling the screen a moment later.
17
- * 2. It was component-local, and `Router` swaps the component type on a tab
18
- * change, so `9 → 1 → 9` destroyed it.
19
- */
20
- describe("status line state", () => {
21
- test("survives leaving the tab and coming back", () => {
22
- const shown = run([
23
- {
24
- type: "STYLES_STATUS_SET",
25
- status: { text: "Selection reset to the live style", tone: "success" },
26
- },
27
- ]);
28
- expect(shown.styles.status?.text).toBe("Selection reset to the live style");
29
-
30
- // Navigating away and back is what unmounts the screen.
31
- const roundTrip = run(
32
- [
33
- { type: "NAVIGATE", route: { screen: "plugins" } },
34
- { type: "NAVIGATE", route: { screen: "styles" } },
35
- ],
36
- shown,
37
- );
38
- expect(roundTrip.styles.status?.text).toBe(
39
- "Selection reset to the live style",
40
- );
41
- });
42
-
43
- test("a new message replaces the previous one", () => {
44
- const state = run([
45
- { type: "STYLES_STATUS_SET", status: { text: "first", tone: "success" } },
46
- { type: "STYLES_STATUS_SET", status: { text: "second", tone: "error" } },
47
- ]);
48
- expect(state.styles.status).toEqual({ text: "second", tone: "error" });
49
- });
50
-
51
- test("clearing returns the line to its default", () => {
52
- const state = run([
53
- { type: "STYLES_STATUS_SET", status: { text: "done", tone: "success" } },
54
- { type: "STYLES_STATUS_CLEAR" },
55
- ]);
56
- expect(state.styles.status).toBeNull();
57
- });
58
-
59
- test("starts empty", () => {
60
- expect(initialState.styles.status).toBeNull();
61
- });
62
-
63
- test("keeps the tone, so an error cannot render as a success", () => {
64
- const state = run([
65
- {
66
- type: "STYLES_STATUS_SET",
67
- status: { text: "Already talking to GitHub", tone: "error" },
68
- },
69
- ]);
70
- expect(state.styles.status?.tone).toBe("error");
71
- });
72
- });
@@ -1,212 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { promises as fs } from "node:fs";
3
- import os from "node:os";
4
- import path from "node:path";
5
- import {
6
- UNKNOWN_VERSION,
7
- diffVersions,
8
- isKnownVersion,
9
- loadSeenVersions,
10
- saveSeenVersions,
11
- } from "../services/version-snapshot.js";
12
-
13
- /**
14
- * Plugins get updated by things magus does not control — Claude Code's own
15
- * `plugin update` writes installed_plugins.json without touching magus's
16
- * bookkeeping. Diffing what we last rendered against reality catches every
17
- * source; logging only magus's own actions catches none of them.
18
- */
19
-
20
- describe("isKnownVersion", () => {
21
- test('"0.0.0" is not a usable version', () => {
22
- // Claude Code records this for a plugin whose version it cannot determine.
23
- expect(isKnownVersion(UNKNOWN_VERSION)).toBe(false);
24
- expect(isKnownVersion(undefined)).toBe(false);
25
- expect(isKnownVersion("")).toBe(false);
26
- expect(isKnownVersion("1.0.0")).toBe(true);
27
- });
28
- });
29
-
30
- describe("diffVersions — first run", () => {
31
- test("no baseline reports nothing at all", () => {
32
- // Without this, the first launch would flag every installed plugin as new,
33
- // which is both wrong and the fastest way to train someone to ignore badges.
34
- expect(
35
- diffVersions(null, { "dev@magus": "2.12.1", "a@mp": UNKNOWN_VERSION }),
36
- ).toEqual([]);
37
- });
38
-
39
- test("an empty baseline is NOT the same as no baseline", () => {
40
- // A real snapshot that happens to be empty means we have looked before and
41
- // saw nothing, so anything present now is genuinely new.
42
- const changes = diffVersions({}, { "dev@magus": "2.12.1" });
43
- expect(changes).toEqual([
44
- { pluginId: "dev@magus", kind: "installed", to: "2.12.1" },
45
- ]);
46
- });
47
- });
48
-
49
- describe("diffVersions — installs", () => {
50
- test("a plugin absent from the baseline is reported as installed, not updated", () => {
51
- const changes = diffVersions(
52
- { "other@mp": "1.0.0" },
53
- { "new@mp": "3.1.0" },
54
- );
55
- expect(changes).toEqual([
56
- { pluginId: "new@mp", kind: "installed", to: "3.1.0" },
57
- ]);
58
- });
59
-
60
- test("an install with an unknown version is still reported", () => {
61
- // The version is unusable but the install is real — the UI shows "new"
62
- // with no version rather than suppressing the plugin entirely.
63
- const changes = diffVersions({}, { "a@mp": UNKNOWN_VERSION });
64
- expect(changes).toEqual([
65
- { pluginId: "a@mp", kind: "installed", to: UNKNOWN_VERSION },
66
- ]);
67
- });
68
- });
69
-
70
- describe("diffVersions — updates", () => {
71
- test("reports a genuine version transition", () => {
72
- expect(
73
- diffVersions({ "dev@magus": "2.12.0" }, { "dev@magus": "2.12.1" }),
74
- ).toEqual([
75
- { pluginId: "dev@magus", kind: "updated", from: "2.12.0", to: "2.12.1" },
76
- ]);
77
- });
78
-
79
- test("unchanged versions report nothing", () => {
80
- expect(
81
- diffVersions({ "dev@magus": "2.12.1" }, { "dev@magus": "2.12.1" }),
82
- ).toEqual([]);
83
- });
84
-
85
- test("learning a previously unknown version is not an update", () => {
86
- // This is exactly what v4.22.0's registry overlay caused: versions that
87
- // could not be resolved suddenly could be. Reporting it would announce
88
- // magus's own bugfix as if the user's plugins had changed.
89
- expect(
90
- diffVersions({ "a@mp": UNKNOWN_VERSION }, { "a@mp": "5.3.0" }),
91
- ).toEqual([]);
92
- });
93
-
94
- test("losing a known version is not an update either", () => {
95
- expect(
96
- diffVersions({ "a@mp": "5.3.0" }, { "a@mp": UNKNOWN_VERSION }),
97
- ).toEqual([]);
98
- });
99
-
100
- test("a downgrade IS reported", () => {
101
- // Rolling back is a real change, arguably the most important to surface.
102
- expect(diffVersions({ "a@mp": "2.0.0" }, { "a@mp": "1.0.0" })).toEqual([
103
- { pluginId: "a@mp", kind: "updated", from: "2.0.0", to: "1.0.0" },
104
- ]);
105
- });
106
- });
107
-
108
- describe("diffVersions — removals and mixed sets", () => {
109
- test("a removed plugin is not reported", () => {
110
- expect(diffVersions({ "gone@mp": "1.0.0" }, {})).toEqual([]);
111
- });
112
-
113
- test("classifies each plugin independently in one pass", () => {
114
- const changes = diffVersions(
115
- { same: "1.0.0", moved: "2.0.0", learned: UNKNOWN_VERSION },
116
- { same: "1.0.0", moved: "2.1.0", learned: "9.0.0", fresh: "0.1.0" },
117
- );
118
- expect(changes).toEqual([
119
- { pluginId: "moved", kind: "updated", from: "2.0.0", to: "2.1.0" },
120
- { pluginId: "fresh", kind: "installed", to: "0.1.0" },
121
- ]);
122
- });
123
- });
124
-
125
- describe("per-project baselines", () => {
126
- /**
127
- * The baseline used to be one flat map for the whole machine, but the
128
- * versions written into it are resolved per project. Two projects
129
- * legitimately on different versions overwrote each other's baseline, so
130
- * every differing plugin was reported as "updated" on the next render — a
131
- * change that never happened.
132
- *
133
- * CLAUDE_CONFIG_DIR, not HOME: os.homedir() reads the passwd database on
134
- * macOS and ignores $HOME, so a HOME-only override writes to the operator's
135
- * real ~/.claude. That is not hypothetical — it happened while writing these.
136
- */
137
- const withSandbox = async (fn: () => Promise<void>): Promise<void> => {
138
- const dir = await fs.mkdtemp(path.join(os.tmpdir(), "vsnap-"));
139
- const prev = process.env.CLAUDE_CONFIG_DIR;
140
- process.env.CLAUDE_CONFIG_DIR = dir;
141
- try {
142
- await fn();
143
- } finally {
144
- // biome-ignore lint/performance/noDelete: assigning undefined to process.env stores the string "undefined"; only delete unsets the variable
145
- if (prev === undefined) delete process.env.CLAUDE_CONFIG_DIR;
146
- else process.env.CLAUDE_CONFIG_DIR = prev;
147
- await fs.rm(dir, { recursive: true, force: true });
148
- }
149
- };
150
-
151
- test("a project with no baseline of its own reports nothing", async () => {
152
- await withSandbox(async () => {
153
- await saveSeenVersions("/proj/a", { "setup@magus": "2.2.0" });
154
- const baseline = await loadSeenVersions("/proj/b");
155
- expect(baseline).toBeNull();
156
- expect(diffVersions(baseline, { "setup@magus": "2.5.0" })).toEqual([]);
157
- });
158
- });
159
-
160
- test("saving one project preserves the others", async () => {
161
- await withSandbox(async () => {
162
- await saveSeenVersions("/proj/a", { "designer@magus": "2.0.0" });
163
- await saveSeenVersions("/proj/b", { "designer@magus": "2.0.1" });
164
- expect(await loadSeenVersions("/proj/a")).toEqual({
165
- "designer@magus": "2.0.0",
166
- });
167
- expect(await loadSeenVersions("/proj/b")).toEqual({
168
- "designer@magus": "2.0.1",
169
- });
170
- });
171
- });
172
-
173
- test("a real update inside one project is still reported", async () => {
174
- await withSandbox(async () => {
175
- await saveSeenVersions("/proj/a", { "dev@magus": "3.0.0" });
176
- const previous = await loadSeenVersions("/proj/a");
177
- expect(diffVersions(previous, { "dev@magus": "3.0.1" })).toEqual([
178
- { pluginId: "dev@magus", kind: "updated", from: "3.0.0", to: "3.0.1" },
179
- ]);
180
- });
181
- });
182
-
183
- test("a pre-migration flat baseline is adopted once, then replaced", async () => {
184
- await withSandbox(async () => {
185
- const file = path.join(
186
- process.env.CLAUDE_CONFIG_DIR as string,
187
- "magus-version-snapshot.json",
188
- );
189
- await fs.writeFile(
190
- file,
191
- JSON.stringify({
192
- seen: { "dev@magus": "3.0.0" },
193
- seededAt: "2026-07-29T00:00:00.000Z",
194
- updatedAt: "2026-07-29T00:00:00.000Z",
195
- }),
196
- );
197
-
198
- // Legacy shape carries no project, so it stands in for whoever asks first
199
- // rather than being thrown away.
200
- expect(await loadSeenVersions("/proj/a")).toEqual({
201
- "dev@magus": "3.0.0",
202
- });
203
-
204
- await saveSeenVersions("/proj/a", { "dev@magus": "3.0.1" });
205
- // Once per-project entries exist, the flat map is no longer consulted.
206
- expect(await loadSeenVersions("/proj/b")).toBeNull();
207
- expect(await loadSeenVersions("/proj/a")).toEqual({
208
- "dev@magus": "3.0.1",
209
- });
210
- });
211
- });
212
- });