@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.
- package/AGENTS.md +19 -30
- package/README.md +69 -1
- package/docs/architecture.md +89 -165
- package/package.json +11 -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
|
@@ -33,6 +33,11 @@ describe("MANAGED_PI_PACKAGES + JITI_PACKAGES contract", () => {
|
|
|
33
33
|
|
|
34
34
|
it("upstream jiti first, legacy fork fallback", () => {
|
|
35
35
|
expect(JITI_PACKAGES).toEqual(["jiti", "@mariozechner/jiti"]);
|
|
36
|
+
// Primary lookup MUST be bare "jiti" — that's what `packages/server/
|
|
37
|
+
// package.json#dependencies.jiti` resolves on a clean npm install.
|
|
38
|
+
// Regression for v0.5.3 fork-name drift; see change:
|
|
39
|
+
// enable-standalone-npm-install task 7.3.
|
|
40
|
+
expect(JITI_PACKAGES[0]).toBe("jiti");
|
|
36
41
|
});
|
|
37
42
|
});
|
|
38
43
|
|
|
@@ -181,6 +186,29 @@ describe("ToolResolver.resolveJiti — anchor walk-up + argv fallback", () => {
|
|
|
181
186
|
expect(url).not.toBeNull();
|
|
182
187
|
});
|
|
183
188
|
|
|
189
|
+
it("resolves jiti shipped as a direct dep of pi-dashboard-server (own-tree, no pi anywhere)", () => {
|
|
190
|
+
// Simulates the npm-install path post enable-standalone-npm-install:
|
|
191
|
+
// - no managed pi at ~/.pi-dashboard/
|
|
192
|
+
// - no system pi on PATH
|
|
193
|
+
// - no caller-supplied anchor
|
|
194
|
+
// - jiti lives in pi-dashboard-server's own node_modules, reachable
|
|
195
|
+
// via createRequire(argv[1]) walk-up
|
|
196
|
+
const argv = "/usr/local/lib/node_modules/@blackbelt-technology/pi-dashboard-server/bin/pi-dashboard.mjs";
|
|
197
|
+
const jitiPkgJson = "/usr/local/lib/node_modules/@blackbelt-technology/pi-dashboard-server/node_modules/jiti/package.json";
|
|
198
|
+
const url = new ToolResolver().resolveJiti({
|
|
199
|
+
_managedDir: MANAGED_DIR,
|
|
200
|
+
// Managed pi pkg.json + legacy variant miss; only the jiti register exists.
|
|
201
|
+
_pathExists: (p) => p === path.join(path.dirname(jitiPkgJson), "lib", "jiti-register.mjs"),
|
|
202
|
+
_whichPi: () => null,
|
|
203
|
+
_realpath: (p) => p,
|
|
204
|
+
_argv1: argv,
|
|
205
|
+
resolver: makeResolver({ "jiti/package.json": jitiPkgJson }),
|
|
206
|
+
});
|
|
207
|
+
expect(url).not.toBeNull();
|
|
208
|
+
expect(url!.startsWith("file://")).toBe(true);
|
|
209
|
+
expect(url!).toMatch(/\/jiti\/lib\/jiti-register\.mjs$/);
|
|
210
|
+
});
|
|
211
|
+
|
|
184
212
|
it("returns null when every anchor misses", () => {
|
|
185
213
|
const url = new ToolResolver().resolveJiti({
|
|
186
214
|
_managedDir: MANAGED_DIR,
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests: ToolResolver.buildSpawnEnv applies
|
|
3
|
+
* ensureWindowsSystemPath on Windows and is a no-op on POSIX.
|
|
4
|
+
*
|
|
5
|
+
* See change: fix-windows-path-system32-missing.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect, vi } from "vitest";
|
|
8
|
+
|
|
9
|
+
// Mock node:fs.existsSync to allow injecting an `exists` impl via opts;
|
|
10
|
+
// binary-lookup itself uses `existsSync` for other resolution code that
|
|
11
|
+
// we don't exercise here.
|
|
12
|
+
vi.mock("node:fs", () => ({ existsSync: () => false, realpathSync: (p: string) => p }));
|
|
13
|
+
|
|
14
|
+
import { ToolResolver } from "../platform/binary-lookup.js";
|
|
15
|
+
|
|
16
|
+
describe("ToolResolver.buildSpawnEnv with platform override", () => {
|
|
17
|
+
it("on win32: adds System32 to PATH even when inherited PATH is empty", () => {
|
|
18
|
+
const resolver = new ToolResolver({});
|
|
19
|
+
const env = resolver.buildSpawnEnv(
|
|
20
|
+
{ PATH: "", SYSTEMROOT: "C:\\Windows" },
|
|
21
|
+
{ platform: "win32", exists: () => true },
|
|
22
|
+
);
|
|
23
|
+
expect(env.PATH).toContain("C:\\Windows\\System32");
|
|
24
|
+
expect(env.PATH).toContain("C:\\Windows\\System32\\WindowsPowerShell\\v1.0");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("on win32: does not duplicate System32 when already present", () => {
|
|
28
|
+
const resolver = new ToolResolver({});
|
|
29
|
+
const env = resolver.buildSpawnEnv(
|
|
30
|
+
{ PATH: "C:\\Windows\\System32;C:\\other", SYSTEMROOT: "C:\\Windows" },
|
|
31
|
+
{ platform: "win32", exists: () => true },
|
|
32
|
+
);
|
|
33
|
+
// Count substring occurrences (case-insensitive). buildSpawnEnv may
|
|
34
|
+
// splice POSIX `:` delimiters on a darwin host into the prepended
|
|
35
|
+
// segment, so splitting by `;` is unreliable; substring count is the
|
|
36
|
+
// right invariant for de-dup.
|
|
37
|
+
const lower = (env.PATH ?? "").toLowerCase();
|
|
38
|
+
const re = /c:\\windows\\system32(?![\\\w])/g;
|
|
39
|
+
const matches = lower.match(re) ?? [];
|
|
40
|
+
expect(matches.length).toBe(1);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("on linux: does not inject Windows paths", () => {
|
|
44
|
+
const resolver = new ToolResolver({});
|
|
45
|
+
const env = resolver.buildSpawnEnv(
|
|
46
|
+
{ PATH: "/usr/bin" },
|
|
47
|
+
{ platform: "linux", exists: () => true },
|
|
48
|
+
);
|
|
49
|
+
expect(env.PATH).not.toContain("System32");
|
|
50
|
+
expect(env.PATH).not.toContain("C:\\Windows");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("on darwin: does not inject Windows paths", () => {
|
|
54
|
+
const resolver = new ToolResolver({});
|
|
55
|
+
const env = resolver.buildSpawnEnv(
|
|
56
|
+
{ PATH: "/usr/local/bin:/usr/bin" },
|
|
57
|
+
{ platform: "darwin", exists: () => true },
|
|
58
|
+
);
|
|
59
|
+
expect(env.PATH).not.toContain("System32");
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -180,5 +180,21 @@ describe("ToolResolver", () => {
|
|
|
180
180
|
expect(env.PATH).toContain("/extra/one");
|
|
181
181
|
expect(env.PATH).toContain("/extra/two");
|
|
182
182
|
});
|
|
183
|
+
|
|
184
|
+
it("strips ELECTRON_RUN_AS_NODE and other Electron vars from the spawn env", () => {
|
|
185
|
+
const resolver = new ToolResolver();
|
|
186
|
+
const env = resolver.buildSpawnEnv({
|
|
187
|
+
PATH: "/usr/bin",
|
|
188
|
+
ELECTRON_RUN_AS_NODE: "1",
|
|
189
|
+
ELECTRON_DEFAULT_ERROR_MODE: "1",
|
|
190
|
+
ELECTRON_ENABLE_STACK_DUMPING: "1",
|
|
191
|
+
MY_APP_VAR: "hello",
|
|
192
|
+
});
|
|
193
|
+
expect(env.ELECTRON_RUN_AS_NODE).toBeUndefined();
|
|
194
|
+
expect(env.ELECTRON_DEFAULT_ERROR_MODE).toBeUndefined();
|
|
195
|
+
expect(env.ELECTRON_ENABLE_STACK_DUMPING).toBeUndefined();
|
|
196
|
+
// Non-Electron vars preserved
|
|
197
|
+
expect(env.MY_APP_VAR).toBe("hello");
|
|
198
|
+
});
|
|
183
199
|
});
|
|
184
200
|
});
|
|
@@ -181,5 +181,72 @@ describe("shared bridge-register", () => {
|
|
|
181
181
|
const settings = readSettings();
|
|
182
182
|
expect(settings.packages).toContain("/app/extension");
|
|
183
183
|
});
|
|
184
|
+
|
|
185
|
+
it("identity-dedups across install layouts (same package.json#name wins last)", () => {
|
|
186
|
+
// Three install layouts, all registering the same extension package
|
|
187
|
+
// name under different absolute paths: dev workspace, .app bundle,
|
|
188
|
+
// npm-global. After all three register, only the most recent path
|
|
189
|
+
// should remain.
|
|
190
|
+
const devPath = path.join(tmpDir, "dev", "ext");
|
|
191
|
+
const appPath = path.join(tmpDir, "app", "ext");
|
|
192
|
+
const globalPath = path.join(tmpDir, "global", "ext");
|
|
193
|
+
for (const p of [devPath, appPath, globalPath]) {
|
|
194
|
+
fs.mkdirSync(p, { recursive: true });
|
|
195
|
+
fs.writeFileSync(
|
|
196
|
+
path.join(p, "package.json"),
|
|
197
|
+
'{"name":"@blackbelt-technology/pi-dashboard-extension"}',
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
registerBridgeExtension(devPath);
|
|
202
|
+
registerBridgeExtension(appPath);
|
|
203
|
+
registerBridgeExtension(globalPath);
|
|
204
|
+
|
|
205
|
+
const settings = readSettings();
|
|
206
|
+
const packages = settings.packages as string[];
|
|
207
|
+
expect(packages).toEqual([globalPath]);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("preserves entries with different package names", () => {
|
|
211
|
+
const otherExt = path.join(tmpDir, "other", "ext");
|
|
212
|
+
fs.mkdirSync(otherExt, { recursive: true });
|
|
213
|
+
fs.writeFileSync(
|
|
214
|
+
path.join(otherExt, "package.json"),
|
|
215
|
+
'{"name":"some-unrelated-extension"}',
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
const dashExt = path.join(tmpDir, "dash", "ext");
|
|
219
|
+
fs.mkdirSync(dashExt, { recursive: true });
|
|
220
|
+
fs.writeFileSync(
|
|
221
|
+
path.join(dashExt, "package.json"),
|
|
222
|
+
'{"name":"@blackbelt-technology/pi-dashboard-extension"}',
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
writeSettings({ packages: [otherExt] });
|
|
226
|
+
registerBridgeExtension(dashExt);
|
|
227
|
+
|
|
228
|
+
const settings = readSettings();
|
|
229
|
+
const packages = settings.packages as string[];
|
|
230
|
+
expect(packages).toContain(otherExt);
|
|
231
|
+
expect(packages).toContain(dashExt);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("leaves npm:-scheme entries untouched during identity dedup", () => {
|
|
235
|
+
const npmEntry = "npm:@blackbelt-technology/pi-dashboard-extension";
|
|
236
|
+
const localExt = path.join(tmpDir, "local", "ext");
|
|
237
|
+
fs.mkdirSync(localExt, { recursive: true });
|
|
238
|
+
fs.writeFileSync(
|
|
239
|
+
path.join(localExt, "package.json"),
|
|
240
|
+
'{"name":"@blackbelt-technology/pi-dashboard-extension"}',
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
writeSettings({ packages: [npmEntry] });
|
|
244
|
+
registerBridgeExtension(localExt);
|
|
245
|
+
|
|
246
|
+
const settings = readSettings();
|
|
247
|
+
const packages = settings.packages as string[];
|
|
248
|
+
expect(packages).toContain(npmEntry);
|
|
249
|
+
expect(packages).toContain(localExt);
|
|
250
|
+
});
|
|
184
251
|
});
|
|
185
252
|
});
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repo-level invariant: the on-demand Electron CI workflow and its reusable
|
|
3
|
+
* build workflow MUST NOT publish to npm, create a GitHub Release, push
|
|
4
|
+
* tags, or otherwise affect the public update channel of installed users.
|
|
5
|
+
*
|
|
6
|
+
* The CI dev build's version slug (`<base>-ci.<stamp>.<branch>.<sha7>`) is
|
|
7
|
+
* a SemVer prerelease ranked strictly below the base stable version, so
|
|
8
|
+
* `electron-updater` with default `allowPrerelease: false` would not offer
|
|
9
|
+
* it as an update. Defense-in-depth: this lint enforces that the workflows
|
|
10
|
+
* themselves contain no publishing or release-creating actions, even by
|
|
11
|
+
* accident in a future PR.
|
|
12
|
+
*
|
|
13
|
+
* If this test fails, remove the offending action from the workflow. If
|
|
14
|
+
* you genuinely need to publish from CI, do it in publish.yml (which is
|
|
15
|
+
* gated on a tag push or explicit dispatch with a version input).
|
|
16
|
+
*
|
|
17
|
+
* See change: add-ci-electron-on-demand-build (proposal.md Safety section,
|
|
18
|
+
* design.md Decision 4).
|
|
19
|
+
*/
|
|
20
|
+
import { describe, it } from "vitest";
|
|
21
|
+
import fs from "node:fs";
|
|
22
|
+
import path from "node:path";
|
|
23
|
+
import url from "node:url";
|
|
24
|
+
|
|
25
|
+
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
|
|
26
|
+
const REPO_ROOT = path.resolve(__dirname, "..", "..", "..", "..");
|
|
27
|
+
const CI_ELECTRON_PATH = path.join(REPO_ROOT, ".github", "workflows", "ci-electron.yml");
|
|
28
|
+
const REUSABLE_PATH = path.join(REPO_ROOT, ".github", "workflows", "_electron-build.yml");
|
|
29
|
+
|
|
30
|
+
// Patterns whose presence indicates a side-effect we forbid in these
|
|
31
|
+
// workflows. The reusable workflow is the SHARED build definition, so a
|
|
32
|
+
// publishing action there would silently leak into the release flow too —
|
|
33
|
+
// but we want it kept clean for clarity and to keep the no-side-effects
|
|
34
|
+
// invariant easy to reason about.
|
|
35
|
+
const FORBIDDEN_PATTERNS: Array<{ pattern: RegExp; reason: string }> = [
|
|
36
|
+
{
|
|
37
|
+
pattern: /softprops\/action-gh-release/,
|
|
38
|
+
reason: "creates GitHub Releases — must only happen in publish.yml",
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
pattern: /actions\/create-release/,
|
|
42
|
+
reason: "creates GitHub Releases — must only happen in publish.yml",
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
pattern: /\bnpm\s+publish\b/,
|
|
46
|
+
reason: "publishes to npm — must only happen in publish.yml's publish job",
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
pattern: /\bgit\s+push\s+origin\s+v\d/,
|
|
50
|
+
reason: "pushes a version tag — must only happen in publish.yml's prepare job",
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
pattern: /\bgit\s+tag\s+["']?v\d/,
|
|
54
|
+
reason: "creates a version tag — must only happen in publish.yml's prepare job",
|
|
55
|
+
},
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Strip YAML full-line comments before scanning. Comments are legitimately
|
|
60
|
+
* allowed to discuss what's forbidden ("No `npm publish`") without being
|
|
61
|
+
* the forbidden thing itself. We strip lines whose first non-whitespace
|
|
62
|
+
* char is `#`. Inline trailing comments are preserved (rare in this codebase
|
|
63
|
+
* and risky to strip because of `run: |` shell blocks where `#` is a real
|
|
64
|
+
* shell comment leader).
|
|
65
|
+
*/
|
|
66
|
+
function stripYamlComments(content: string): string {
|
|
67
|
+
return content
|
|
68
|
+
.split("\n")
|
|
69
|
+
.filter((line) => !/^\s*#/.test(line))
|
|
70
|
+
.join("\n");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function assertNoForbidden(filePath: string, content: string): void {
|
|
74
|
+
const stripped = stripYamlComments(content);
|
|
75
|
+
for (const { pattern, reason } of FORBIDDEN_PATTERNS) {
|
|
76
|
+
const m = stripped.match(pattern);
|
|
77
|
+
if (m) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
`${path.basename(filePath)} contains forbidden pattern ${pattern} ` +
|
|
80
|
+
`(${reason}). Matched: ${JSON.stringify(m[0])}. ` +
|
|
81
|
+
`See change: add-ci-electron-on-demand-build (design.md Decision 4).`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
describe("ci-electron.yml — no side effects on registries or update channels", () => {
|
|
88
|
+
it("ci-electron.yml contains no forbidden publishing/release actions", () => {
|
|
89
|
+
const content = fs.readFileSync(CI_ELECTRON_PATH, "utf8");
|
|
90
|
+
assertNoForbidden(CI_ELECTRON_PATH, content);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("_electron-build.yml (shared) contains no forbidden publishing/release actions", () => {
|
|
94
|
+
// The reusable workflow is consumed by BOTH publish.yml (release flow)
|
|
95
|
+
// AND ci-electron.yml (on-demand). Keeping it free of publishing actions
|
|
96
|
+
// means publishing stays cleanly in publish.yml's own jobs, never
|
|
97
|
+
// accidentally inherited by the on-demand path.
|
|
98
|
+
const content = fs.readFileSync(REUSABLE_PATH, "utf8");
|
|
99
|
+
assertNoForbidden(REUSABLE_PATH, content);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("ci-electron.yml only fires on workflow_dispatch (no push/pr/schedule)", () => {
|
|
103
|
+
const content = fs.readFileSync(CI_ELECTRON_PATH, "utf8");
|
|
104
|
+
// Match the top-level `on:` block — must contain `workflow_dispatch:`
|
|
105
|
+
// and MUST NOT contain `push:`, `pull_request:`, or `schedule:`.
|
|
106
|
+
const onMatch = content.match(/^on:\s*\n((?:\s+\S.*\n)+?)(?=^\S|\n^[a-z])/m);
|
|
107
|
+
if (!onMatch) {
|
|
108
|
+
throw new Error("ci-electron.yml has no top-level `on:` block");
|
|
109
|
+
}
|
|
110
|
+
const onBlock = onMatch[1];
|
|
111
|
+
if (!/workflow_dispatch:/.test(onBlock)) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
"ci-electron.yml MUST trigger on workflow_dispatch only. Found `on:` block:\n" +
|
|
114
|
+
onBlock,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
for (const trigger of ["push:", "pull_request:", "schedule:", "release:"]) {
|
|
118
|
+
if (new RegExp(`^\\s+${trigger}`, "m").test(onBlock)) {
|
|
119
|
+
throw new Error(
|
|
120
|
+
`ci-electron.yml MUST NOT trigger on '${trigger}'. v1 is dispatch-only ` +
|
|
121
|
+
"to keep the no-side-effects invariant easy to reason about. " +
|
|
122
|
+
"If a different trigger is genuinely needed, update the spec first. " +
|
|
123
|
+
"See change: add-ci-electron-on-demand-build.\nFound `on:` block:\n" +
|
|
124
|
+
onBlock,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
});
|
|
@@ -519,3 +519,43 @@ describe("loadConfig spawnRegisterTimeoutMs", () => {
|
|
|
519
519
|
expect(loadConfig().spawnRegisterTimeoutMs).toBe(30000);
|
|
520
520
|
});
|
|
521
521
|
});
|
|
522
|
+
|
|
523
|
+
// See change: add-dynamic-pwa-manifest-naming.
|
|
524
|
+
describe("dashboardName", () => {
|
|
525
|
+
let testDir: string;
|
|
526
|
+
let configFile: string;
|
|
527
|
+
let origHome: string;
|
|
528
|
+
|
|
529
|
+
beforeEach(() => {
|
|
530
|
+
testDir = path.join(os.tmpdir(), `test-config-${Date.now()}`);
|
|
531
|
+
fs.mkdirSync(path.join(testDir, ".pi", "dashboard"), { recursive: true });
|
|
532
|
+
configFile = path.join(testDir, ".pi", "dashboard", "config.json");
|
|
533
|
+
origHome = process.env.HOME!;
|
|
534
|
+
process.env.HOME = testDir;
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
afterEach(() => {
|
|
538
|
+
process.env.HOME = origHome;
|
|
539
|
+
if (fs.existsSync(testDir)) fs.rmSync(testDir, { recursive: true });
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
it("is undefined when absent from config", () => {
|
|
543
|
+
fs.writeFileSync(configFile, JSON.stringify({}));
|
|
544
|
+
expect(loadConfig().dashboardName).toBeUndefined();
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
it("round-trips a non-empty string", () => {
|
|
548
|
+
fs.writeFileSync(configFile, JSON.stringify({ dashboardName: "Home NAS" }));
|
|
549
|
+
expect(loadConfig().dashboardName).toBe("Home NAS");
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
it("is undefined for whitespace-only override", () => {
|
|
553
|
+
fs.writeFileSync(configFile, JSON.stringify({ dashboardName: " " }));
|
|
554
|
+
expect(loadConfig().dashboardName).toBeUndefined();
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
it("is undefined for non-string override", () => {
|
|
558
|
+
fs.writeFileSync(configFile, JSON.stringify({ dashboardName: 42 }));
|
|
559
|
+
expect(loadConfig().dashboardName).toBeUndefined();
|
|
560
|
+
});
|
|
561
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for `dashboard-paths.ts` helpers.
|
|
3
|
+
*
|
|
4
|
+
* Pins each getter accepts a `{ homedir }` override and falls back to
|
|
5
|
+
* `os.homedir()` when none is provided. Asserts that the live server
|
|
6
|
+
* log path and the legacy installer log path resolve to *distinct*
|
|
7
|
+
* files even though both end in `server.log`.
|
|
8
|
+
*
|
|
9
|
+
* See change: harvest-bootstrap-survivor-fixes (cherry-pick 1).
|
|
10
|
+
*/
|
|
11
|
+
import os from "node:os";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import { describe, expect, it } from "vitest";
|
|
14
|
+
import {
|
|
15
|
+
getDashboardConfigDir,
|
|
16
|
+
getDashboardServerLogPath,
|
|
17
|
+
getInstallerLogPath,
|
|
18
|
+
getManagedDir,
|
|
19
|
+
} from "../dashboard-paths.js";
|
|
20
|
+
|
|
21
|
+
describe("dashboard-paths getters", () => {
|
|
22
|
+
describe("getDashboardConfigDir", () => {
|
|
23
|
+
it("no-arg resolves to ~/.pi/dashboard", () => {
|
|
24
|
+
expect(getDashboardConfigDir()).toBe(path.join(os.homedir(), ".pi", "dashboard"));
|
|
25
|
+
});
|
|
26
|
+
it("honours { homedir } override", () => {
|
|
27
|
+
expect(getDashboardConfigDir({ homedir: "/fake/home" })).toBe(
|
|
28
|
+
path.join("/fake/home", ".pi", "dashboard"),
|
|
29
|
+
);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe("getDashboardServerLogPath", () => {
|
|
34
|
+
it("no-arg resolves to ~/.pi/dashboard/server.log", () => {
|
|
35
|
+
expect(getDashboardServerLogPath()).toBe(
|
|
36
|
+
path.join(os.homedir(), ".pi", "dashboard", "server.log"),
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
it("honours { homedir } override", () => {
|
|
40
|
+
expect(getDashboardServerLogPath({ homedir: "/fake/home" })).toBe(
|
|
41
|
+
path.join("/fake/home", ".pi", "dashboard", "server.log"),
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe("getManagedDir (re-export)", () => {
|
|
47
|
+
it("no-arg resolves to ~/.pi-dashboard", () => {
|
|
48
|
+
expect(getManagedDir()).toBe(path.join(os.homedir(), ".pi-dashboard"));
|
|
49
|
+
});
|
|
50
|
+
it("honours { homedir } override", () => {
|
|
51
|
+
expect(getManagedDir({ homedir: "/fake/home" })).toBe(
|
|
52
|
+
path.join("/fake/home", ".pi-dashboard"),
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe("getInstallerLogPath", () => {
|
|
58
|
+
it("no-arg resolves to ~/.pi-dashboard/server.log", () => {
|
|
59
|
+
expect(getInstallerLogPath()).toBe(
|
|
60
|
+
path.join(os.homedir(), ".pi-dashboard", "server.log"),
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
it("honours { homedir } override", () => {
|
|
64
|
+
expect(getInstallerLogPath({ homedir: "/fake/home" })).toBe(
|
|
65
|
+
path.join("/fake/home", ".pi-dashboard", "server.log"),
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe("live server log vs installer log are distinct", () => {
|
|
71
|
+
it("the two log paths must never collide for the same homedir", () => {
|
|
72
|
+
const serverLog = getDashboardServerLogPath({ homedir: "/fake/home" });
|
|
73
|
+
const installerLog = getInstallerLogPath({ homedir: "/fake/home" });
|
|
74
|
+
expect(serverLog).not.toBe(installerLog);
|
|
75
|
+
// Different parent dirs even though both basename to server.log
|
|
76
|
+
expect(path.basename(serverLog)).toBe("server.log");
|
|
77
|
+
expect(path.basename(installerLog)).toBe("server.log");
|
|
78
|
+
expect(path.dirname(serverLog)).not.toBe(path.dirname(installerLog));
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for ensureWindowsSystemPath.
|
|
3
|
+
*
|
|
4
|
+
* See change: fix-windows-path-system32-missing.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect } from "vitest";
|
|
7
|
+
import { ensureWindowsSystemPath } from "../platform/ensure-windows-path.js";
|
|
8
|
+
|
|
9
|
+
const WIN_ENV_BASE = {
|
|
10
|
+
SYSTEMROOT: "C:\\Windows",
|
|
11
|
+
LOCALAPPDATA: "C:\\Users\\u\\AppData\\Local",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const EXPECTED_CANDIDATES = [
|
|
15
|
+
"C:\\Windows\\System32",
|
|
16
|
+
"C:\\Windows",
|
|
17
|
+
"C:\\Windows\\System32\\Wbem",
|
|
18
|
+
"C:\\Windows\\System32\\WindowsPowerShell\\v1.0",
|
|
19
|
+
"C:\\Windows\\System32\\OpenSSH",
|
|
20
|
+
"C:\\Users\\u\\AppData\\Local\\Microsoft\\WindowsApps",
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const allExist = () => true;
|
|
24
|
+
const noneExist = () => false;
|
|
25
|
+
|
|
26
|
+
describe("ensureWindowsSystemPath", () => {
|
|
27
|
+
describe("non-Windows hosts", () => {
|
|
28
|
+
it("returns env unchanged on darwin", () => {
|
|
29
|
+
const env = { PATH: "/usr/bin:/bin" };
|
|
30
|
+
const out = ensureWindowsSystemPath(env, { platform: "darwin", exists: allExist });
|
|
31
|
+
expect(out).toBe(env);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("returns env unchanged on linux", () => {
|
|
35
|
+
const env = { PATH: "/usr/bin:/bin" };
|
|
36
|
+
const out = ensureWindowsSystemPath(env, { platform: "linux", exists: allExist });
|
|
37
|
+
expect(out).toBe(env);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("Windows host", () => {
|
|
42
|
+
it("prepends all 6 candidates when PATH empty and all exist", () => {
|
|
43
|
+
const env = { ...WIN_ENV_BASE, PATH: "" };
|
|
44
|
+
const out = ensureWindowsSystemPath(env, { platform: "win32", exists: allExist });
|
|
45
|
+
const parts = (out.PATH ?? "").split(";").filter(Boolean);
|
|
46
|
+
expect(parts).toEqual(EXPECTED_CANDIDATES);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("does not duplicate System32 when already in PATH", () => {
|
|
50
|
+
const env = {
|
|
51
|
+
...WIN_ENV_BASE,
|
|
52
|
+
PATH: "C:\\foo;C:\\Windows\\System32;C:\\bar",
|
|
53
|
+
};
|
|
54
|
+
const out = ensureWindowsSystemPath(env, { platform: "win32", exists: allExist });
|
|
55
|
+
const occurrences = (out.PATH ?? "")
|
|
56
|
+
.toLowerCase()
|
|
57
|
+
.split(";")
|
|
58
|
+
.filter((p) => p === "c:\\windows\\system32").length;
|
|
59
|
+
expect(occurrences).toBe(1);
|
|
60
|
+
// Original PATH ordering preserved at the tail.
|
|
61
|
+
expect(out.PATH).toContain("C:\\foo;C:\\Windows\\System32;C:\\bar");
|
|
62
|
+
// Other 5 prepended.
|
|
63
|
+
expect(out.PATH).toContain("C:\\Windows\\System32\\Wbem");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("skips Wbem when it does not exist on disk", () => {
|
|
67
|
+
const exists = (p: string) => !p.endsWith("Wbem");
|
|
68
|
+
const env = { ...WIN_ENV_BASE, PATH: "" };
|
|
69
|
+
const out = ensureWindowsSystemPath(env, { platform: "win32", exists });
|
|
70
|
+
expect(out.PATH).not.toContain("Wbem");
|
|
71
|
+
expect(out.PATH).toContain("C:\\Windows\\System32");
|
|
72
|
+
expect(out.PATH).toContain("WindowsPowerShell");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("returns env unchanged when no candidates exist on disk", () => {
|
|
76
|
+
const env = { ...WIN_ENV_BASE, PATH: "C:\\already\\here" };
|
|
77
|
+
const out = ensureWindowsSystemPath(env, { platform: "win32", exists: noneExist });
|
|
78
|
+
expect(out).toBe(env);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("is idempotent: second call adds nothing", () => {
|
|
82
|
+
const env = { ...WIN_ENV_BASE, PATH: "" };
|
|
83
|
+
const once = ensureWindowsSystemPath(env, { platform: "win32", exists: allExist });
|
|
84
|
+
const twice = ensureWindowsSystemPath(once, { platform: "win32", exists: allExist });
|
|
85
|
+
expect(twice.PATH).toBe(once.PATH);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("treats lowercase System32 in PATH as already-present", () => {
|
|
89
|
+
const env = {
|
|
90
|
+
...WIN_ENV_BASE,
|
|
91
|
+
PATH: "c:\\windows\\system32;c:\\elsewhere",
|
|
92
|
+
};
|
|
93
|
+
const out = ensureWindowsSystemPath(env, { platform: "win32", exists: allExist });
|
|
94
|
+
const lower = (out.PATH ?? "").toLowerCase();
|
|
95
|
+
const occurrences = lower.split(";").filter((p) => p === "c:\\windows\\system32").length;
|
|
96
|
+
expect(occurrences).toBe(1);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("defaults SYSTEMROOT to C:\\Windows when env lacks it", () => {
|
|
100
|
+
const env = { PATH: "", LOCALAPPDATA: "C:\\Users\\u\\AppData\\Local" };
|
|
101
|
+
const out = ensureWindowsSystemPath(env, { platform: "win32", exists: allExist });
|
|
102
|
+
expect(out.PATH).toContain("C:\\Windows\\System32");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("skips WindowsApps when LOCALAPPDATA missing", () => {
|
|
106
|
+
const env = { SYSTEMROOT: "C:\\Windows", PATH: "" };
|
|
107
|
+
const out = ensureWindowsSystemPath(env, { platform: "win32", exists: allExist });
|
|
108
|
+
expect(out.PATH).not.toContain("WindowsApps");
|
|
109
|
+
expect(out.PATH).toContain("C:\\Windows\\System32");
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
});
|