@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
|
@@ -31,16 +31,15 @@ describe("parseArgs", () => {
|
|
|
31
31
|
expect(result.subcommand).toBe("status");
|
|
32
32
|
});
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
});
|
|
34
|
+
// NOTE: `upgrade-pi` subcommand tests removed.
|
|
35
|
+
// The `upgrade-pi` subcommand was deliberately removed in change
|
|
36
|
+
// `eliminate-electron-runtime-install` (tasks 3.0.a + 3.5b, 2026-05-23)
|
|
37
|
+
// when bootstrap-install was deleted. `SUBCOMMANDS` is now
|
|
38
|
+
// `["start", "stop", "restart", "status"]`. The pi-core upgrade path
|
|
39
|
+
// survives via the `POST /api/pi-core/update` REST endpoint instead.
|
|
40
|
+
// These two tests were documented as deferred to a "Phase 3.9 sweep"
|
|
41
|
+
// in eliminate-electron-runtime-install/tasks.md task 5.9; this is
|
|
42
|
+
// that sweep.
|
|
44
43
|
|
|
45
44
|
it("parses subcommand with flags", () => {
|
|
46
45
|
const result = parseArgs(["start", "--port", "3000", "--pi-port", "4000"]);
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for `packages/server/bin/pi-dashboard.mjs` --version short-circuit.
|
|
3
|
+
*
|
|
4
|
+
* Bug B (see openspec/changes/fix-electron-cold-launch-probe-cascade):
|
|
5
|
+
* The wrapper previously resolved jiti BEFORE parsing argv, exiting 1
|
|
6
|
+
* with "cannot find jiti" even for metadata queries like --version.
|
|
7
|
+
* That killed `probeNpmGlobal` in launch-source.ts, which calls
|
|
8
|
+
* `pi-dashboard --version` and rejects null/empty/non-zero responses.
|
|
9
|
+
*
|
|
10
|
+
* Contract enforced by these tests:
|
|
11
|
+
* - `--version` / `-v` / `version` SHALL print sibling package.json's
|
|
12
|
+
* `version` to stdout and exit 0, EVEN when jiti is missing.
|
|
13
|
+
* - Any other subcommand (start, status, no args) SHALL preserve the
|
|
14
|
+
* pre-fix behaviour: exit 1 with "cannot find jiti" install hint
|
|
15
|
+
* when the wrapper can't resolve jiti.
|
|
16
|
+
* - When sibling package.json is unreadable / malformed, the short
|
|
17
|
+
* circuit MUST NOT silently succeed with an empty string — it
|
|
18
|
+
* SHALL fall through to the existing jiti-resolve path.
|
|
19
|
+
*/
|
|
20
|
+
import { describe, it, expect, beforeAll } from "vitest";
|
|
21
|
+
import { spawnSync } from "node:child_process";
|
|
22
|
+
import { mkdtempSync, writeFileSync, readFileSync, existsSync, rmSync, cpSync } from "node:fs";
|
|
23
|
+
import { tmpdir } from "node:os";
|
|
24
|
+
import path from "node:path";
|
|
25
|
+
import url from "node:url";
|
|
26
|
+
|
|
27
|
+
const here = path.dirname(url.fileURLToPath(import.meta.url));
|
|
28
|
+
const wrapperPath = path.resolve(here, "..", "..", "bin", "pi-dashboard.mjs");
|
|
29
|
+
const sibPkgJson = path.resolve(here, "..", "..", "package.json");
|
|
30
|
+
|
|
31
|
+
beforeAll(() => {
|
|
32
|
+
if (!existsSync(wrapperPath)) throw new Error(`Wrapper missing at ${wrapperPath}`);
|
|
33
|
+
if (!existsSync(sibPkgJson)) throw new Error(`Sibling package.json missing at ${sibPkgJson}`);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const expectedVersion = JSON.parse(readFileSync(sibPkgJson, "utf-8")).version as string;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Copy the wrapper + a chosen package.json into an isolated tmp dir with
|
|
40
|
+
* NO node_modules adjacency. createRequire from there cannot resolve
|
|
41
|
+
* jiti, so the jiti-miss path is exercised.
|
|
42
|
+
*/
|
|
43
|
+
function makeIsolatedWrapper(pkgJsonContent: string | null): { wrapper: string; cleanup: () => void } {
|
|
44
|
+
const tmp = mkdtempSync(path.join(tmpdir(), "pi-dashboard-cli-version-"));
|
|
45
|
+
// Wrapper has to live at .../bin/pi-dashboard.mjs because it computes
|
|
46
|
+
// sibling package.json as `../package.json` relative to itself.
|
|
47
|
+
const binDir = path.join(tmp, "bin");
|
|
48
|
+
cpSync(path.dirname(wrapperPath), binDir, { recursive: true });
|
|
49
|
+
if (pkgJsonContent !== null) {
|
|
50
|
+
writeFileSync(path.join(tmp, "package.json"), pkgJsonContent);
|
|
51
|
+
}
|
|
52
|
+
const wrapper = path.join(binDir, "pi-dashboard.mjs");
|
|
53
|
+
return { wrapper, cleanup: () => rmSync(tmp, { recursive: true, force: true }) };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
describe("bin/pi-dashboard.mjs --version short-circuit (Bug B)", () => {
|
|
57
|
+
it("(a) --version with no jiti reachable → exit 0, stdout = pkg.version", () => {
|
|
58
|
+
const fakePkg = JSON.stringify({ name: "pi-dashboard-test", version: "9.9.9-isolated" });
|
|
59
|
+
const { wrapper, cleanup } = makeIsolatedWrapper(fakePkg);
|
|
60
|
+
try {
|
|
61
|
+
const result = spawnSync(process.execPath, [wrapper, "--version"], {
|
|
62
|
+
encoding: "utf-8",
|
|
63
|
+
env: { ...process.env, NODE_PATH: "" },
|
|
64
|
+
timeout: 10_000,
|
|
65
|
+
});
|
|
66
|
+
expect(result.status).toBe(0);
|
|
67
|
+
expect(result.stdout.trim()).toBe("9.9.9-isolated");
|
|
68
|
+
expect(result.stderr).not.toContain("cannot find jiti");
|
|
69
|
+
} finally {
|
|
70
|
+
cleanup();
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("(a') -v shortform behaves identically", () => {
|
|
75
|
+
const fakePkg = JSON.stringify({ name: "pi-dashboard-test", version: "1.2.3-short" });
|
|
76
|
+
const { wrapper, cleanup } = makeIsolatedWrapper(fakePkg);
|
|
77
|
+
try {
|
|
78
|
+
const result = spawnSync(process.execPath, [wrapper, "-v"], {
|
|
79
|
+
encoding: "utf-8",
|
|
80
|
+
env: { ...process.env, NODE_PATH: "" },
|
|
81
|
+
timeout: 10_000,
|
|
82
|
+
});
|
|
83
|
+
expect(result.status).toBe(0);
|
|
84
|
+
expect(result.stdout.trim()).toBe("1.2.3-short");
|
|
85
|
+
} finally {
|
|
86
|
+
cleanup();
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("(b) start with no jiti reachable → exit 1, stderr contains install-hint", () => {
|
|
91
|
+
const fakePkg = JSON.stringify({ name: "pi-dashboard-test", version: "9.9.9-isolated" });
|
|
92
|
+
const { wrapper, cleanup } = makeIsolatedWrapper(fakePkg);
|
|
93
|
+
try {
|
|
94
|
+
const result = spawnSync(process.execPath, [wrapper, "start"], {
|
|
95
|
+
encoding: "utf-8",
|
|
96
|
+
env: { ...process.env, NODE_PATH: "" },
|
|
97
|
+
timeout: 10_000,
|
|
98
|
+
});
|
|
99
|
+
expect(result.status).toBe(1);
|
|
100
|
+
expect(result.stderr).toContain("pi-dashboard: cannot find jiti");
|
|
101
|
+
expect(result.stderr).toContain("npm install -g @earendil-works/pi-coding-agent");
|
|
102
|
+
expect(result.stdout.trim()).not.toBe("9.9.9-isolated");
|
|
103
|
+
} finally {
|
|
104
|
+
cleanup();
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("(c) --version on healthy install → exit 0 without re-execing cli.ts", () => {
|
|
109
|
+
const result = spawnSync(process.execPath, [wrapperPath, "--version"], {
|
|
110
|
+
encoding: "utf-8",
|
|
111
|
+
timeout: 15_000,
|
|
112
|
+
});
|
|
113
|
+
expect(result.status).toBe(0);
|
|
114
|
+
expect(result.stdout.trim()).toBe(expectedVersion);
|
|
115
|
+
// No cli.ts startup banner — proves no re-exec.
|
|
116
|
+
expect(result.stdout).not.toMatch(/Dashboard server/i);
|
|
117
|
+
expect(result.stderr).not.toContain("cannot find jiti");
|
|
118
|
+
}, 30_000);
|
|
119
|
+
|
|
120
|
+
it("(d) --version with corrupt sibling package.json → falls through to jiti path", () => {
|
|
121
|
+
const { wrapper, cleanup } = makeIsolatedWrapper("{ this is not valid json");
|
|
122
|
+
try {
|
|
123
|
+
const result = spawnSync(process.execPath, [wrapper, "--version"], {
|
|
124
|
+
encoding: "utf-8",
|
|
125
|
+
env: { ...process.env, NODE_PATH: "" },
|
|
126
|
+
timeout: 10_000,
|
|
127
|
+
});
|
|
128
|
+
// Fall-through: jiti unreachable in tmp dir → legacy install-hint fires.
|
|
129
|
+
// Critically: NOT a silent exit 0 with empty version.
|
|
130
|
+
expect(result.status).toBe(1);
|
|
131
|
+
expect(result.stderr).toContain("pi-dashboard: cannot find jiti");
|
|
132
|
+
} finally {
|
|
133
|
+
cleanup();
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("(d') --version with missing sibling package.json → falls through", () => {
|
|
138
|
+
const { wrapper, cleanup } = makeIsolatedWrapper(null);
|
|
139
|
+
try {
|
|
140
|
+
const result = spawnSync(process.execPath, [wrapper, "--version"], {
|
|
141
|
+
encoding: "utf-8",
|
|
142
|
+
env: { ...process.env, NODE_PATH: "" },
|
|
143
|
+
timeout: 10_000,
|
|
144
|
+
});
|
|
145
|
+
expect(result.status).toBe(1);
|
|
146
|
+
expect(result.stderr).toContain("pi-dashboard: cannot find jiti");
|
|
147
|
+
} finally {
|
|
148
|
+
cleanup();
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
});
|
|
@@ -62,6 +62,15 @@ function makePrefs(pinnedDirs: string[] = []): PreferencesStore {
|
|
|
62
62
|
pinDirectory: vi.fn(),
|
|
63
63
|
unpinDirectory: vi.fn(),
|
|
64
64
|
reorderPinnedDirs: vi.fn(),
|
|
65
|
+
getWorkspaces: vi.fn(() => []),
|
|
66
|
+
createWorkspace: vi.fn(() => null),
|
|
67
|
+
renameWorkspace: vi.fn(() => false),
|
|
68
|
+
deleteWorkspace: vi.fn(() => false),
|
|
69
|
+
setWorkspaceCollapsed: vi.fn(() => false),
|
|
70
|
+
addFolderToWorkspace: vi.fn(() => false),
|
|
71
|
+
removeFolderFromWorkspace: vi.fn(() => false),
|
|
72
|
+
reorderWorkspaceFolders: vi.fn(() => false),
|
|
73
|
+
reorderWorkspaces: vi.fn(() => false),
|
|
65
74
|
flush: vi.fn(),
|
|
66
75
|
dispose: vi.fn(),
|
|
67
76
|
};
|
|
@@ -55,6 +55,15 @@ function createMockPreferencesStore(): PreferencesStore {
|
|
|
55
55
|
pinDirectory: vi.fn(),
|
|
56
56
|
unpinDirectory: vi.fn(),
|
|
57
57
|
reorderPinnedDirs: vi.fn(),
|
|
58
|
+
getWorkspaces: vi.fn(() => []),
|
|
59
|
+
createWorkspace: vi.fn(() => null),
|
|
60
|
+
renameWorkspace: vi.fn(() => false),
|
|
61
|
+
deleteWorkspace: vi.fn(() => false),
|
|
62
|
+
setWorkspaceCollapsed: vi.fn(() => false),
|
|
63
|
+
addFolderToWorkspace: vi.fn(() => false),
|
|
64
|
+
removeFolderFromWorkspace: vi.fn(() => false),
|
|
65
|
+
reorderWorkspaceFolders: vi.fn(() => false),
|
|
66
|
+
reorderWorkspaces: vi.fn(() => false),
|
|
58
67
|
flush: vi.fn(),
|
|
59
68
|
dispose: vi.fn(),
|
|
60
69
|
};
|
|
@@ -63,6 +63,15 @@ function createMockPreferencesStore(): PreferencesStore {
|
|
|
63
63
|
pinDirectory: vi.fn(),
|
|
64
64
|
unpinDirectory: vi.fn(),
|
|
65
65
|
reorderPinnedDirs: vi.fn(),
|
|
66
|
+
getWorkspaces: vi.fn(() => []),
|
|
67
|
+
createWorkspace: vi.fn(() => null),
|
|
68
|
+
renameWorkspace: vi.fn(() => false),
|
|
69
|
+
deleteWorkspace: vi.fn(() => false),
|
|
70
|
+
setWorkspaceCollapsed: vi.fn(() => false),
|
|
71
|
+
addFolderToWorkspace: vi.fn(() => false),
|
|
72
|
+
removeFolderFromWorkspace: vi.fn(() => false),
|
|
73
|
+
reorderWorkspaceFolders: vi.fn(() => false),
|
|
74
|
+
reorderWorkspaces: vi.fn(() => false),
|
|
66
75
|
flush: vi.fn(),
|
|
67
76
|
dispose: vi.fn(),
|
|
68
77
|
};
|
|
@@ -62,6 +62,15 @@ function createMockPreferencesStore(): PreferencesStore {
|
|
|
62
62
|
pinDirectory: vi.fn(),
|
|
63
63
|
unpinDirectory: vi.fn(),
|
|
64
64
|
reorderPinnedDirs: vi.fn(),
|
|
65
|
+
getWorkspaces: vi.fn(() => []),
|
|
66
|
+
createWorkspace: vi.fn(() => null),
|
|
67
|
+
renameWorkspace: vi.fn(() => false),
|
|
68
|
+
deleteWorkspace: vi.fn(() => false),
|
|
69
|
+
setWorkspaceCollapsed: vi.fn(() => false),
|
|
70
|
+
addFolderToWorkspace: vi.fn(() => false),
|
|
71
|
+
removeFolderFromWorkspace: vi.fn(() => false),
|
|
72
|
+
reorderWorkspaceFolders: vi.fn(() => false),
|
|
73
|
+
reorderWorkspaces: vi.fn(() => false),
|
|
65
74
|
flush: vi.fn(),
|
|
66
75
|
dispose: vi.fn(),
|
|
67
76
|
};
|
|
@@ -60,6 +60,15 @@ function createMockPreferencesStore(pinnedDirs: string[] = []): PreferencesStore
|
|
|
60
60
|
pinDirectory: vi.fn(),
|
|
61
61
|
unpinDirectory: vi.fn(),
|
|
62
62
|
reorderPinnedDirs: vi.fn(),
|
|
63
|
+
getWorkspaces: vi.fn(() => []),
|
|
64
|
+
createWorkspace: vi.fn(() => null),
|
|
65
|
+
renameWorkspace: vi.fn(() => false),
|
|
66
|
+
deleteWorkspace: vi.fn(() => false),
|
|
67
|
+
setWorkspaceCollapsed: vi.fn(() => false),
|
|
68
|
+
addFolderToWorkspace: vi.fn(() => false),
|
|
69
|
+
removeFolderFromWorkspace: vi.fn(() => false),
|
|
70
|
+
reorderWorkspaceFolders: vi.fn(() => false),
|
|
71
|
+
reorderWorkspaces: vi.fn(() => false),
|
|
63
72
|
flush: vi.fn(),
|
|
64
73
|
dispose: vi.fn(),
|
|
65
74
|
};
|
|
@@ -115,6 +115,59 @@ describe("/api/doctor", () => {
|
|
|
115
115
|
}
|
|
116
116
|
});
|
|
117
117
|
|
|
118
|
+
it("probeServer reads process state, never spawns a subprocess", async () => {
|
|
119
|
+
// Set the env vars the new probeServer reads directly.
|
|
120
|
+
const prev = {
|
|
121
|
+
DASHBOARD_STARTER: process.env.DASHBOARD_STARTER,
|
|
122
|
+
NODE_ENV: process.env.NODE_ENV,
|
|
123
|
+
DASHBOARD_INSTALLABLE_TOTAL: process.env.DASHBOARD_INSTALLABLE_TOTAL,
|
|
124
|
+
DASHBOARD_INSTALLABLE_INSTALLED: process.env.DASHBOARD_INSTALLABLE_INSTALLED,
|
|
125
|
+
};
|
|
126
|
+
process.env.DASHBOARD_STARTER = "Electron";
|
|
127
|
+
process.env.NODE_ENV = "production";
|
|
128
|
+
process.env.DASHBOARD_INSTALLABLE_TOTAL = "3";
|
|
129
|
+
process.env.DASHBOARD_INSTALLABLE_INSTALLED = "3";
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
// Inject a deps override that captures what probeServer returns
|
|
133
|
+
// by building default deps and calling probeServer directly.
|
|
134
|
+
const { buildDefaultDepsForTest } = await import("../routes/doctor-routes.js") as {
|
|
135
|
+
buildDefaultDepsForTest?: () => SharedChecksDeps;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// We don't export buildDefaultDeps directly, so instead assert via
|
|
139
|
+
// the route: if the self-curl deadlock were present it would time out;
|
|
140
|
+
// instead the route must complete quickly (< 500 ms).
|
|
141
|
+
app = await makeApp();
|
|
142
|
+
const start = Date.now();
|
|
143
|
+
const res = await app.inject({ method: "GET", url: "/api/doctor" });
|
|
144
|
+
const elapsed = Date.now() - start;
|
|
145
|
+
|
|
146
|
+
expect(res.statusCode).toBe(200);
|
|
147
|
+
// Must complete well under the old 3 s curl timeout. The full
|
|
148
|
+
// doctor run includes binary-detection checks that can take ~1 s
|
|
149
|
+
// on slow CI; we just assert no self-curl deadlock (< 3 s).
|
|
150
|
+
expect(elapsed).toBeLessThan(3000);
|
|
151
|
+
|
|
152
|
+
// The server check row should say "running" / "ok" since we are
|
|
153
|
+
// processing this request inside the running server.
|
|
154
|
+
const body = res.json() as DoctorReport;
|
|
155
|
+
const serverRow = body.checks.find((c) => c.name === "Dashboard server");
|
|
156
|
+
expect(serverRow).toBeDefined();
|
|
157
|
+
expect(serverRow?.status).toBe("ok");
|
|
158
|
+
} finally {
|
|
159
|
+
// Restore env
|
|
160
|
+
if (prev.DASHBOARD_STARTER === undefined) delete process.env.DASHBOARD_STARTER;
|
|
161
|
+
else process.env.DASHBOARD_STARTER = prev.DASHBOARD_STARTER;
|
|
162
|
+
if (prev.NODE_ENV === undefined) delete process.env.NODE_ENV;
|
|
163
|
+
else process.env.NODE_ENV = prev.NODE_ENV;
|
|
164
|
+
if (prev.DASHBOARD_INSTALLABLE_TOTAL === undefined) delete process.env.DASHBOARD_INSTALLABLE_TOTAL;
|
|
165
|
+
else process.env.DASHBOARD_INSTALLABLE_TOTAL = prev.DASHBOARD_INSTALLABLE_TOTAL;
|
|
166
|
+
if (prev.DASHBOARD_INSTALLABLE_INSTALLED === undefined) delete process.env.DASHBOARD_INSTALLABLE_INSTALLED;
|
|
167
|
+
else process.env.DASHBOARD_INSTALLABLE_INSTALLED = prev.DASHBOARD_INSTALLABLE_INSTALLED;
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
118
171
|
it("returns 200 with a single fallback row when buildDeps throws", async () => {
|
|
119
172
|
app = await makeApp(() => {
|
|
120
173
|
throw new Error("boom — deps unavailable");
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the `queue_update` extension-to-server message handling.
|
|
3
|
+
* Validates that the server caches Session.pendingQueues wholesale and
|
|
4
|
+
* broadcasts session_updated to subscribers.
|
|
5
|
+
* See change: add-followup-edit-and-steer-cancel.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
8
|
+
import { WebSocket } from "ws";
|
|
9
|
+
import { mkdtempSync, writeFileSync } from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import os from "node:os";
|
|
12
|
+
import { createServer, type DashboardServer } from "../server.js";
|
|
13
|
+
|
|
14
|
+
const wait = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
|
15
|
+
|
|
16
|
+
describe("event-wiring: queue_update caches Session.pendingQueues and broadcasts", () => {
|
|
17
|
+
let server: DashboardServer;
|
|
18
|
+
let piPort: number;
|
|
19
|
+
let browserPort: number;
|
|
20
|
+
let testPort = 19800;
|
|
21
|
+
|
|
22
|
+
beforeEach(async () => {
|
|
23
|
+
testPort += 2;
|
|
24
|
+
browserPort = testPort;
|
|
25
|
+
piPort = testPort + 1;
|
|
26
|
+
server = await createServer({
|
|
27
|
+
port: browserPort,
|
|
28
|
+
piPort,
|
|
29
|
+
dev: true,
|
|
30
|
+
autoShutdown: false,
|
|
31
|
+
shutdownIdleSeconds: 999,
|
|
32
|
+
tunnel: false,
|
|
33
|
+
editor: { idleTimeoutMinutes: 10, maxInstances: 3 },
|
|
34
|
+
});
|
|
35
|
+
await server.start();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
afterEach(async () => {
|
|
39
|
+
await server.stop();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("wholesale replaces Session.pendingQueues on each queue_update event", async () => {
|
|
43
|
+
const { sessionManager } = server;
|
|
44
|
+
const SID = "queue-test-sess";
|
|
45
|
+
const tmpDir = mkdtempSync(path.join(os.tmpdir(), "pi-queue-test-"));
|
|
46
|
+
const sessionFile = path.join(tmpDir, "s.jsonl");
|
|
47
|
+
writeFileSync(sessionFile, "");
|
|
48
|
+
|
|
49
|
+
const bridgeWs = new WebSocket(`ws://localhost:${piPort}`);
|
|
50
|
+
await new Promise<void>((resolve, reject) => {
|
|
51
|
+
bridgeWs.on("error", reject);
|
|
52
|
+
bridgeWs.on("open", () => {
|
|
53
|
+
bridgeWs.send(JSON.stringify({
|
|
54
|
+
type: "session_register",
|
|
55
|
+
sessionId: SID,
|
|
56
|
+
cwd: tmpDir,
|
|
57
|
+
source: "cli",
|
|
58
|
+
sessionFile,
|
|
59
|
+
}));
|
|
60
|
+
bridgeWs.send(JSON.stringify({ type: "replay_complete", sessionId: SID }));
|
|
61
|
+
resolve();
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
await wait(80);
|
|
65
|
+
|
|
66
|
+
// Initial state: queues start empty after register.
|
|
67
|
+
expect(sessionManager.get(SID)?.pendingQueues).toEqual({ steering: [], followUp: [] });
|
|
68
|
+
|
|
69
|
+
// 1. Bridge emits queue_update with steering only
|
|
70
|
+
bridgeWs.send(JSON.stringify({
|
|
71
|
+
type: "queue_update",
|
|
72
|
+
sessionId: SID,
|
|
73
|
+
steering: ["first"],
|
|
74
|
+
followUp: [],
|
|
75
|
+
}));
|
|
76
|
+
await wait(60);
|
|
77
|
+
expect(sessionManager.get(SID)?.pendingQueues).toEqual({ steering: ["first"], followUp: [] });
|
|
78
|
+
|
|
79
|
+
// 2. Bridge emits queue_update with both queues populated — wholesale replace
|
|
80
|
+
bridgeWs.send(JSON.stringify({
|
|
81
|
+
type: "queue_update",
|
|
82
|
+
sessionId: SID,
|
|
83
|
+
steering: ["alpha", "beta"],
|
|
84
|
+
followUp: ["wrap up"],
|
|
85
|
+
}));
|
|
86
|
+
await wait(60);
|
|
87
|
+
expect(sessionManager.get(SID)?.pendingQueues).toEqual({
|
|
88
|
+
steering: ["alpha", "beta"],
|
|
89
|
+
followUp: ["wrap up"],
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// 3. Bridge emits empty snapshot (drain finished or clear ran)
|
|
93
|
+
bridgeWs.send(JSON.stringify({
|
|
94
|
+
type: "queue_update",
|
|
95
|
+
sessionId: SID,
|
|
96
|
+
steering: [],
|
|
97
|
+
followUp: [],
|
|
98
|
+
}));
|
|
99
|
+
await wait(60);
|
|
100
|
+
expect(sessionManager.get(SID)?.pendingQueues).toEqual({ steering: [], followUp: [] });
|
|
101
|
+
|
|
102
|
+
bridgeWs.close();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("resets Session.pendingQueues to empty on session re-register", async () => {
|
|
106
|
+
const { sessionManager } = server;
|
|
107
|
+
const SID = "queue-rereg-sess";
|
|
108
|
+
const tmpDir = mkdtempSync(path.join(os.tmpdir(), "pi-queue-rereg-"));
|
|
109
|
+
const sessionFile = path.join(tmpDir, "s.jsonl");
|
|
110
|
+
writeFileSync(sessionFile, "");
|
|
111
|
+
|
|
112
|
+
// First bridge connects and populates the queues.
|
|
113
|
+
const ws1 = new WebSocket(`ws://localhost:${piPort}`);
|
|
114
|
+
await new Promise<void>((resolve) => {
|
|
115
|
+
ws1.on("open", () => {
|
|
116
|
+
ws1.send(JSON.stringify({
|
|
117
|
+
type: "session_register",
|
|
118
|
+
sessionId: SID,
|
|
119
|
+
cwd: tmpDir,
|
|
120
|
+
source: "cli",
|
|
121
|
+
sessionFile,
|
|
122
|
+
}));
|
|
123
|
+
ws1.send(JSON.stringify({ type: "replay_complete", sessionId: SID }));
|
|
124
|
+
ws1.send(JSON.stringify({
|
|
125
|
+
type: "queue_update",
|
|
126
|
+
sessionId: SID,
|
|
127
|
+
steering: ["a", "b"],
|
|
128
|
+
followUp: ["c"],
|
|
129
|
+
}));
|
|
130
|
+
setTimeout(resolve, 100);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
expect(sessionManager.get(SID)?.pendingQueues?.steering).toHaveLength(2);
|
|
134
|
+
expect(sessionManager.get(SID)?.pendingQueues?.followUp).toHaveLength(1);
|
|
135
|
+
ws1.close();
|
|
136
|
+
await wait(80);
|
|
137
|
+
|
|
138
|
+
// Second bridge re-registers same sessionId — pendingQueues MUST reset.
|
|
139
|
+
const ws2 = new WebSocket(`ws://localhost:${piPort}`);
|
|
140
|
+
await new Promise<void>((resolve) => {
|
|
141
|
+
ws2.on("open", () => {
|
|
142
|
+
ws2.send(JSON.stringify({
|
|
143
|
+
type: "session_register",
|
|
144
|
+
sessionId: SID,
|
|
145
|
+
cwd: tmpDir,
|
|
146
|
+
source: "cli",
|
|
147
|
+
sessionFile,
|
|
148
|
+
}));
|
|
149
|
+
ws2.send(JSON.stringify({ type: "replay_complete", sessionId: SID }));
|
|
150
|
+
setTimeout(resolve, 100);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
expect(sessionManager.get(SID)?.pendingQueues).toEqual({ steering: [], followUp: [] });
|
|
154
|
+
ws2.close();
|
|
155
|
+
});
|
|
156
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression test: when a new session registers for a cwd that has a
|
|
3
|
+
* pending auto-resume, `resuming: false` must be set on the OLD session
|
|
4
|
+
* (pendingResume.oldSessionId), not the newly-registered one.
|
|
5
|
+
*
|
|
6
|
+
* Root cause: the original code used `sessionId` (new session) instead
|
|
7
|
+
* of `pendingResume.oldSessionId`, making the update a no-op on the new
|
|
8
|
+
* session and leaving the old session permanently stuck at `resuming: true`.
|
|
9
|
+
* `consume()` also cancels the 30s timeout, so `onTimeout` never fired.
|
|
10
|
+
*
|
|
11
|
+
* See change: fix-electron-server-launch-node-bin (resume stuck bug).
|
|
12
|
+
*/
|
|
13
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
14
|
+
import { WebSocket } from "ws";
|
|
15
|
+
import { mkdtempSync, writeFileSync } from "node:fs";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import os from "node:os";
|
|
18
|
+
import { createServer, type DashboardServer } from "../server.js";
|
|
19
|
+
|
|
20
|
+
const wait = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
|
21
|
+
|
|
22
|
+
describe("event-wiring: pending-resume clears old session resuming flag", () => {
|
|
23
|
+
let server: DashboardServer;
|
|
24
|
+
let piPort: number;
|
|
25
|
+
let browserPort: number;
|
|
26
|
+
let testPort = 19700;
|
|
27
|
+
|
|
28
|
+
beforeEach(async () => {
|
|
29
|
+
testPort += 2;
|
|
30
|
+
browserPort = testPort;
|
|
31
|
+
piPort = testPort + 1;
|
|
32
|
+
server = await createServer({
|
|
33
|
+
port: browserPort,
|
|
34
|
+
piPort,
|
|
35
|
+
dev: true,
|
|
36
|
+
autoShutdown: false,
|
|
37
|
+
shutdownIdleSeconds: 999,
|
|
38
|
+
tunnel: false,
|
|
39
|
+
editor: { idleTimeoutMinutes: 10, maxInstances: 3 },
|
|
40
|
+
});
|
|
41
|
+
await server.start();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
afterEach(async () => {
|
|
45
|
+
await server.stop();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("clears resuming on oldSessionId (not newSessionId) when resumed session registers", async () => {
|
|
49
|
+
const { sessionManager, browserGateway } = server;
|
|
50
|
+
|
|
51
|
+
const OLD_SESSION = "old-session-aaa";
|
|
52
|
+
const NEW_SESSION = "new-session-bbb";
|
|
53
|
+
// Use a real temp dir so meta-persistence doesn't fail on mkdir.
|
|
54
|
+
const tmpDir = mkdtempSync(path.join(os.tmpdir(), "pi-resume-test-"));
|
|
55
|
+
const CWD = tmpDir;
|
|
56
|
+
const sessionFile = path.join(tmpDir, "old.jsonl");
|
|
57
|
+
writeFileSync(sessionFile, ""); // create empty file
|
|
58
|
+
|
|
59
|
+
// 1. Seed the old session as ended with resuming:true.
|
|
60
|
+
// register() sets status:"active", so follow up with update() to ended+resuming.
|
|
61
|
+
sessionManager.register({ id: OLD_SESSION, cwd: CWD, sessionFile, source: "terminal" });
|
|
62
|
+
sessionManager.update(OLD_SESSION, { status: "ended", resuming: true });
|
|
63
|
+
expect(sessionManager.get(OLD_SESSION)?.resuming).toBe(true);
|
|
64
|
+
|
|
65
|
+
// 2. Record a pending resume for the cwd (mirrors handleSendPrompt auto-resume path)
|
|
66
|
+
browserGateway.pendingResumeRegistry.record(CWD, {
|
|
67
|
+
text: "continue from here",
|
|
68
|
+
oldSessionId: OLD_SESSION,
|
|
69
|
+
sessionFile,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// 3. A new session with the same cwd registers (the resumed pi process connecting back)
|
|
73
|
+
const newBridgeWs = new WebSocket(`ws://localhost:${piPort}`);
|
|
74
|
+
await new Promise<void>((resolve, reject) => {
|
|
75
|
+
newBridgeWs.on("error", reject);
|
|
76
|
+
newBridgeWs.on("open", () => {
|
|
77
|
+
newBridgeWs.send(JSON.stringify({
|
|
78
|
+
type: "session_register",
|
|
79
|
+
sessionId: NEW_SESSION,
|
|
80
|
+
cwd: CWD,
|
|
81
|
+
source: "cli",
|
|
82
|
+
}));
|
|
83
|
+
newBridgeWs.send(JSON.stringify({ type: "replay_complete", sessionId: NEW_SESSION }));
|
|
84
|
+
resolve();
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
await wait(200); // let event-wiring process
|
|
88
|
+
|
|
89
|
+
// 4. OLD session's resuming flag must now be false
|
|
90
|
+
expect(
|
|
91
|
+
sessionManager.get(OLD_SESSION)?.resuming,
|
|
92
|
+
`Old session ${OLD_SESSION} should have resuming:false after new session registered`,
|
|
93
|
+
).toBe(false);
|
|
94
|
+
|
|
95
|
+
// 5. NEW session must not have a spurious resuming:false (it never had resuming:true)
|
|
96
|
+
// New session's resuming should be undefined (never set).
|
|
97
|
+
const newResuming = sessionManager.get(NEW_SESSION)?.resuming;
|
|
98
|
+
expect(
|
|
99
|
+
newResuming,
|
|
100
|
+
`New session ${NEW_SESSION} should not have resuming set; got ${newResuming}`,
|
|
101
|
+
).toBeFalsy();
|
|
102
|
+
|
|
103
|
+
newBridgeWs.close();
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -1,29 +1,37 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tests for /api/health response shape
|
|
2
|
+
* Tests for /api/health response shape.
|
|
3
3
|
*
|
|
4
4
|
* Asserts:
|
|
5
5
|
* - `pid` field is present (regression pin).
|
|
6
|
-
* - `
|
|
6
|
+
* - `launchSource` field is present and reflects DASHBOARD_STARTER.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* `launchSource` replaces the legacy `starter` field per change:
|
|
9
|
+
* eliminate-electron-runtime-install (task 3.2). It is the single source
|
|
10
|
+
* of truth for arm-aware client gating (e.g. hiding pi-core update UI
|
|
11
|
+
* under Electron, since bundled node_modules/ is read-only there).
|
|
12
12
|
*/
|
|
13
|
-
import { describe, it, expect, afterEach } from "vitest";
|
|
13
|
+
import { describe, it, expect, afterEach, beforeEach } from "vitest";
|
|
14
14
|
import { createTestServer, type TestServerHandle } from "../test-support/test-server.js";
|
|
15
15
|
|
|
16
16
|
let handle: TestServerHandle | undefined;
|
|
17
|
+
let savedStarter: string | undefined;
|
|
18
|
+
|
|
19
|
+
describe("GET /api/health — shape", () => {
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
savedStarter = process.env.DASHBOARD_STARTER;
|
|
22
|
+
});
|
|
17
23
|
|
|
18
|
-
describe("GET /api/health — Phase A shape", () => {
|
|
19
24
|
afterEach(async () => {
|
|
20
25
|
if (handle) {
|
|
21
26
|
try { await handle.stop(); } catch { /* already stopped */ }
|
|
22
27
|
handle = undefined;
|
|
23
28
|
}
|
|
29
|
+
if (savedStarter === undefined) delete process.env.DASHBOARD_STARTER;
|
|
30
|
+
else process.env.DASHBOARD_STARTER = savedStarter;
|
|
24
31
|
});
|
|
25
32
|
|
|
26
33
|
it("includes pid field (regression pin)", async () => {
|
|
34
|
+
delete process.env.DASHBOARD_STARTER;
|
|
27
35
|
handle = await createTestServer();
|
|
28
36
|
const res = await fetch(`http://localhost:${handle.httpPort}/api/health`);
|
|
29
37
|
expect(res.status).toBe(200);
|
|
@@ -32,12 +40,27 @@ describe("GET /api/health — Phase A shape", () => {
|
|
|
32
40
|
expect(body.pid).toBe(process.pid);
|
|
33
41
|
});
|
|
34
42
|
|
|
35
|
-
it("
|
|
43
|
+
it("launchSource defaults to 'standalone' when DASHBOARD_STARTER unset", async () => {
|
|
44
|
+
delete process.env.DASHBOARD_STARTER;
|
|
45
|
+
handle = await createTestServer();
|
|
46
|
+
const res = await fetch(`http://localhost:${handle.httpPort}/api/health`);
|
|
47
|
+
const body = await res.json() as Record<string, unknown>;
|
|
48
|
+
expect(body.launchSource).toBe("standalone");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("launchSource is 'electron' when DASHBOARD_STARTER=Electron", async () => {
|
|
52
|
+
process.env.DASHBOARD_STARTER = "Electron";
|
|
53
|
+
handle = await createTestServer();
|
|
54
|
+
const res = await fetch(`http://localhost:${handle.httpPort}/api/health`);
|
|
55
|
+
const body = await res.json() as Record<string, unknown>;
|
|
56
|
+
expect(body.launchSource).toBe("electron");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("launchSource is 'bridge' when DASHBOARD_STARTER=Bridge", async () => {
|
|
60
|
+
process.env.DASHBOARD_STARTER = "Bridge";
|
|
36
61
|
handle = await createTestServer();
|
|
37
62
|
const res = await fetch(`http://localhost:${handle.httpPort}/api/health`);
|
|
38
|
-
expect(res.status).toBe(200);
|
|
39
63
|
const body = await res.json() as Record<string, unknown>;
|
|
40
|
-
|
|
41
|
-
expect(body.starter).toBe("Standalone");
|
|
64
|
+
expect(body.launchSource).toBe("bridge");
|
|
42
65
|
});
|
|
43
66
|
});
|