@blackbelt-technology/pi-agent-dashboard 0.5.3 → 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.
- package/AGENTS.md +19 -30
- package/README.md +69 -1
- package/docs/architecture.md +89 -165
- package/package.json +10 -7
- package/packages/extension/package.json +2 -2
- package/packages/extension/src/__tests__/bridge-default-model-gate.test.ts +47 -0
- package/packages/extension/src/__tests__/bridge-followup-chat-order.test.ts +215 -0
- package/packages/extension/src/__tests__/bridge-followup-multi-entry.test.ts +202 -0
- package/packages/extension/src/__tests__/bridge-queue-update-forward.test.ts +77 -0
- package/packages/extension/src/__tests__/bridge-retry-ordering.test.ts +148 -0
- package/packages/extension/src/__tests__/bridge-shadow-queue-drain.test.ts +221 -0
- package/packages/extension/src/__tests__/bridge-shadow-queue-gate.test.ts +299 -0
- package/packages/extension/src/__tests__/bridge-shutdown-reset.test.ts +238 -0
- package/packages/extension/src/__tests__/bridge-slash-command-routing.test.ts +127 -31
- package/packages/extension/src/__tests__/command-handler.test.ts +105 -3
- package/packages/extension/src/__tests__/fixtures/usage-limit-error-strings.ts +127 -0
- package/packages/extension/src/__tests__/source-detector.test.ts +15 -0
- package/packages/extension/src/__tests__/usage-limit-orderer.test.ts +12 -0
- package/packages/extension/src/bridge-default-model-gate.ts +32 -0
- package/packages/extension/src/bridge.ts +299 -20
- package/packages/extension/src/command-handler.ts +53 -7
- package/packages/extension/src/dashboard-default-adapter.ts +5 -0
- package/packages/extension/src/prompt-bus.ts +15 -0
- package/packages/extension/src/slash-dispatch.ts +30 -15
- package/packages/extension/src/source-detector.ts +13 -5
- package/packages/extension/src/usage-limit-orderer.ts +18 -1
- package/packages/server/bin/pi-dashboard.mjs +62 -14
- package/packages/server/package.json +9 -5
- package/packages/server/src/__tests__/browser-gateway-register-handler.test.ts +69 -0
- package/packages/server/src/__tests__/cli-env-no-clobber.test.ts +46 -0
- package/packages/server/src/__tests__/cli-no-bootstrap-references.test.ts +69 -0
- package/packages/server/src/__tests__/cli-parse.test.ts +9 -10
- package/packages/server/src/__tests__/cli-version.test.ts +151 -0
- package/packages/server/src/__tests__/directory-service-openspec-enabled.test.ts +9 -0
- package/packages/server/src/__tests__/directory-service-refresh-force.test.ts +9 -0
- package/packages/server/src/__tests__/directory-service-specs-mtime.test.ts +9 -0
- package/packages/server/src/__tests__/directory-service-toctou.test.ts +9 -0
- package/packages/server/src/__tests__/directory-service.test.ts +9 -0
- package/packages/server/src/__tests__/doctor-route.test.ts +53 -0
- package/packages/server/src/__tests__/event-wiring-queue-state.test.ts +156 -0
- package/packages/server/src/__tests__/event-wiring-resume-clear.test.ts +105 -0
- package/packages/server/src/__tests__/health-shape.test.ts +35 -12
- package/packages/server/src/__tests__/installed-package-enricher.test.ts +12 -12
- package/packages/server/src/__tests__/is-activity-event.test.ts +4 -7
- package/packages/server/src/__tests__/package-routes.test.ts +6 -2
- package/packages/server/src/__tests__/pi-changelog-routes.test.ts +10 -13
- package/packages/server/src/__tests__/pi-core-checker.test.ts +2 -2
- package/packages/server/src/__tests__/pi-version-skew.test.ts +3 -2
- package/packages/server/src/__tests__/plugin-activation-routes.test.ts +267 -0
- package/packages/server/src/__tests__/plugin-intent-cache.test.ts +75 -0
- package/packages/server/src/__tests__/preferences-store.test.ts +196 -0
- package/packages/server/src/__tests__/reattach-placement.test.ts +9 -0
- package/packages/server/src/__tests__/recommended-routes.test.ts +2 -2
- package/packages/server/src/__tests__/recovery-server.test.ts +203 -0
- package/packages/server/src/__tests__/session-action-handler-clear-queue.test.ts +153 -0
- package/packages/server/src/__tests__/session-action-handler-headless-reload.test.ts +43 -0
- package/packages/server/src/__tests__/session-order-manager.test.ts +9 -0
- package/packages/server/src/__tests__/session-order-reboot.test.ts +9 -0
- package/packages/server/src/__tests__/session-ordering-integration.test.ts +9 -0
- package/packages/server/src/browser-gateway.ts +83 -5
- package/packages/server/src/browser-handlers/directory-handler.ts +69 -0
- package/packages/server/src/browser-handlers/session-action-handler.ts +89 -0
- package/packages/server/src/browser-handlers/subscription-handler.ts +23 -0
- package/packages/server/src/changelog-parser.ts +1 -1
- package/packages/server/src/cli.ts +68 -250
- package/packages/server/src/event-status-extraction.ts +14 -62
- package/packages/server/src/event-wiring.ts +23 -10
- package/packages/server/src/memory-session-manager.ts +4 -0
- package/packages/server/src/pi-core-checker.ts +1 -1
- package/packages/server/src/pi-dev-version-check.ts +1 -1
- package/packages/server/src/pi-version-skew.ts +24 -46
- package/packages/server/src/plugin-intent-cache.ts +67 -0
- package/packages/server/src/preferences-store.ts +199 -13
- package/packages/server/src/recovery-server.ts +366 -0
- package/packages/server/src/routes/__tests__/manifest-route.test.ts +138 -0
- package/packages/server/src/routes/doctor-routes.ts +26 -21
- package/packages/server/src/routes/manifest-route.ts +162 -0
- package/packages/server/src/routes/openspec-routes.ts +4 -25
- package/packages/server/src/routes/pi-changelog-routes.ts +5 -24
- package/packages/server/src/routes/pi-core-routes.ts +3 -23
- package/packages/server/src/routes/plugin-activation-routes.ts +193 -0
- package/packages/server/src/routes/recommended-routes.ts +21 -0
- package/packages/server/src/routes/system-routes.ts +73 -11
- package/packages/server/src/server.ts +105 -307
- package/packages/server/src/session-api.ts +5 -63
- package/packages/shared/package.json +1 -1
- package/packages/shared/src/__tests__/binary-lookup-resolveJiti.test.ts +28 -0
- package/packages/shared/src/__tests__/binary-lookup-spawn-env.test.ts +61 -0
- package/packages/shared/src/__tests__/binary-lookup.test.ts +16 -0
- package/packages/shared/src/__tests__/bridge-register.test.ts +67 -0
- package/packages/shared/src/__tests__/ci-electron-no-side-effects.test.ts +129 -0
- package/packages/shared/src/__tests__/config.test.ts +40 -0
- package/packages/shared/src/__tests__/dashboard-paths.test.ts +81 -0
- package/packages/shared/src/__tests__/ensure-windows-path.test.ts +112 -0
- package/packages/shared/src/__tests__/intent-types.test.ts +120 -0
- package/packages/shared/src/__tests__/jiti-packages-parity.test.ts +85 -0
- package/packages/shared/src/__tests__/legacy-managed-dir.test.ts +59 -0
- package/packages/shared/src/__tests__/no-direct-child-process.test.ts +12 -0
- package/packages/shared/src/__tests__/no-electron-execpath-spawn.test.ts +149 -0
- package/packages/shared/src/__tests__/no-flow-command-route-claims.test.ts +71 -0
- package/packages/shared/src/__tests__/no-flow-references-in-shell.test.ts +221 -0
- package/packages/shared/src/__tests__/no-managed-dir-reference.test.ts +134 -0
- package/packages/shared/src/__tests__/no-pi-dashboard-version-jiti-gate.test.ts +41 -0
- package/packages/shared/src/__tests__/no-primitive-direct-import.test.ts +235 -0
- package/packages/shared/src/__tests__/no-server-imports-in-resolver.test.ts +53 -0
- package/packages/shared/src/__tests__/node-spawn-jiti-contract.test.ts +54 -101
- package/packages/shared/src/__tests__/node-spawn.test.ts +29 -13
- package/packages/shared/src/__tests__/pi-package-resolver.test.ts +300 -0
- package/packages/shared/src/__tests__/plugin-activation-contracts.test.ts +74 -0
- package/packages/shared/src/__tests__/plugin-bridge-classify-source.test.ts +73 -0
- package/packages/shared/src/__tests__/plugin-bridge-register-extended.test.ts +17 -5
- package/packages/shared/src/__tests__/plugin-bridge-register-packages.test.ts +233 -0
- package/packages/shared/src/__tests__/plugin-bridge-register.test.ts +19 -9
- package/packages/shared/src/__tests__/publish-workflow-contract.test.ts +154 -15
- package/packages/shared/src/__tests__/recommended-extensions.test.ts +28 -10
- package/packages/shared/src/__tests__/resolver-parity-with-scanner.test.ts +76 -0
- package/packages/shared/src/__tests__/server-identity.test.ts +127 -0
- package/packages/shared/src/__tests__/server-launcher.test.ts +35 -0
- package/packages/shared/src/__tests__/source-matching.test.ts +5 -5
- package/packages/shared/src/__tests__/sync-versions-spec.test.ts +76 -0
- package/packages/shared/src/__tests__/tool-registry-definitions.test.ts +50 -2
- package/packages/shared/src/bridge-register.ts +35 -2
- package/packages/shared/src/browser-protocol.ts +176 -2
- package/packages/shared/src/config.ts +12 -0
- package/packages/shared/src/dashboard-paths.ts +69 -0
- package/packages/shared/src/dashboard-plugin/index.ts +2 -0
- package/packages/shared/src/dashboard-plugin/intent-types.ts +93 -0
- package/packages/shared/src/dashboard-plugin/manifest-types.ts +55 -1
- package/packages/shared/src/dashboard-plugin/plugin-status.ts +82 -0
- package/packages/shared/src/dashboard-plugin/slot-props.ts +11 -0
- package/packages/shared/src/dashboard-plugin/slot-types.ts +16 -2
- package/packages/shared/src/dashboard-plugin/ui-primitives.ts +287 -0
- package/packages/shared/src/dashboard-starter.ts +22 -0
- package/packages/shared/src/doctor-core.ts +49 -27
- package/packages/shared/src/launch-source-types.ts +9 -9
- package/packages/shared/src/legacy-managed-dir.ts +97 -0
- package/packages/shared/src/mdns-discovery.ts +4 -1
- package/packages/shared/src/pi-package-resolver.ts +388 -0
- package/packages/shared/src/platform/binary-lookup.ts +27 -3
- package/packages/shared/src/platform/ensure-windows-path.ts +95 -0
- package/packages/shared/src/platform/exec.ts +22 -0
- package/packages/shared/src/platform/node-spawn.ts +42 -41
- package/packages/shared/src/plugin-bridge-register.ts +275 -18
- package/packages/shared/src/protocol.ts +94 -2
- package/packages/shared/src/recommended-extensions.ts +34 -10
- package/packages/shared/src/server-identity.ts +74 -5
- package/packages/shared/src/server-launcher.ts +20 -0
- package/packages/shared/src/source-matching.ts +1 -1
- package/packages/shared/src/tool-registry/__tests__/node-script-toargv-fallback.test.ts +84 -0
- package/packages/shared/src/tool-registry/definitions.ts +91 -7
- package/packages/shared/src/types.ts +12 -8
- package/scripts/maybe-patch-package.cjs +44 -0
- package/packages/server/src/__tests__/bootstrap-install-from-list.test.ts +0 -263
- package/packages/server/src/__tests__/bootstrap-queue.test.ts +0 -120
- package/packages/server/src/__tests__/bootstrap-routes.test.ts +0 -125
- package/packages/server/src/__tests__/bootstrap-state.test.ts +0 -119
- package/packages/server/src/__tests__/cli-bootstrap.test.ts +0 -36
- package/packages/server/src/__tests__/event-status-extraction-flow.test.ts +0 -55
- package/packages/server/src/__tests__/legacy-pi-cleanup.test.ts +0 -149
- package/packages/server/src/__tests__/post-install-openspec-refresh.test.ts +0 -180
- package/packages/server/src/__tests__/post-install-rescan.test.ts +0 -134
- package/packages/server/src/__tests__/system-routes-reextract.test.ts +0 -91
- package/packages/server/src/bootstrap-install-from-list.ts +0 -232
- package/packages/server/src/bootstrap-queue.ts +0 -130
- package/packages/server/src/bootstrap-state.ts +0 -159
- package/packages/server/src/legacy-pi-cleanup.ts +0 -151
- package/packages/server/src/routes/bootstrap-routes.ts +0 -125
- package/packages/shared/src/__tests__/bootstrap/README.md +0 -133
- package/packages/shared/src/__tests__/bootstrap/__snapshots__/cube.test.ts.snap +0 -378
- package/packages/shared/src/__tests__/bootstrap/assertions.ts +0 -136
- package/packages/shared/src/__tests__/bootstrap/cube.test.ts +0 -47
- package/packages/shared/src/__tests__/bootstrap/cube.ts +0 -66
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/a-electron.test.ts.snap +0 -84
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/b-npm-global.test.ts.snap +0 -90
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/c-dev-monorepo.test.ts.snap +0 -34
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/d-overrides.test.ts.snap +0 -20
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/e-stale-partial.test.ts.snap +0 -62
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/f-cwd-variants.test.ts.snap +0 -34
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/g-windows-specifics.test.ts.snap +0 -49
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/j-path-gui-minimal.test.ts.snap +0 -12
- package/packages/shared/src/__tests__/bootstrap/families/a-electron.test.ts +0 -156
- package/packages/shared/src/__tests__/bootstrap/families/b-npm-global.test.ts +0 -157
- package/packages/shared/src/__tests__/bootstrap/families/c-dev-monorepo.test.ts +0 -102
- package/packages/shared/src/__tests__/bootstrap/families/d-overrides.test.ts +0 -76
- package/packages/shared/src/__tests__/bootstrap/families/e-stale-partial.test.ts +0 -94
- package/packages/shared/src/__tests__/bootstrap/families/f-cwd-variants.test.ts +0 -87
- package/packages/shared/src/__tests__/bootstrap/families/g-windows-specifics.test.ts +0 -143
- package/packages/shared/src/__tests__/bootstrap/families/h-home-drift.test.ts +0 -64
- package/packages/shared/src/__tests__/bootstrap/families/i-malformed-settings.test.ts +0 -77
- package/packages/shared/src/__tests__/bootstrap/families/index.ts +0 -19
- package/packages/shared/src/__tests__/bootstrap/families/j-path-gui-minimal.test.ts +0 -61
- package/packages/shared/src/__tests__/bootstrap/families/k-dashboard-absent.test.ts +0 -50
- package/packages/shared/src/__tests__/bootstrap/families/l-instance-coordination.test.ts +0 -272
- package/packages/shared/src/__tests__/bootstrap/fixtures/dev-monorepo.ts +0 -58
- package/packages/shared/src/__tests__/bootstrap/fixtures/electron-layout.ts +0 -84
- package/packages/shared/src/__tests__/bootstrap/fixtures/index.ts +0 -9
- package/packages/shared/src/__tests__/bootstrap/fixtures/managed-install.ts +0 -85
- package/packages/shared/src/__tests__/bootstrap/fixtures/npm-global-layout.ts +0 -122
- package/packages/shared/src/__tests__/bootstrap/fixtures/pi-versions.ts +0 -36
- package/packages/shared/src/__tests__/bootstrap/fixtures/settings-json.ts +0 -39
- package/packages/shared/src/__tests__/bootstrap/harness.smoke.test.ts +0 -220
- package/packages/shared/src/__tests__/bootstrap/harness.ts +0 -413
- package/packages/shared/src/__tests__/bootstrap/scenarios-skipped.ts +0 -125
- package/packages/shared/src/__tests__/bootstrap/scenarios.ts +0 -132
- package/packages/shared/src/__tests__/bootstrap-install-resolve-npm.test.ts +0 -72
- package/packages/shared/src/__tests__/install-managed-node-bootstrap-order.test.ts +0 -68
- package/packages/shared/src/__tests__/install-managed-node.test.ts +0 -192
- package/packages/shared/src/__tests__/installable-list.test.ts +0 -130
- package/packages/shared/src/__tests__/no-installable-list-in-bridge.test.ts +0 -52
- package/packages/shared/src/bootstrap-install.ts +0 -406
- package/packages/shared/src/installable-list.ts +0 -152
- package/packages/shared/src/launch-source-flag.ts +0 -14
|
@@ -1,42 +1,42 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Pin the jiti
|
|
2
|
+
* Pin the jiti behavioural contract for `shouldUrlWrapEntry()`.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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.
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
|
122
|
-
"It must mention either
|
|
123
|
-
"
|
|
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
|
-
"
|
|
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
|
|
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
|
-
//
|
|
81
|
-
//
|
|
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
|
|
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: "
|
|
204
|
-
entry: "
|
|
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(
|
|
209
|
-
|
|
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
|
|
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(
|
|
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
|
+
});
|