@blackbelt-technology/pi-agent-dashboard 0.5.3 → 0.5.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +19 -30
- package/README.md +69 -1
- package/docs/architecture.md +89 -165
- package/package.json +10 -7
- package/packages/extension/package.json +2 -2
- package/packages/extension/src/__tests__/bridge-default-model-gate.test.ts +47 -0
- package/packages/extension/src/__tests__/bridge-followup-chat-order.test.ts +215 -0
- package/packages/extension/src/__tests__/bridge-followup-multi-entry.test.ts +202 -0
- package/packages/extension/src/__tests__/bridge-queue-update-forward.test.ts +77 -0
- package/packages/extension/src/__tests__/bridge-retry-ordering.test.ts +148 -0
- package/packages/extension/src/__tests__/bridge-shadow-queue-drain.test.ts +221 -0
- package/packages/extension/src/__tests__/bridge-shadow-queue-gate.test.ts +299 -0
- package/packages/extension/src/__tests__/bridge-shutdown-reset.test.ts +238 -0
- package/packages/extension/src/__tests__/bridge-slash-command-routing.test.ts +127 -31
- package/packages/extension/src/__tests__/command-handler.test.ts +105 -3
- package/packages/extension/src/__tests__/fixtures/usage-limit-error-strings.ts +127 -0
- package/packages/extension/src/__tests__/source-detector.test.ts +15 -0
- package/packages/extension/src/__tests__/usage-limit-orderer.test.ts +12 -0
- package/packages/extension/src/bridge-default-model-gate.ts +32 -0
- package/packages/extension/src/bridge.ts +299 -20
- package/packages/extension/src/command-handler.ts +53 -7
- package/packages/extension/src/dashboard-default-adapter.ts +5 -0
- package/packages/extension/src/prompt-bus.ts +15 -0
- package/packages/extension/src/slash-dispatch.ts +30 -15
- package/packages/extension/src/source-detector.ts +13 -5
- package/packages/extension/src/usage-limit-orderer.ts +18 -1
- package/packages/server/bin/pi-dashboard.mjs +62 -14
- package/packages/server/package.json +9 -5
- package/packages/server/src/__tests__/browser-gateway-register-handler.test.ts +69 -0
- package/packages/server/src/__tests__/cli-env-no-clobber.test.ts +46 -0
- package/packages/server/src/__tests__/cli-no-bootstrap-references.test.ts +69 -0
- package/packages/server/src/__tests__/cli-parse.test.ts +9 -10
- package/packages/server/src/__tests__/cli-version.test.ts +151 -0
- package/packages/server/src/__tests__/directory-service-openspec-enabled.test.ts +9 -0
- package/packages/server/src/__tests__/directory-service-refresh-force.test.ts +9 -0
- package/packages/server/src/__tests__/directory-service-specs-mtime.test.ts +9 -0
- package/packages/server/src/__tests__/directory-service-toctou.test.ts +9 -0
- package/packages/server/src/__tests__/directory-service.test.ts +9 -0
- package/packages/server/src/__tests__/doctor-route.test.ts +53 -0
- package/packages/server/src/__tests__/event-wiring-queue-state.test.ts +156 -0
- package/packages/server/src/__tests__/event-wiring-resume-clear.test.ts +105 -0
- package/packages/server/src/__tests__/health-shape.test.ts +35 -12
- package/packages/server/src/__tests__/installed-package-enricher.test.ts +12 -12
- package/packages/server/src/__tests__/is-activity-event.test.ts +4 -7
- package/packages/server/src/__tests__/package-routes.test.ts +6 -2
- package/packages/server/src/__tests__/pi-changelog-routes.test.ts +10 -13
- package/packages/server/src/__tests__/pi-core-checker.test.ts +2 -2
- package/packages/server/src/__tests__/pi-version-skew.test.ts +3 -2
- package/packages/server/src/__tests__/plugin-activation-routes.test.ts +267 -0
- package/packages/server/src/__tests__/plugin-intent-cache.test.ts +75 -0
- package/packages/server/src/__tests__/preferences-store.test.ts +196 -0
- package/packages/server/src/__tests__/reattach-placement.test.ts +9 -0
- package/packages/server/src/__tests__/recommended-routes.test.ts +2 -2
- package/packages/server/src/__tests__/recovery-server.test.ts +203 -0
- package/packages/server/src/__tests__/session-action-handler-clear-queue.test.ts +153 -0
- package/packages/server/src/__tests__/session-action-handler-headless-reload.test.ts +43 -0
- package/packages/server/src/__tests__/session-order-manager.test.ts +9 -0
- package/packages/server/src/__tests__/session-order-reboot.test.ts +9 -0
- package/packages/server/src/__tests__/session-ordering-integration.test.ts +9 -0
- package/packages/server/src/browser-gateway.ts +83 -5
- package/packages/server/src/browser-handlers/directory-handler.ts +69 -0
- package/packages/server/src/browser-handlers/session-action-handler.ts +89 -0
- package/packages/server/src/browser-handlers/subscription-handler.ts +23 -0
- package/packages/server/src/changelog-parser.ts +1 -1
- package/packages/server/src/cli.ts +68 -250
- package/packages/server/src/event-status-extraction.ts +14 -62
- package/packages/server/src/event-wiring.ts +23 -10
- package/packages/server/src/memory-session-manager.ts +4 -0
- package/packages/server/src/pi-core-checker.ts +1 -1
- package/packages/server/src/pi-dev-version-check.ts +1 -1
- package/packages/server/src/pi-version-skew.ts +24 -46
- package/packages/server/src/plugin-intent-cache.ts +67 -0
- package/packages/server/src/preferences-store.ts +199 -13
- package/packages/server/src/recovery-server.ts +366 -0
- package/packages/server/src/routes/__tests__/manifest-route.test.ts +138 -0
- package/packages/server/src/routes/doctor-routes.ts +26 -21
- package/packages/server/src/routes/manifest-route.ts +162 -0
- package/packages/server/src/routes/openspec-routes.ts +4 -25
- package/packages/server/src/routes/pi-changelog-routes.ts +5 -24
- package/packages/server/src/routes/pi-core-routes.ts +3 -23
- package/packages/server/src/routes/plugin-activation-routes.ts +193 -0
- package/packages/server/src/routes/recommended-routes.ts +21 -0
- package/packages/server/src/routes/system-routes.ts +73 -11
- package/packages/server/src/server.ts +105 -307
- package/packages/server/src/session-api.ts +5 -63
- package/packages/shared/package.json +1 -1
- package/packages/shared/src/__tests__/binary-lookup-resolveJiti.test.ts +28 -0
- package/packages/shared/src/__tests__/binary-lookup-spawn-env.test.ts +61 -0
- package/packages/shared/src/__tests__/binary-lookup.test.ts +16 -0
- package/packages/shared/src/__tests__/bridge-register.test.ts +67 -0
- package/packages/shared/src/__tests__/ci-electron-no-side-effects.test.ts +129 -0
- package/packages/shared/src/__tests__/config.test.ts +40 -0
- package/packages/shared/src/__tests__/dashboard-paths.test.ts +81 -0
- package/packages/shared/src/__tests__/ensure-windows-path.test.ts +112 -0
- package/packages/shared/src/__tests__/intent-types.test.ts +120 -0
- package/packages/shared/src/__tests__/jiti-packages-parity.test.ts +85 -0
- package/packages/shared/src/__tests__/legacy-managed-dir.test.ts +59 -0
- package/packages/shared/src/__tests__/no-direct-child-process.test.ts +12 -0
- package/packages/shared/src/__tests__/no-electron-execpath-spawn.test.ts +149 -0
- package/packages/shared/src/__tests__/no-flow-command-route-claims.test.ts +71 -0
- package/packages/shared/src/__tests__/no-flow-references-in-shell.test.ts +221 -0
- package/packages/shared/src/__tests__/no-managed-dir-reference.test.ts +134 -0
- package/packages/shared/src/__tests__/no-pi-dashboard-version-jiti-gate.test.ts +41 -0
- package/packages/shared/src/__tests__/no-primitive-direct-import.test.ts +235 -0
- package/packages/shared/src/__tests__/no-server-imports-in-resolver.test.ts +53 -0
- package/packages/shared/src/__tests__/node-spawn-jiti-contract.test.ts +54 -101
- package/packages/shared/src/__tests__/node-spawn.test.ts +29 -13
- package/packages/shared/src/__tests__/pi-package-resolver.test.ts +300 -0
- package/packages/shared/src/__tests__/plugin-activation-contracts.test.ts +74 -0
- package/packages/shared/src/__tests__/plugin-bridge-classify-source.test.ts +73 -0
- package/packages/shared/src/__tests__/plugin-bridge-register-extended.test.ts +17 -5
- package/packages/shared/src/__tests__/plugin-bridge-register-packages.test.ts +233 -0
- package/packages/shared/src/__tests__/plugin-bridge-register.test.ts +19 -9
- package/packages/shared/src/__tests__/publish-workflow-contract.test.ts +154 -15
- package/packages/shared/src/__tests__/recommended-extensions.test.ts +28 -10
- package/packages/shared/src/__tests__/resolver-parity-with-scanner.test.ts +76 -0
- package/packages/shared/src/__tests__/server-identity.test.ts +127 -0
- package/packages/shared/src/__tests__/server-launcher.test.ts +35 -0
- package/packages/shared/src/__tests__/source-matching.test.ts +5 -5
- package/packages/shared/src/__tests__/sync-versions-spec.test.ts +76 -0
- package/packages/shared/src/__tests__/tool-registry-definitions.test.ts +50 -2
- package/packages/shared/src/bridge-register.ts +35 -2
- package/packages/shared/src/browser-protocol.ts +176 -2
- package/packages/shared/src/config.ts +12 -0
- package/packages/shared/src/dashboard-paths.ts +69 -0
- package/packages/shared/src/dashboard-plugin/index.ts +2 -0
- package/packages/shared/src/dashboard-plugin/intent-types.ts +93 -0
- package/packages/shared/src/dashboard-plugin/manifest-types.ts +55 -1
- package/packages/shared/src/dashboard-plugin/plugin-status.ts +82 -0
- package/packages/shared/src/dashboard-plugin/slot-props.ts +11 -0
- package/packages/shared/src/dashboard-plugin/slot-types.ts +16 -2
- package/packages/shared/src/dashboard-plugin/ui-primitives.ts +287 -0
- package/packages/shared/src/dashboard-starter.ts +22 -0
- package/packages/shared/src/doctor-core.ts +49 -27
- package/packages/shared/src/launch-source-types.ts +9 -9
- package/packages/shared/src/legacy-managed-dir.ts +97 -0
- package/packages/shared/src/mdns-discovery.ts +4 -1
- package/packages/shared/src/pi-package-resolver.ts +388 -0
- package/packages/shared/src/platform/binary-lookup.ts +27 -3
- package/packages/shared/src/platform/ensure-windows-path.ts +95 -0
- package/packages/shared/src/platform/exec.ts +22 -0
- package/packages/shared/src/platform/node-spawn.ts +42 -41
- package/packages/shared/src/plugin-bridge-register.ts +275 -18
- package/packages/shared/src/protocol.ts +94 -2
- package/packages/shared/src/recommended-extensions.ts +34 -10
- package/packages/shared/src/server-identity.ts +74 -5
- package/packages/shared/src/server-launcher.ts +20 -0
- package/packages/shared/src/source-matching.ts +1 -1
- package/packages/shared/src/tool-registry/__tests__/node-script-toargv-fallback.test.ts +84 -0
- package/packages/shared/src/tool-registry/definitions.ts +91 -7
- package/packages/shared/src/types.ts +12 -8
- package/scripts/maybe-patch-package.cjs +44 -0
- package/packages/server/src/__tests__/bootstrap-install-from-list.test.ts +0 -263
- package/packages/server/src/__tests__/bootstrap-queue.test.ts +0 -120
- package/packages/server/src/__tests__/bootstrap-routes.test.ts +0 -125
- package/packages/server/src/__tests__/bootstrap-state.test.ts +0 -119
- package/packages/server/src/__tests__/cli-bootstrap.test.ts +0 -36
- package/packages/server/src/__tests__/event-status-extraction-flow.test.ts +0 -55
- package/packages/server/src/__tests__/legacy-pi-cleanup.test.ts +0 -149
- package/packages/server/src/__tests__/post-install-openspec-refresh.test.ts +0 -180
- package/packages/server/src/__tests__/post-install-rescan.test.ts +0 -134
- package/packages/server/src/__tests__/system-routes-reextract.test.ts +0 -91
- package/packages/server/src/bootstrap-install-from-list.ts +0 -232
- package/packages/server/src/bootstrap-queue.ts +0 -130
- package/packages/server/src/bootstrap-state.ts +0 -159
- package/packages/server/src/legacy-pi-cleanup.ts +0 -151
- package/packages/server/src/routes/bootstrap-routes.ts +0 -125
- package/packages/shared/src/__tests__/bootstrap/README.md +0 -133
- package/packages/shared/src/__tests__/bootstrap/__snapshots__/cube.test.ts.snap +0 -378
- package/packages/shared/src/__tests__/bootstrap/assertions.ts +0 -136
- package/packages/shared/src/__tests__/bootstrap/cube.test.ts +0 -47
- package/packages/shared/src/__tests__/bootstrap/cube.ts +0 -66
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/a-electron.test.ts.snap +0 -84
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/b-npm-global.test.ts.snap +0 -90
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/c-dev-monorepo.test.ts.snap +0 -34
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/d-overrides.test.ts.snap +0 -20
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/e-stale-partial.test.ts.snap +0 -62
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/f-cwd-variants.test.ts.snap +0 -34
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/g-windows-specifics.test.ts.snap +0 -49
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/j-path-gui-minimal.test.ts.snap +0 -12
- package/packages/shared/src/__tests__/bootstrap/families/a-electron.test.ts +0 -156
- package/packages/shared/src/__tests__/bootstrap/families/b-npm-global.test.ts +0 -157
- package/packages/shared/src/__tests__/bootstrap/families/c-dev-monorepo.test.ts +0 -102
- package/packages/shared/src/__tests__/bootstrap/families/d-overrides.test.ts +0 -76
- package/packages/shared/src/__tests__/bootstrap/families/e-stale-partial.test.ts +0 -94
- package/packages/shared/src/__tests__/bootstrap/families/f-cwd-variants.test.ts +0 -87
- package/packages/shared/src/__tests__/bootstrap/families/g-windows-specifics.test.ts +0 -143
- package/packages/shared/src/__tests__/bootstrap/families/h-home-drift.test.ts +0 -64
- package/packages/shared/src/__tests__/bootstrap/families/i-malformed-settings.test.ts +0 -77
- package/packages/shared/src/__tests__/bootstrap/families/index.ts +0 -19
- package/packages/shared/src/__tests__/bootstrap/families/j-path-gui-minimal.test.ts +0 -61
- package/packages/shared/src/__tests__/bootstrap/families/k-dashboard-absent.test.ts +0 -50
- package/packages/shared/src/__tests__/bootstrap/families/l-instance-coordination.test.ts +0 -272
- package/packages/shared/src/__tests__/bootstrap/fixtures/dev-monorepo.ts +0 -58
- package/packages/shared/src/__tests__/bootstrap/fixtures/electron-layout.ts +0 -84
- package/packages/shared/src/__tests__/bootstrap/fixtures/index.ts +0 -9
- package/packages/shared/src/__tests__/bootstrap/fixtures/managed-install.ts +0 -85
- package/packages/shared/src/__tests__/bootstrap/fixtures/npm-global-layout.ts +0 -122
- package/packages/shared/src/__tests__/bootstrap/fixtures/pi-versions.ts +0 -36
- package/packages/shared/src/__tests__/bootstrap/fixtures/settings-json.ts +0 -39
- package/packages/shared/src/__tests__/bootstrap/harness.smoke.test.ts +0 -220
- package/packages/shared/src/__tests__/bootstrap/harness.ts +0 -413
- package/packages/shared/src/__tests__/bootstrap/scenarios-skipped.ts +0 -125
- package/packages/shared/src/__tests__/bootstrap/scenarios.ts +0 -132
- package/packages/shared/src/__tests__/bootstrap-install-resolve-npm.test.ts +0 -72
- package/packages/shared/src/__tests__/install-managed-node-bootstrap-order.test.ts +0 -68
- package/packages/shared/src/__tests__/install-managed-node.test.ts +0 -192
- package/packages/shared/src/__tests__/installable-list.test.ts +0 -130
- package/packages/shared/src/__tests__/no-installable-list-in-bridge.test.ts +0 -52
- package/packages/shared/src/bootstrap-install.ts +0 -406
- package/packages/shared/src/installable-list.ts +0 -152
- package/packages/shared/src/launch-source-flag.ts +0 -14
|
@@ -1,263 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Integration tests for bootstrapInstallFromList.
|
|
3
|
-
*
|
|
4
|
-
* All file I/O and install calls are injected via opts so no real
|
|
5
|
-
* filesystem or subprocesses are touched.
|
|
6
|
-
*
|
|
7
|
-
* See change: simplify-electron-bootstrap-derived-state.
|
|
8
|
-
*/
|
|
9
|
-
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
10
|
-
import { createBootstrapState } from "../bootstrap-state.js";
|
|
11
|
-
import {
|
|
12
|
-
bootstrapInstallFromList,
|
|
13
|
-
type PackageInstaller,
|
|
14
|
-
} from "../bootstrap-install-from-list.js";
|
|
15
|
-
import type { InstallablePackage, InstallableList } from "@blackbelt-technology/pi-dashboard-shared/installable-list.js";
|
|
16
|
-
|
|
17
|
-
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
18
|
-
|
|
19
|
-
function makePackage(overrides: Partial<InstallablePackage> = {}): InstallablePackage {
|
|
20
|
-
return {
|
|
21
|
-
name: "test-pkg",
|
|
22
|
-
version: "1.0.0",
|
|
23
|
-
required: true,
|
|
24
|
-
kind: "npm",
|
|
25
|
-
...overrides,
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function makeList(packages: InstallablePackage[]): InstallableList {
|
|
30
|
-
return { version: "1", packages };
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Build a `bootstrapInstallFromList` opts object that bypasses all real I/O.
|
|
35
|
-
* - `listResult`: the installable list (null = file absent).
|
|
36
|
-
* - `installedNames`: package names that are already installed.
|
|
37
|
-
* - `npmInstall`/`piInstall`: injectable install fns (default: succeed).
|
|
38
|
-
*/
|
|
39
|
-
interface FakeOpts {
|
|
40
|
-
listResult: InstallableList | null;
|
|
41
|
-
installedNames?: string[];
|
|
42
|
-
npmInstall?: PackageInstaller;
|
|
43
|
-
piInstall?: PackageInstaller;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function buildOpts(fake: FakeOpts, extra?: object) {
|
|
47
|
-
const installedSet = new Set(fake.installedNames ?? []);
|
|
48
|
-
return {
|
|
49
|
-
configDir: "/fake/config",
|
|
50
|
-
managedDir: "/fake/managed",
|
|
51
|
-
isInstalled: (pkg: InstallablePackage) => installedSet.has(pkg.name),
|
|
52
|
-
npmInstall: fake.npmInstall ?? (async () => { /* succeed */ }),
|
|
53
|
-
piInstall: fake.piInstall ?? (async () => { /* succeed */ }),
|
|
54
|
-
// Override readInstallableList via module mock — done per test via vi.mock
|
|
55
|
-
// (see below). We instead inject listResult via a wrapping helper.
|
|
56
|
-
...extra,
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// We cannot easily mock `readInstallableList` without vi.mock at module level.
|
|
61
|
-
// Instead we factor out a testable inner function and re-export it.
|
|
62
|
-
// Since we cannot easily mock the module import inside bootstrapInstallFromList,
|
|
63
|
-
// we use a different approach: inject a `_readList` seam via opts.
|
|
64
|
-
//
|
|
65
|
-
// However, the current public API doesn't expose that seam. We'll test via
|
|
66
|
-
// the observable side effects (bootstrap state + thrown errors) and fake the
|
|
67
|
-
// injectable installers. For the list itself, we monkey-patch the module.
|
|
68
|
-
//
|
|
69
|
-
// Pragmatic solution: use vi.mock to replace readInstallableList.
|
|
70
|
-
|
|
71
|
-
vi.mock("@blackbelt-technology/pi-dashboard-shared/installable-list.js", () => ({
|
|
72
|
-
readInstallableList: vi.fn(),
|
|
73
|
-
}));
|
|
74
|
-
|
|
75
|
-
import { readInstallableList } from "@blackbelt-technology/pi-dashboard-shared/installable-list.js";
|
|
76
|
-
const mockReadList = vi.mocked(readInstallableList);
|
|
77
|
-
|
|
78
|
-
// ── Tests ──────────────────────────────────────────────────────────────────
|
|
79
|
-
|
|
80
|
-
describe("bootstrapInstallFromList", () => {
|
|
81
|
-
beforeEach(() => {
|
|
82
|
-
vi.clearAllMocks();
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
// ── Test 1: no installable.json (Bridge/Standalone parity) ──────────────
|
|
86
|
-
|
|
87
|
-
describe("file-absent path", () => {
|
|
88
|
-
it("returns immediately without setting installable state or calling any installer", async () => {
|
|
89
|
-
mockReadList.mockResolvedValue(null);
|
|
90
|
-
const state = createBootstrapState();
|
|
91
|
-
const npmInstall = vi.fn();
|
|
92
|
-
const piInstall = vi.fn();
|
|
93
|
-
|
|
94
|
-
await bootstrapInstallFromList(state, {
|
|
95
|
-
configDir: "/fake/config",
|
|
96
|
-
managedDir: "/fake/managed",
|
|
97
|
-
npmInstall,
|
|
98
|
-
piInstall,
|
|
99
|
-
isInstalled: () => false,
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
// No installer calls.
|
|
103
|
-
expect(npmInstall).not.toHaveBeenCalled();
|
|
104
|
-
expect(piInstall).not.toHaveBeenCalled();
|
|
105
|
-
|
|
106
|
-
// installable field NOT set (file was absent — no tracking started).
|
|
107
|
-
expect(state.get().installable).toBeUndefined();
|
|
108
|
-
|
|
109
|
-
// Status remains ready.
|
|
110
|
-
expect(state.get().status).toBe("ready");
|
|
111
|
-
});
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
// ── Test 2: synthetic installable.json ──────────────────────────────────
|
|
115
|
-
|
|
116
|
-
describe("with installable.json present", () => {
|
|
117
|
-
it("skips already-installed npm package, installs missing required + optional, final state is correct", async () => {
|
|
118
|
-
const alreadyInstalled = makePackage({ name: "already-installed-pkg", required: false });
|
|
119
|
-
const missingRequired = makePackage({ name: "missing-required-pkg", required: true });
|
|
120
|
-
const missingOptional = makePackage({ name: "missing-optional-pkg", required: false });
|
|
121
|
-
|
|
122
|
-
mockReadList.mockResolvedValue(
|
|
123
|
-
makeList([alreadyInstalled, missingRequired, missingOptional]),
|
|
124
|
-
);
|
|
125
|
-
|
|
126
|
-
const state = createBootstrapState();
|
|
127
|
-
const npmInstall = vi.fn().mockResolvedValue(undefined);
|
|
128
|
-
|
|
129
|
-
await bootstrapInstallFromList(state, {
|
|
130
|
-
configDir: "/fake/config",
|
|
131
|
-
managedDir: "/fake/managed",
|
|
132
|
-
npmInstall,
|
|
133
|
-
piInstall: vi.fn(),
|
|
134
|
-
isInstalled: (pkg) => pkg.name === "already-installed-pkg",
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
// Two install calls (already-installed skipped).
|
|
138
|
-
expect(npmInstall).toHaveBeenCalledTimes(2);
|
|
139
|
-
expect(npmInstall.mock.calls[0][0].name).toBe("missing-required-pkg");
|
|
140
|
-
expect(npmInstall.mock.calls[1][0].name).toBe("missing-optional-pkg");
|
|
141
|
-
|
|
142
|
-
// Final state: installed=3 (1 pre-installed + 2 freshly installed), failed=0.
|
|
143
|
-
const installable = state.get().installable;
|
|
144
|
-
expect(installable).toBeDefined();
|
|
145
|
-
expect(installable!.total).toBe(3);
|
|
146
|
-
expect(installable!.installed).toBe(3);
|
|
147
|
-
expect(installable!.failed).toHaveLength(0);
|
|
148
|
-
|
|
149
|
-
// Status remains ready (no error).
|
|
150
|
-
expect(state.get().status).toBe("ready");
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it("installs pi-extension packages via piInstall", async () => {
|
|
154
|
-
const pkg = makePackage({ name: "my-extension", kind: "pi-extension", required: true });
|
|
155
|
-
mockReadList.mockResolvedValue(makeList([pkg]));
|
|
156
|
-
|
|
157
|
-
const state = createBootstrapState();
|
|
158
|
-
const piInstall = vi.fn().mockResolvedValue(undefined);
|
|
159
|
-
const npmInstall = vi.fn();
|
|
160
|
-
|
|
161
|
-
await bootstrapInstallFromList(state, {
|
|
162
|
-
configDir: "/fake/config",
|
|
163
|
-
managedDir: "/fake/managed",
|
|
164
|
-
npmInstall,
|
|
165
|
-
piInstall,
|
|
166
|
-
isInstalled: () => false,
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
expect(piInstall).toHaveBeenCalledOnce();
|
|
170
|
-
expect(npmInstall).not.toHaveBeenCalled();
|
|
171
|
-
expect(piInstall.mock.calls[0][0].name).toBe("my-extension");
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
it("optional package failure is recorded in failed[] but does not throw", async () => {
|
|
175
|
-
const optionalFail = makePackage({ name: "optional-bad", required: false });
|
|
176
|
-
mockReadList.mockResolvedValue(makeList([optionalFail]));
|
|
177
|
-
|
|
178
|
-
const state = createBootstrapState();
|
|
179
|
-
const npmInstall = vi.fn().mockRejectedValue(new Error("network error"));
|
|
180
|
-
|
|
181
|
-
await expect(
|
|
182
|
-
bootstrapInstallFromList(state, {
|
|
183
|
-
configDir: "/fake/config",
|
|
184
|
-
managedDir: "/fake/managed",
|
|
185
|
-
npmInstall,
|
|
186
|
-
piInstall: vi.fn(),
|
|
187
|
-
isInstalled: () => false,
|
|
188
|
-
}),
|
|
189
|
-
).resolves.toBeUndefined();
|
|
190
|
-
|
|
191
|
-
const installable = state.get().installable;
|
|
192
|
-
expect(installable!.failed).toEqual(["optional-bad"]);
|
|
193
|
-
expect(installable!.installed).toBe(0);
|
|
194
|
-
expect(state.get().status).toBe("ready");
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
it("required package failure sets status=failed and throws", async () => {
|
|
198
|
-
const requiredFail = makePackage({ name: "required-bad", required: true });
|
|
199
|
-
mockReadList.mockResolvedValue(makeList([requiredFail]));
|
|
200
|
-
|
|
201
|
-
const state = createBootstrapState();
|
|
202
|
-
const npmInstall = vi.fn().mockRejectedValue(new Error("disk full"));
|
|
203
|
-
|
|
204
|
-
await expect(
|
|
205
|
-
bootstrapInstallFromList(state, {
|
|
206
|
-
configDir: "/fake/config",
|
|
207
|
-
managedDir: "/fake/managed",
|
|
208
|
-
npmInstall,
|
|
209
|
-
piInstall: vi.fn(),
|
|
210
|
-
isInstalled: () => false,
|
|
211
|
-
}),
|
|
212
|
-
).rejects.toThrow('Required package "required-bad" failed to install');
|
|
213
|
-
|
|
214
|
-
expect(state.get().status).toBe("failed");
|
|
215
|
-
expect(state.get().error?.message).toContain("required-bad");
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
it("deprecated and defaultOff packages are skipped entirely", async () => {
|
|
219
|
-
const deprecated = makePackage({ name: "old-pkg", deprecated: true });
|
|
220
|
-
const defaultOff = makePackage({ name: "opt-pkg", defaultOff: true });
|
|
221
|
-
const normal = makePackage({ name: "normal-pkg", required: true });
|
|
222
|
-
mockReadList.mockResolvedValue(makeList([deprecated, defaultOff, normal]));
|
|
223
|
-
|
|
224
|
-
const state = createBootstrapState();
|
|
225
|
-
const npmInstall = vi.fn().mockResolvedValue(undefined);
|
|
226
|
-
|
|
227
|
-
await bootstrapInstallFromList(state, {
|
|
228
|
-
configDir: "/fake/config",
|
|
229
|
-
managedDir: "/fake/managed",
|
|
230
|
-
npmInstall,
|
|
231
|
-
piInstall: vi.fn(),
|
|
232
|
-
isInstalled: () => false,
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
// Only "normal-pkg" is processed (total=1, not 3).
|
|
236
|
-
expect(npmInstall).toHaveBeenCalledOnce();
|
|
237
|
-
expect(state.get().installable!.total).toBe(1);
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
it("emits progress steps during install", async () => {
|
|
241
|
-
const pkg = makePackage({ name: "tracked-pkg" });
|
|
242
|
-
mockReadList.mockResolvedValue(makeList([pkg]));
|
|
243
|
-
|
|
244
|
-
const state = createBootstrapState();
|
|
245
|
-
const progressSteps: string[] = [];
|
|
246
|
-
state.subscribe((s) => {
|
|
247
|
-
if (s.progress) progressSteps.push(s.progress.step);
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
const npmInstall = vi.fn().mockResolvedValue(undefined);
|
|
251
|
-
|
|
252
|
-
await bootstrapInstallFromList(state, {
|
|
253
|
-
configDir: "/fake/config",
|
|
254
|
-
managedDir: "/fake/managed",
|
|
255
|
-
npmInstall,
|
|
256
|
-
piInstall: vi.fn(),
|
|
257
|
-
isInstalled: () => false,
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
expect(progressSteps).toContain("tracked-pkg");
|
|
261
|
-
});
|
|
262
|
-
});
|
|
263
|
-
});
|
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unit tests for the in-memory bootstrap ticket queue.
|
|
3
|
-
*
|
|
4
|
-
* See change: unified-bootstrap-install.
|
|
5
|
-
*/
|
|
6
|
-
import { describe, it, expect } from "vitest";
|
|
7
|
-
import { createBootstrapQueue } from "../bootstrap-queue.js";
|
|
8
|
-
|
|
9
|
-
describe("bootstrap-queue", () => {
|
|
10
|
-
it("enqueue returns a unique ticketId + pending result", () => {
|
|
11
|
-
const q = createBootstrapQueue();
|
|
12
|
-
const a = q.enqueue(async () => "A");
|
|
13
|
-
const b = q.enqueue(async () => "B");
|
|
14
|
-
expect(a.ticketId).not.toBe(b.ticketId);
|
|
15
|
-
expect(q.size()).toBe(2);
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it("flushAll runs handlers in enqueue order and resolves results", async () => {
|
|
19
|
-
const q = createBootstrapQueue();
|
|
20
|
-
const order: string[] = [];
|
|
21
|
-
const a = q.enqueue(async () => {
|
|
22
|
-
order.push("a");
|
|
23
|
-
return "A";
|
|
24
|
-
});
|
|
25
|
-
const b = q.enqueue(async () => {
|
|
26
|
-
order.push("b");
|
|
27
|
-
return "B";
|
|
28
|
-
});
|
|
29
|
-
await q.flushAll();
|
|
30
|
-
expect(order).toEqual(["a", "b"]);
|
|
31
|
-
await expect(a.result).resolves.toBe("A");
|
|
32
|
-
await expect(b.result).resolves.toBe("B");
|
|
33
|
-
expect(q.size()).toBe(0);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it("handler exceptions reject the ticket promise", async () => {
|
|
37
|
-
const q = createBootstrapQueue();
|
|
38
|
-
const t = q.enqueue(async () => {
|
|
39
|
-
throw new Error("boom");
|
|
40
|
-
});
|
|
41
|
-
await q.flushAll();
|
|
42
|
-
await expect(t.result).rejects.toThrow("boom");
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it("onTicketComplete fires success=true for resolved handlers", async () => {
|
|
46
|
-
const q = createBootstrapQueue();
|
|
47
|
-
const events: Array<{ ticketId: string; success: boolean; error?: string }> = [];
|
|
48
|
-
q.onTicketComplete((e) => events.push(e));
|
|
49
|
-
const t = q.enqueue(async () => 42);
|
|
50
|
-
await q.flushAll();
|
|
51
|
-
await t.result;
|
|
52
|
-
expect(events).toEqual([{ ticketId: t.ticketId, success: true }]);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it("onTicketComplete fires success=false with error message on rejection", async () => {
|
|
56
|
-
const q = createBootstrapQueue();
|
|
57
|
-
const events: Array<{ ticketId: string; success: boolean; error?: string }> = [];
|
|
58
|
-
q.onTicketComplete((e) => events.push(e));
|
|
59
|
-
const t = q.enqueue(async () => {
|
|
60
|
-
throw new Error("oh no");
|
|
61
|
-
});
|
|
62
|
-
await q.flushAll();
|
|
63
|
-
await t.result.catch(() => undefined);
|
|
64
|
-
expect(events).toEqual([
|
|
65
|
-
{ ticketId: t.ticketId, success: false, error: "oh no" },
|
|
66
|
-
]);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it("onTicketComplete returns an unsubscribe function", async () => {
|
|
70
|
-
const q = createBootstrapQueue();
|
|
71
|
-
const events: unknown[] = [];
|
|
72
|
-
const off = q.onTicketComplete((e) => events.push(e));
|
|
73
|
-
off();
|
|
74
|
-
q.enqueue(async () => "x");
|
|
75
|
-
await q.flushAll();
|
|
76
|
-
expect(events).toEqual([]);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it("clear drops pending tickets with an error result and broadcasts completion", async () => {
|
|
80
|
-
const q = createBootstrapQueue();
|
|
81
|
-
const events: Array<{ ticketId: string; success: boolean; error?: string }> = [];
|
|
82
|
-
q.onTicketComplete((e) => events.push(e));
|
|
83
|
-
const t = q.enqueue(async () => "never runs");
|
|
84
|
-
q.clear("server shutting down");
|
|
85
|
-
await t.result.catch(() => undefined);
|
|
86
|
-
expect(events).toHaveLength(1);
|
|
87
|
-
expect(events[0]).toMatchObject({
|
|
88
|
-
ticketId: t.ticketId,
|
|
89
|
-
success: false,
|
|
90
|
-
error: "server shutting down",
|
|
91
|
-
});
|
|
92
|
-
expect(q.size()).toBe(0);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it("multiple listeners all receive the completion event", async () => {
|
|
96
|
-
const q = createBootstrapQueue();
|
|
97
|
-
const a: unknown[] = [];
|
|
98
|
-
const b: unknown[] = [];
|
|
99
|
-
q.onTicketComplete((e) => a.push(e));
|
|
100
|
-
q.onTicketComplete((e) => b.push(e));
|
|
101
|
-
const t = q.enqueue(async () => "ok");
|
|
102
|
-
await q.flushAll();
|
|
103
|
-
await t.result;
|
|
104
|
-
expect(a).toHaveLength(1);
|
|
105
|
-
expect(b).toHaveLength(1);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it("a listener that throws does not block other listeners", async () => {
|
|
109
|
-
const q = createBootstrapQueue();
|
|
110
|
-
const seen: unknown[] = [];
|
|
111
|
-
q.onTicketComplete(() => {
|
|
112
|
-
throw new Error("listener crash");
|
|
113
|
-
});
|
|
114
|
-
q.onTicketComplete((e) => seen.push(e));
|
|
115
|
-
const t = q.enqueue(async () => "ok");
|
|
116
|
-
await q.flushAll();
|
|
117
|
-
await t.result;
|
|
118
|
-
expect(seen).toHaveLength(1);
|
|
119
|
-
});
|
|
120
|
-
});
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Route tests for `/api/bootstrap/*`.
|
|
3
|
-
*
|
|
4
|
-
* Spins up a minimal Fastify instance with the bootstrap routes wired
|
|
5
|
-
* to a fresh state store and a pair of spy triggers. No real network
|
|
6
|
-
* access, no real subprocesses.
|
|
7
|
-
*
|
|
8
|
-
* See change: unified-bootstrap-install.
|
|
9
|
-
*/
|
|
10
|
-
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
11
|
-
import Fastify, { type FastifyInstance } from "fastify";
|
|
12
|
-
import { createBootstrapState, type BootstrapStateStore } from "../bootstrap-state.js";
|
|
13
|
-
import { registerBootstrapRoutes } from "../routes/bootstrap-routes.js";
|
|
14
|
-
|
|
15
|
-
const noopGuard = async () => {
|
|
16
|
-
/* allow all requests in tests */
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
interface Harness {
|
|
20
|
-
app: FastifyInstance;
|
|
21
|
-
state: BootstrapStateStore;
|
|
22
|
-
upgradeCalls: string[];
|
|
23
|
-
retryCalls: string[];
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
async function makeHarness(): Promise<Harness> {
|
|
27
|
-
const app = Fastify({ logger: false });
|
|
28
|
-
const state = createBootstrapState();
|
|
29
|
-
const upgradeCalls: string[] = [];
|
|
30
|
-
const retryCalls: string[] = [];
|
|
31
|
-
|
|
32
|
-
registerBootstrapRoutes(app, {
|
|
33
|
-
bootstrapState: state,
|
|
34
|
-
networkGuard: noopGuard,
|
|
35
|
-
triggerUpgradePi: async (ticketId) => {
|
|
36
|
-
upgradeCalls.push(ticketId);
|
|
37
|
-
},
|
|
38
|
-
triggerRetry: async (ticketId) => {
|
|
39
|
-
retryCalls.push(ticketId);
|
|
40
|
-
},
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
await app.ready();
|
|
44
|
-
return { app, state, upgradeCalls, retryCalls };
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
describe("bootstrap-routes", () => {
|
|
48
|
-
let h: Harness;
|
|
49
|
-
|
|
50
|
-
beforeEach(async () => {
|
|
51
|
-
h = await makeHarness();
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
afterEach(async () => {
|
|
55
|
-
await h.app.close();
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
describe("GET /api/bootstrap/status", () => {
|
|
59
|
-
it("returns the current state (default ready)", async () => {
|
|
60
|
-
const res = await h.app.inject({ method: "GET", url: "/api/bootstrap/status" });
|
|
61
|
-
expect(res.statusCode).toBe(200);
|
|
62
|
-
expect(res.json()).toEqual({ status: "ready" });
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it("reflects subsequent state changes", async () => {
|
|
66
|
-
h.state.set({ status: "installing", progress: { step: "pi" } });
|
|
67
|
-
const res = await h.app.inject({ method: "GET", url: "/api/bootstrap/status" });
|
|
68
|
-
expect(res.json()).toMatchObject({
|
|
69
|
-
status: "installing",
|
|
70
|
-
progress: { step: "pi" },
|
|
71
|
-
});
|
|
72
|
-
});
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
describe("POST /api/bootstrap/upgrade-pi", () => {
|
|
76
|
-
it("returns 202 with a ticketId and invokes the trigger", async () => {
|
|
77
|
-
const res = await h.app.inject({ method: "POST", url: "/api/bootstrap/upgrade-pi" });
|
|
78
|
-
expect(res.statusCode).toBe(202);
|
|
79
|
-
const body = res.json() as { ticketId: string; status: string };
|
|
80
|
-
expect(body.status).toBe("accepted");
|
|
81
|
-
expect(typeof body.ticketId).toBe("string");
|
|
82
|
-
expect(body.ticketId.length).toBeGreaterThan(0);
|
|
83
|
-
// Trigger runs async — await a microtask.
|
|
84
|
-
await new Promise((r) => setImmediate(r));
|
|
85
|
-
expect(h.upgradeCalls).toEqual([body.ticketId]);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it("returns 409 when an install is already in progress", async () => {
|
|
89
|
-
h.state.set({ status: "installing" });
|
|
90
|
-
const res = await h.app.inject({ method: "POST", url: "/api/bootstrap/upgrade-pi" });
|
|
91
|
-
expect(res.statusCode).toBe(409);
|
|
92
|
-
expect(h.upgradeCalls).toEqual([]);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it("is allowed when status is failed (to upgrade after a previous failure)", async () => {
|
|
96
|
-
h.state.set({ status: "failed", error: { message: "network" } });
|
|
97
|
-
const res = await h.app.inject({ method: "POST", url: "/api/bootstrap/upgrade-pi" });
|
|
98
|
-
expect(res.statusCode).toBe(202);
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
describe("POST /api/bootstrap/retry", () => {
|
|
103
|
-
it("returns 409 when status is ready", async () => {
|
|
104
|
-
const res = await h.app.inject({ method: "POST", url: "/api/bootstrap/retry" });
|
|
105
|
-
expect(res.statusCode).toBe(409);
|
|
106
|
-
expect(h.retryCalls).toEqual([]);
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it("returns 409 when status is installing", async () => {
|
|
110
|
-
h.state.set({ status: "installing" });
|
|
111
|
-
const res = await h.app.inject({ method: "POST", url: "/api/bootstrap/retry" });
|
|
112
|
-
expect(res.statusCode).toBe(409);
|
|
113
|
-
expect(h.retryCalls).toEqual([]);
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it("returns 202 when status is failed and invokes the trigger", async () => {
|
|
117
|
-
h.state.set({ status: "failed", error: { message: "network" } });
|
|
118
|
-
const res = await h.app.inject({ method: "POST", url: "/api/bootstrap/retry" });
|
|
119
|
-
expect(res.statusCode).toBe(202);
|
|
120
|
-
const body = res.json() as { ticketId: string };
|
|
121
|
-
await new Promise((r) => setImmediate(r));
|
|
122
|
-
expect(h.retryCalls).toEqual([body.ticketId]);
|
|
123
|
-
});
|
|
124
|
-
});
|
|
125
|
-
});
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unit tests for the in-memory bootstrap state store.
|
|
3
|
-
*
|
|
4
|
-
* See change: unified-bootstrap-install.
|
|
5
|
-
*/
|
|
6
|
-
import { describe, it, expect } from "vitest";
|
|
7
|
-
import { createBootstrapState } from "../bootstrap-state.js";
|
|
8
|
-
|
|
9
|
-
describe("bootstrap-state", () => {
|
|
10
|
-
it("defaults to status=ready", () => {
|
|
11
|
-
const s = createBootstrapState();
|
|
12
|
-
expect(s.get()).toEqual({ status: "ready" });
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it("applies initial overrides", () => {
|
|
16
|
-
const s = createBootstrapState({
|
|
17
|
-
status: "installing",
|
|
18
|
-
progress: { step: "pi", output: "starting" },
|
|
19
|
-
});
|
|
20
|
-
const state = s.get();
|
|
21
|
-
expect(state.status).toBe("installing");
|
|
22
|
-
expect(state.progress).toEqual({ step: "pi", output: "starting" });
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it("set merges partial into state", () => {
|
|
26
|
-
const s = createBootstrapState();
|
|
27
|
-
s.set({ status: "installing", progress: { step: "pi" } });
|
|
28
|
-
expect(s.get().status).toBe("installing");
|
|
29
|
-
s.set({ progress: { step: "openspec" } });
|
|
30
|
-
expect(s.get().progress).toEqual({ step: "openspec" });
|
|
31
|
-
expect(s.get().status).toBe("installing");
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it("set with undefined explicitly clears a key", () => {
|
|
35
|
-
const s = createBootstrapState({ progress: { step: "pi" } });
|
|
36
|
-
expect(s.get().progress).toBeDefined();
|
|
37
|
-
s.set({ progress: undefined });
|
|
38
|
-
expect(s.get().progress).toBeUndefined();
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it("notifies subscribers on set", () => {
|
|
42
|
-
const s = createBootstrapState();
|
|
43
|
-
const calls: string[] = [];
|
|
44
|
-
s.subscribe((st) => calls.push(st.status));
|
|
45
|
-
s.set({ status: "installing" });
|
|
46
|
-
s.set({ status: "ready" });
|
|
47
|
-
expect(calls).toEqual(["installing", "ready"]);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it("subscribe returns an unsubscribe function", () => {
|
|
51
|
-
const s = createBootstrapState();
|
|
52
|
-
const calls: string[] = [];
|
|
53
|
-
const off = s.subscribe((st) => calls.push(st.status));
|
|
54
|
-
s.set({ status: "installing" });
|
|
55
|
-
off();
|
|
56
|
-
s.set({ status: "ready" });
|
|
57
|
-
expect(calls).toEqual(["installing"]);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it("listener errors do not stop other listeners", () => {
|
|
61
|
-
const s = createBootstrapState();
|
|
62
|
-
const calls: string[] = [];
|
|
63
|
-
s.subscribe(() => {
|
|
64
|
-
throw new Error("boom");
|
|
65
|
-
});
|
|
66
|
-
s.subscribe((st) => calls.push(st.status));
|
|
67
|
-
s.set({ status: "installing" });
|
|
68
|
-
expect(calls).toEqual(["installing"]);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it("dispose clears all listeners", () => {
|
|
72
|
-
const s = createBootstrapState();
|
|
73
|
-
const calls: string[] = [];
|
|
74
|
-
s.subscribe((st) => calls.push(st.status));
|
|
75
|
-
s.dispose();
|
|
76
|
-
s.set({ status: "installing" });
|
|
77
|
-
expect(calls).toEqual([]);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it("get returns a fresh snapshot (external mutation does not affect store)", () => {
|
|
81
|
-
const s = createBootstrapState({ progress: { step: "pi" } });
|
|
82
|
-
const snap = s.get();
|
|
83
|
-
snap.status = "failed";
|
|
84
|
-
expect(s.get().status).toBe("ready");
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
describe("lastInstallPackages", () => {
|
|
88
|
-
it("defaults to an empty array", () => {
|
|
89
|
-
const s = createBootstrapState();
|
|
90
|
-
expect(s.getLastInstallPackages()).toEqual([]);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it("records and returns a fresh copy", () => {
|
|
94
|
-
const s = createBootstrapState();
|
|
95
|
-
s.setLastInstallPackages(["pi", "openspec"]);
|
|
96
|
-
const got = s.getLastInstallPackages();
|
|
97
|
-
expect(got).toEqual(["pi", "openspec"]);
|
|
98
|
-
// External mutation does not affect the stored value.
|
|
99
|
-
got.push("tsx");
|
|
100
|
-
expect(s.getLastInstallPackages()).toEqual(["pi", "openspec"]);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it("accepts a readonly input without type error", () => {
|
|
104
|
-
const s = createBootstrapState();
|
|
105
|
-
const readonlyInput: readonly string[] = ["a", "b"];
|
|
106
|
-
s.setLastInstallPackages(readonlyInput);
|
|
107
|
-
expect(s.getLastInstallPackages()).toEqual(["a", "b"]);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it("is independent of status broadcast (not part of snapshot)", () => {
|
|
111
|
-
const s = createBootstrapState();
|
|
112
|
-
const seen: string[] = [];
|
|
113
|
-
s.subscribe((st) => seen.push(st.status));
|
|
114
|
-
s.setLastInstallPackages(["pi"]);
|
|
115
|
-
// setLastInstallPackages MUST NOT trigger a listener.
|
|
116
|
-
expect(seen).toEqual([]);
|
|
117
|
-
});
|
|
118
|
-
});
|
|
119
|
-
});
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Regression test: the local `registry.rescan(...)` call in
|
|
3
|
-
* `packages/server/src/cli.ts` was removed and ownership of the
|
|
4
|
-
* post-install rescan moved to the centralized
|
|
5
|
-
* `bootstrapState.subscribe` hook in `server.ts`.
|
|
6
|
-
*
|
|
7
|
-
* This test reads `cli.ts` as text and asserts no direct rescan call
|
|
8
|
-
* remains, plus a forwarding comment is present.
|
|
9
|
-
*
|
|
10
|
-
* See change: fix-openspec-buttons-after-bootstrap-install.
|
|
11
|
-
*/
|
|
12
|
-
import { describe, it, expect } from "vitest";
|
|
13
|
-
import { readFileSync } from "node:fs";
|
|
14
|
-
import { fileURLToPath } from "node:url";
|
|
15
|
-
import path from "node:path";
|
|
16
|
-
|
|
17
|
-
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
18
|
-
const cliPath = path.resolve(here, "..", "cli.ts");
|
|
19
|
-
|
|
20
|
-
describe("cli.ts post-install rescan ownership", () => {
|
|
21
|
-
const source = readFileSync(cliPath, "utf8");
|
|
22
|
-
|
|
23
|
-
it("does not contain a direct registry.rescan(...) call", () => {
|
|
24
|
-
// Allow comments mentioning rescan, but disallow real call expressions.
|
|
25
|
-
// Strip line comments and block comments first.
|
|
26
|
-
const stripped = source
|
|
27
|
-
.replace(/\/\/[^\n]*/g, "")
|
|
28
|
-
.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
29
|
-
expect(stripped).not.toMatch(/\.rescan\s*\(/);
|
|
30
|
-
expect(stripped).not.toMatch(/\bRescannable\b/);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it("contains a comment forwarding to the centralized server.ts hook", () => {
|
|
34
|
-
expect(source).toMatch(/fix-openspec-buttons-after-bootstrap-install/);
|
|
35
|
-
});
|
|
36
|
-
});
|