@blackbelt-technology/pi-agent-dashboard 0.4.6 → 0.5.1
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 +339 -190
- package/README.md +50 -7
- package/docs/architecture.md +238 -23
- package/package.json +23 -13
- package/packages/extension/package.json +11 -3
- package/packages/extension/src/__tests__/ask-user-tool.test.ts +1 -1
- package/packages/extension/src/__tests__/build-provider-catalogue.test.ts +176 -0
- package/packages/extension/src/__tests__/command-handler.test.ts +68 -0
- package/packages/extension/src/__tests__/enrich-model-metadata.test.ts +1 -1
- package/packages/extension/src/__tests__/markdown-image-inliner.test.ts +355 -0
- package/packages/extension/src/__tests__/no-tui-multiselect-arm-regression.test.ts +1 -1
- package/packages/extension/src/__tests__/openspec-activity-detector.test.ts +68 -0
- package/packages/extension/src/__tests__/prompt-expander.test.ts +45 -0
- package/packages/extension/src/__tests__/provider-register-reload.test.ts +74 -0
- package/packages/extension/src/__tests__/retry-tracker.test.ts +147 -0
- package/packages/extension/src/__tests__/server-launcher.test.ts +24 -1
- package/packages/extension/src/__tests__/session-sync.test.ts +72 -0
- package/packages/extension/src/__tests__/usage-limit-orderer.test.ts +105 -0
- package/packages/extension/src/ask-user-tool.ts +1 -1
- package/packages/extension/src/bridge-context.ts +1 -1
- package/packages/extension/src/bridge.ts +169 -4
- package/packages/extension/src/command-handler.ts +65 -2
- package/packages/extension/src/flow-event-wiring.ts +1 -1
- package/packages/extension/src/markdown-image-inliner.ts +268 -0
- package/packages/extension/src/multiselect-list.ts +1 -1
- package/packages/extension/src/pi-env.d.ts +16 -9
- package/packages/extension/src/prompt-expander.ts +50 -2
- package/packages/extension/src/provider-register.ts +132 -8
- package/packages/extension/src/retry-tracker.ts +123 -0
- package/packages/extension/src/server-launcher.ts +18 -1
- package/packages/extension/src/session-sync.ts +15 -1
- package/packages/extension/src/usage-limit-orderer.ts +76 -0
- package/packages/server/package.json +6 -6
- package/packages/server/src/__tests__/auto-attach-slug-defense.test.ts +104 -0
- package/packages/server/src/__tests__/bootstrap-install-from-list.test.ts +263 -0
- package/packages/server/src/__tests__/browser-gateway-snapshot-on-connect.test.ts +143 -0
- package/packages/server/src/__tests__/build-auth-status.test.ts +190 -0
- package/packages/server/src/__tests__/changelog-fs.test.ts +171 -0
- package/packages/server/src/__tests__/changelog-parser.test.ts +220 -0
- package/packages/server/src/__tests__/changelog-remote.test.ts +193 -0
- package/packages/server/src/__tests__/cli-parse.test.ts +22 -4
- package/packages/server/src/__tests__/cold-boot-openspec-broadcast.test.ts +161 -0
- package/packages/server/src/__tests__/directory-service-refresh-force.test.ts +1 -1
- package/packages/server/src/__tests__/directory-service-specs-mtime.test.ts +1 -1
- package/packages/server/src/__tests__/directory-service-toctou.test.ts +1 -1
- package/packages/server/src/__tests__/directory-service.test.ts +1 -1
- package/packages/server/src/__tests__/doctor-route.test.ts +132 -0
- package/packages/server/src/__tests__/event-wiring-providers-list.test.ts +154 -0
- package/packages/server/src/__tests__/fixtures/pi-changelog-slice.md +180 -0
- package/packages/server/src/__tests__/fork-empty-session-preflight.test.ts +268 -0
- package/packages/server/src/__tests__/has-openspec-dir.test.ts +64 -0
- package/packages/server/src/__tests__/headless-pid-registry.test.ts +83 -0
- package/packages/server/src/__tests__/health-shape.test.ts +43 -0
- package/packages/server/src/__tests__/idle-timer-respects-terminals.test.ts +115 -0
- package/packages/server/src/__tests__/is-pi-process.test.ts +1 -1
- package/packages/server/src/__tests__/openspec-connect-snapshot.test.ts +92 -0
- package/packages/server/src/__tests__/package-manager-wrapper-resolve.test.ts +4 -4
- package/packages/server/src/__tests__/package-routes.test.ts +1 -1
- package/packages/server/src/__tests__/pending-fork-registry.test.ts +48 -24
- package/packages/server/src/__tests__/pi-changelog-integration.test.ts +165 -0
- package/packages/server/src/__tests__/pi-changelog-routes.test.ts +409 -0
- package/packages/server/src/__tests__/pi-core-checker.test.ts +155 -13
- package/packages/server/src/__tests__/pi-core-updater-managed-path.test.ts +236 -0
- package/packages/server/src/__tests__/pi-core-updater.test.ts +1 -1
- package/packages/server/src/__tests__/pi-dev-version-check.test.ts +184 -0
- package/packages/server/src/__tests__/pi-version-skew.test.ts +4 -4
- package/packages/server/src/__tests__/process-manager-codes.test.ts +80 -0
- package/packages/server/src/__tests__/process-manager-managed-path.test.ts +73 -0
- package/packages/server/src/__tests__/provider-auth-routes.test.ts +12 -4
- package/packages/server/src/__tests__/provider-auth-storage.test.ts +42 -11
- package/packages/server/src/__tests__/provider-catalogue-cache.test.ts +44 -0
- package/packages/server/src/__tests__/recommended-routes.test.ts +1 -1
- package/packages/server/src/__tests__/session-action-handler-spawn-error.test.ts +17 -2
- package/packages/server/src/__tests__/session-action-handler-spawn.test.ts +150 -0
- package/packages/server/src/__tests__/session-discovery-skill-firstmessage.test.ts +95 -0
- package/packages/server/src/__tests__/spawn-correlation-token-integration.test.ts +91 -0
- package/packages/server/src/__tests__/spawn-failure-log.test.ts +118 -0
- package/packages/server/src/__tests__/spawn-preflight.test.ts +91 -0
- package/packages/server/src/__tests__/spawn-register-watchdog.test.ts +250 -0
- package/packages/server/src/__tests__/spawn-token.test.ts +57 -0
- package/packages/server/src/__tests__/subscription-handler.test.ts +98 -6
- package/packages/server/src/__tests__/system-routes-reextract.test.ts +91 -0
- package/packages/server/src/__tests__/system-routes-spawn-failures.test.ts +84 -0
- package/packages/server/src/__tests__/terminal-manager.test.ts +45 -0
- package/packages/server/src/bootstrap-install-from-list.ts +232 -0
- package/packages/server/src/bootstrap-state.ts +18 -0
- package/packages/server/src/browser-gateway.ts +70 -24
- package/packages/server/src/browser-handlers/directory-handler.ts +4 -0
- package/packages/server/src/browser-handlers/handler-context.ts +9 -0
- package/packages/server/src/browser-handlers/session-action-handler.ts +159 -18
- package/packages/server/src/browser-handlers/subscription-handler.ts +50 -3
- package/packages/server/src/changelog-fs.ts +167 -0
- package/packages/server/src/changelog-parser.ts +321 -0
- package/packages/server/src/changelog-remote.ts +134 -0
- package/packages/server/src/cli.ts +23 -2
- package/packages/server/src/directory-service.ts +31 -0
- package/packages/server/src/event-wiring.ts +105 -5
- package/packages/server/src/headless-pid-registry.ts +54 -5
- package/packages/server/src/home-lock.d.ts +124 -0
- package/packages/server/src/home-lock.js +330 -0
- package/packages/server/src/home-lock.js.map +1 -0
- package/packages/server/src/idle-timer.ts +15 -1
- package/packages/server/src/pending-client-correlations.ts +73 -0
- package/packages/server/src/pending-fork-registry.ts +24 -12
- package/packages/server/src/pi-core-checker.ts +77 -17
- package/packages/server/src/pi-core-updater.ts +81 -15
- package/packages/server/src/pi-dev-version-check.ts +145 -0
- package/packages/server/src/pi-gateway.ts +10 -0
- package/packages/server/src/pi-version-skew.ts +12 -4
- package/packages/server/src/process-manager.ts +115 -21
- package/packages/server/src/provider-auth-handlers.ts +9 -0
- package/packages/server/src/provider-auth-storage.ts +83 -51
- package/packages/server/src/provider-catalogue-cache.ts +47 -0
- package/packages/server/src/routes/doctor-routes.ts +140 -0
- package/packages/server/src/routes/pi-changelog-routes.ts +194 -0
- package/packages/server/src/routes/pi-core-routes.ts +1 -1
- package/packages/server/src/routes/provider-auth-routes.ts +14 -1
- package/packages/server/src/routes/provider-routes.ts +4 -4
- package/packages/server/src/routes/system-routes.ts +38 -1
- package/packages/server/src/server.ts +85 -66
- package/packages/server/src/session-api.ts +54 -3
- package/packages/server/src/session-bootstrap.ts +27 -12
- package/packages/server/src/session-discovery.ts +11 -4
- package/packages/server/src/session-file-reader.ts +1 -1
- package/packages/server/src/session-scanner.ts +4 -2
- package/packages/server/src/spawn-failure-log.ts +130 -0
- package/packages/server/src/spawn-preflight.ts +82 -0
- package/packages/server/src/spawn-register-watchdog.ts +291 -0
- package/packages/server/src/spawn-token.ts +20 -0
- package/packages/server/src/terminal-manager.ts +12 -1
- package/packages/shared/package.json +1 -1
- package/packages/shared/src/__tests__/bootstrap/__snapshots__/cube.test.ts.snap +25 -17
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/a-electron.test.ts.snap +5 -4
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/b-npm-global.test.ts.snap +6 -5
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/c-dev-monorepo.test.ts.snap +1 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/e-stale-partial.test.ts.snap +5 -4
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/f-cwd-variants.test.ts.snap +2 -1
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/g-windows-specifics.test.ts.snap +6 -3
- package/packages/shared/src/__tests__/bootstrap/fixtures/dev-monorepo.ts +1 -1
- package/packages/shared/src/__tests__/bootstrap-install-resolve-npm.test.ts +72 -0
- package/packages/shared/src/__tests__/browser-protocol-types.test.ts +47 -1
- package/packages/shared/src/__tests__/changelog-types.test.ts +78 -0
- package/packages/shared/src/__tests__/config.test.ts +48 -0
- package/packages/shared/src/__tests__/dashboard-starter.test.ts +40 -0
- package/packages/shared/src/__tests__/detached-spawn.test.ts +24 -0
- package/packages/shared/src/__tests__/doctor-core.test.ts +134 -0
- package/packages/shared/src/__tests__/doctor-fault-tolerance.test.ts +218 -0
- package/packages/shared/src/__tests__/doctor-format.test.ts +121 -0
- package/packages/shared/src/__tests__/install-managed-node-bootstrap-order.test.ts +68 -0
- package/packages/shared/src/__tests__/install-managed-node.test.ts +192 -0
- package/packages/shared/src/__tests__/installable-list.test.ts +130 -0
- package/packages/shared/src/__tests__/managed-node-path.test.ts +122 -0
- package/packages/shared/src/__tests__/managed-runtime-strategy.test.ts +74 -0
- package/packages/shared/src/__tests__/no-installable-list-in-bridge.test.ts +52 -0
- package/packages/shared/src/__tests__/no-raw-openspec-status-in-skills.test.ts +6 -1
- package/packages/shared/src/__tests__/node-spawn-jiti-contract.test.ts +56 -20
- package/packages/shared/src/__tests__/resolve-jiti.test.ts +140 -9
- package/packages/shared/src/__tests__/skill-block-parser.test.ts +153 -0
- package/packages/shared/src/__tests__/tool-registry-definitions.test.ts +1 -1
- package/packages/shared/src/bootstrap-install.ts +197 -3
- package/packages/shared/src/browser-protocol.ts +155 -1
- package/packages/shared/src/changelog-types.ts +111 -0
- package/packages/shared/src/config.ts +15 -0
- package/packages/shared/src/dashboard-starter.ts +33 -0
- package/packages/shared/src/doctor-core.ts +821 -0
- package/packages/shared/src/index.ts +9 -0
- package/packages/shared/src/installable-list.ts +152 -0
- package/packages/shared/src/launch-source-flag.ts +14 -0
- package/packages/shared/src/launch-source-types.ts +18 -0
- package/packages/shared/src/openspec-activity-detector.ts +25 -7
- package/packages/shared/src/platform/detached-spawn.ts +13 -2
- package/packages/shared/src/platform/managed-node-path.ts +77 -0
- package/packages/shared/src/platform/node-spawn.ts +29 -21
- package/packages/shared/src/protocol.ts +54 -2
- package/packages/shared/src/resolve-jiti.ts +62 -9
- package/packages/shared/src/rest-api.ts +4 -0
- package/packages/shared/src/skill-block-parser.ts +115 -0
- package/packages/shared/src/tool-registry/__tests__/managed-runtime-strategy.test.ts +166 -0
- package/packages/shared/src/tool-registry/definitions.ts +33 -8
- package/packages/shared/src/tool-registry/strategies.ts +42 -0
- package/packages/shared/src/types.ts +64 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mergeInstallableList, readInstallableList, writeInstallableList } from "../installable-list.js";
|
|
3
|
+
import type { InstallableList } from "../installable-list.js";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import os from "node:os";
|
|
7
|
+
|
|
8
|
+
// ── mergeInstallableList tests ──────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
describe("mergeInstallableList", () => {
|
|
11
|
+
it("keep-user-pin: user version wins when it differs from bundled", () => {
|
|
12
|
+
const existing: InstallableList = {
|
|
13
|
+
version: "1",
|
|
14
|
+
packages: [{ name: "tsx", version: "^4.0.0", required: true, kind: "npm" }],
|
|
15
|
+
};
|
|
16
|
+
const bundled: InstallableList = {
|
|
17
|
+
version: "2",
|
|
18
|
+
packages: [{ name: "tsx", version: "^5.0.0", required: true, kind: "npm" }],
|
|
19
|
+
};
|
|
20
|
+
const { list, warnings } = mergeInstallableList(existing, bundled);
|
|
21
|
+
const found = list.packages.find((p) => p.name === "tsx");
|
|
22
|
+
expect(found?.version).toBe("^4.0.0");
|
|
23
|
+
expect(warnings.length).toBeGreaterThan(0);
|
|
24
|
+
expect(warnings[0]).toContain("tsx");
|
|
25
|
+
expect(warnings[0]).toContain("^4.0.0");
|
|
26
|
+
expect(warnings[0]).toContain("^5.0.0");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("drop-pin-warn: package in existing but not in bundled gets deprecated=true + warning", () => {
|
|
30
|
+
const existing: InstallableList = {
|
|
31
|
+
version: "1",
|
|
32
|
+
packages: [{ name: "old-tool", version: "^1.0.0", required: false, kind: "npm" }],
|
|
33
|
+
};
|
|
34
|
+
const bundled: InstallableList = { version: "2", packages: [] };
|
|
35
|
+
const { list, warnings } = mergeInstallableList(existing, bundled);
|
|
36
|
+
const found = list.packages.find((p) => p.name === "old-tool");
|
|
37
|
+
expect(found?.deprecated).toBe(true);
|
|
38
|
+
expect(warnings.length).toBeGreaterThan(0);
|
|
39
|
+
expect(warnings[0]).toContain("old-tool");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("add-new-required: new required package from bundled is added as-is", () => {
|
|
43
|
+
const existing: InstallableList = { version: "1", packages: [] };
|
|
44
|
+
const bundled: InstallableList = {
|
|
45
|
+
version: "2",
|
|
46
|
+
packages: [{ name: "pi", version: "*", required: true, kind: "npm" }],
|
|
47
|
+
};
|
|
48
|
+
const { list, warnings } = mergeInstallableList(existing, bundled);
|
|
49
|
+
const found = list.packages.find((p) => p.name === "pi");
|
|
50
|
+
expect(found).toBeDefined();
|
|
51
|
+
expect(found?.required).toBe(true);
|
|
52
|
+
expect(found?.defaultOff).toBeFalsy();
|
|
53
|
+
expect(warnings).toHaveLength(0);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("add-new-optional: new optional package from bundled is added with defaultOff=true", () => {
|
|
57
|
+
const existing: InstallableList = { version: "1", packages: [] };
|
|
58
|
+
const bundled: InstallableList = {
|
|
59
|
+
version: "2",
|
|
60
|
+
packages: [{ name: "openspec", version: "*", required: false, kind: "npm" }],
|
|
61
|
+
};
|
|
62
|
+
const { list, warnings } = mergeInstallableList(existing, bundled);
|
|
63
|
+
const found = list.packages.find((p) => p.name === "openspec");
|
|
64
|
+
expect(found).toBeDefined();
|
|
65
|
+
expect(found?.defaultOff).toBe(true);
|
|
66
|
+
expect(warnings).toHaveLength(0);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("version marker in result comes from bundled", () => {
|
|
70
|
+
const existing: InstallableList = { version: "1", packages: [] };
|
|
71
|
+
const bundled: InstallableList = { version: "42", packages: [] };
|
|
72
|
+
const { list } = mergeInstallableList(existing, bundled);
|
|
73
|
+
expect(list.version).toBe("42");
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// ── readInstallableList tests ───────────────────────────────────────────────
|
|
78
|
+
// Use a real temp directory (HOME is already ephemeral in the test runner).
|
|
79
|
+
|
|
80
|
+
describe("readInstallableList", () => {
|
|
81
|
+
let tmpDir: string;
|
|
82
|
+
|
|
83
|
+
beforeEach(() => {
|
|
84
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "installable-test-"));
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
afterEach(() => {
|
|
88
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
89
|
+
vi.restoreAllMocks();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("returns null when file is absent", async () => {
|
|
93
|
+
const result = await readInstallableList(tmpDir);
|
|
94
|
+
expect(result).toBeNull();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("drops entries with invalid kind and warns", async () => {
|
|
98
|
+
const list: InstallableList = {
|
|
99
|
+
version: "1",
|
|
100
|
+
packages: [
|
|
101
|
+
{ name: "good-pkg", version: "*", required: true, kind: "npm" },
|
|
102
|
+
{ name: "bad-pkg", version: "*", required: true, kind: "unknown-kind" as any },
|
|
103
|
+
],
|
|
104
|
+
};
|
|
105
|
+
// Write via writeInstallableList (bypasses the drop-invalid-kind guard).
|
|
106
|
+
const filePath = path.join(tmpDir, "installable.json");
|
|
107
|
+
fs.writeFileSync(filePath, JSON.stringify(list), "utf-8");
|
|
108
|
+
|
|
109
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
110
|
+
const result = await readInstallableList(tmpDir);
|
|
111
|
+
|
|
112
|
+
expect(result).not.toBeNull();
|
|
113
|
+
expect(result!.packages.map((p) => p.name)).toEqual(["good-pkg"]);
|
|
114
|
+
expect(warnSpy).toHaveBeenCalledOnce();
|
|
115
|
+
expect(warnSpy.mock.calls[0]![0]).toContain("bad-pkg");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("reads a valid file and returns the list", async () => {
|
|
119
|
+
const list: InstallableList = {
|
|
120
|
+
version: "3",
|
|
121
|
+
packages: [{ name: "tsx", version: "^5.0.0", required: true, kind: "npm" }],
|
|
122
|
+
};
|
|
123
|
+
await writeInstallableList(list, tmpDir);
|
|
124
|
+
const result = await readInstallableList(tmpDir);
|
|
125
|
+
expect(result).not.toBeNull();
|
|
126
|
+
expect(result!.version).toBe("3");
|
|
127
|
+
expect(result!.packages).toHaveLength(1);
|
|
128
|
+
expect(result!.packages[0]!.name).toBe("tsx");
|
|
129
|
+
});
|
|
130
|
+
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for `prependManagedNodeToPath` and friends.
|
|
3
|
+
*
|
|
4
|
+
* Real tmp HOME (per setup-home tripwire) so `getManagedDir()` resolves
|
|
5
|
+
* under the tmp tree. We create / remove the managed Node binary on
|
|
6
|
+
* disk to flip the present/absent branches.
|
|
7
|
+
*
|
|
8
|
+
* See change: embed-managed-node-runtime (task 2.5).
|
|
9
|
+
*/
|
|
10
|
+
import fs from "node:fs";
|
|
11
|
+
import os from "node:os";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
14
|
+
import {
|
|
15
|
+
getManagedNodeBinDir,
|
|
16
|
+
getManagedNodeBinary,
|
|
17
|
+
isManagedNodePresent,
|
|
18
|
+
prependManagedNodeToPath,
|
|
19
|
+
} from "../platform/managed-node-path.js";
|
|
20
|
+
import { getManagedDir } from "../managed-paths.js";
|
|
21
|
+
|
|
22
|
+
const isWin = process.platform === "win32";
|
|
23
|
+
|
|
24
|
+
describe("getManagedNodeBinDir", () => {
|
|
25
|
+
it("returns <managedDir>/node on Windows", () => {
|
|
26
|
+
const env = { homedir: "/fake/home" };
|
|
27
|
+
expect(getManagedNodeBinDir(env, "win32")).toBe(
|
|
28
|
+
path.join("/fake/home", ".pi-dashboard", "node"),
|
|
29
|
+
);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("returns <managedDir>/node/bin on Unix", () => {
|
|
33
|
+
const env = { homedir: "/fake/home" };
|
|
34
|
+
expect(getManagedNodeBinDir(env, "linux")).toBe(
|
|
35
|
+
path.join("/fake/home", ".pi-dashboard", "node", "bin"),
|
|
36
|
+
);
|
|
37
|
+
expect(getManagedNodeBinDir(env, "darwin")).toBe(
|
|
38
|
+
path.join("/fake/home", ".pi-dashboard", "node", "bin"),
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("getManagedNodeBinary", () => {
|
|
44
|
+
it("uses node.exe on Windows", () => {
|
|
45
|
+
expect(getManagedNodeBinary({ homedir: "/h" }, "win32")).toBe(
|
|
46
|
+
path.join("/h", ".pi-dashboard", "node", "node.exe"),
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("uses bin/node on Unix", () => {
|
|
51
|
+
expect(getManagedNodeBinary({ homedir: "/h" }, "linux")).toBe(
|
|
52
|
+
path.join("/h", ".pi-dashboard", "node", "bin", "node"),
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe("prependManagedNodeToPath", () => {
|
|
58
|
+
let tmpHome: string;
|
|
59
|
+
let origHome: string | undefined;
|
|
60
|
+
|
|
61
|
+
beforeEach(() => {
|
|
62
|
+
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "managed-node-path-"));
|
|
63
|
+
origHome = process.env.HOME;
|
|
64
|
+
process.env.HOME = tmpHome;
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
afterEach(() => {
|
|
68
|
+
if (origHome === undefined) delete process.env.HOME;
|
|
69
|
+
else process.env.HOME = origHome;
|
|
70
|
+
fs.rmSync(tmpHome, { recursive: true, force: true });
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
function installFakeManagedNode(): string {
|
|
74
|
+
// os.homedir() reads HOME on Unix and USERPROFILE on Windows. The
|
|
75
|
+
// setup-home tripwire only sets HOME, so on Windows tests we set
|
|
76
|
+
// USERPROFILE here too.
|
|
77
|
+
if (isWin) process.env.USERPROFILE = tmpHome;
|
|
78
|
+
const dir = getManagedNodeBinDir();
|
|
79
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
80
|
+
const binary = getManagedNodeBinary();
|
|
81
|
+
fs.writeFileSync(binary, "fake");
|
|
82
|
+
return dir;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
it("no-op (returns clone) when managed runtime is absent", () => {
|
|
86
|
+
const base = { PATH: "/usr/bin:/bin", FOO: "bar" };
|
|
87
|
+
const out = prependManagedNodeToPath(base);
|
|
88
|
+
expect(out).not.toBe(base);
|
|
89
|
+
expect(out.PATH).toBe("/usr/bin:/bin");
|
|
90
|
+
expect(out.FOO).toBe("bar");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("does not mutate process.env", () => {
|
|
94
|
+
const beforePath = process.env.PATH;
|
|
95
|
+
prependManagedNodeToPath();
|
|
96
|
+
expect(process.env.PATH).toBe(beforePath);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("prepends managed Node bin dir when present", () => {
|
|
100
|
+
const dir = installFakeManagedNode();
|
|
101
|
+
const base = { PATH: "/usr/bin:/bin", X: "y" };
|
|
102
|
+
const out = prependManagedNodeToPath(base);
|
|
103
|
+
expect(out).not.toBe(base);
|
|
104
|
+
expect(out.PATH?.startsWith(dir)).toBe(true);
|
|
105
|
+
expect(out.PATH).toBe(`${dir}${path.delimiter}/usr/bin:/bin`);
|
|
106
|
+
expect(out.X).toBe("y");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("does not double-prepend when dir already at head", () => {
|
|
110
|
+
const dir = installFakeManagedNode();
|
|
111
|
+
const initial = `${dir}${path.delimiter}/usr/bin`;
|
|
112
|
+
const base = { PATH: initial };
|
|
113
|
+
const out = prependManagedNodeToPath(base);
|
|
114
|
+
expect(out.PATH).toBe(initial);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("isManagedNodePresent flips with the binary on disk", () => {
|
|
118
|
+
expect(isManagedNodePresent()).toBe(false);
|
|
119
|
+
installFakeManagedNode();
|
|
120
|
+
expect(isManagedNodePresent()).toBe(true);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for `managedRuntimeStrategy` in isolation.
|
|
3
|
+
*
|
|
4
|
+
* Strategy receives a `StrategyCtx` (overrides + platform + env). We
|
|
5
|
+
* inject a fake `exists` so no real filesystem is touched.
|
|
6
|
+
*
|
|
7
|
+
* See change: embed-managed-node-runtime (task 2.6).
|
|
8
|
+
*/
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { describe, expect, it } from "vitest";
|
|
11
|
+
import { managedRuntimeStrategy } from "../tool-registry/strategies.js";
|
|
12
|
+
import type { StrategyCtx } from "../tool-registry/types.js";
|
|
13
|
+
|
|
14
|
+
function ctx(opts: Partial<StrategyCtx> = {}): StrategyCtx {
|
|
15
|
+
return {
|
|
16
|
+
overrides: {},
|
|
17
|
+
platform: opts.platform ?? "linux",
|
|
18
|
+
env: opts.env ?? { homedir: "/fake/home" },
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe("managedRuntimeStrategy", () => {
|
|
23
|
+
it("Unix node: returns <managedDir>/node/bin/node when present", () => {
|
|
24
|
+
const expected = path.join("/fake/home", ".pi-dashboard", "node", "bin", "node");
|
|
25
|
+
const s = managedRuntimeStrategy("node", { exists: (p) => p === expected });
|
|
26
|
+
const r = s.run(ctx({ platform: "linux" }));
|
|
27
|
+
expect(r.ok).toBe(true);
|
|
28
|
+
if (r.ok) expect(r.path).toBe(expected);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("Unix npm: returns <managedDir>/node/bin/npm when present", () => {
|
|
32
|
+
const expected = path.join("/fake/home", ".pi-dashboard", "node", "bin", "npm");
|
|
33
|
+
const s = managedRuntimeStrategy("npm", { exists: (p) => p === expected });
|
|
34
|
+
const r = s.run(ctx({ platform: "linux" }));
|
|
35
|
+
expect(r.ok).toBe(true);
|
|
36
|
+
if (r.ok) expect(r.path).toBe(expected);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("Windows node: returns <managedDir>/node/node.exe when present", () => {
|
|
40
|
+
const expected = path.join("/fake/home", ".pi-dashboard", "node", "node.exe");
|
|
41
|
+
const s = managedRuntimeStrategy("node", { exists: (p) => p === expected });
|
|
42
|
+
const r = s.run(ctx({ platform: "win32" }));
|
|
43
|
+
expect(r.ok).toBe(true);
|
|
44
|
+
if (r.ok) expect(r.path).toBe(expected);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("Windows npm: returns <managedDir>/node/npm.cmd when present", () => {
|
|
48
|
+
const expected = path.join("/fake/home", ".pi-dashboard", "node", "npm.cmd");
|
|
49
|
+
const s = managedRuntimeStrategy("npm", { exists: (p) => p === expected });
|
|
50
|
+
const r = s.run(ctx({ platform: "win32" }));
|
|
51
|
+
expect(r.ok).toBe(true);
|
|
52
|
+
if (r.ok) expect(r.path).toBe(expected);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("Windows npx: returns <managedDir>/node/npx.cmd when present", () => {
|
|
56
|
+
const expected = path.join("/fake/home", ".pi-dashboard", "node", "npx.cmd");
|
|
57
|
+
const s = managedRuntimeStrategy("npx", { exists: (p) => p === expected });
|
|
58
|
+
const r = s.run(ctx({ platform: "win32" }));
|
|
59
|
+
expect(r.ok).toBe(true);
|
|
60
|
+
if (r.ok) expect(r.path).toBe(expected);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("returns failure with reason when binary absent", () => {
|
|
64
|
+
const s = managedRuntimeStrategy("node", { exists: () => false });
|
|
65
|
+
const r = s.run(ctx({ platform: "linux" }));
|
|
66
|
+
expect(r.ok).toBe(false);
|
|
67
|
+
if (!r.ok) expect(r.reason).toMatch(/missing:/);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("strategy is named 'managed' (classifies as managed source)", () => {
|
|
71
|
+
const s = managedRuntimeStrategy("node");
|
|
72
|
+
expect(s.name).toBe("managed");
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract test: the bridge auto-spawn code path (server-launcher.ts) must
|
|
3
|
+
* NOT import from `installable-list`. Only Electron seeds `installable.json`
|
|
4
|
+
* on first run; Bridge and Standalone starters must not produce or consume
|
|
5
|
+
* that file.
|
|
6
|
+
*
|
|
7
|
+
* This is a static source scan — no runtime execution. If this test fails,
|
|
8
|
+
* a dependency on installable-list was accidentally added to the bridge
|
|
9
|
+
* launcher which would break the "file-absent is a no-op" contract on
|
|
10
|
+
* Bridge/Standalone bootstraps.
|
|
11
|
+
*
|
|
12
|
+
* See change: simplify-electron-bootstrap-derived-state (task 5.7).
|
|
13
|
+
*/
|
|
14
|
+
import { describe, expect, it } from "vitest";
|
|
15
|
+
import fs from "node:fs";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import url from "node:url";
|
|
18
|
+
|
|
19
|
+
const here = path.dirname(url.fileURLToPath(import.meta.url));
|
|
20
|
+
const repoRoot = path.resolve(here, "..", "..", "..", "..");
|
|
21
|
+
|
|
22
|
+
/** Files that form the bridge auto-spawn contract. */
|
|
23
|
+
const BRIDGE_SPAWN_FILES: readonly string[] = [
|
|
24
|
+
"packages/extension/src/server-launcher.ts",
|
|
25
|
+
"packages/extension/src/server-auto-start.ts",
|
|
26
|
+
"packages/extension/src/connection.ts",
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
describe("bridge auto-spawn does not reference installable-list", () => {
|
|
30
|
+
for (const rel of BRIDGE_SPAWN_FILES) {
|
|
31
|
+
it(`${rel} does not import from installable-list`, () => {
|
|
32
|
+
const file = path.resolve(repoRoot, rel);
|
|
33
|
+
if (!fs.existsSync(file)) {
|
|
34
|
+
// File absent (optional extension file) — contract trivially satisfied.
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const source = fs.readFileSync(file, "utf-8");
|
|
38
|
+
|
|
39
|
+
// Strip line comments before checking so a commented-out import
|
|
40
|
+
// does not trigger a false positive.
|
|
41
|
+
const stripped = source
|
|
42
|
+
.replace(/\/\/[^\n]*/g, "")
|
|
43
|
+
.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
44
|
+
|
|
45
|
+
expect(
|
|
46
|
+
stripped,
|
|
47
|
+
`${rel} must not import from "installable-list" — only Electron seeds installable.json. ` +
|
|
48
|
+
`Bridge/Standalone starters must not produce or consume that file.`,
|
|
49
|
+
).not.toMatch(/installable-list/);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
});
|
|
@@ -38,7 +38,12 @@ const GOVERNED_SKILLS = [
|
|
|
38
38
|
];
|
|
39
39
|
|
|
40
40
|
describe("OpenSpec workflow skills must use effective-status.sh", () => {
|
|
41
|
-
|
|
41
|
+
// SKIPPED: the .pi/skills/openspec-*/SKILL.md files this lint targets are
|
|
42
|
+
// vendored from upstream and gitignored (see .pi/.gitignore) — they get
|
|
43
|
+
// overwritten on every install, so we can't enforce dashboard-local
|
|
44
|
+
// semantics on them from here. Re-enable if/when those skills are
|
|
45
|
+
// forked into the repo, or move the wrapper-enforcement upstream.
|
|
46
|
+
it.skip("no raw `openspec status --json` outside the wrapper script", async () => {
|
|
42
47
|
const here = path.dirname(url.fileURLToPath(import.meta.url));
|
|
43
48
|
const repoRoot = path.resolve(here, "..", "..", "..", "..");
|
|
44
49
|
const skillsRoot = path.resolve(repoRoot, ".pi", "skills");
|
|
@@ -1,22 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Pin
|
|
2
|
+
* Pin the jiti version contract for `shouldUrlWrapEntry()`.
|
|
3
3
|
*
|
|
4
4
|
* The Windows-non-tsx arm in `platform/node-spawn.ts::shouldUrlWrapEntry`
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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.
|
|
8
12
|
*
|
|
9
13
|
* This test ensures:
|
|
10
14
|
* 1. The offline-cacache pin in `packages/electron/offline-packages.json`
|
|
11
|
-
* stays
|
|
12
|
-
*
|
|
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:
|
|
13
18
|
* - re-verify the contract on Windows
|
|
14
19
|
* - add a per-jiti-version branch
|
|
15
20
|
* - switch the bundled loader to tsx
|
|
16
21
|
* 2. The `shouldUrlWrapEntry` header comment documents the contract
|
|
17
22
|
* so future contributors discover the constraint at the call site.
|
|
18
23
|
*
|
|
19
|
-
* See
|
|
24
|
+
* See changes: fix-electron-windows-installer-and-server-bootstrap (Defect 2),
|
|
25
|
+
* migrate-pi-fork-to-earendil (E.6).
|
|
20
26
|
*/
|
|
21
27
|
import { describe, it, expect } from "vitest";
|
|
22
28
|
import fs from "node:fs";
|
|
@@ -40,34 +46,54 @@ const NODE_SPAWN_PATH = path.join(
|
|
|
40
46
|
"node-spawn.ts",
|
|
41
47
|
);
|
|
42
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
|
+
|
|
43
55
|
describe("jiti version contract for shouldUrlWrapEntry", () => {
|
|
44
|
-
it("offline-packages.json pins
|
|
56
|
+
it("offline-packages.json pins pi-coding-agent at a verified version under a supported fork", () => {
|
|
45
57
|
const raw = fs.readFileSync(OFFLINE_PACKAGES_PATH, "utf8");
|
|
46
58
|
const manifest = JSON.parse(raw) as {
|
|
47
59
|
packages: { name: string; version: string }[];
|
|
48
60
|
};
|
|
49
|
-
|
|
50
|
-
|
|
61
|
+
|
|
62
|
+
const supportedNames = VERIFIED_PI_PINS.map((p) => p.name);
|
|
63
|
+
const piEntry = manifest.packages.find((p) =>
|
|
64
|
+
supportedNames.includes(p.name),
|
|
51
65
|
);
|
|
52
66
|
if (!piEntry) {
|
|
53
67
|
throw new Error(
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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).`,
|
|
57
73
|
);
|
|
58
74
|
}
|
|
59
|
-
|
|
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(", ");
|
|
60
83
|
throw new Error(
|
|
61
|
-
`pi-coding-agent pinned at ${piEntry.version}, but ` +
|
|
62
|
-
`shouldUrlWrapEntry()'s Windows-non-tsx arm only supports
|
|
84
|
+
`pi-coding-agent pinned at ${piEntry.name}@${piEntry.version}, but ` +
|
|
85
|
+
`shouldUrlWrapEntry()'s Windows-non-tsx arm only supports verified pins: ` +
|
|
86
|
+
`${allowedRanges}. ` +
|
|
63
87
|
`Newer jiti versions (e.g. 2.6.5 in pi 0.71.x) misnormalize ` +
|
|
64
88
|
`file:/// URL entries on Windows. Either re-verify the contract, ` +
|
|
65
89
|
`add a per-jiti-version branch in shouldUrlWrapEntry(), or switch ` +
|
|
66
|
-
`the bundled loader to tsx. See
|
|
67
|
-
`fix-electron-windows-installer-and-server-bootstrap (Defect 2)
|
|
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).`,
|
|
68
93
|
);
|
|
69
94
|
}
|
|
70
|
-
|
|
95
|
+
|
|
96
|
+
expect(piEntry.version.startsWith(verifiedPin.versionPrefix)).toBe(true);
|
|
71
97
|
});
|
|
72
98
|
|
|
73
99
|
it("node-spawn.ts source contains the documented JITI VERSION CONTRACT block", () => {
|
|
@@ -75,7 +101,17 @@ describe("jiti version contract for shouldUrlWrapEntry", () => {
|
|
|
75
101
|
|
|
76
102
|
// Contract block markers
|
|
77
103
|
expect(source).toContain("JITI VERSION CONTRACT");
|
|
78
|
-
|
|
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
|
+
}
|
|
79
115
|
|
|
80
116
|
// Version drift markers (at least one of these identifies the broken jiti)
|
|
81
117
|
const hasVersionDriftMarker =
|