@blackbelt-technology/pi-agent-dashboard 0.5.2 → 0.5.4

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 (212) hide show
  1. package/AGENTS.md +19 -30
  2. package/README.md +69 -1
  3. package/docs/architecture.md +89 -165
  4. package/package.json +11 -7
  5. package/packages/extension/package.json +2 -2
  6. package/packages/extension/src/__tests__/bridge-default-model-gate.test.ts +47 -0
  7. package/packages/extension/src/__tests__/bridge-followup-chat-order.test.ts +215 -0
  8. package/packages/extension/src/__tests__/bridge-followup-multi-entry.test.ts +202 -0
  9. package/packages/extension/src/__tests__/bridge-queue-update-forward.test.ts +77 -0
  10. package/packages/extension/src/__tests__/bridge-retry-ordering.test.ts +148 -0
  11. package/packages/extension/src/__tests__/bridge-shadow-queue-drain.test.ts +221 -0
  12. package/packages/extension/src/__tests__/bridge-shadow-queue-gate.test.ts +299 -0
  13. package/packages/extension/src/__tests__/bridge-shutdown-reset.test.ts +238 -0
  14. package/packages/extension/src/__tests__/bridge-slash-command-routing.test.ts +127 -31
  15. package/packages/extension/src/__tests__/command-handler.test.ts +105 -3
  16. package/packages/extension/src/__tests__/fixtures/usage-limit-error-strings.ts +127 -0
  17. package/packages/extension/src/__tests__/source-detector.test.ts +15 -0
  18. package/packages/extension/src/__tests__/usage-limit-orderer.test.ts +12 -0
  19. package/packages/extension/src/bridge-default-model-gate.ts +32 -0
  20. package/packages/extension/src/bridge.ts +299 -20
  21. package/packages/extension/src/command-handler.ts +53 -7
  22. package/packages/extension/src/dashboard-default-adapter.ts +5 -0
  23. package/packages/extension/src/prompt-bus.ts +15 -0
  24. package/packages/extension/src/slash-dispatch.ts +30 -15
  25. package/packages/extension/src/source-detector.ts +13 -5
  26. package/packages/extension/src/usage-limit-orderer.ts +18 -1
  27. package/packages/server/bin/pi-dashboard.mjs +62 -14
  28. package/packages/server/package.json +9 -5
  29. package/packages/server/src/__tests__/browser-gateway-register-handler.test.ts +69 -0
  30. package/packages/server/src/__tests__/cli-env-no-clobber.test.ts +46 -0
  31. package/packages/server/src/__tests__/cli-no-bootstrap-references.test.ts +69 -0
  32. package/packages/server/src/__tests__/cli-parse.test.ts +9 -10
  33. package/packages/server/src/__tests__/cli-version.test.ts +151 -0
  34. package/packages/server/src/__tests__/directory-service-openspec-enabled.test.ts +9 -0
  35. package/packages/server/src/__tests__/directory-service-refresh-force.test.ts +9 -0
  36. package/packages/server/src/__tests__/directory-service-specs-mtime.test.ts +9 -0
  37. package/packages/server/src/__tests__/directory-service-toctou.test.ts +9 -0
  38. package/packages/server/src/__tests__/directory-service.test.ts +9 -0
  39. package/packages/server/src/__tests__/doctor-route.test.ts +53 -0
  40. package/packages/server/src/__tests__/event-wiring-queue-state.test.ts +156 -0
  41. package/packages/server/src/__tests__/event-wiring-resume-clear.test.ts +105 -0
  42. package/packages/server/src/__tests__/health-shape.test.ts +35 -12
  43. package/packages/server/src/__tests__/installed-package-enricher.test.ts +12 -12
  44. package/packages/server/src/__tests__/is-activity-event.test.ts +4 -7
  45. package/packages/server/src/__tests__/package-routes.test.ts +6 -2
  46. package/packages/server/src/__tests__/pi-changelog-routes.test.ts +10 -13
  47. package/packages/server/src/__tests__/pi-core-checker.test.ts +2 -2
  48. package/packages/server/src/__tests__/pi-version-skew.test.ts +3 -2
  49. package/packages/server/src/__tests__/plugin-activation-routes.test.ts +267 -0
  50. package/packages/server/src/__tests__/plugin-intent-cache.test.ts +75 -0
  51. package/packages/server/src/__tests__/preferences-store.test.ts +196 -0
  52. package/packages/server/src/__tests__/reattach-placement.test.ts +9 -0
  53. package/packages/server/src/__tests__/recommended-routes.test.ts +2 -2
  54. package/packages/server/src/__tests__/recovery-server.test.ts +203 -0
  55. package/packages/server/src/__tests__/session-action-handler-clear-queue.test.ts +153 -0
  56. package/packages/server/src/__tests__/session-action-handler-headless-reload.test.ts +43 -0
  57. package/packages/server/src/__tests__/session-order-manager.test.ts +9 -0
  58. package/packages/server/src/__tests__/session-order-reboot.test.ts +9 -0
  59. package/packages/server/src/__tests__/session-ordering-integration.test.ts +9 -0
  60. package/packages/server/src/browser-gateway.ts +83 -5
  61. package/packages/server/src/browser-handlers/directory-handler.ts +69 -0
  62. package/packages/server/src/browser-handlers/session-action-handler.ts +89 -0
  63. package/packages/server/src/browser-handlers/subscription-handler.ts +23 -0
  64. package/packages/server/src/changelog-parser.ts +1 -1
  65. package/packages/server/src/cli.ts +68 -250
  66. package/packages/server/src/event-status-extraction.ts +14 -62
  67. package/packages/server/src/event-wiring.ts +23 -10
  68. package/packages/server/src/memory-session-manager.ts +4 -0
  69. package/packages/server/src/pi-core-checker.ts +1 -1
  70. package/packages/server/src/pi-dev-version-check.ts +1 -1
  71. package/packages/server/src/pi-version-skew.ts +24 -46
  72. package/packages/server/src/plugin-intent-cache.ts +67 -0
  73. package/packages/server/src/preferences-store.ts +199 -13
  74. package/packages/server/src/recovery-server.ts +366 -0
  75. package/packages/server/src/routes/__tests__/manifest-route.test.ts +138 -0
  76. package/packages/server/src/routes/doctor-routes.ts +26 -21
  77. package/packages/server/src/routes/manifest-route.ts +162 -0
  78. package/packages/server/src/routes/openspec-routes.ts +4 -25
  79. package/packages/server/src/routes/pi-changelog-routes.ts +5 -24
  80. package/packages/server/src/routes/pi-core-routes.ts +3 -23
  81. package/packages/server/src/routes/plugin-activation-routes.ts +193 -0
  82. package/packages/server/src/routes/recommended-routes.ts +21 -0
  83. package/packages/server/src/routes/system-routes.ts +73 -11
  84. package/packages/server/src/server.ts +105 -307
  85. package/packages/server/src/session-api.ts +5 -63
  86. package/packages/shared/package.json +1 -1
  87. package/packages/shared/src/__tests__/binary-lookup-resolveJiti.test.ts +28 -0
  88. package/packages/shared/src/__tests__/binary-lookup-spawn-env.test.ts +61 -0
  89. package/packages/shared/src/__tests__/binary-lookup.test.ts +16 -0
  90. package/packages/shared/src/__tests__/bridge-register.test.ts +67 -0
  91. package/packages/shared/src/__tests__/ci-electron-no-side-effects.test.ts +129 -0
  92. package/packages/shared/src/__tests__/config.test.ts +40 -0
  93. package/packages/shared/src/__tests__/dashboard-paths.test.ts +81 -0
  94. package/packages/shared/src/__tests__/ensure-windows-path.test.ts +112 -0
  95. package/packages/shared/src/__tests__/intent-types.test.ts +120 -0
  96. package/packages/shared/src/__tests__/jiti-packages-parity.test.ts +85 -0
  97. package/packages/shared/src/__tests__/legacy-managed-dir.test.ts +59 -0
  98. package/packages/shared/src/__tests__/no-direct-child-process.test.ts +12 -0
  99. package/packages/shared/src/__tests__/no-electron-execpath-spawn.test.ts +149 -0
  100. package/packages/shared/src/__tests__/no-flow-command-route-claims.test.ts +71 -0
  101. package/packages/shared/src/__tests__/no-flow-references-in-shell.test.ts +221 -0
  102. package/packages/shared/src/__tests__/no-managed-dir-reference.test.ts +134 -0
  103. package/packages/shared/src/__tests__/no-pi-dashboard-version-jiti-gate.test.ts +41 -0
  104. package/packages/shared/src/__tests__/no-primitive-direct-import.test.ts +235 -0
  105. package/packages/shared/src/__tests__/no-server-imports-in-resolver.test.ts +53 -0
  106. package/packages/shared/src/__tests__/node-spawn-jiti-contract.test.ts +54 -101
  107. package/packages/shared/src/__tests__/node-spawn.test.ts +29 -13
  108. package/packages/shared/src/__tests__/pi-package-resolver.test.ts +300 -0
  109. package/packages/shared/src/__tests__/plugin-activation-contracts.test.ts +74 -0
  110. package/packages/shared/src/__tests__/plugin-bridge-classify-source.test.ts +73 -0
  111. package/packages/shared/src/__tests__/plugin-bridge-register-extended.test.ts +17 -5
  112. package/packages/shared/src/__tests__/plugin-bridge-register-packages.test.ts +233 -0
  113. package/packages/shared/src/__tests__/plugin-bridge-register.test.ts +19 -9
  114. package/packages/shared/src/__tests__/publish-workflow-contract.test.ts +154 -15
  115. package/packages/shared/src/__tests__/recommended-extensions.test.ts +28 -10
  116. package/packages/shared/src/__tests__/resolver-parity-with-scanner.test.ts +76 -0
  117. package/packages/shared/src/__tests__/server-identity.test.ts +127 -0
  118. package/packages/shared/src/__tests__/server-launcher.test.ts +35 -0
  119. package/packages/shared/src/__tests__/source-matching.test.ts +5 -5
  120. package/packages/shared/src/__tests__/sync-versions-spec.test.ts +76 -0
  121. package/packages/shared/src/__tests__/tool-registry-definitions.test.ts +50 -2
  122. package/packages/shared/src/bridge-register.ts +35 -2
  123. package/packages/shared/src/browser-protocol.ts +176 -2
  124. package/packages/shared/src/config.ts +12 -0
  125. package/packages/shared/src/dashboard-paths.ts +69 -0
  126. package/packages/shared/src/dashboard-plugin/index.ts +2 -0
  127. package/packages/shared/src/dashboard-plugin/intent-types.ts +93 -0
  128. package/packages/shared/src/dashboard-plugin/manifest-types.ts +55 -1
  129. package/packages/shared/src/dashboard-plugin/plugin-status.ts +82 -0
  130. package/packages/shared/src/dashboard-plugin/slot-props.ts +11 -0
  131. package/packages/shared/src/dashboard-plugin/slot-types.ts +16 -2
  132. package/packages/shared/src/dashboard-plugin/ui-primitives.ts +287 -0
  133. package/packages/shared/src/dashboard-starter.ts +22 -0
  134. package/packages/shared/src/doctor-core.ts +49 -27
  135. package/packages/shared/src/launch-source-types.ts +9 -9
  136. package/packages/shared/src/legacy-managed-dir.ts +97 -0
  137. package/packages/shared/src/mdns-discovery.ts +4 -1
  138. package/packages/shared/src/pi-package-resolver.ts +388 -0
  139. package/packages/shared/src/platform/binary-lookup.ts +27 -3
  140. package/packages/shared/src/platform/ensure-windows-path.ts +95 -0
  141. package/packages/shared/src/platform/exec.ts +22 -0
  142. package/packages/shared/src/platform/node-spawn.ts +42 -41
  143. package/packages/shared/src/plugin-bridge-register.ts +275 -18
  144. package/packages/shared/src/protocol.ts +94 -2
  145. package/packages/shared/src/recommended-extensions.ts +34 -10
  146. package/packages/shared/src/server-identity.ts +74 -5
  147. package/packages/shared/src/server-launcher.ts +20 -0
  148. package/packages/shared/src/source-matching.ts +1 -1
  149. package/packages/shared/src/tool-registry/__tests__/node-script-toargv-fallback.test.ts +84 -0
  150. package/packages/shared/src/tool-registry/definitions.ts +91 -7
  151. package/packages/shared/src/types.ts +12 -8
  152. package/scripts/maybe-patch-package.cjs +44 -0
  153. package/packages/server/src/__tests__/bootstrap-install-from-list.test.ts +0 -263
  154. package/packages/server/src/__tests__/bootstrap-queue.test.ts +0 -120
  155. package/packages/server/src/__tests__/bootstrap-routes.test.ts +0 -125
  156. package/packages/server/src/__tests__/bootstrap-state.test.ts +0 -119
  157. package/packages/server/src/__tests__/cli-bootstrap.test.ts +0 -36
  158. package/packages/server/src/__tests__/event-status-extraction-flow.test.ts +0 -55
  159. package/packages/server/src/__tests__/legacy-pi-cleanup.test.ts +0 -149
  160. package/packages/server/src/__tests__/post-install-openspec-refresh.test.ts +0 -180
  161. package/packages/server/src/__tests__/post-install-rescan.test.ts +0 -134
  162. package/packages/server/src/__tests__/system-routes-reextract.test.ts +0 -91
  163. package/packages/server/src/bootstrap-install-from-list.ts +0 -232
  164. package/packages/server/src/bootstrap-queue.ts +0 -130
  165. package/packages/server/src/bootstrap-state.ts +0 -159
  166. package/packages/server/src/legacy-pi-cleanup.ts +0 -151
  167. package/packages/server/src/routes/bootstrap-routes.ts +0 -125
  168. package/packages/shared/src/__tests__/bootstrap/README.md +0 -133
  169. package/packages/shared/src/__tests__/bootstrap/__snapshots__/cube.test.ts.snap +0 -378
  170. package/packages/shared/src/__tests__/bootstrap/assertions.ts +0 -136
  171. package/packages/shared/src/__tests__/bootstrap/cube.test.ts +0 -47
  172. package/packages/shared/src/__tests__/bootstrap/cube.ts +0 -66
  173. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/a-electron.test.ts.snap +0 -84
  174. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/b-npm-global.test.ts.snap +0 -90
  175. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/c-dev-monorepo.test.ts.snap +0 -34
  176. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/d-overrides.test.ts.snap +0 -20
  177. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/e-stale-partial.test.ts.snap +0 -62
  178. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/f-cwd-variants.test.ts.snap +0 -34
  179. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/g-windows-specifics.test.ts.snap +0 -49
  180. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/j-path-gui-minimal.test.ts.snap +0 -12
  181. package/packages/shared/src/__tests__/bootstrap/families/a-electron.test.ts +0 -156
  182. package/packages/shared/src/__tests__/bootstrap/families/b-npm-global.test.ts +0 -157
  183. package/packages/shared/src/__tests__/bootstrap/families/c-dev-monorepo.test.ts +0 -102
  184. package/packages/shared/src/__tests__/bootstrap/families/d-overrides.test.ts +0 -76
  185. package/packages/shared/src/__tests__/bootstrap/families/e-stale-partial.test.ts +0 -94
  186. package/packages/shared/src/__tests__/bootstrap/families/f-cwd-variants.test.ts +0 -87
  187. package/packages/shared/src/__tests__/bootstrap/families/g-windows-specifics.test.ts +0 -143
  188. package/packages/shared/src/__tests__/bootstrap/families/h-home-drift.test.ts +0 -64
  189. package/packages/shared/src/__tests__/bootstrap/families/i-malformed-settings.test.ts +0 -77
  190. package/packages/shared/src/__tests__/bootstrap/families/index.ts +0 -19
  191. package/packages/shared/src/__tests__/bootstrap/families/j-path-gui-minimal.test.ts +0 -61
  192. package/packages/shared/src/__tests__/bootstrap/families/k-dashboard-absent.test.ts +0 -50
  193. package/packages/shared/src/__tests__/bootstrap/families/l-instance-coordination.test.ts +0 -272
  194. package/packages/shared/src/__tests__/bootstrap/fixtures/dev-monorepo.ts +0 -58
  195. package/packages/shared/src/__tests__/bootstrap/fixtures/electron-layout.ts +0 -84
  196. package/packages/shared/src/__tests__/bootstrap/fixtures/index.ts +0 -9
  197. package/packages/shared/src/__tests__/bootstrap/fixtures/managed-install.ts +0 -85
  198. package/packages/shared/src/__tests__/bootstrap/fixtures/npm-global-layout.ts +0 -122
  199. package/packages/shared/src/__tests__/bootstrap/fixtures/pi-versions.ts +0 -36
  200. package/packages/shared/src/__tests__/bootstrap/fixtures/settings-json.ts +0 -39
  201. package/packages/shared/src/__tests__/bootstrap/harness.smoke.test.ts +0 -220
  202. package/packages/shared/src/__tests__/bootstrap/harness.ts +0 -413
  203. package/packages/shared/src/__tests__/bootstrap/scenarios-skipped.ts +0 -125
  204. package/packages/shared/src/__tests__/bootstrap/scenarios.ts +0 -132
  205. package/packages/shared/src/__tests__/bootstrap-install-resolve-npm.test.ts +0 -72
  206. package/packages/shared/src/__tests__/install-managed-node-bootstrap-order.test.ts +0 -68
  207. package/packages/shared/src/__tests__/install-managed-node.test.ts +0 -192
  208. package/packages/shared/src/__tests__/installable-list.test.ts +0 -130
  209. package/packages/shared/src/__tests__/no-installable-list-in-bridge.test.ts +0 -52
  210. package/packages/shared/src/bootstrap-install.ts +0 -406
  211. package/packages/shared/src/installable-list.ts +0 -152
  212. package/packages/shared/src/launch-source-flag.ts +0 -14
@@ -1,42 +1,42 @@
1
1
  /**
2
- * Pin the jiti version contract for `shouldUrlWrapEntry()`.
2
+ * Pin the jiti behavioural contract for `shouldUrlWrapEntry()`.
3
3
  *
4
- * The Windows-non-tsx arm in `platform/node-spawn.ts::shouldUrlWrapEntry`
5
- * relies on jiti's file:/// URL handling. This was VERIFIED on:
6
- * - `@mariozechner/pi-coding-agent@0.70.x` shipping `jiti@2.x` (original baseline)
7
- * - `@earendil-works/pi-coding-agent@0.74.x` shipping `jiti@^2.7.0` (current baseline)
8
- * It was BROKEN on `pi-coding-agent@0.71.x` shipping `jiti@2.6.5`, which
9
- * misnormalised triple-slash file:/// URLs on Windows. Keep that data
10
- * point in the contract so a contributor recognises the regression
11
- * pattern if it recurs.
4
+ * History
5
+ * -------
6
+ * Earlier versions of this contract attempted to pin a "verified-good"
7
+ * jiti version that correctly normalised `file:///` triple-slash URL
8
+ * entries on Windows. Live testing on Windows 11 + Node 22.18.0 + jiti
9
+ * 2.7.0 (shipped under `@earendil-works/pi-coding-agent@0.74.x`) showed
10
+ * that jiti still misnormalises those URLs:
11
+ *
12
+ * Error: Cannot find module
13
+ * 'file:///C:/pi-dash-app/file:/C:/pi-dash-app/.../cli.ts'
14
+ *
15
+ * The triple-slash entry is rewritten to single-slash and then resolved
16
+ * against cwd as if it were a relative specifier. Rather than chase
17
+ * jiti versions, the contract now requires raw entry paths whenever
18
+ * the loader is jiti, on every OS. Node's drive-letter heuristic
19
+ * accepts raw `C:\…` argv entries directly, which covers the common
20
+ * standalone-install layout where pi + the dashboard sit under
21
+ * `C:\Users\<u>\.pi-dashboard\…`.
12
22
  *
13
23
  * This test ensures:
14
- * 1. The offline-cacache pin in `packages/electron/offline-packages.json`
15
- * stays on a supported pi version under one of the two supported
16
- * forks. A bump outside the verified set fires this test and
17
- * forces the contributor to either:
18
- * - re-verify the contract on Windows
19
- * - add a per-jiti-version branch
20
- * - switch the bundled loader to tsx
21
- * 2. The `shouldUrlWrapEntry` header comment documents the contract
22
- * so future contributors discover the constraint at the call site.
24
+ * 1. `shouldUrlWrapEntry` returns `false` for jiti loaders on every
25
+ * platform (POSIX + win32). Locks the behavioural rule against
26
+ * regression.
27
+ * 2. The `shouldUrlWrapEntry` source comment still documents the
28
+ * Windows breakage so future contributors discover the constraint.
23
29
  *
24
- * See changes: fix-electron-windows-installer-and-server-bootstrap (Defect 2),
25
- * migrate-pi-fork-to-earendil (E.6).
30
+ * See change: fix-windows-standalone-spawn.
26
31
  */
27
32
  import { describe, it, expect } from "vitest";
28
33
  import fs from "node:fs";
29
34
  import path from "node:path";
30
35
  import url from "node:url";
36
+ import { shouldUrlWrapEntry, isJitiLoader } from "../platform/node-spawn.js";
31
37
 
32
38
  const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
33
39
  const REPO_ROOT = path.resolve(__dirname, "..", "..", "..", "..");
34
- const OFFLINE_PACKAGES_PATH = path.join(
35
- REPO_ROOT,
36
- "packages",
37
- "electron",
38
- "offline-packages.json",
39
- );
40
40
  const NODE_SPAWN_PATH = path.join(
41
41
  REPO_ROOT,
42
42
  "packages",
@@ -46,97 +46,50 @@ const NODE_SPAWN_PATH = path.join(
46
46
  "node-spawn.ts",
47
47
  );
48
48
 
49
- /** Versions verified against the Windows file:/// jiti contract. */
50
- const VERIFIED_PI_PINS: ReadonlyArray<{ name: string; versionPrefix: string }> = [
51
- { name: "@earendil-works/pi-coding-agent", versionPrefix: "0.74." },
52
- { name: "@mariozechner/pi-coding-agent", versionPrefix: "0.70." },
53
- ];
54
-
55
- describe("jiti version contract for shouldUrlWrapEntry", () => {
56
- it("offline-packages.json pins pi-coding-agent at a verified version under a supported fork", () => {
57
- const raw = fs.readFileSync(OFFLINE_PACKAGES_PATH, "utf8");
58
- const manifest = JSON.parse(raw) as {
59
- packages: { name: string; version: string }[];
60
- };
61
-
62
- const supportedNames = VERIFIED_PI_PINS.map((p) => p.name);
63
- const piEntry = manifest.packages.find((p) =>
64
- supportedNames.includes(p.name),
65
- );
66
- if (!piEntry) {
67
- throw new Error(
68
- `No supported pi-coding-agent fork found in offline-packages.json. ` +
69
- `Expected one of: ${supportedNames.join(", ")}. ` +
70
- `The offline cacache must include pi-coding-agent. ` +
71
- `See changes: fix-electron-windows-installer-and-server-bootstrap (Defect 2), ` +
72
- `migrate-pi-fork-to-earendil (E.6).`,
73
- );
74
- }
75
-
76
- const verifiedPin = VERIFIED_PI_PINS.find(
77
- (p) => p.name === piEntry.name && piEntry.version.startsWith(p.versionPrefix),
78
- );
79
- if (!verifiedPin) {
80
- const allowedRanges = VERIFIED_PI_PINS
81
- .map((p) => `${p.name}@${p.versionPrefix}x`)
82
- .join(", ");
83
- throw new Error(
84
- `pi-coding-agent pinned at ${piEntry.name}@${piEntry.version}, but ` +
85
- `shouldUrlWrapEntry()'s Windows-non-tsx arm only supports verified pins: ` +
86
- `${allowedRanges}. ` +
87
- `Newer jiti versions (e.g. 2.6.5 in pi 0.71.x) misnormalize ` +
88
- `file:/// URL entries on Windows. Either re-verify the contract, ` +
89
- `add a per-jiti-version branch in shouldUrlWrapEntry(), or switch ` +
90
- `the bundled loader to tsx. See changes: ` +
91
- `fix-electron-windows-installer-and-server-bootstrap (Defect 2), ` +
92
- `migrate-pi-fork-to-earendil (E.6).`,
93
- );
94
- }
49
+ describe("jiti behavioural contract for shouldUrlWrapEntry", () => {
50
+ it("jiti loader entry passed RAW on every platform", () => {
51
+ const jitiLoader =
52
+ "file:///C:/Users/x/.pi-dashboard/node_modules/@earendil-works/pi-coding-agent/node_modules/jiti/lib/jiti-register.mjs";
53
+ expect(isJitiLoader(jitiLoader)).toBe(true);
54
+ expect(shouldUrlWrapEntry(jitiLoader, "win32")).toBe(false);
55
+ expect(shouldUrlWrapEntry(jitiLoader, "linux")).toBe(false);
56
+ expect(shouldUrlWrapEntry(jitiLoader, "darwin")).toBe(false);
57
+ });
95
58
 
96
- expect(piEntry.version.startsWith(verifiedPin.versionPrefix)).toBe(true);
59
+ it("tsx loader → entry passed RAW on every platform (unchanged)", () => {
60
+ const tsxLoader = "file:///home/u/node_modules/tsx/dist/esm/index.mjs";
61
+ expect(shouldUrlWrapEntry(tsxLoader, "win32")).toBe(false);
62
+ expect(shouldUrlWrapEntry(tsxLoader, "linux")).toBe(false);
97
63
  });
98
64
 
99
- it("node-spawn.ts source contains the documented JITI VERSION CONTRACT block", () => {
65
+ it("node-spawn.ts source documents the Windows jiti breakage", () => {
100
66
  const source = fs.readFileSync(NODE_SPAWN_PATH, "utf8");
101
67
 
102
- // Contract block markers
103
68
  expect(source).toContain("JITI VERSION CONTRACT");
104
- // Documented baseline references (at least one of the verified version markers).
105
- const hasBaselineMarker =
106
- source.includes("0.74.") || source.includes("0.70.x");
107
- if (!hasBaselineMarker) {
108
- throw new Error(
109
- "shouldUrlWrapEntry() docstring is missing the verified-baseline marker. " +
110
- "It must mention at least one of: '0.74.' (current earendil baseline) " +
111
- "or '0.70.x' (legacy mariozechner baseline). See change: " +
112
- "migrate-pi-fork-to-earendil (E.7).",
113
- );
114
- }
115
-
116
- // Version drift markers (at least one of these identifies the broken jiti)
117
- const hasVersionDriftMarker =
118
- source.includes("0.71") || source.includes("2.6.5");
119
- if (!hasVersionDriftMarker) {
69
+ // Documented Windows-breakage marker. The original error signature
70
+ // (single-slash file:/<cwd>/file:/…) is the cheapest fingerprint
71
+ // for a contributor matching a new repro to this contract.
72
+ const hasBreakageMarker =
73
+ /file:\/{1,3}.*file:\//.test(source) ||
74
+ /misnormali[sz]e/i.test(source);
75
+ if (!hasBreakageMarker) {
120
76
  throw new Error(
121
- "shouldUrlWrapEntry() docstring is missing the version-drift marker. " +
122
- "It must mention either '0.71' or '2.6.5' so contributors can " +
123
- "identify the known-broken jiti versions. See change: " +
124
- "fix-electron-windows-installer-and-server-bootstrap (Defect 2).",
77
+ "shouldUrlWrapEntry() docstring is missing the Windows-breakage marker. " +
78
+ "It must mention either the `file:/<cwd>/file:/…` error signature " +
79
+ "or the word 'misnormalise'. See change: fix-windows-standalone-spawn.",
125
80
  );
126
81
  }
127
82
 
128
- // Remediation guidance markers (at least one)
83
+ // Remediation guidance markers (at least one).
129
84
  const hasRemediationGuidance =
130
85
  /re-verify/i.test(source) ||
131
86
  /per-version branch/i.test(source) ||
132
- /per-jiti-version/i.test(source) ||
133
- /switch.*to tsx/i.test(source);
87
+ /per-jiti-version/i.test(source);
134
88
  if (!hasRemediationGuidance) {
135
89
  throw new Error(
136
90
  "shouldUrlWrapEntry() docstring is missing remediation guidance. " +
137
- "It must mention at least one of: re-verify, per-version branch, " +
138
- "or switch to tsx. See change: " +
139
- "fix-electron-windows-installer-and-server-bootstrap (Defect 2).",
91
+ "It must mention at least one of: re-verify, per-version branch. " +
92
+ "See change: fix-windows-standalone-spawn.",
140
93
  );
141
94
  }
142
95
  });
@@ -62,14 +62,14 @@ describe("isTsxLoader", () => {
62
62
  });
63
63
 
64
64
  describe("spawnNodeScript", () => {
65
- it("URL-wraps both loader and entry when loader is NOT tsx (jiti/default)", () => {
65
+ it("URL-wraps loader but passes RAW entry when loader is jiti (any platform)", () => {
66
66
  const spawnSpy = vi
67
67
  .spyOn(execModule, "spawn")
68
68
  .mockImplementation(() => ({ unref: () => {} } as unknown as ReturnType<typeof execModule.spawn>));
69
69
 
70
70
  spawnNodeScript({
71
71
  nodeBin: "C:\\Program Files\\nodejs\\node.exe",
72
- loader: "B:\\jiti\\register.mjs",
72
+ loader: "B:\\jiti\\lib\\jiti-register.mjs",
73
73
  entry: "B:\\Dev\\cli.ts",
74
74
  args: ["start", "--dev"],
75
75
  });
@@ -77,12 +77,11 @@ describe("spawnNodeScript", () => {
77
77
  expect(spawnSpy).toHaveBeenCalledTimes(1);
78
78
  const [bin, argv] = spawnSpy.mock.calls[0]!;
79
79
  expect(bin).toBe("C:\\Program Files\\nodejs\\node.exe");
80
- // On Linux host: entry stays raw even with a Windows-styled path
81
- // (shouldUrlWrapEntry consults process.platform, which is Linux).
82
- // The Windows-wrapped branch is exercised separately via shouldUrlWrapEntry.
80
+ // jiti loader entry RAW everywhere (jiti misnormalises file:///
81
+ // URL entries on Windows; POSIX never needed the wrap).
83
82
  expect(argv).toEqual([
84
83
  "--import",
85
- "file:///B:/jiti/register.mjs",
84
+ "file:///B:/jiti/lib/jiti-register.mjs",
86
85
  "B:\\Dev\\cli.ts",
87
86
  "start",
88
87
  "--dev",
@@ -197,16 +196,25 @@ describe("buildNodeImportArgvParts", () => {
197
196
  expect(parts.slice(3)).toEqual(["start", "--port", "8000"]);
198
197
  });
199
198
 
200
- it("Windows + jiti: entry URL-wrapped", async () => {
199
+ it("Windows + jiti: entry passed RAW (jiti misnormalises file:/// URLs on Windows)", async () => {
200
+ // See change: fix-windows-standalone-spawn. Live repro on
201
+ // Win11 + Node 22.18.0 + jiti 2.7.0 showed the URL-wrapped entry
202
+ // re-resolved against cwd; Node's drive-letter heuristic accepts
203
+ // raw `C:\…` argv entries so the wrap is no longer needed.
201
204
  const { buildNodeImportArgvParts } = await import("../platform/node-spawn.js");
202
205
  const parts = buildNodeImportArgvParts({
203
- loader: "B:\\Dev\\jiti\\lib\\jiti-register.mjs",
204
- entry: "B:\\srv\\cli.ts",
206
+ loader: "C:\\Users\\u\\.pi-dashboard\\node_modules\\jiti\\lib\\jiti-register.mjs",
207
+ entry: "C:\\Users\\u\\.pi-dashboard\\node_modules\\@earendil-works\\pi-agent-dashboard\\packages\\server\\src\\cli.ts",
205
208
  args: ["start"],
206
209
  platform: "win32",
207
210
  });
208
- expect(parts[1]).toBe("file:///B:/Dev/jiti/lib/jiti-register.mjs");
209
- expect(parts[2]).toBe("file:///B:/srv/cli.ts");
211
+ expect(parts[1]).toBe(
212
+ "file:///C:/Users/u/.pi-dashboard/node_modules/jiti/lib/jiti-register.mjs",
213
+ );
214
+ // Entry is RAW — NOT URL-wrapped — because the loader is jiti.
215
+ expect(parts[2]).toBe(
216
+ "C:\\Users\\u\\.pi-dashboard\\node_modules\\@earendil-works\\pi-agent-dashboard\\packages\\server\\src\\cli.ts",
217
+ );
210
218
  });
211
219
 
212
220
  it("tsx loader: entry RAW on any platform", async () => {
@@ -245,9 +253,17 @@ describe("shouldUrlWrapEntry", () => {
245
253
  expect(shouldUrlWrapEntry(jiti, "darwin")).toBe(false);
246
254
  });
247
255
 
248
- it("returns true for non-tsx loader on Windows (drive letters need file://)", () => {
256
+ it("returns false for jiti loader on Windows (jiti misnormalises file:/// entries)", () => {
257
+ // See change: fix-windows-standalone-spawn.
249
258
  const jiti = "file:///C:/node_modules/@mariozechner/jiti/lib/jiti-register.mjs";
250
- expect(shouldUrlWrapEntry(jiti, "win32")).toBe(true);
259
+ expect(shouldUrlWrapEntry(jiti, "win32")).toBe(false);
260
+ });
261
+
262
+ it("returns true for an unknown loader on Windows (drive-letter URL-scheme protection)", () => {
263
+ // A hypothetical non-tsx, non-jiti loader (or a future Node default
264
+ // resolver) still needs the wrap for edge-case drives like `B:`/`A:`.
265
+ const unknown = "file:///C:/node_modules/some-other-loader/index.mjs";
266
+ expect(shouldUrlWrapEntry(unknown, "win32")).toBe(true);
251
267
  });
252
268
 
253
269
  it("returns false when no loader is provided, regardless of platform", () => {
@@ -0,0 +1,300 @@
1
+ /**
2
+ * pi-package-resolver tests — real-fs tmp dirs, no mocking.
3
+ *
4
+ * Each test builds a virtual `~/.pi/agent/` (via `agentDir` injection)
5
+ * and optionally a virtual `<cwd>/.pi/` for project-scope cases. The
6
+ * resolver's three deps (`agentDir`, `cwd`, `npmRoot`) are all injected
7
+ * so tests are hermetic and never read the developer's real settings.
8
+ */
9
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
10
+ import * as fs from "node:fs";
11
+ import * as os from "node:os";
12
+ import * as path from "node:path";
13
+
14
+ import { resolvePiPackage, resolvePiPackageEntry } from "../pi-package-resolver.js";
15
+
16
+ let root: string;
17
+ let agentDir: string;
18
+ let cwd: string;
19
+ let npmRoot: string;
20
+
21
+ beforeEach(() => {
22
+ root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-resolver-test-"));
23
+ agentDir = path.join(root, ".pi", "agent");
24
+ cwd = path.join(root, "project");
25
+ npmRoot = path.join(root, "global-npm", "node_modules");
26
+ fs.mkdirSync(agentDir, { recursive: true });
27
+ fs.mkdirSync(cwd, { recursive: true });
28
+ fs.mkdirSync(npmRoot, { recursive: true });
29
+ });
30
+
31
+ afterEach(() => {
32
+ fs.rmSync(root, { recursive: true, force: true });
33
+ });
34
+
35
+ // ── helpers ─────────────────────────────────────────────────────────
36
+
37
+ function writeSettings(scope: "user" | "project", body: Record<string, unknown>): void {
38
+ const settingsPath =
39
+ scope === "user"
40
+ ? path.join(agentDir, "settings.json")
41
+ : path.join(cwd, ".pi", "settings.json");
42
+ fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
43
+ fs.writeFileSync(settingsPath, JSON.stringify(body, null, 2));
44
+ }
45
+
46
+ function writePackage(pkgDir: string, pkgJson: Record<string, unknown>, files: Record<string, string> = {}): void {
47
+ fs.mkdirSync(pkgDir, { recursive: true });
48
+ fs.writeFileSync(path.join(pkgDir, "package.json"), JSON.stringify(pkgJson, null, 2));
49
+ for (const [rel, content] of Object.entries(files)) {
50
+ const full = path.join(pkgDir, rel);
51
+ fs.mkdirSync(path.dirname(full), { recursive: true });
52
+ fs.writeFileSync(full, content);
53
+ }
54
+ }
55
+
56
+ // ── 2.2: npm scope (global) ─────────────────────────────────────────
57
+
58
+ describe("npm: install resolution", () => {
59
+ it("resolves an npm: peer in global scope via npmRoot", () => {
60
+ writeSettings("user", { packages: ["npm:@pi/anthropic-messages"] });
61
+ const pkgDir = path.join(npmRoot, "@pi", "anthropic-messages");
62
+ writePackage(pkgDir, {
63
+ name: "@pi/anthropic-messages",
64
+ exports: { ".": "./extensions/index.js" },
65
+ }, {
66
+ "extensions/index.js": "export default function() {}",
67
+ });
68
+
69
+ const result = resolvePiPackage("@pi/anthropic-messages", { agentDir, npmRoot });
70
+ expect(result).not.toBeNull();
71
+ expect(result!.packageDir).toBe(pkgDir);
72
+ expect(result!.entryPath).toBe(path.join(pkgDir, "extensions", "index.js"));
73
+ expect(result!.scope).toBe("user");
74
+ expect(result!.source).toBe("npm:@pi/anthropic-messages");
75
+ expect(result!.packageJsonName).toBe("@pi/anthropic-messages");
76
+ });
77
+
78
+ it("ignores @version suffix in npm: spec when resolving", () => {
79
+ // Use a single-segment version to avoid email-obfuscation in source files.
80
+ writeSettings("user", { packages: ["npm:foo@latest"] });
81
+ const pkgDir = path.join(npmRoot, "foo");
82
+ writePackage(pkgDir, { name: "foo", main: "index.js" }, { "index.js": "" });
83
+
84
+ const result = resolvePiPackage("foo", { agentDir, npmRoot });
85
+ expect(result?.packageDir).toBe(pkgDir);
86
+ });
87
+ });
88
+
89
+ // ── 2.3: git scope ──────────────────────────────────────────────────
90
+
91
+ describe("git: install resolution", () => {
92
+ it("resolves https github URL to ~/.pi/agent/git/<host>/<path>", () => {
93
+ writeSettings("user", {
94
+ packages: ["https://github.com/BlackBeltTechnology/pi-anthropic-messages.git"],
95
+ });
96
+ const pkgDir = path.join(agentDir, "git", "github.com", "BlackBeltTechnology", "pi-anthropic-messages");
97
+ writePackage(pkgDir, {
98
+ name: "@pi/anthropic-messages",
99
+ main: "./extensions/index.ts",
100
+ }, {
101
+ "extensions/index.ts": "export default function() {}",
102
+ });
103
+
104
+ const result = resolvePiPackage("@pi/anthropic-messages", { agentDir, npmRoot });
105
+ expect(result?.packageDir).toBe(pkgDir);
106
+ expect(result?.entryPath).toBe(path.join(pkgDir, "extensions", "index.ts"));
107
+ expect(result?.scope).toBe("user");
108
+ });
109
+
110
+ it("handles git+https:// and git@ shorthand forms", () => {
111
+ writeSettings("user", { packages: ["git@github.com:owner/repo.git"] });
112
+ const pkgDir = path.join(agentDir, "git", "github.com", "owner", "repo");
113
+ writePackage(pkgDir, { name: "thing", main: "x.js" }, { "x.js": "" });
114
+
115
+ expect(resolvePiPackage("thing", { agentDir, npmRoot })?.packageDir).toBe(pkgDir);
116
+ });
117
+ });
118
+
119
+ // ── 2.4: absolute local path ────────────────────────────────────────
120
+
121
+ describe("absolute path resolution", () => {
122
+ it("resolves an absolute path entry to itself", () => {
123
+ const pkgDir = path.join(root, "elsewhere", "my-pkg");
124
+ writePackage(pkgDir, { name: "my-pkg", main: "entry.js" }, { "entry.js": "" });
125
+ writeSettings("user", { packages: [pkgDir] });
126
+
127
+ const result = resolvePiPackage("my-pkg", { agentDir, npmRoot });
128
+ expect(result?.packageDir).toBe(pkgDir);
129
+ expect(result?.entryPath).toBe(path.join(pkgDir, "entry.js"));
130
+ });
131
+ });
132
+
133
+ // ── 2.5: relative path in project-scope ─────────────────────────────
134
+
135
+ describe("relative path resolution under project scope", () => {
136
+ it("resolves a relative entry against <cwd>/.pi/", () => {
137
+ const pkgDir = path.join(cwd, "..", "sibling");
138
+ writePackage(pkgDir, { name: "sibling-pkg", main: "ok.ts" }, { "ok.ts": "" });
139
+ writeSettings("project", { packages: ["../../sibling"] }); // relative to <cwd>/.pi/
140
+
141
+ const result = resolvePiPackage("sibling-pkg", { agentDir, cwd, npmRoot });
142
+ expect(result?.scope).toBe("project");
143
+ expect(path.resolve(result!.packageDir)).toBe(path.resolve(pkgDir));
144
+ });
145
+ });
146
+
147
+ // ── 2.6: scope precedence ───────────────────────────────────────────
148
+
149
+ describe("scope precedence", () => {
150
+ it("project wins over user by default when both define the same name", () => {
151
+ const projPkg = path.join(root, "proj-impl");
152
+ const userPkg = path.join(root, "user-impl");
153
+ writePackage(projPkg, { name: "shared", main: "proj.js" }, { "proj.js": "" });
154
+ writePackage(userPkg, { name: "shared", main: "user.js" }, { "user.js": "" });
155
+ writeSettings("project", { packages: [projPkg] });
156
+ writeSettings("user", { packages: [userPkg] });
157
+
158
+ const result = resolvePiPackage("shared", { agentDir, cwd, npmRoot });
159
+ expect(result?.scope).toBe("project");
160
+ expect(result?.packageDir).toBe(projPkg);
161
+ });
162
+
163
+ it("scope:'user' skips project even when both have a match", () => {
164
+ const projPkg = path.join(root, "proj-impl");
165
+ const userPkg = path.join(root, "user-impl");
166
+ writePackage(projPkg, { name: "shared", main: "proj.js" }, { "proj.js": "" });
167
+ writePackage(userPkg, { name: "shared", main: "user.js" }, { "user.js": "" });
168
+ writeSettings("project", { packages: [projPkg] });
169
+ writeSettings("user", { packages: [userPkg] });
170
+
171
+ expect(resolvePiPackage("shared", { agentDir, cwd, npmRoot, scope: "user" })?.packageDir).toBe(userPkg);
172
+ });
173
+
174
+ it("scope:'project' returns null without cwd", () => {
175
+ expect(resolvePiPackage("anything", { agentDir, npmRoot, scope: "project" })).toBeNull();
176
+ });
177
+ });
178
+
179
+ // ── 2.7: entry-point priority chain ─────────────────────────────────
180
+
181
+ describe("entry-point resolution priority", () => {
182
+ function setupPkg(pkgJson: Record<string, unknown>, files: Record<string, string>): string {
183
+ const pkgDir = path.join(root, "ep-test");
184
+ writePackage(pkgDir, { name: "ep-test", ...pkgJson }, files);
185
+ writeSettings("user", { packages: [pkgDir] });
186
+ return pkgDir;
187
+ }
188
+
189
+ it("exports['.'] wins over main and pi.extensions", () => {
190
+ const dir = setupPkg(
191
+ {
192
+ exports: { ".": "./from-exports.js" },
193
+ main: "./from-main.js",
194
+ pi: { extensions: ["./from-pi.js"] },
195
+ },
196
+ { "from-exports.js": "", "from-main.js": "", "from-pi.js": "" },
197
+ );
198
+ expect(resolvePiPackageEntry("ep-test", { agentDir, npmRoot })).toBe(path.join(dir, "from-exports.js"));
199
+ });
200
+
201
+ it("exports conditional import/default/node fields resolve to the first present string", () => {
202
+ const dir = setupPkg(
203
+ { exports: { ".": { default: "./d.js", import: "./i.js" } } },
204
+ { "d.js": "", "i.js": "" },
205
+ );
206
+ // import takes priority per design D4
207
+ expect(resolvePiPackageEntry("ep-test", { agentDir, npmRoot })).toBe(path.join(dir, "i.js"));
208
+ });
209
+
210
+ it("main wins when exports absent", () => {
211
+ const dir = setupPkg(
212
+ { main: "./from-main.js", pi: { extensions: ["./from-pi.js"] } },
213
+ { "from-main.js": "", "from-pi.js": "" },
214
+ );
215
+ expect(resolvePiPackageEntry("ep-test", { agentDir, npmRoot })).toBe(path.join(dir, "from-main.js"));
216
+ });
217
+
218
+ it("pi.extensions[0] wins when neither exports nor main", () => {
219
+ const dir = setupPkg(
220
+ { pi: { extensions: ["./from-pi.ts"] } },
221
+ { "from-pi.ts": "" },
222
+ );
223
+ expect(resolvePiPackageEntry("ep-test", { agentDir, npmRoot })).toBe(path.join(dir, "from-pi.ts"));
224
+ });
225
+
226
+ it("index.js fallback wins when no entry fields", () => {
227
+ const dir = setupPkg({}, { "index.js": "" });
228
+ expect(resolvePiPackageEntry("ep-test", { agentDir, npmRoot })).toBe(path.join(dir, "index.js"));
229
+ });
230
+
231
+ it("index.ts fallback wins when no index.js", () => {
232
+ const dir = setupPkg({}, { "index.ts": "" });
233
+ expect(resolvePiPackageEntry("ep-test", { agentDir, npmRoot })).toBe(path.join(dir, "index.ts"));
234
+ });
235
+
236
+ it("returns entryPath:null when package matched but no candidate exists", () => {
237
+ const dir = setupPkg({ main: "./missing.js" }, {});
238
+ const result = resolvePiPackage("ep-test", { agentDir, npmRoot });
239
+ expect(result?.packageDir).toBe(dir);
240
+ expect(result?.entryPath).toBeNull();
241
+ });
242
+ });
243
+
244
+ // ── 2.8: not in any settings → null ─────────────────────────────────
245
+
246
+ describe("misses return null", () => {
247
+ it("returns null when no settings entry matches the spec", () => {
248
+ writeSettings("user", { packages: [] });
249
+ expect(resolvePiPackage("@some/missing", { agentDir, npmRoot })).toBeNull();
250
+ expect(resolvePiPackageEntry("@some/missing", { agentDir, npmRoot })).toBeNull();
251
+ });
252
+
253
+ it("returns null when a candidate package's package.json#name differs", () => {
254
+ const pkgDir = path.join(root, "wrong-name");
255
+ writePackage(pkgDir, { name: "actually-foo", main: "x.js" }, { "x.js": "" });
256
+ writeSettings("user", { packages: [pkgDir] });
257
+
258
+ expect(resolvePiPackage("requested-bar", { agentDir, npmRoot })).toBeNull();
259
+ });
260
+ });
261
+
262
+ // ── 2.9: corrupt package.json — keep walking ────────────────────────
263
+
264
+ describe("graceful degradation", () => {
265
+ it("skips a package with malformed package.json and continues", () => {
266
+ const badDir = path.join(root, "bad-pkg");
267
+ fs.mkdirSync(badDir, { recursive: true });
268
+ fs.writeFileSync(path.join(badDir, "package.json"), "{ this is not json");
269
+
270
+ const goodDir = path.join(root, "good-pkg");
271
+ writePackage(goodDir, { name: "target", main: "ok.js" }, { "ok.js": "" });
272
+
273
+ writeSettings("user", { packages: [badDir, goodDir] });
274
+
275
+ const result = resolvePiPackage("target", { agentDir, npmRoot });
276
+ expect(result?.packageDir).toBe(goodDir);
277
+ });
278
+ });
279
+
280
+ // ── 2.10: missing settings.json → null ──────────────────────────────
281
+
282
+ describe("missing settings.json", () => {
283
+ it("returns null without throwing when ~/.pi/agent/settings.json is absent", () => {
284
+ // beforeEach creates the agentDir but no settings.json inside it.
285
+ expect(resolvePiPackage("anything", { agentDir, npmRoot })).toBeNull();
286
+ });
287
+
288
+ it("returns null when the global settings.json contains invalid JSON", () => {
289
+ fs.writeFileSync(path.join(agentDir, "settings.json"), "not json at all");
290
+ expect(resolvePiPackage("anything", { agentDir, npmRoot })).toBeNull();
291
+ });
292
+
293
+ it("handles {source: '...'} object-form entries", () => {
294
+ const pkgDir = path.join(root, "obj-form");
295
+ writePackage(pkgDir, { name: "obj-pkg", main: "e.js" }, { "e.js": "" });
296
+ writeSettings("user", { packages: [{ source: pkgDir, extensions: [] }] });
297
+
298
+ expect(resolvePiPackage("obj-pkg", { agentDir, npmRoot })?.packageDir).toBe(pkgDir);
299
+ });
300
+ });
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Repo-lint contracts for change: add-plugin-activation-ui.
3
+ *
4
+ * - SettingsPanel STILL calls <SettingsSectionSlot tab="..."> for every
5
+ * legacy tab value, preserving backward compatibility for plugins that
6
+ * target a specific tab via `claim.tab`.
7
+ * - browser-protocol introduces NO new message types in this change
8
+ * (toggles ride on existing plugin_config_update; requirement installs
9
+ * ride on existing package_progress / package_operation_complete).
10
+ * - /api/health payload always carries a `startedAt: ISO` field.
11
+ */
12
+ import { describe, it, expect } from "vitest";
13
+ import fs from "node:fs";
14
+ import path from "node:path";
15
+
16
+ const REPO_ROOT = path.resolve(__dirname, "../../../..");
17
+
18
+ describe("add-plugin-activation-ui repo-lint", () => {
19
+ it("SettingsPanel does NOT render plugin-contributed settings in legacy tabs", () => {
20
+ // Plugin-contributed `settings-section` claims render ONLY under the
21
+ // owning plugin's row in Settings ▸ Plugins. The legacy
22
+ // <SettingsSectionSlot tab="..." /> consumers have been removed; the
23
+ // `claim.tab` manifest field is now an inert hint.
24
+ // See change: add-plugin-activation-ui (settings-consolidation).
25
+ const panel = fs.readFileSync(
26
+ path.join(REPO_ROOT, "packages/client/src/components/SettingsPanel.tsx"),
27
+ "utf-8",
28
+ );
29
+ expect(panel.includes("<SettingsSectionSlot")).toBe(false);
30
+ expect(panel.includes("SettingsSectionSlot")).toBe(false);
31
+ });
32
+
33
+ it("browser-protocol introduces no new message types in this change", () => {
34
+ // This change adds zero new variants to ServerToBrowserMessage. Toggles
35
+ // reuse `plugin_config_update`; requirement installs reuse the existing
36
+ // package_progress / package_operation_complete pair.
37
+ const proto = fs.readFileSync(
38
+ path.join(REPO_ROOT, "packages/shared/src/browser-protocol.ts"),
39
+ "utf-8",
40
+ );
41
+ // Sentinel: the existing fields are intact.
42
+ expect(proto.includes('"plugin_config_update"')).toBe(true);
43
+ expect(proto.includes('"package_progress"')).toBe(true);
44
+ expect(proto.includes('"package_operation_complete"')).toBe(true);
45
+ // Forbidden: anything that smells like a new plugin-specific message
46
+ // type added by THIS change.
47
+ const forbidden = [
48
+ '"plugin_install_progress"',
49
+ '"plugin_install_complete"',
50
+ '"plugin_uninstall_progress"',
51
+ '"plugin_uninstall_complete"',
52
+ '"plugin_toggle_complete"',
53
+ ];
54
+ for (const f of forbidden) {
55
+ expect(
56
+ proto.includes(f),
57
+ `browser-protocol.ts must NOT define ${f}; plugin operations ride ` +
58
+ "on existing package_progress / package_operation_complete / " +
59
+ "plugin_config_update.",
60
+ ).toBe(false);
61
+ }
62
+ });
63
+
64
+ it("system-routes /api/health payload includes startedAt (ISO 8601 timestamp)", () => {
65
+ const sys = fs.readFileSync(
66
+ path.join(REPO_ROOT, "packages/server/src/routes/system-routes.ts"),
67
+ "utf-8",
68
+ );
69
+ expect(sys.includes("startedAt:")).toBe(true);
70
+ // ISO format produced by Date.prototype.toISOString().
71
+ expect(/serverStartTime.*toISOString\(\)|new Date\(serverStartTime\)\.toISOString\(\)/.test(sys))
72
+ .toBe(true);
73
+ });
74
+ });