@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
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure tests for classifyBridgeSource — see change fix-pi-flows-end-to-end
|
|
3
|
+
* (Group 2, task 2.5).
|
|
4
|
+
*/
|
|
5
|
+
import { describe, it, expect } from "vitest";
|
|
6
|
+
import { classifyBridgeSource } from "../plugin-bridge-register.js";
|
|
7
|
+
|
|
8
|
+
describe("classifyBridgeSource", () => {
|
|
9
|
+
it("returns 'packages[]' when bridge path is in packages array (string form)", () => {
|
|
10
|
+
expect(
|
|
11
|
+
classifyBridgeSource({ packages: ["/abs/bridge.js"] }, "/abs/bridge.js"),
|
|
12
|
+
).toBe("packages[]");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("returns 'packages[]' for object-form package source", () => {
|
|
16
|
+
expect(
|
|
17
|
+
classifyBridgeSource(
|
|
18
|
+
{ packages: [{ source: "/abs/bridge.js", extensions: ["foo"] }] },
|
|
19
|
+
"/abs/bridge.js",
|
|
20
|
+
),
|
|
21
|
+
).toBe("packages[]");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("returns 'dashboardPluginBridges' when only the legacy key references it", () => {
|
|
25
|
+
expect(
|
|
26
|
+
classifyBridgeSource(
|
|
27
|
+
{ dashboardPluginBridges: { "dashboard-demo": "/abs/bridge.js" } },
|
|
28
|
+
"/abs/bridge.js",
|
|
29
|
+
),
|
|
30
|
+
).toBe("dashboardPluginBridges");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("packages[] wins over dashboardPluginBridges when both reference the same path", () => {
|
|
34
|
+
expect(
|
|
35
|
+
classifyBridgeSource(
|
|
36
|
+
{
|
|
37
|
+
packages: ["/abs/bridge.js"],
|
|
38
|
+
dashboardPluginBridges: { "dashboard-demo": "/abs/bridge.js" },
|
|
39
|
+
},
|
|
40
|
+
"/abs/bridge.js",
|
|
41
|
+
),
|
|
42
|
+
).toBe("packages[]");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("returns 'none' when neither registry references the path", () => {
|
|
46
|
+
expect(
|
|
47
|
+
classifyBridgeSource(
|
|
48
|
+
{ packages: ["/other"], dashboardPluginBridges: { "dashboard-other": "/y" } },
|
|
49
|
+
"/abs/bridge.js",
|
|
50
|
+
),
|
|
51
|
+
).toBe("none");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("returns 'none' for empty settings", () => {
|
|
55
|
+
expect(classifyBridgeSource({}, "/abs/bridge.js")).toBe("none");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("tolerates malformed settings (null / array / primitive)", () => {
|
|
59
|
+
expect(classifyBridgeSource(null, "/abs/bridge.js")).toBe("none");
|
|
60
|
+
expect(classifyBridgeSource([], "/abs/bridge.js")).toBe("none");
|
|
61
|
+
expect(classifyBridgeSource(42, "/abs/bridge.js")).toBe("none");
|
|
62
|
+
expect(classifyBridgeSource("string", "/abs/bridge.js")).toBe("none");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("ignores non-matching packages entries", () => {
|
|
66
|
+
expect(
|
|
67
|
+
classifyBridgeSource(
|
|
68
|
+
{ packages: ["/a", { source: "/b" }, { not_source: "/abs/bridge.js" }] },
|
|
69
|
+
"/abs/bridge.js",
|
|
70
|
+
),
|
|
71
|
+
).toBe("none");
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -48,7 +48,10 @@ describe("bridge auto-register boot + disable lifecycle", () => {
|
|
|
48
48
|
expect(Object.keys(managed)).toHaveLength(0);
|
|
49
49
|
});
|
|
50
50
|
|
|
51
|
-
it("preserves user-owned packages array", () => {
|
|
51
|
+
it("preserves user-owned packages array and appends managed bridge", () => {
|
|
52
|
+
// Per change fix-pi-flows-end-to-end Group 1: dual-write appends the
|
|
53
|
+
// managed bridge to packages[] (with ownership marker) while leaving
|
|
54
|
+
// user entries untouched in original order.
|
|
52
55
|
fs.mkdirSync(path.join(homedir, ".pi", "agent"), { recursive: true });
|
|
53
56
|
fs.writeFileSync(
|
|
54
57
|
settingsPath(),
|
|
@@ -58,15 +61,24 @@ describe("bridge auto-register boot + disable lifecycle", () => {
|
|
|
58
61
|
);
|
|
59
62
|
registerPluginBridge("demo", "/demo/bridge.js", { homedir });
|
|
60
63
|
const settings = readSettings();
|
|
61
|
-
expect(settings.packages).toEqual([
|
|
64
|
+
expect(settings.packages).toEqual([
|
|
65
|
+
"/user/my-extension",
|
|
66
|
+
"/user/another",
|
|
67
|
+
"/demo/bridge.js",
|
|
68
|
+
]);
|
|
62
69
|
});
|
|
63
70
|
|
|
64
71
|
it("surfaces path-mismatch conflict without overwriting", () => {
|
|
65
|
-
|
|
66
|
-
|
|
72
|
+
// On-disk path must exist for a real conflict (self-heal would otherwise
|
|
73
|
+
// silently replace — see add-plugin-activation-ui).
|
|
74
|
+
const oldPath = path.join(tmpDir, "old-bridge.js");
|
|
75
|
+
const newPath = path.join(tmpDir, "new-bridge.js");
|
|
76
|
+
fs.writeFileSync(oldPath, "// existing bridge");
|
|
77
|
+
registerPluginBridge("openspec", oldPath, { homedir });
|
|
78
|
+
const result = registerPluginBridge("openspec", newPath, { homedir });
|
|
67
79
|
expect(result.type).toBe("conflict");
|
|
68
80
|
// Original path preserved
|
|
69
81
|
const managed = listManagedBridges({ homedir });
|
|
70
|
-
expect(managed["dashboard-openspec"]).toBe(
|
|
82
|
+
expect(managed["dashboard-openspec"]).toBe(oldPath);
|
|
71
83
|
});
|
|
72
84
|
});
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the dual-write (packages[] + dashboardPluginBridges) and
|
|
3
|
+
* reconciliation behaviour added by change `fix-pi-flows-end-to-end`
|
|
4
|
+
* Group 1.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import os from "node:os";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import {
|
|
11
|
+
registerPluginBridge,
|
|
12
|
+
deregisterPluginBridge,
|
|
13
|
+
reconcilePluginBridgePackages,
|
|
14
|
+
ensurePackageEntry,
|
|
15
|
+
removePackageEntry,
|
|
16
|
+
listManagedPackageOwnership,
|
|
17
|
+
} from "../plugin-bridge-register.js";
|
|
18
|
+
|
|
19
|
+
let tmpDir: string;
|
|
20
|
+
let homedir: string;
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "plugin-bridge-pkg-test-"));
|
|
24
|
+
homedir = tmpDir;
|
|
25
|
+
delete process.env.PI_DASHBOARD_DISABLE_PLUGIN_BRIDGE_PACKAGES_WRITE;
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
30
|
+
delete process.env.PI_DASHBOARD_DISABLE_PLUGIN_BRIDGE_PACKAGES_WRITE;
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
function settingsPath() {
|
|
34
|
+
return path.join(homedir, ".pi", "agent", "settings.json");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function readSettings(): Record<string, unknown> {
|
|
38
|
+
return JSON.parse(fs.readFileSync(settingsPath(), "utf-8"));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function writeSettings(s: Record<string, unknown>) {
|
|
42
|
+
fs.mkdirSync(path.dirname(settingsPath()), { recursive: true });
|
|
43
|
+
fs.writeFileSync(settingsPath(), JSON.stringify(s, null, 2) + "\n");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
47
|
+
// Pure helpers
|
|
48
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
describe("ensurePackageEntry / removePackageEntry (pure)", () => {
|
|
51
|
+
it("adds new entry and records ownership", () => {
|
|
52
|
+
const packages: unknown[] = [];
|
|
53
|
+
const ownership: Record<string, string> = {};
|
|
54
|
+
const added = ensurePackageEntry(packages, ownership, "/a", "dashboard-x");
|
|
55
|
+
expect(added).toBe(true);
|
|
56
|
+
expect(packages).toEqual(["/a"]);
|
|
57
|
+
expect(ownership).toEqual({ "/a": "dashboard-x" });
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("is idempotent (no-op when present)", () => {
|
|
61
|
+
const packages: unknown[] = ["/a"];
|
|
62
|
+
const ownership: Record<string, string> = { "/a": "dashboard-x" };
|
|
63
|
+
const added = ensurePackageEntry(packages, ownership, "/a", "dashboard-x");
|
|
64
|
+
expect(added).toBe(false);
|
|
65
|
+
expect(packages).toEqual(["/a"]);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("records ownership for pre-existing user entry without duplicating", () => {
|
|
69
|
+
const packages: unknown[] = ["/a"]; // user added
|
|
70
|
+
const ownership: Record<string, string> = {};
|
|
71
|
+
const added = ensurePackageEntry(packages, ownership, "/a", "dashboard-x");
|
|
72
|
+
expect(added).toBe(false);
|
|
73
|
+
expect(packages).toEqual(["/a"]);
|
|
74
|
+
expect(ownership).toEqual({ "/a": "dashboard-x" });
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("removes only owned entries; leaves user entries", () => {
|
|
78
|
+
const packages: unknown[] = ["/user", "/a"];
|
|
79
|
+
const ownership: Record<string, string> = { "/a": "dashboard-x" };
|
|
80
|
+
const removed = removePackageEntry(packages, ownership, "dashboard-x");
|
|
81
|
+
expect(removed).toBe(true);
|
|
82
|
+
expect(packages).toEqual(["/user"]);
|
|
83
|
+
expect(ownership).toEqual({});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("multi-owner round trip", () => {
|
|
87
|
+
const packages: unknown[] = [];
|
|
88
|
+
const ownership: Record<string, string> = {};
|
|
89
|
+
ensurePackageEntry(packages, ownership, "/a", "dashboard-x");
|
|
90
|
+
ensurePackageEntry(packages, ownership, "/b", "dashboard-y");
|
|
91
|
+
expect(packages).toEqual(["/a", "/b"]);
|
|
92
|
+
removePackageEntry(packages, ownership, "dashboard-x");
|
|
93
|
+
expect(packages).toEqual(["/b"]);
|
|
94
|
+
expect(ownership).toEqual({ "/b": "dashboard-y" });
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("handles object-form PackageSource entries", () => {
|
|
98
|
+
const packages: unknown[] = [{ source: "/a" }];
|
|
99
|
+
const ownership: Record<string, string> = {};
|
|
100
|
+
const added = ensurePackageEntry(packages, ownership, "/a", "dashboard-x");
|
|
101
|
+
expect(added).toBe(false);
|
|
102
|
+
expect(packages).toEqual([{ source: "/a" }]);
|
|
103
|
+
expect(ownership).toEqual({ "/a": "dashboard-x" });
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
108
|
+
// Dual-write end-to-end
|
|
109
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
describe("registerPluginBridge dual-write", () => {
|
|
112
|
+
it("writes both dashboardPluginBridges AND packages[] entries", () => {
|
|
113
|
+
registerPluginBridge("demo", "/abs/bridge.js", { homedir });
|
|
114
|
+
const s = readSettings();
|
|
115
|
+
expect((s.dashboardPluginBridges as Record<string, string>)["dashboard-demo"]).toBe(
|
|
116
|
+
"/abs/bridge.js",
|
|
117
|
+
);
|
|
118
|
+
expect(s.packages).toContain("/abs/bridge.js");
|
|
119
|
+
expect(
|
|
120
|
+
(s._dashboardManagedPackages as Record<string, string>)["/abs/bridge.js"],
|
|
121
|
+
).toBe("dashboard-demo");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("env escape hatch skips packages[] write", () => {
|
|
125
|
+
process.env.PI_DASHBOARD_DISABLE_PLUGIN_BRIDGE_PACKAGES_WRITE = "1";
|
|
126
|
+
registerPluginBridge("demo", "/abs/bridge.js", { homedir });
|
|
127
|
+
const s = readSettings();
|
|
128
|
+
expect((s.dashboardPluginBridges as Record<string, string>)["dashboard-demo"]).toBe(
|
|
129
|
+
"/abs/bridge.js",
|
|
130
|
+
);
|
|
131
|
+
expect(s.packages ?? []).not.toContain("/abs/bridge.js");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("idempotent across repeated registration", () => {
|
|
135
|
+
registerPluginBridge("demo", "/abs/bridge.js", { homedir });
|
|
136
|
+
registerPluginBridge("demo", "/abs/bridge.js", { homedir });
|
|
137
|
+
const s = readSettings();
|
|
138
|
+
const pkgs = s.packages as unknown[];
|
|
139
|
+
expect(pkgs.filter((e) => e === "/abs/bridge.js")).toHaveLength(1);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("preserves user-added packages entry on conflict-free re-register", () => {
|
|
143
|
+
writeSettings({ packages: ["/user/pkg"] });
|
|
144
|
+
registerPluginBridge("demo", "/abs/bridge.js", { homedir });
|
|
145
|
+
const s = readSettings();
|
|
146
|
+
expect(s.packages).toEqual(["/user/pkg", "/abs/bridge.js"]);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe("deregisterPluginBridge dual-remove", () => {
|
|
151
|
+
it("removes both dashboardPluginBridges AND packages[] entries; keeps user entries", () => {
|
|
152
|
+
writeSettings({ packages: ["/user/pkg"] });
|
|
153
|
+
registerPluginBridge("demo", "/abs/bridge.js", { homedir });
|
|
154
|
+
deregisterPluginBridge("demo", { homedir });
|
|
155
|
+
const s = readSettings();
|
|
156
|
+
expect((s.dashboardPluginBridges as Record<string, string>)["dashboard-demo"]).toBeUndefined();
|
|
157
|
+
expect(s.packages).toEqual(["/user/pkg"]);
|
|
158
|
+
expect((s._dashboardManagedPackages as Record<string, string>)["/abs/bridge.js"]).toBeUndefined();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("no-op when plugin never registered", () => {
|
|
162
|
+
writeSettings({ packages: ["/user/pkg"] });
|
|
163
|
+
deregisterPluginBridge("ghost", { homedir });
|
|
164
|
+
const s = readSettings();
|
|
165
|
+
expect(s.packages).toEqual(["/user/pkg"]);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe("reconcilePluginBridgePackages", () => {
|
|
170
|
+
it("adds missing packages[] entry for pre-existing dashboardPluginBridges key", () => {
|
|
171
|
+
writeSettings({
|
|
172
|
+
dashboardPluginBridges: { "dashboard-demo": "/abs/bridge.js" },
|
|
173
|
+
packages: ["/user/pkg"],
|
|
174
|
+
});
|
|
175
|
+
const summary = reconcilePluginBridgePackages({ homedir });
|
|
176
|
+
expect(summary).toEqual([
|
|
177
|
+
{ pluginId: "demo", bridgePath: "/abs/bridge.js", action: "added" },
|
|
178
|
+
]);
|
|
179
|
+
const s = readSettings();
|
|
180
|
+
expect(s.packages).toContain("/abs/bridge.js");
|
|
181
|
+
expect(
|
|
182
|
+
(s._dashboardManagedPackages as Record<string, string>)["/abs/bridge.js"],
|
|
183
|
+
).toBe("dashboard-demo");
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("is idempotent — second run produces no mutation", () => {
|
|
187
|
+
writeSettings({
|
|
188
|
+
dashboardPluginBridges: { "dashboard-demo": "/abs/bridge.js" },
|
|
189
|
+
});
|
|
190
|
+
reconcilePluginBridgePackages({ homedir });
|
|
191
|
+
const summary = reconcilePluginBridgePackages({ homedir });
|
|
192
|
+
expect(summary).toEqual([
|
|
193
|
+
{ pluginId: "demo", bridgePath: "/abs/bridge.js", action: "already" },
|
|
194
|
+
]);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("env escape hatch produces empty summary and no writes", () => {
|
|
198
|
+
process.env.PI_DASHBOARD_DISABLE_PLUGIN_BRIDGE_PACKAGES_WRITE = "1";
|
|
199
|
+
writeSettings({
|
|
200
|
+
dashboardPluginBridges: { "dashboard-demo": "/abs/bridge.js" },
|
|
201
|
+
});
|
|
202
|
+
const summary = reconcilePluginBridgePackages({ homedir });
|
|
203
|
+
expect(summary).toEqual([]);
|
|
204
|
+
const s = readSettings();
|
|
205
|
+
expect(s.packages ?? []).not.toContain("/abs/bridge.js");
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("preserves user packages while reconciling multiple managed bridges", () => {
|
|
209
|
+
writeSettings({
|
|
210
|
+
dashboardPluginBridges: {
|
|
211
|
+
"dashboard-x": "/abs/x.js",
|
|
212
|
+
"dashboard-y": "/abs/y.js",
|
|
213
|
+
},
|
|
214
|
+
packages: ["/user/a", { source: "/user/b" }],
|
|
215
|
+
});
|
|
216
|
+
reconcilePluginBridgePackages({ homedir });
|
|
217
|
+
const s = readSettings();
|
|
218
|
+
expect(s.packages).toEqual(["/user/a", { source: "/user/b" }, "/abs/x.js", "/abs/y.js"]);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
describe("listManagedPackageOwnership", () => {
|
|
223
|
+
it("returns empty map when nothing registered", () => {
|
|
224
|
+
expect(listManagedPackageOwnership({ homedir })).toEqual({});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("returns ownership map after dual-write registration", () => {
|
|
228
|
+
registerPluginBridge("demo", "/abs/bridge.js", { homedir });
|
|
229
|
+
expect(listManagedPackageOwnership({ homedir })).toEqual({
|
|
230
|
+
"/abs/bridge.js": "dashboard-demo",
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
});
|
|
@@ -44,21 +44,29 @@ describe("registerPluginBridge", () => {
|
|
|
44
44
|
});
|
|
45
45
|
|
|
46
46
|
it("returns conflict when entry exists with different path", () => {
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
// The on-disk path must exist for a real conflict (otherwise the
|
|
48
|
+
// self-heal path silently replaces — see add-plugin-activation-ui).
|
|
49
|
+
const oldPath = path.join(tmpDir, "old-bridge.js");
|
|
50
|
+
const newPath = path.join(tmpDir, "new-bridge.js");
|
|
51
|
+
fs.writeFileSync(oldPath, "// existing bridge");
|
|
52
|
+
registerPluginBridge("demo", oldPath, { homedir });
|
|
53
|
+
const result = registerPluginBridge("demo", newPath, { homedir });
|
|
49
54
|
expect(result.type).toBe("conflict");
|
|
50
55
|
if (result.type === "conflict") {
|
|
51
|
-
expect(result.existingPath).toBe(
|
|
52
|
-
expect(result.newPath).toBe(
|
|
56
|
+
expect(result.existingPath).toBe(oldPath);
|
|
57
|
+
expect(result.newPath).toBe(newPath);
|
|
53
58
|
}
|
|
54
59
|
// Should not overwrite
|
|
55
60
|
const s = readSettings();
|
|
56
61
|
const managed = s.dashboardPluginBridges as Record<string, string>;
|
|
57
|
-
expect(managed["dashboard-demo"]).toBe(
|
|
62
|
+
expect(managed["dashboard-demo"]).toBe(oldPath);
|
|
58
63
|
});
|
|
59
64
|
|
|
60
|
-
it("
|
|
61
|
-
//
|
|
65
|
+
it("appends managed bridge to packages[] while preserving user-owned entries", () => {
|
|
66
|
+
// Per change fix-pi-flows-end-to-end Group 1: dual-write into packages[]
|
|
67
|
+
// is required (pi-coding-agent reads packages[], not dashboardPluginBridges).
|
|
68
|
+
// User entries MUST be preserved in original order; the managed bridge
|
|
69
|
+
// path MUST be appended and recorded in the ownership map.
|
|
62
70
|
fs.mkdirSync(path.join(homedir, ".pi", "agent"), { recursive: true });
|
|
63
71
|
fs.writeFileSync(
|
|
64
72
|
settingsPath(),
|
|
@@ -67,8 +75,10 @@ describe("registerPluginBridge", () => {
|
|
|
67
75
|
|
|
68
76
|
registerPluginBridge("demo", "/demo/bridge.js", { homedir });
|
|
69
77
|
const s = readSettings();
|
|
70
|
-
|
|
71
|
-
expect(
|
|
78
|
+
expect(s.packages).toEqual(["/user/extension1", "/user/extension2", "/demo/bridge.js"]);
|
|
79
|
+
expect((s._dashboardManagedPackages as Record<string, string>)["/demo/bridge.js"]).toBe(
|
|
80
|
+
"dashboard-demo",
|
|
81
|
+
);
|
|
72
82
|
});
|
|
73
83
|
});
|
|
74
84
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Repo-level invariant: `.github/workflows/publish.yml`'s `electron` job
|
|
3
|
-
* MUST `needs: [prepare, publish]`
|
|
3
|
+
* MUST `needs: [prepare, publish]` AND MUST delegate to the reusable
|
|
4
|
+
* workflow `.github/workflows/_electron-build.yml`, which MUST itself set
|
|
5
|
+
* `strategy.fail-fast: false`.
|
|
4
6
|
*
|
|
5
7
|
* Why: the bundled-server step in the electron matrix runs `npm install`
|
|
6
8
|
* against the live npm registry, which depends on `@blackbelt-technology/*`
|
|
@@ -12,14 +14,18 @@
|
|
|
12
14
|
* other four matrix variants — losing diagnostic output and wasting runner
|
|
13
15
|
* minutes.
|
|
14
16
|
*
|
|
15
|
-
* If this test fails, restore the
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
17
|
+
* If this test fails, restore the contract in `publish.yml` + `_electron-build.yml`:
|
|
18
|
+
* publish.yml:
|
|
19
|
+
* electron:
|
|
20
|
+
* needs: [prepare, publish]
|
|
21
|
+
* uses: ./.github/workflows/_electron-build.yml
|
|
22
|
+
* _electron-build.yml:
|
|
23
|
+
* jobs:
|
|
24
|
+
* build:
|
|
25
|
+
* strategy:
|
|
26
|
+
* fail-fast: false
|
|
21
27
|
*
|
|
22
|
-
* See
|
|
28
|
+
* See changes: publish-fix-macos, add-ci-electron-on-demand-build.
|
|
23
29
|
*/
|
|
24
30
|
import { describe, it, expect } from "vitest";
|
|
25
31
|
import fs from "node:fs";
|
|
@@ -29,6 +35,18 @@ import url from "node:url";
|
|
|
29
35
|
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
|
|
30
36
|
const REPO_ROOT = path.resolve(__dirname, "..", "..", "..", "..");
|
|
31
37
|
const WORKFLOW_PATH = path.join(REPO_ROOT, ".github", "workflows", "publish.yml");
|
|
38
|
+
const REUSABLE_WORKFLOW_PATH = path.join(
|
|
39
|
+
REPO_ROOT,
|
|
40
|
+
".github",
|
|
41
|
+
"workflows",
|
|
42
|
+
"_electron-build.yml",
|
|
43
|
+
);
|
|
44
|
+
const CI_ELECTRON_WORKFLOW_PATH = path.join(
|
|
45
|
+
REPO_ROOT,
|
|
46
|
+
".github",
|
|
47
|
+
"workflows",
|
|
48
|
+
"ci-electron.yml",
|
|
49
|
+
);
|
|
32
50
|
|
|
33
51
|
/**
|
|
34
52
|
* Extract the YAML body of a top-level job by name. Returns the lines
|
|
@@ -105,21 +123,108 @@ describe("publish.yml — electron job dependency-graph contract", () => {
|
|
|
105
123
|
expect(names).toContain("publish");
|
|
106
124
|
});
|
|
107
125
|
|
|
108
|
-
it("electron job
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
|
|
126
|
+
it("electron job delegates to the reusable workflow via `uses:`", () => {
|
|
127
|
+
// After change add-ci-electron-on-demand-build, the electron job is a
|
|
128
|
+
// thin consumer of the shared workflow. The same definition serves
|
|
129
|
+
// both publish.yml and ci-electron.yml.
|
|
130
|
+
const m = electronBlock.match(
|
|
131
|
+
/^\s{4}uses:\s*\.\/\.github\/workflows\/_electron-build\.yml\s*$/m,
|
|
132
|
+
);
|
|
112
133
|
if (!m) {
|
|
113
134
|
throw new Error(
|
|
114
|
-
"electron job
|
|
115
|
-
"
|
|
116
|
-
"
|
|
135
|
+
"electron job MUST be `uses: ./.github/workflows/_electron-build.yml`. " +
|
|
136
|
+
"The reusable workflow is the sole definition of the build matrix. " +
|
|
137
|
+
"See change: add-ci-electron-on-demand-build.\n" +
|
|
117
138
|
"Job block was:\n" +
|
|
118
139
|
electronBlock,
|
|
119
140
|
);
|
|
120
141
|
}
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe("_electron-build.yml — reusable workflow contract", () => {
|
|
146
|
+
const reusableYaml = fs.readFileSync(REUSABLE_WORKFLOW_PATH, "utf8");
|
|
147
|
+
|
|
148
|
+
it("sets `fail-fast: false` on the build job", () => {
|
|
149
|
+
// Without `fail-fast: false`, a single OS failure cascades and cancels the
|
|
150
|
+
// other matrix variants — losing diagnostic output and wasting runner
|
|
151
|
+
// minutes. Locked here because the assertion moved out of publish.yml
|
|
152
|
+
// in change add-ci-electron-on-demand-build (electron job is now a thin
|
|
153
|
+
// `uses:` shim). See change: publish-fix-macos.
|
|
154
|
+
const m = reusableYaml.match(/^\s+fail-fast:\s*(\S+)\s*$/m);
|
|
155
|
+
if (!m) {
|
|
156
|
+
throw new Error(
|
|
157
|
+
"_electron-build.yml `strategy.fail-fast` is absent — the GitHub Actions " +
|
|
158
|
+
"default of `true` would re-introduce the run-#34 cascade. " +
|
|
159
|
+
"Set `fail-fast: false` on jobs.build.strategy. See change: publish-fix-macos.",
|
|
160
|
+
);
|
|
161
|
+
}
|
|
121
162
|
expect(m[1]).toBe("false");
|
|
122
163
|
});
|
|
164
|
+
|
|
165
|
+
it("declares the input contract documented in design.md", () => {
|
|
166
|
+
// Inputs locked: version, ref, legs, source_only_bundle,
|
|
167
|
+
// artifact_retention_days, artifact_name_suffix. Adding inputs is
|
|
168
|
+
// safe; removing or renaming requires updating both callers
|
|
169
|
+
// (publish.yml + ci-electron.yml) plus the spec.
|
|
170
|
+
for (const key of [
|
|
171
|
+
"version",
|
|
172
|
+
"ref",
|
|
173
|
+
"legs",
|
|
174
|
+
"source_only_bundle",
|
|
175
|
+
"artifact_retention_days",
|
|
176
|
+
]) {
|
|
177
|
+
const re = new RegExp(`^\\s+${key}:\\s*$`, "m");
|
|
178
|
+
if (!re.test(reusableYaml)) {
|
|
179
|
+
throw new Error(
|
|
180
|
+
`_electron-build.yml is missing required input '${key}'. ` +
|
|
181
|
+
"See change: add-ci-electron-on-demand-build (design.md Decision 2).",
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("contains a runnable-bundle assertion step (fix-ci-electron-runnable-bundles)", () => {
|
|
188
|
+
// Defence-in-depth gate: when `inputs.source_only_bundle == false`, the
|
|
189
|
+
// reusable workflow MUST verify that bundle-server.mjs produced a
|
|
190
|
+
// complete `resources/server/node_modules/@blackbelt-technology/
|
|
191
|
+
// pi-dashboard-server/src/cli.ts`. Without this, a regression in
|
|
192
|
+
// sync-versions.js or the bundle layout could silently ship a
|
|
193
|
+
// non-runnable artefact — the exact failure mode that motivated
|
|
194
|
+
// change fix-ci-electron-runnable-bundles. Match a permissive name
|
|
195
|
+
// regex so the step can be renamed without breaking the contract, as
|
|
196
|
+
// long as intent is preserved.
|
|
197
|
+
const stepNameRe = /^\s+-\s+name:\s*.*(runnable[-\s]bundle|cli\.ts.*exists).*/im;
|
|
198
|
+
if (!stepNameRe.test(reusableYaml)) {
|
|
199
|
+
throw new Error(
|
|
200
|
+
"_electron-build.yml is missing the runnable-bundle assertion step. " +
|
|
201
|
+
"Expected a step whose `name:` matches /runnable[- ]bundle|cli\\.ts.*exists/i. " +
|
|
202
|
+
"See change: fix-ci-electron-runnable-bundles.",
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("contains no forbidden side-effect actions (npm publish, gh-release, tag push)", () => {
|
|
208
|
+
// The reusable workflow MUST be a pure artifact producer. Publishing
|
|
209
|
+
// remains the sole responsibility of publish.yml's `publish` +
|
|
210
|
+
// `github-release` jobs. See change: add-ci-electron-on-demand-build
|
|
211
|
+
// (proposal.md "Safety lints" + spec ci-electron-on-demand-build).
|
|
212
|
+
const forbidden = [
|
|
213
|
+
/softprops\/action-gh-release/,
|
|
214
|
+
/actions\/create-release/,
|
|
215
|
+
/npm\s+publish/,
|
|
216
|
+
/git\s+push\s+origin\s+v/,
|
|
217
|
+
];
|
|
218
|
+
for (const re of forbidden) {
|
|
219
|
+
if (re.test(reusableYaml)) {
|
|
220
|
+
throw new Error(
|
|
221
|
+
`_electron-build.yml contains forbidden action matching ${re}. ` +
|
|
222
|
+
"The reusable workflow MUST NOT publish or release. See change: " +
|
|
223
|
+
"add-ci-electron-on-demand-build.",
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
});
|
|
123
228
|
});
|
|
124
229
|
|
|
125
230
|
// ── Prerelease safety contract ───────────────────────────────────────────────────────
|
|
@@ -202,6 +307,40 @@ function parseJobSteps(jobBlock: string): Array<{ run: string }> {
|
|
|
202
307
|
return steps;
|
|
203
308
|
}
|
|
204
309
|
|
|
310
|
+
describe("ci-electron.yml — runnable-bundle contract", () => {
|
|
311
|
+
// Pin the post-fix-ci-electron-runnable-bundles invariant: CI-dispatched
|
|
312
|
+
// Electron artefacts MUST ship with a complete `resources/server/
|
|
313
|
+
// node_modules/` tree so the unzipped installer is runnable on a user's
|
|
314
|
+
// desktop. The earlier value (`true`, from add-ci-electron-on-demand-build
|
|
315
|
+
// Decision 3) was invalidated by eliminate-electron-runtime-install's
|
|
316
|
+
// removal of every runtime install path. See change:
|
|
317
|
+
// fix-ci-electron-runnable-bundles.
|
|
318
|
+
const ciYaml = fs.readFileSync(CI_ELECTRON_WORKFLOW_PATH, "utf8");
|
|
319
|
+
|
|
320
|
+
it("build job passes `source_only_bundle: false` to the reusable workflow", () => {
|
|
321
|
+
// Match the key in any whitespace alignment, but the value MUST be
|
|
322
|
+
// the literal `false`. `true` would re-introduce the broken-on-unzip
|
|
323
|
+
// failure mode (BundledServerMissingError on cli.ts).
|
|
324
|
+
const m = ciYaml.match(/^\s+source_only_bundle:\s*(\S+)\s*$/m);
|
|
325
|
+
if (!m) {
|
|
326
|
+
throw new Error(
|
|
327
|
+
"ci-electron.yml does not pass `source_only_bundle:` to the reusable " +
|
|
328
|
+
"workflow. Expected `source_only_bundle: false`. See change: " +
|
|
329
|
+
"fix-ci-electron-runnable-bundles.",
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
if (m[1] !== "false") {
|
|
333
|
+
throw new Error(
|
|
334
|
+
`ci-electron.yml passes \`source_only_bundle: ${m[1]}\` — ` +
|
|
335
|
+
"expected `false`. Source-only bundles ship without " +
|
|
336
|
+
"`resources/server/node_modules/` and fail at launch with " +
|
|
337
|
+
"BundledServerMissingError. See change: fix-ci-electron-runnable-bundles.",
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
expect(m[1]).toBe("false");
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
|
|
205
344
|
describe("publish.yml — prepare job lockfile-regen contract", () => {
|
|
206
345
|
const yaml = fs.readFileSync(WORKFLOW_PATH, "utf8");
|
|
207
346
|
const prepareBlock = extractJobBlock(yaml, "prepare");
|