magus-cli 7.7.0 → 7.8.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 (83) hide show
  1. package/package.json +4 -4
  2. package/src/__tests__/blackbox-parity.test.ts +29 -15
  3. package/src/__tests__/blackbox-plugin-state.test.ts +25 -6
  4. package/src/__tests__/blind-780-cli-tools.test.ts +808 -0
  5. package/src/__tests__/blind-780-deprecated.test.ts +631 -0
  6. package/src/__tests__/blind-780-failure-reasons.test.ts +1004 -0
  7. package/src/__tests__/blind-780-plugin-resolution.test.ts +431 -0
  8. package/src/__tests__/blind-780-review2.test.ts +1203 -0
  9. package/src/__tests__/cli-update-view.test.ts +3 -3
  10. package/src/__tests__/deprecated-remove.test.ts +827 -0
  11. package/src/__tests__/doctor.test.ts +159 -1
  12. package/src/__tests__/failure-popup-review2.test.tsx +920 -0
  13. package/src/__tests__/failure-popup.test.tsx +419 -0
  14. package/src/__tests__/final-fixes-780.test.tsx +724 -0
  15. package/src/__tests__/format-update-outcome.test.ts +6 -32
  16. package/src/__tests__/gitignore-profile.test.ts +1 -9
  17. package/src/__tests__/models-screen-state.test.ts +18 -40
  18. package/src/__tests__/plugin-resolution.test.ts +52 -13
  19. package/src/__tests__/plugin-row-text.test.ts +30 -37
  20. package/src/__tests__/profile-dependencies.test.ts +7 -0
  21. package/src/__tests__/profile-startup-old-format.test.ts +0 -3
  22. package/src/__tests__/profile-startup.test.ts +40 -12
  23. package/src/__tests__/stale-load-repair.test.ts +31 -23
  24. package/src/__tests__/update-apply.test.ts +13 -29
  25. package/src/__tests__/update-plan.test.ts +23 -17
  26. package/src/__tests__/useStaleLoadModal.test.ts +1 -57
  27. package/src/__tests__/version-unknown-state.test.ts +15 -17
  28. package/src/cli/doctor.ts +151 -30
  29. package/src/cli/install.ts +5 -6
  30. package/src/cli/update-view.ts +58 -15
  31. package/src/cli/update.ts +27 -5
  32. package/src/services/catalog-cache-store.ts +8 -6
  33. package/src/services/claude-cli.ts +106 -19
  34. package/src/services/claude-settings.ts +84 -36
  35. package/src/services/plugin-env-needs.ts +69 -0
  36. package/src/services/plugin-manager.ts +31 -19
  37. package/src/services/plugin-resolution.ts +34 -42
  38. package/src/services/profile-activate.ts +1 -2
  39. package/src/services/profile-startup.ts +18 -43
  40. package/src/services/profile-state.ts +72 -2
  41. package/src/services/resolver.ts +95 -1
  42. package/src/services/update-engine.ts +326 -31
  43. package/src/services/update-plan.ts +293 -33
  44. package/src/types/index.ts +7 -0
  45. package/src/ui/App.tsx +90 -146
  46. package/src/ui/adapters/cliToolsAdapter.ts +203 -1
  47. package/src/ui/adapters/failureReport.ts +137 -0
  48. package/src/ui/adapters/pluginsAdapter.ts +297 -45
  49. package/src/ui/adapters/startupReports.ts +282 -0
  50. package/src/ui/components/modals/FailureListModal.tsx +231 -0
  51. package/src/ui/components/modals/MessageModal.tsx +7 -12
  52. package/src/ui/components/modals/ModalContainer.tsx +10 -5
  53. package/src/ui/components/modals/index.ts +1 -0
  54. package/src/ui/hooks/startupQueue.ts +56 -0
  55. package/src/ui/hooks/useGitignoreModal.ts +30 -52
  56. package/src/ui/hooks/useProfileStartupModal.ts +119 -142
  57. package/src/ui/hooks/useStaleLoadModal.ts +35 -65
  58. package/src/ui/keymap.ts +14 -1
  59. package/src/ui/renderers/mcpRenderers.tsx +110 -3
  60. package/src/ui/renderers/pluginRenderers.tsx +77 -6
  61. package/src/ui/renderers/pluginRowText.ts +25 -25
  62. package/src/ui/renderers/pluginStatusText.ts +18 -1
  63. package/src/ui/screens/AliasScreen.tsx +38 -45
  64. package/src/ui/screens/CliToolsScreen.tsx +65 -54
  65. package/src/ui/screens/EnvVarsScreen.tsx +17 -5
  66. package/src/ui/screens/GitignoreScreen.tsx +48 -67
  67. package/src/ui/screens/McpRegistryScreen.tsx +67 -38
  68. package/src/ui/screens/McpScreen.tsx +46 -19
  69. package/src/ui/screens/ModelsScreen.tsx +46 -86
  70. package/src/ui/screens/PluginsScreen.tsx +313 -221
  71. package/src/ui/screens/ProfilesScreen.tsx +79 -40
  72. package/src/ui/screens/SkillsScreen.tsx +83 -80
  73. package/src/ui/screens/StylesScreen.tsx +211 -275
  74. package/src/ui/state/AppContext.tsx +66 -44
  75. package/src/ui/state/DimensionsContext.tsx +3 -22
  76. package/src/ui/state/reducer.ts +12 -100
  77. package/src/ui/state/types.ts +48 -69
  78. package/src/utils/user-path.ts +60 -0
  79. package/src/__tests__/notices-rules.test.ts +0 -476
  80. package/src/__tests__/notices.test.tsx +0 -604
  81. package/src/__tests__/styles-status-line.test.ts +0 -72
  82. package/src/__tests__/version-snapshot.test.ts +0 -212
  83. package/src/services/version-snapshot.ts +0 -189
@@ -303,7 +303,9 @@ describe("repairStaleLoad — the backup", () => {
303
303
  const result = await repairStaleLoad(PLUGIN, "0.5.0", here, root);
304
304
 
305
305
  expect(result.ok).toBe(false);
306
- expect(result.error).toContain("could not back up");
306
+ expect(result.error).toBe(
307
+ "could not save a backup before changing anything, so nothing was changed",
308
+ );
307
309
  expect(result.rewritten).toBe(0);
308
310
  expect(result.backup).toBeNull();
309
311
  expect(await fs.readFile(registryFile, "utf8")).toBe(before);
@@ -328,7 +330,7 @@ describe("repairStaleLoad — refusals", () => {
328
330
  const result = await repairStaleLoad(PLUGIN, "0.7.0", here, root);
329
331
 
330
332
  expect(result.ok).toBe(false);
331
- expect(result.error).toContain("no cache directory for v0.7.0");
333
+ expect(result.error).toBe("v0.7.0 is not downloaded — run magus install");
332
334
  expect(result.backup).toBeNull();
333
335
  expect(await fs.readFile(registryFile, "utf8")).toBe(before);
334
336
  expect(await backups()).toEqual([]);
@@ -340,7 +342,7 @@ describe("repairStaleLoad — refusals", () => {
340
342
  const result = await repairStaleLoad(PLUGIN, "0.5.0", here, root);
341
343
 
342
344
  expect(result.ok).toBe(false);
343
- expect(result.error).toContain("has no rows");
345
+ expect(result.error).toBe("it is not installed on this machine");
344
346
  });
345
347
 
346
348
  test("a string that is not a plugin id is refused", async () => {
@@ -360,7 +362,9 @@ describe("repairStaleLoad — refusals", () => {
360
362
  const result = await repairStaleLoad(PLUGIN, "0.5.0", here, root);
361
363
 
362
364
  expect(result.ok).toBe(false);
363
- expect(result.error).toContain("not a readable registry");
365
+ expect(result.error).toBe(
366
+ "Claude Code's list of installed plugins cannot be read, so nothing was changed — run magus doctor",
367
+ );
364
368
  expect(await fs.readFile(registryFile, "utf8")).toBe("{ not json");
365
369
  });
366
370
  });
@@ -400,8 +404,9 @@ describe("repairStaleLoad — scope reach", () => {
400
404
  const result = await repairStaleLoad(PLUGIN, "0.5.0", here, root);
401
405
 
402
406
  expect(result.ok).toBe(false);
403
- expect(result.error).toContain("machine-wide user-scope record at v0.3.0");
404
- expect(result.error).toContain("--scope user");
407
+ expect(result.error).toBe(
408
+ "Claude Code loads the USER install of it here — update it at USER scope",
409
+ );
405
410
  expect(result.rewritten).toBe(0);
406
411
  expect(await fs.readFile(registryFile, "utf8")).toBe(before);
407
412
  expect(await backups()).toEqual([]);
@@ -457,8 +462,9 @@ describe("repairStaleLoad — scope reach", () => {
457
462
  const result = await repairStaleLoad(PLUGIN, "0.5.0", here, root);
458
463
 
459
464
  expect(result.ok).toBe(false);
460
- expect(result.error).toContain("machine-wide managed-scope record");
461
- expect(result.error).toContain("administrator policy");
465
+ expect(result.error).toBe(
466
+ "Claude Code loads the install your administrator manages here — ask them to update it",
467
+ );
462
468
  });
463
469
 
464
470
  test("a user-scope row that is not the winner stays where it is", async () => {
@@ -485,13 +491,11 @@ describe("repairStaleLoad — scope reach", () => {
485
491
  expect(after[1].installPath).toBe(cacheFor("0.4.0"));
486
492
  });
487
493
 
488
- test("a rootless row from another directory is relevant but never rewritten", async () => {
489
- // The loader's filter compares repository roots and two nulls are equal
490
- // there, so this row genuinely competes — `isRelevantInstallation` says so
491
- // and plugin-resolution.test.ts pins it. Writing it is a different question:
492
- // "the same repository" cannot be true of two directories that are in no
493
- // repository, so the repair reports that it could not fix this rather than
494
- // editing a stranger's row.
494
+ test("a rootless row from another directory neither competes nor is rewritten", async () => {
495
+ // Claude Code 2.1.284 matches nothing by root in a folder that is in no
496
+ // repository, so the other folder's older row does not load here and this
497
+ // folder's own row does. Nothing to repair, and the stranger's row is left
498
+ // exactly as it was.
495
499
  const looseHere = path.join(base, "loose-here");
496
500
  const looseOther = path.join(base, "loose-other");
497
501
  await fs.ensureDir(looseHere);
@@ -507,13 +511,13 @@ describe("repairStaleLoad — scope reach", () => {
507
511
  looseHere,
508
512
  repoRootForPath(looseHere),
509
513
  ),
510
- ).toBe(true);
514
+ ).toBe(false);
511
515
 
512
516
  const result = await repairStaleLoad(PLUGIN, "0.5.0", looseHere, root);
513
517
 
514
518
  expect(result.rewritten).toBe(0);
515
- expect(result.ok).toBe(false);
516
- expect(result.error).toContain("still resolves v0.3.0");
519
+ expect(result.ok).toBe(true);
520
+ expect(result.loaded).toBe("0.5.0");
517
521
  expect((await readRegistry()).plugins[PLUGIN][0].version).toBe("0.3.0");
518
522
  });
519
523
  });
@@ -557,7 +561,9 @@ describe("repairStaleLoad — a concurrent write is refused, not clobbered", ()
557
561
  const result = await repairStaleLoad(PLUGIN, "0.5.0", here, root);
558
562
 
559
563
  expect(result.ok).toBe(false);
560
- expect(result.error).toContain("changed on disk after it was read");
564
+ expect(result.error).toBe(
565
+ "another Claude Code session changed its plugins at the same moment, so nothing was changed — run it again",
566
+ );
561
567
  expect(result.rewritten).toBe(0);
562
568
  } finally {
563
569
  spy.mockRestore();
@@ -694,7 +700,9 @@ describe("repairStaleLoad — the post-write assertion", () => {
694
700
  expect(result.rewritten).toBe(1);
695
701
  expect(result.loaded).toBe("0.9.0");
696
702
  expect(result.ok).toBe(false);
697
- expect(result.error).toContain("still resolves v0.9.0");
703
+ expect(result.error).toContain(
704
+ "Claude Code still loads v0.9.0 in this folder",
705
+ );
698
706
  });
699
707
 
700
708
  test("the assertion reads the file from DISK, not the object it mutated", async () => {
@@ -1147,7 +1155,7 @@ describe("B4: an update brings the loader forward in the same run", () => {
1147
1155
  expect(result.ok).toBe(1);
1148
1156
  expect(result.failed).toEqual([]);
1149
1157
  expect(details).toEqual([
1150
- "project → 0.5.0 · Claude Code now loads v0.5.0 here (1 older record moved forward)",
1158
+ "project → 0.5.0 · Claude Code now loads v0.5.0 in this folder",
1151
1159
  ]);
1152
1160
  expect(await loadedVersionHere(PLUGIN, here, root)).toBe("0.5.0");
1153
1161
  // Moved forward, nothing removed.
@@ -1265,7 +1273,7 @@ describe("7.5.2: Claude Code 2.1.281 updates another project's row of the reposi
1265
1273
  [here, "0.5.0"],
1266
1274
  ]);
1267
1275
  expect(details).toEqual([
1268
- `project → 0.5.0 · Claude Code updated ${mainTree} instead of this project — moved this project's record to v0.5.0 too`,
1276
+ "project → 0.5.0 · Claude Code updated another folder of this repository instead of this one — this folder now loads v0.5.0 too",
1269
1277
  ]);
1270
1278
  expect(await loadedVersionHere(PLUGIN, here, root)).toBe("0.5.0");
1271
1279
  });
@@ -1335,7 +1343,7 @@ describe("7.5.2: Claude Code 2.1.281 updates another project's row of the reposi
1335
1343
  ?.version,
1336
1344
  ).toBe("0.4.0");
1337
1345
  expect(details[0]).toContain(
1338
- `no change — the installer exited 0 but ${PLUGIN} is still 0.4.0 (Claude Code updated ${mainTree} instead of this project, and this project's record could not follow: no cache directory for v0.6.0`,
1346
+ `no change — the installer exited 0 but ${PLUGIN} is still 0.4.0 (Claude Code updated another folder of this repository instead of this one, and this folder could not follow: v0.6.0 is not downloaded — run magus install)`,
1339
1347
  );
1340
1348
  });
1341
1349
  });
@@ -26,7 +26,6 @@ import type {
26
26
  UpdatePlan,
27
27
  } from "../services/update-plan.js";
28
28
  import type { InstalledPluginEntry, ResolvedBin } from "../types/index.js";
29
- import { noMoveReasons } from "../ui/adapters/pluginsAdapter.js";
30
29
 
31
30
  const PROJECT = "/tmp/project";
32
31
 
@@ -776,25 +775,6 @@ describe("applyPlugins — read-back against beforeVersions", () => {
776
775
  expect(result.ok).toBe(1);
777
776
  expect(result.unchanged).toEqual([]);
778
777
  });
779
-
780
- test("the TUI's collector names the same cause the CLI prints", async () => {
781
- // The screen used to branch on `failed.length` alone: a no-op update
782
- // closed with a success and a row that had not moved.
783
- const { deps } = stubDeps("1.0.0");
784
- const reasons = noMoveReasons();
785
- const result = await applyPlugins(
786
- [item({ beforeVersions: { project: "1.0.0" } })],
787
- PROJECT,
788
- deps,
789
- reasons.reporter,
790
- );
791
- expect(result.unchanged).toEqual(["dev@magus"]);
792
- expect(reasons.lines(result.unchanged)).toEqual([
793
- "dev@magus: the installer exited 0 but dev@magus is still 1.0.0",
794
- ]);
795
- // An item with no recorded cause still gets a line.
796
- expect(reasons.lines(["other@magus"])).toEqual(["other@magus"]);
797
- });
798
778
  });
799
779
 
800
780
  // ── B4: installed must mean LOADED ──────────────────────────────────────────
@@ -847,8 +827,7 @@ describe("B4: an install or update ends with the loader on the new version", ()
847
827
  {
848
828
  ok: true,
849
829
  name: "dev@magus",
850
- detail:
851
- "project → 2.0.0 · Claude Code now loads v2.0.0 here (3 older records moved forward)",
830
+ detail: "project → 2.0.0 · Claude Code now loads v2.0.0 in this folder",
852
831
  },
853
832
  ]);
854
833
  });
@@ -876,8 +855,10 @@ describe("B4: an install or update ends with the loader on the new version", ()
876
855
  {
877
856
  ok: false,
878
857
  name: "dev@magus",
858
+ // The version still loading IS the reason; the repair's own message
859
+ // said it a second time, in the mechanism's words.
879
860
  detail:
880
- "installed v2.0.0, but Claude Code still loads v1.0.0 here — the loader still resolves v1.0.0 here, not v2.0.0",
861
+ "installed v2.0.0, but Claude Code still loads v1.0.0 in this folder",
881
862
  },
882
863
  ]);
883
864
  });
@@ -917,7 +898,7 @@ describe("B4: an install or update ends with the loader on the new version", ()
917
898
  await expect(
918
899
  updatePluginInScope("dev@magus", "project", PROJECT, deps),
919
900
  ).rejects.toThrow(
920
- "installed v2.0.0, but Claude Code still loads v1.0.0 here",
901
+ "installed v2.0.0, but Claude Code still loads v1.0.0 in this folder",
921
902
  );
922
903
  });
923
904
 
@@ -938,7 +919,9 @@ describe("B4: an install or update ends with the loader on the new version", ()
938
919
  const { deps } = loaderDeps("1.0.0", false);
939
920
  await expect(
940
921
  updatePluginInScope("dev@magus", "user", PROJECT, deps),
941
- ).rejects.toThrow("the loader still resolves v1.0.0 here, not v2.0.0");
922
+ ).rejects.toThrow(
923
+ "installed v2.0.0, but Claude Code still loads v1.0.0 in this folder",
924
+ );
942
925
  });
943
926
  });
944
927
 
@@ -1067,7 +1050,8 @@ describe("7.5.2: an update Claude Code applied to another project's row", () =>
1067
1050
  {
1068
1051
  ok: true,
1069
1052
  name: "dev@magus",
1070
- detail: `project → 8.1.3 · Claude Code updated ${OTHER} instead of this project — moved this project's record to v8.1.3 too`,
1053
+ detail:
1054
+ "project → 8.1.3 · Claude Code updated another folder of this repository instead of this one — this folder now loads v8.1.3 too",
1071
1055
  },
1072
1056
  ]);
1073
1057
  });
@@ -1084,7 +1068,7 @@ describe("7.5.2: an update Claude Code applied to another project's row", () =>
1084
1068
  // forward by the loader check (B4), and no misdirection is reported.
1085
1069
  expect(repaired).toEqual(["8.1.3"]);
1086
1070
  expect(rows[0]?.detail).toBe(
1087
- "project → 8.1.3 · Claude Code now loads v8.1.3 here (1 older record moved forward)",
1071
+ "project → 8.1.3 · Claude Code now loads v8.1.3 in this folder",
1088
1072
  );
1089
1073
  });
1090
1074
 
@@ -1097,7 +1081,7 @@ describe("7.5.2: an update Claude Code applied to another project's row", () =>
1097
1081
  expect(row(HERE).version).toBe("8.0.0");
1098
1082
  expect(result.unchanged).toEqual(["dev@magus"]);
1099
1083
  expect(rows[0]?.detail).toBe(
1100
- `no change — the installer exited 0 but dev@magus is still 8.0.0 (Claude Code updated ${OTHER} instead of this project, and this project's record could not follow: no cache directory for v8.1.3)`,
1084
+ "no change — the installer exited 0 but dev@magus is still 8.0.0 (Claude Code updated another folder of this repository instead of this one, and this folder could not follow: no cache directory for v8.1.3)",
1101
1085
  );
1102
1086
  });
1103
1087
 
@@ -1125,7 +1109,7 @@ describe("7.5.2: an update Claude Code applied to another project's row", () =>
1125
1109
  expect(repaired).toEqual([]);
1126
1110
  expect(result.unchanged).toEqual(["dev@magus"]);
1127
1111
  expect(rows[0]?.detail).toBe(
1128
- `no change — the installer exited 0 but dev@magus is still 8.0.0 (Claude Code updated ${OTHER} instead of this project, and this project's record could not follow: it went to v8.5.0, not the v8.1.3 this update asked for)`,
1112
+ "no change — the installer exited 0 but dev@magus is still 8.0.0 (Claude Code updated another folder of this repository instead of this one, and this folder could not follow: it went to v8.5.0, not the v8.1.3 this update asked for)",
1129
1113
  );
1130
1114
  });
1131
1115
 
@@ -430,19 +430,23 @@ describe("per-scope decisions — a stale scope is never masked", () => {
430
430
  // No note: there is nothing for the reader to do about it.
431
431
  expect(item.note).toBeUndefined();
432
432
  // And it is not confused with the withdrawn case, which is the next test.
433
- expect(item.reason).not.toBe("not-offered");
433
+ expect(item.reason).not.toBe("deprecated");
434
434
  });
435
435
 
436
- test("an installed plugin ABSENT from every catalog is 'not offered'", () => {
436
+ test("a declared plugin ABSENT from every catalog is deprecated", () => {
437
437
  // The genuinely withdrawn case: no catalog entry at all, the installed
438
438
  // version known only from the registry.
439
439
  const [item] = planPluginUpdates(
440
440
  { "withdrawn@magus": "latest" },
441
441
  catalog(info({ id: "dev@magus", version: "4.6.1" })),
442
442
  );
443
- expect(item.action).toBe("unknown");
444
- expect(item.note).toContain("not offered by any configured marketplace");
445
- expect(item.reason).toBe("not-offered");
443
+ // Its marketplace was read and does not list it: no longer published, so
444
+ // the work is removing it — here from the profile alone, since nothing is
445
+ // installed to uninstall.
446
+ expect(item.action).toBe("remove");
447
+ expect(item.note).toBe("no longer published");
448
+ expect(item.reason).toBe("deprecated");
449
+ expect(item.scopes).toEqual([]);
446
450
  });
447
451
  });
448
452
 
@@ -993,19 +997,20 @@ describe("classifyPlugin — an orphan is never 'current'", () => {
993
997
  info({ id: "dev@magus", version: "4.7.0" }),
994
998
  ),
995
999
  );
996
- expect(item.action).toBe("unknown");
1000
+ expect(item.action).toBe("remove");
997
1001
  expect(item.action).not.toBe("current");
998
1002
  expect(item.available).toBeNull();
999
1003
  expect(item.target).toBeNull();
1000
- expect(item.scopes).toEqual([]);
1004
+ // A removal's scopes are where it is uninstalled: every scope installed here.
1005
+ expect(item.scopes).toEqual(["project"]);
1001
1006
  });
1002
1007
 
1003
1008
  // `moved` is GONE, and `movedTo` with it. It was inferred from a bare name
1004
1009
  // match: a plugin published to two marketplaces — claudish is — reported
1005
1010
  // "moved → magus" when nothing had moved and both copies were installed.
1006
1011
  // The inference cannot tell a move from a dual publication, so it is not made
1007
- // at all. An orphan is `not-offered`, which is a fact rather than a guess.
1008
- test("an orphan is 'not offered', and never guesses where it went", () => {
1012
+ // at all. An orphan is `deprecated`, which is a fact rather than a guess.
1013
+ test("an orphan is deprecated, and never guesses where it went", () => {
1009
1014
  const [item] = planPluginUpdates(
1010
1015
  { "seo@magus": "latest" },
1011
1016
  catalog(
@@ -1014,13 +1019,13 @@ describe("classifyPlugin — an orphan is never 'current'", () => {
1014
1019
  ),
1015
1020
  );
1016
1021
  expect(item).toMatchObject({
1017
- action: "unknown",
1018
- reason: "not-offered",
1022
+ action: "remove",
1023
+ reason: "deprecated",
1019
1024
  });
1020
1025
  expect(item.note).not.toMatch(/moved/i);
1021
1026
  });
1022
1027
 
1023
- test("an orphan whose marketplace we DID read is 'not offered'", () => {
1028
+ test("an orphan whose marketplace we DID read is deprecated", () => {
1024
1029
  const [item] = planPluginUpdates(
1025
1030
  { "seo@magus": "latest" },
1026
1031
  catalog(
@@ -1029,9 +1034,9 @@ describe("classifyPlugin — an orphan is never 'current'", () => {
1029
1034
  ),
1030
1035
  );
1031
1036
  expect(item).toMatchObject({
1032
- action: "unknown",
1033
- reason: "not-offered",
1034
- note: "not offered by any configured marketplace",
1037
+ action: "remove",
1038
+ reason: "deprecated",
1039
+ note: "no longer published",
1035
1040
  });
1036
1041
  });
1037
1042
 
@@ -1062,7 +1067,7 @@ describe("classifyPlugin — an orphan is never 'current'", () => {
1062
1067
  expect(item.reason).toBe("marketplace-unread");
1063
1068
  });
1064
1069
 
1065
- test("an orphan is unknown under an exact pin too", () => {
1070
+ test("an orphan is deprecated under an exact pin too", () => {
1066
1071
  const [item] = planPluginUpdates(
1067
1072
  { "seo@magus": "1.8.0" },
1068
1073
  catalog(
@@ -1070,7 +1075,7 @@ describe("classifyPlugin — an orphan is never 'current'", () => {
1070
1075
  info({ id: "dev@magus", version: "4.7.0" }),
1071
1076
  ),
1072
1077
  );
1073
- expect(item.action).toBe("unknown");
1078
+ expect(item.action).toBe("remove");
1074
1079
  });
1075
1080
  });
1076
1081
 
@@ -1359,6 +1364,7 @@ describe("countPluginStates", () => {
1359
1364
  repair: 1,
1360
1365
  current: 0,
1361
1366
  "stale-load": 0,
1367
+ remove: 0,
1362
1368
  unknown: 1,
1363
1369
  });
1364
1370
  });
@@ -1,8 +1,5 @@
1
1
  import { describe, expect, it } from "bun:test";
2
- import {
3
- buildStaleLoadModal,
4
- staleLoadRepairNotices,
5
- } from "../ui/hooks/useStaleLoadModal";
2
+ import { buildStaleLoadModal } from "../ui/hooks/useStaleLoadModal";
6
3
 
7
4
  /**
8
5
  * The startup popup for a broken plugin.
@@ -137,56 +134,3 @@ describe("buildStaleLoadModal", () => {
137
134
  });
138
135
  });
139
136
  });
140
-
141
- describe("staleLoadRepairNotices", () => {
142
- it("says what loads NOW for the repaired plugins, in one line", () => {
143
- expect(
144
- staleLoadRepairNotices([
145
- { pluginId: "madbench@magus", target: "0.5.0", ok: true },
146
- { pluginId: "dev@magus", target: "7.4.0", ok: true },
147
- ]),
148
- ).toEqual([
149
- {
150
- text: "Now loading madbench@magus v0.5.0, dev@magus v7.4.0",
151
- tone: "success",
152
- },
153
- ]);
154
- });
155
-
156
- it("reports a failed repair with its own reason, never as a success", () => {
157
- expect(
158
- staleLoadRepairNotices([
159
- {
160
- pluginId: "madbench@magus",
161
- target: "0.5.0",
162
- ok: false,
163
- error: "the loader still resolves v0.3.0 here, not v0.5.0",
164
- },
165
- ]),
166
- ).toEqual([
167
- {
168
- text: "madbench@magus not repaired: the loader still resolves v0.3.0 here, not v0.5.0",
169
- tone: "warning",
170
- },
171
- ]);
172
- });
173
-
174
- it("falls back to a plain reason rather than an empty line", () => {
175
- expect(
176
- staleLoadRepairNotices([
177
- { pluginId: "a@magus", target: "1.0.0", ok: false },
178
- ]),
179
- ).toEqual([
180
- {
181
- text: "a@magus not repaired: repair did not take effect",
182
- tone: "warning",
183
- },
184
- ]);
185
- });
186
-
187
- it("says so when there was nothing to repair", () => {
188
- expect(staleLoadRepairNotices([])).toEqual([
189
- { text: "Nothing needed repairing", tone: "success" },
190
- ]);
191
- });
192
- });
@@ -178,7 +178,7 @@ const magusCatalog = (...extra: PluginInfo[]) =>
178
178
  );
179
179
 
180
180
  describe("planPluginUpdates — a pin its own marketplace does not offer", () => {
181
- test("declared, not installed, marketplace read, plugin absent → unknown", () => {
181
+ test("declared, not installed, marketplace read, plugin absent → remove", () => {
182
182
  // The bug: the !installed branch ran before any catalog guard, so
183
183
  // `agentdev@magus` (retired) and `seo@magus` (moved to magus-marketing)
184
184
  // planned as `install` with the literal pin as target on every run,
@@ -188,20 +188,20 @@ describe("planPluginUpdates — a pin its own marketplace does not offer", () =>
188
188
  { "agentdev@magus": "latest" },
189
189
  magusCatalog(),
190
190
  );
191
+ // Deprecated now, and the work is dropping the pin from the profile:
192
+ // still never an `install` of something nothing can fetch.
191
193
  expect(items).toHaveLength(1);
192
- expect(items[0]?.action).toBe("unknown");
194
+ expect(items[0]?.action).toBe("remove");
193
195
  expect(items[0]?.target).toBeNull();
194
- expect(items[0]?.note).toContain(
195
- "not offered by any configured marketplace",
196
- );
197
- expect(items[0]?.reason).toBe("not-offered");
196
+ expect(items[0]?.note).toBe("no longer published");
197
+ expect(items[0]?.reason).toBe("deprecated");
198
198
  });
199
199
 
200
- test("an exact pin its marketplace does not offer is also unknown", () => {
200
+ test("an exact pin its marketplace does not offer is also deprecated", () => {
201
201
  const items = planPluginUpdates({ "gone@magus": "3.0.0" }, magusCatalog());
202
- expect(items[0]?.action).toBe("unknown");
202
+ expect(items[0]?.action).toBe("remove");
203
203
  expect(items[0]?.target).toBeNull();
204
- expect(items[0]?.reason).toBe("not-offered");
204
+ expect(items[0]?.reason).toBe("deprecated");
205
205
  });
206
206
 
207
207
  test("declared, not installed, and the catalog does offer it → install", () => {
@@ -345,14 +345,14 @@ describe("installed with no comparable catalog version — two different causes"
345
345
  // The distinction this test exists for is between the two REASONS, and it
346
346
  // still holds. What changed is the verdict: declaring no version is not a
347
347
  // problem, so the row is `current` and says nothing. Being withdrawn IS a
348
- // problem, so the next test's row is `unknown` and says so.
348
+ // problem, so the next test's row is `remove` and says so.
349
349
  expect(items[0]?.reason).toBe("no-published-version");
350
- expect(items[0]?.reason).not.toBe("not-offered");
350
+ expect(items[0]?.reason).not.toBe("deprecated");
351
351
  expect(items[0]?.action).toBe("current");
352
352
  expect(items[0]?.note).toBeUndefined();
353
353
  });
354
354
 
355
- test("absent from the catalog entirely IS 'not offered'", () => {
355
+ test("absent from the catalog entirely IS deprecated", () => {
356
356
  const items = planPluginUpdates(
357
357
  { "gone@magus": "latest" },
358
358
  new Map([
@@ -366,11 +366,9 @@ describe("installed with no comparable catalog version — two different causes"
366
366
  ],
367
367
  ]),
368
368
  );
369
- expect(items[0]?.action).toBe("unknown");
370
- expect(items[0]?.note).toContain(
371
- "not offered by any configured marketplace",
372
- );
373
- expect(items[0]?.reason).toBe("not-offered");
369
+ expect(items[0]?.action).toBe("remove");
370
+ expect(items[0]?.note).toBe("no longer published");
371
+ expect(items[0]?.reason).toBe("deprecated");
374
372
  });
375
373
  });
376
374