@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,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration: verify the kill-fork-kills-parent bug stays fixed.
|
|
3
|
+
*
|
|
4
|
+
* Simulates the race window where parent + fork are both registered in the
|
|
5
|
+
* same cwd and bridges connect in arbitrary order. Asserts the registry
|
|
6
|
+
* resolves each sessionId to its OWN PID via the three-tier link.
|
|
7
|
+
*
|
|
8
|
+
* See change: spawn-correlation-token.
|
|
9
|
+
*/
|
|
10
|
+
import { describe, it, expect } from "vitest";
|
|
11
|
+
import { EventEmitter } from "node:events";
|
|
12
|
+
import { mkdtempSync } from "node:fs";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { tmpdir } from "node:os";
|
|
15
|
+
import { createHeadlessPidRegistry } from "../headless-pid-registry.js";
|
|
16
|
+
import { createPendingForkRegistry } from "../pending-fork-registry.js";
|
|
17
|
+
import { mintSpawnToken } from "../spawn-token.js";
|
|
18
|
+
|
|
19
|
+
function mockProc() {
|
|
20
|
+
return new EventEmitter() as any;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function tmpPidFile() {
|
|
24
|
+
return join(mkdtempSync(join(tmpdir(), "spawn-corr-")), "pids.json");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe("spawn-correlation-token: kill-fork-doesn't-kill-parent regression", () => {
|
|
28
|
+
it("two same-cwd spawns: each session resolves to its OWN pid via token", () => {
|
|
29
|
+
// Setup: simulate dashboard spawning parent, then fork, in the same cwd.
|
|
30
|
+
// Each spawn mints a token; registry stores entries by pid + token.
|
|
31
|
+
const registry = createHeadlessPidRegistry({ pidFilePath: tmpPidFile() });
|
|
32
|
+
const tokenParent = mintSpawnToken();
|
|
33
|
+
const tokenFork = mintSpawnToken();
|
|
34
|
+
expect(tokenParent).not.toBe(tokenFork);
|
|
35
|
+
|
|
36
|
+
registry.register(1000, "/proj", mockProc(), tokenParent);
|
|
37
|
+
registry.register(1234, "/proj", mockProc(), tokenFork);
|
|
38
|
+
|
|
39
|
+
// Bridge connect order is reversed (fork first, parent second) — the
|
|
40
|
+
// worst-case race that produced the original bug.
|
|
41
|
+
expect(registry.linkByToken(tokenFork, "S_fork")).toBe(true);
|
|
42
|
+
expect(registry.linkByToken(tokenParent, "S_parent")).toBe(true);
|
|
43
|
+
|
|
44
|
+
// Critical: each session resolves to its OWN pid. Pre-fix this would
|
|
45
|
+
// have given S_fork → 1000 (parent) and S_parent → 1234 (fork) due to
|
|
46
|
+
// cwd-FIFO ordering.
|
|
47
|
+
expect(registry.getPid("S_fork")).toBe(1234);
|
|
48
|
+
expect(registry.getPid("S_parent")).toBe(1000);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("legacy bridge fallback: linkByPid is exact even without tokens", () => {
|
|
52
|
+
const registry = createHeadlessPidRegistry({ pidFilePath: tmpPidFile() });
|
|
53
|
+
// Legacy bridges don't send spawnToken; only pid.
|
|
54
|
+
registry.register(1000, "/proj", mockProc()); // no token
|
|
55
|
+
registry.register(1234, "/proj", mockProc()); // no token
|
|
56
|
+
|
|
57
|
+
// Race-order register messages, but pid-link is direct lookup.
|
|
58
|
+
expect(registry.linkByPid("S_fork", 1234)).toBe(true);
|
|
59
|
+
expect(registry.linkByPid("S_parent", 1000)).toBe(true);
|
|
60
|
+
|
|
61
|
+
expect(registry.getPid("S_fork")).toBe(1234);
|
|
62
|
+
expect(registry.getPid("S_parent")).toBe(1000);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("fork registry: per-token keying separates two forks in same cwd", () => {
|
|
66
|
+
const forkRegistry = createPendingForkRegistry();
|
|
67
|
+
const tokenA = mintSpawnToken();
|
|
68
|
+
const tokenB = mintSpawnToken();
|
|
69
|
+
|
|
70
|
+
// Two forks issued in the same cwd, each with its own token. Pre-fix
|
|
71
|
+
// (cwd-keyed registry) the second recordFork would overwrite the first.
|
|
72
|
+
forkRegistry.recordFork(tokenA, "parent-A");
|
|
73
|
+
forkRegistry.recordFork(tokenB, "parent-B");
|
|
74
|
+
|
|
75
|
+
// Bridge connect order arbitrary; each token resolves to its OWN parent.
|
|
76
|
+
expect(forkRegistry.consumeFork(tokenB)).toBe("parent-B");
|
|
77
|
+
expect(forkRegistry.consumeFork(tokenA)).toBe("parent-A");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("stale token (server-restart-mid-spawn) degrades to lower-tier match", () => {
|
|
81
|
+
const registry = createHeadlessPidRegistry({ pidFilePath: tmpPidFile() });
|
|
82
|
+
// Bridge holds an env-var token from before server restart; the new
|
|
83
|
+
// server has no entry for that token. linkByToken returns false; the
|
|
84
|
+
// event-wiring caller falls through to linkByPid.
|
|
85
|
+
registry.register(1000, "/proj", mockProc()); // post-restart entry, no token
|
|
86
|
+
expect(registry.linkByToken("stale_tok_from_old_server", "S")).toBe(false);
|
|
87
|
+
// Caller falls back:
|
|
88
|
+
expect(registry.linkByPid("S", 1000)).toBe(true);
|
|
89
|
+
expect(registry.getPid("S")).toBe(1000);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for spawn-failure-log.ts.
|
|
3
|
+
* See change: spawn-failure-diagnostics.
|
|
4
|
+
*/
|
|
5
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
6
|
+
import { mkdtempSync, rmSync, writeFileSync, mkdirSync, statSync, existsSync } from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import os from "node:os";
|
|
9
|
+
|
|
10
|
+
import { appendSpawnFailure, readSpawnFailures, _setLogDirForTests } from "../spawn-failure-log.js";
|
|
11
|
+
import type { SpawnFailureEntry } from "../spawn-failure-log.js";
|
|
12
|
+
|
|
13
|
+
function makeEntry(overrides: Partial<SpawnFailureEntry> = {}): SpawnFailureEntry {
|
|
14
|
+
return {
|
|
15
|
+
ts: new Date().toISOString(),
|
|
16
|
+
cwd: "/tmp/test",
|
|
17
|
+
strategy: "headless",
|
|
18
|
+
code: "PI_CRASHED",
|
|
19
|
+
message: "Pi exited immediately",
|
|
20
|
+
...overrides,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe("spawn-failure-log", () => {
|
|
25
|
+
let tmpDir: string;
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
tmpDir = mkdtempSync(path.join(os.tmpdir(), "sfl-test-"));
|
|
29
|
+
_setLogDirForTests(tmpDir);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
_setLogDirForTests(null);
|
|
34
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("appends an entry and reads it back", () => {
|
|
38
|
+
const entry = makeEntry();
|
|
39
|
+
appendSpawnFailure(entry);
|
|
40
|
+
const entries = readSpawnFailures(10);
|
|
41
|
+
expect(entries).toHaveLength(1);
|
|
42
|
+
expect(entries[0]!.cwd).toBe(entry.cwd);
|
|
43
|
+
expect(entries[0]!.code).toBe(entry.code);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("returns [] when no log file exists", () => {
|
|
47
|
+
expect(readSpawnFailures(10)).toEqual([]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("returns [] for limit 0", () => {
|
|
51
|
+
appendSpawnFailure(makeEntry());
|
|
52
|
+
expect(readSpawnFailures(0)).toEqual([]);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("returns [] for negative limit", () => {
|
|
56
|
+
appendSpawnFailure(makeEntry());
|
|
57
|
+
expect(readSpawnFailures(-5)).toEqual([]);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("returns last N entries when more than limit exist", () => {
|
|
61
|
+
for (let i = 0; i < 5; i++) {
|
|
62
|
+
appendSpawnFailure(makeEntry({ message: `msg ${i}` }));
|
|
63
|
+
}
|
|
64
|
+
const result = readSpawnFailures(3);
|
|
65
|
+
expect(result).toHaveLength(3);
|
|
66
|
+
expect(result[2]!.message).toBe("msg 4");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("skips malformed lines", () => {
|
|
70
|
+
const logFile = path.join(tmpDir, "spawn-failures.log");
|
|
71
|
+
writeFileSync(logFile, `not-json\n${JSON.stringify(makeEntry({ message: "good" }))}\n`);
|
|
72
|
+
const entries = readSpawnFailures(10);
|
|
73
|
+
expect(entries).toHaveLength(1);
|
|
74
|
+
expect(entries[0]!.message).toBe("good");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("never throws when append fails", () => {
|
|
78
|
+
const logFile = path.join(tmpDir, "spawn-failures.log");
|
|
79
|
+
// Create as dir to force write failure.
|
|
80
|
+
mkdirSync(logFile);
|
|
81
|
+
expect(() => appendSpawnFailure(makeEntry())).not.toThrow();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("rotates when file exceeds 10 MB", () => {
|
|
85
|
+
const logFile = path.join(tmpDir, "spawn-failures.log");
|
|
86
|
+
const logFile1 = path.join(tmpDir, "spawn-failures.log.1");
|
|
87
|
+
|
|
88
|
+
// Write a file >10MB.
|
|
89
|
+
const bigContent = "x".repeat(10 * 1024 * 1024 + 1);
|
|
90
|
+
writeFileSync(logFile, bigContent);
|
|
91
|
+
|
|
92
|
+
appendSpawnFailure(makeEntry({ message: "after rotation" }));
|
|
93
|
+
|
|
94
|
+
expect(existsSync(logFile1)).toBe(true);
|
|
95
|
+
const entries = readSpawnFailures(10);
|
|
96
|
+
expect(entries.some((e) => e.message === "after rotation")).toBe(true);
|
|
97
|
+
expect(statSync(logFile).size).toBeLessThan(bigContent.length);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("reads from both .log.1 and .log (older first)", () => {
|
|
101
|
+
const logFile = path.join(tmpDir, "spawn-failures.log");
|
|
102
|
+
const logFile1 = path.join(tmpDir, "spawn-failures.log.1");
|
|
103
|
+
|
|
104
|
+
writeFileSync(logFile1, JSON.stringify(makeEntry({ message: "old" })) + "\n");
|
|
105
|
+
writeFileSync(logFile, JSON.stringify(makeEntry({ message: "new" })) + "\n");
|
|
106
|
+
|
|
107
|
+
const entries = readSpawnFailures(10);
|
|
108
|
+
expect(entries).toHaveLength(2);
|
|
109
|
+
expect(entries[0]!.message).toBe("old");
|
|
110
|
+
expect(entries[1]!.message).toBe("new");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("clamps limit at 500", () => {
|
|
114
|
+
for (let i = 0; i < 10; i++) appendSpawnFailure(makeEntry());
|
|
115
|
+
const entries = readSpawnFailures(9999);
|
|
116
|
+
expect(entries.length).toBeLessThanOrEqual(500);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for spawn-preflight.ts — pure validation, no process spawning.
|
|
3
|
+
* See change: spawn-failure-diagnostics.
|
|
4
|
+
*/
|
|
5
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
6
|
+
import { mkdirSync, mkdtempSync, writeFileSync, rmSync, chmodSync } from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import os from "node:os";
|
|
9
|
+
|
|
10
|
+
// We mock the ToolResolver so no binary lookup is attempted.
|
|
11
|
+
vi.mock("@blackbelt-technology/pi-dashboard-shared/platform/binary-lookup.js", () => ({
|
|
12
|
+
ToolResolver: function MockToolResolver() {
|
|
13
|
+
return {
|
|
14
|
+
resolvePi: vi.fn().mockReturnValue(["pi"]),
|
|
15
|
+
resolveNode: vi.fn().mockReturnValue("/usr/bin/node"),
|
|
16
|
+
};
|
|
17
|
+
},
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
import { preflightSpawn } from "../spawn-preflight.js";
|
|
21
|
+
import { ToolResolver } from "@blackbelt-technology/pi-dashboard-shared/platform/binary-lookup.js";
|
|
22
|
+
|
|
23
|
+
function makeResolver(overrides: { resolvePi?: () => string[] | null; resolveNode?: () => string | null }) {
|
|
24
|
+
return {
|
|
25
|
+
resolvePi: overrides.resolvePi ?? (() => ["pi"]),
|
|
26
|
+
resolveNode: overrides.resolveNode ?? (() => "/usr/bin/node"),
|
|
27
|
+
} as unknown as InstanceType<typeof ToolResolver>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe("preflightSpawn", () => {
|
|
31
|
+
let tmpDir: string;
|
|
32
|
+
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
tmpDir = mkdtempSync(path.join(os.tmpdir(), "preflight-test-"));
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
afterEach(() => {
|
|
38
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("returns ok when all checks pass", () => {
|
|
42
|
+
const result = preflightSpawn(tmpDir, { resolver: makeResolver({}) });
|
|
43
|
+
expect(result.ok).toBe(true);
|
|
44
|
+
expect(result.reasons).toHaveLength(0);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("returns DIR_MISSING when cwd does not exist", () => {
|
|
48
|
+
const result = preflightSpawn("/nonexistent/does/not/exist", { resolver: makeResolver({}) });
|
|
49
|
+
expect(result.ok).toBe(false);
|
|
50
|
+
expect(result.reasons.some((r) => r.code === "DIR_MISSING")).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("returns DIR_NOT_DIRECTORY when cwd is a file", () => {
|
|
54
|
+
const filePath = path.join(tmpDir, "regular-file.txt");
|
|
55
|
+
writeFileSync(filePath, "hello");
|
|
56
|
+
const result = preflightSpawn(filePath, { resolver: makeResolver({}) });
|
|
57
|
+
expect(result.ok).toBe(false);
|
|
58
|
+
expect(result.reasons.some((r) => r.code === "DIR_NOT_DIRECTORY")).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("returns PI_NOT_FOUND when pi binary unresolvable", () => {
|
|
62
|
+
const result = preflightSpawn(tmpDir, { resolver: makeResolver({ resolvePi: () => null }) });
|
|
63
|
+
expect(result.ok).toBe(false);
|
|
64
|
+
expect(result.reasons.some((r) => r.code === "PI_NOT_FOUND")).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("returns NODE_NOT_FOUND when node binary unresolvable", () => {
|
|
68
|
+
const result = preflightSpawn(tmpDir, { resolver: makeResolver({ resolveNode: () => null }) });
|
|
69
|
+
expect(result.ok).toBe(false);
|
|
70
|
+
expect(result.reasons.some((r) => r.code === "NODE_NOT_FOUND")).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("accumulates multiple reasons (no short-circuit)", () => {
|
|
74
|
+
const result = preflightSpawn("/nonexistent/does/not/exist", {
|
|
75
|
+
resolver: makeResolver({ resolvePi: () => null }),
|
|
76
|
+
});
|
|
77
|
+
expect(result.ok).toBe(false);
|
|
78
|
+
expect(result.reasons.some((r) => r.code === "DIR_MISSING")).toBe(true);
|
|
79
|
+
expect(result.reasons.some((r) => r.code === "PI_NOT_FOUND")).toBe(true);
|
|
80
|
+
expect(result.reasons.length).toBeGreaterThanOrEqual(2);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("does not call login-shell resolver (resolver.resolvePi is called, not whichViaLoginShell)", () => {
|
|
84
|
+
const mockResolver = makeResolver({});
|
|
85
|
+
const spyPi = vi.spyOn(mockResolver, "resolvePi");
|
|
86
|
+
const spyNode = vi.spyOn(mockResolver, "resolveNode");
|
|
87
|
+
preflightSpawn(tmpDir, { resolver: mockResolver });
|
|
88
|
+
expect(spyPi).toHaveBeenCalled();
|
|
89
|
+
expect(spyNode).toHaveBeenCalled();
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for SpawnRegisterWatchdog.
|
|
3
|
+
* Uses vitest fake timers. See change: spawn-failure-diagnostics.
|
|
4
|
+
*/
|
|
5
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
6
|
+
import WebSocket from "ws";
|
|
7
|
+
|
|
8
|
+
// Silence appendSpawnFailure in unit tests.
|
|
9
|
+
vi.mock("../spawn-failure-log.js", () => ({
|
|
10
|
+
appendSpawnFailure: vi.fn(),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
import { SpawnRegisterWatchdog } from "../spawn-register-watchdog.js";
|
|
14
|
+
|
|
15
|
+
function makeMockWs(readyState: number = WebSocket.OPEN): { ws: WebSocket; messages: string[] } {
|
|
16
|
+
const messages: string[] = [];
|
|
17
|
+
const ws = {
|
|
18
|
+
readyState,
|
|
19
|
+
send: vi.fn((data: string) => messages.push(data)),
|
|
20
|
+
} as unknown as WebSocket;
|
|
21
|
+
return { ws, messages };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe("SpawnRegisterWatchdog", () => {
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
vi.useFakeTimers();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
vi.useRealTimers();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("clamps timeoutMs below 5000 to 5000", () => {
|
|
34
|
+
const w = new SpawnRegisterWatchdog(1000);
|
|
35
|
+
expect(w.timeoutMs).toBe(5000);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("clamps timeoutMs above 120000 to 120000", () => {
|
|
39
|
+
const w = new SpawnRegisterWatchdog(999999);
|
|
40
|
+
expect(w.timeoutMs).toBe(120000);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("headless arm + clearByPid cancels watchdog", () => {
|
|
44
|
+
const { ws, messages } = makeMockWs();
|
|
45
|
+
const w = new SpawnRegisterWatchdog(10000);
|
|
46
|
+
w.arm({ pid: 123, cwd: "/p/x", mechanism: "headless", ws });
|
|
47
|
+
w.clearByPid(123);
|
|
48
|
+
vi.advanceTimersByTime(15000);
|
|
49
|
+
expect(messages).toHaveLength(0);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("headless arm + clearByCwd (pid mismatch) still cancels watchdog", () => {
|
|
53
|
+
// Regression: Unix headless wraps pi in `sh -c "… | pi"`, so spawnResult.pid
|
|
54
|
+
// is the sh wrapper while session_register reports pi's real pid. Watchdog
|
|
55
|
+
// must clear via cwd even when pid was indexed at arm time.
|
|
56
|
+
const { ws, messages } = makeMockWs();
|
|
57
|
+
const w = new SpawnRegisterWatchdog(10000);
|
|
58
|
+
w.arm({ pid: 51250, cwd: "/p/x", mechanism: "headless", ws });
|
|
59
|
+
w.clearByCwd("/p/x");
|
|
60
|
+
vi.advanceTimersByTime(15000);
|
|
61
|
+
expect(messages).toHaveLength(0);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("tmux arm + clearByCwd cancels watchdog", () => {
|
|
65
|
+
const { ws, messages } = makeMockWs();
|
|
66
|
+
const w = new SpawnRegisterWatchdog(10000);
|
|
67
|
+
w.arm({ cwd: "/p/x", mechanism: "tmux", ws });
|
|
68
|
+
w.clearByCwd("/p/x");
|
|
69
|
+
vi.advanceTimersByTime(15000);
|
|
70
|
+
expect(messages).toHaveLength(0);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("arm without clear fires spawn_register_timeout", () => {
|
|
74
|
+
const { ws, messages } = makeMockWs();
|
|
75
|
+
const w = new SpawnRegisterWatchdog(10000);
|
|
76
|
+
w.arm({ pid: 42, cwd: "/p/y", mechanism: "headless", ws });
|
|
77
|
+
vi.advanceTimersByTime(10001);
|
|
78
|
+
expect(messages).toHaveLength(1);
|
|
79
|
+
const msg = JSON.parse(messages[0]!);
|
|
80
|
+
expect(msg.type).toBe("spawn_register_timeout");
|
|
81
|
+
expect(msg.cwd).toBe("/p/y");
|
|
82
|
+
expect(msg.pid).toBe(42);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("tmux timeout omits pid", () => {
|
|
86
|
+
const { ws, messages } = makeMockWs();
|
|
87
|
+
const w = new SpawnRegisterWatchdog(10000);
|
|
88
|
+
w.arm({ cwd: "/p/z", mechanism: "tmux", ws });
|
|
89
|
+
vi.advanceTimersByTime(10001);
|
|
90
|
+
expect(messages).toHaveLength(1);
|
|
91
|
+
const msg = JSON.parse(messages[0]!);
|
|
92
|
+
expect(msg.pid).toBeUndefined();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("clear on unknown key is a no-op", () => {
|
|
96
|
+
const w = new SpawnRegisterWatchdog(10000);
|
|
97
|
+
expect(() => w.clearByPid(999)).not.toThrow();
|
|
98
|
+
expect(() => w.clearByCwd("/never/seen")).not.toThrow();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("timeout fires silently when ws is closed", () => {
|
|
102
|
+
const { ws, messages } = makeMockWs(WebSocket.CLOSED);
|
|
103
|
+
const w = new SpawnRegisterWatchdog(10000);
|
|
104
|
+
w.arm({ cwd: "/p/q", mechanism: "tmux", ws });
|
|
105
|
+
expect(() => vi.advanceTimersByTime(10001)).not.toThrow();
|
|
106
|
+
expect(messages).toHaveLength(0);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("late clearByCwd within 60s emits spawn_register_recovered", () => {
|
|
110
|
+
const { ws, messages } = makeMockWs();
|
|
111
|
+
const w = new SpawnRegisterWatchdog(10000);
|
|
112
|
+
w.arm({ cwd: "/p/r", mechanism: "tmux", ws });
|
|
113
|
+
|
|
114
|
+
// Fire the watchdog.
|
|
115
|
+
vi.advanceTimersByTime(10001);
|
|
116
|
+
expect(messages[0]).toContain("spawn_register_timeout");
|
|
117
|
+
|
|
118
|
+
// Late registration within 60s.
|
|
119
|
+
vi.advanceTimersByTime(5000);
|
|
120
|
+
w.clearByCwd("/p/r");
|
|
121
|
+
|
|
122
|
+
expect(messages).toHaveLength(2);
|
|
123
|
+
const recovery = JSON.parse(messages[1]!);
|
|
124
|
+
expect(recovery.type).toBe("spawn_register_recovered");
|
|
125
|
+
expect(recovery.cwd).toBe("/p/r");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("late clear past 60s TTL is silent", () => {
|
|
129
|
+
const { ws, messages } = makeMockWs();
|
|
130
|
+
const w = new SpawnRegisterWatchdog(10000);
|
|
131
|
+
w.arm({ cwd: "/p/s", mechanism: "tmux", ws });
|
|
132
|
+
|
|
133
|
+
vi.advanceTimersByTime(10001);
|
|
134
|
+
expect(messages).toHaveLength(1);
|
|
135
|
+
|
|
136
|
+
// Past 60s TTL.
|
|
137
|
+
vi.advanceTimersByTime(61000);
|
|
138
|
+
w.clearByCwd("/p/s");
|
|
139
|
+
|
|
140
|
+
// No recovery message.
|
|
141
|
+
expect(messages).toHaveLength(1);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("recovery skipped when ws closed at recovery time", () => {
|
|
145
|
+
const messages: string[] = [];
|
|
146
|
+
// Start with OPEN, then we'll swap to CLOSED.
|
|
147
|
+
const ws = {
|
|
148
|
+
readyState: WebSocket.OPEN,
|
|
149
|
+
send: vi.fn((data: string) => messages.push(data)),
|
|
150
|
+
} as unknown as WebSocket;
|
|
151
|
+
|
|
152
|
+
const w = new SpawnRegisterWatchdog(10000);
|
|
153
|
+
w.arm({ cwd: "/p/t", mechanism: "tmux", ws });
|
|
154
|
+
vi.advanceTimersByTime(10001);
|
|
155
|
+
|
|
156
|
+
// Close the ws before recovery.
|
|
157
|
+
(ws as unknown as { readyState: number }).readyState = WebSocket.CLOSED;
|
|
158
|
+
|
|
159
|
+
vi.advanceTimersByTime(5000);
|
|
160
|
+
w.clearByCwd("/p/t");
|
|
161
|
+
|
|
162
|
+
// Only the timeout message was sent (before ws was closed).
|
|
163
|
+
const recoveries = messages.filter((m) => m.includes("spawn_register_recovered"));
|
|
164
|
+
expect(recoveries).toHaveLength(0);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// See change: spawn-correlation-token — third index by token.
|
|
169
|
+
describe("SpawnRegisterWatchdog: byToken index", () => {
|
|
170
|
+
beforeEach(() => {
|
|
171
|
+
vi.useFakeTimers();
|
|
172
|
+
});
|
|
173
|
+
afterEach(() => {
|
|
174
|
+
vi.useRealTimers();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("clearByToken cancels the watchdog", () => {
|
|
178
|
+
const w = new SpawnRegisterWatchdog(30_000);
|
|
179
|
+
const { ws, messages } = makeMockWs();
|
|
180
|
+
w.arm({ pid: 100, cwd: "/p", mechanism: "headless", ws, spawnToken: "tok_a" });
|
|
181
|
+
w.clearByToken("tok_a");
|
|
182
|
+
vi.advanceTimersByTime(60_000);
|
|
183
|
+
expect(messages.filter((m) => m.includes("spawn_register_timeout"))).toHaveLength(0);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("clearByToken removes entry from cwd and pid indices too", () => {
|
|
187
|
+
const w = new SpawnRegisterWatchdog(30_000);
|
|
188
|
+
const { ws } = makeMockWs();
|
|
189
|
+
w.arm({ pid: 100, cwd: "/p", mechanism: "headless", ws, spawnToken: "tok_a" });
|
|
190
|
+
w.clearByToken("tok_a");
|
|
191
|
+
// Subsequent clearByPid / clearByCwd are no-ops (entry already removed).
|
|
192
|
+
w.clearByPid(100);
|
|
193
|
+
w.clearByCwd("/p");
|
|
194
|
+
// No exception, no double-clear.
|
|
195
|
+
expect(true).toBe(true);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("clearByPid also clears the token index", () => {
|
|
199
|
+
const w = new SpawnRegisterWatchdog(30_000);
|
|
200
|
+
const { ws, messages } = makeMockWs();
|
|
201
|
+
w.arm({ pid: 100, cwd: "/p", mechanism: "headless", ws, spawnToken: "tok_a" });
|
|
202
|
+
w.clearByPid(100);
|
|
203
|
+
// Token-keyed clear is now a no-op (already cleaned up).
|
|
204
|
+
w.clearByToken("tok_a");
|
|
205
|
+
vi.advanceTimersByTime(60_000);
|
|
206
|
+
expect(messages.filter((m) => m.includes("spawn_register_timeout"))).toHaveLength(0);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("tmux arm without pid: token clears watchdog", () => {
|
|
210
|
+
const w = new SpawnRegisterWatchdog(30_000);
|
|
211
|
+
const { ws, messages } = makeMockWs();
|
|
212
|
+
w.arm({ cwd: "/p", mechanism: "tmux", ws, spawnToken: "tok_b" });
|
|
213
|
+
w.clearByToken("tok_b");
|
|
214
|
+
vi.advanceTimersByTime(60_000);
|
|
215
|
+
expect(messages.filter((m) => m.includes("spawn_register_timeout"))).toHaveLength(0);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("late clearByToken after timeout emits recovered", () => {
|
|
219
|
+
const w = new SpawnRegisterWatchdog(30_000);
|
|
220
|
+
const { ws, messages } = makeMockWs();
|
|
221
|
+
w.arm({ pid: 100, cwd: "/p", mechanism: "headless", ws, spawnToken: "tok_c" });
|
|
222
|
+
vi.advanceTimersByTime(31_000); // timeout fires
|
|
223
|
+
expect(messages.filter((m) => m.includes("spawn_register_timeout"))).toHaveLength(1);
|
|
224
|
+
w.clearByToken("tok_c");
|
|
225
|
+
expect(messages.filter((m) => m.includes("spawn_register_recovered"))).toHaveLength(1);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("two simultaneous arms with distinct tokens, distinct cwds: token-clears each independently", () => {
|
|
229
|
+
const w = new SpawnRegisterWatchdog(30_000);
|
|
230
|
+
const { ws: ws1, messages: m1 } = makeMockWs();
|
|
231
|
+
const { ws: ws2, messages: m2 } = makeMockWs();
|
|
232
|
+
w.arm({ pid: 100, cwd: "/p1", mechanism: "headless", ws: ws1, spawnToken: "tok_x" });
|
|
233
|
+
w.arm({ pid: 200, cwd: "/p2", mechanism: "headless", ws: ws2, spawnToken: "tok_y" });
|
|
234
|
+
w.clearByToken("tok_y");
|
|
235
|
+
vi.advanceTimersByTime(31_000);
|
|
236
|
+
// Only the first arm's timeout fired (second was cleared).
|
|
237
|
+
expect(m1.filter((m) => m.includes("spawn_register_timeout"))).toHaveLength(1);
|
|
238
|
+
expect(m2.filter((m) => m.includes("spawn_register_timeout"))).toHaveLength(0);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("arm without spawnToken behaves as before", () => {
|
|
242
|
+
const w = new SpawnRegisterWatchdog(30_000);
|
|
243
|
+
const { ws, messages } = makeMockWs();
|
|
244
|
+
w.arm({ pid: 100, cwd: "/p", mechanism: "headless", ws });
|
|
245
|
+
// Token-clear with empty / unknown token is a no-op.
|
|
246
|
+
w.clearByToken("tok_unknown");
|
|
247
|
+
vi.advanceTimersByTime(31_000);
|
|
248
|
+
expect(messages.filter((m) => m.includes("spawn_register_timeout"))).toHaveLength(1);
|
|
249
|
+
});
|
|
250
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for spawn correlation token primitives.
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - `mintSpawnToken()` returns distinct UUIDv4 strings.
|
|
6
|
+
* - `buildSpawnEnv(env, { spawnToken })` injects `PI_DASHBOARD_SPAWN_TOKEN`.
|
|
7
|
+
* - Without `spawnToken`, env is unchanged (no leakage).
|
|
8
|
+
*
|
|
9
|
+
* See change: spawn-correlation-token.
|
|
10
|
+
*/
|
|
11
|
+
import { describe, expect, it } from "vitest";
|
|
12
|
+
import { mintSpawnToken, SPAWN_TOKEN_ENV_VAR } from "../spawn-token.js";
|
|
13
|
+
import { buildSpawnEnv } from "../process-manager.js";
|
|
14
|
+
|
|
15
|
+
describe("mintSpawnToken", () => {
|
|
16
|
+
it("returns a UUIDv4 string", () => {
|
|
17
|
+
const t = mintSpawnToken();
|
|
18
|
+
expect(typeof t).toBe("string");
|
|
19
|
+
// UUIDv4: 8-4-4-4-12 hex with version=4 nibble
|
|
20
|
+
expect(t).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("returns distinct tokens on each call", () => {
|
|
24
|
+
const tokens = new Set<string>();
|
|
25
|
+
for (let i = 0; i < 50; i++) tokens.add(mintSpawnToken());
|
|
26
|
+
expect(tokens.size).toBe(50);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe("buildSpawnEnv: spawnToken injection", () => {
|
|
31
|
+
it("sets PI_DASHBOARD_SPAWN_TOKEN when spawnToken is provided", () => {
|
|
32
|
+
const env = buildSpawnEnv({ HOME: "/tmp" }, { spawnToken: "tok_test_123" });
|
|
33
|
+
expect(env[SPAWN_TOKEN_ENV_VAR]).toBe("tok_test_123");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("does not set PI_DASHBOARD_SPAWN_TOKEN when spawnToken is omitted", () => {
|
|
37
|
+
const env = buildSpawnEnv({ HOME: "/tmp" });
|
|
38
|
+
expect(env[SPAWN_TOKEN_ENV_VAR]).toBeUndefined();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("does not set PI_DASHBOARD_SPAWN_TOKEN when opts is empty", () => {
|
|
42
|
+
const env = buildSpawnEnv({ HOME: "/tmp" }, {});
|
|
43
|
+
expect(env[SPAWN_TOKEN_ENV_VAR]).toBeUndefined();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("preserves baseEnv variables unchanged when injecting", () => {
|
|
47
|
+
const env = buildSpawnEnv(
|
|
48
|
+
{ HOME: "/tmp", FOO: "bar", PATH: "/usr/bin" },
|
|
49
|
+
{ spawnToken: "tok_xyz" },
|
|
50
|
+
);
|
|
51
|
+
expect(env.HOME).toBe("/tmp");
|
|
52
|
+
expect(env.FOO).toBe("bar");
|
|
53
|
+
expect(env[SPAWN_TOKEN_ENV_VAR]).toBe("tok_xyz");
|
|
54
|
+
// PATH may be mutated by managed-node prepend, but the raw value should still appear in it.
|
|
55
|
+
expect(env.PATH).toContain("/usr/bin");
|
|
56
|
+
});
|
|
57
|
+
});
|