@huseynsnmz/pi-board 0.15.3 → 0.16.0
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/CHANGELOG.md +13 -4
- package/CONTRIBUTING.md +7 -0
- package/README.md +53 -419
- package/SECURITY.md +19 -0
- package/docs/how-it-works.md +31 -0
- package/docs/reference.md +276 -0
- package/package.json +5 -11
- package/src/index.ts +1 -1
- package/src/runtime/service.mjs +12 -0
- package/src/ui/dashboard.ts +14 -8
- package/IMPLEMENTATION_PLAN.md +0 -920
- package/PRD.md +0 -484
- package/PROGRESS.md +0 -142
- package/VERIFY.md +0 -115
- package/docs/BATCH_SELECTION_READ_FLOW.md +0 -277
- package/docs/EXPLORATION.md +0 -187
- package/docs/PTY_ATTACH_IMPLEMENTATION_PLAN.md +0 -581
- package/docs/RELEASE.md +0 -148
- package/docs/superpowers/plans/2026-08-15-screenlog-gc.md +0 -704
- package/docs/superpowers/plans/2026-08-16-attach-double-cursor-jiggle-retry.md +0 -499
- package/docs/superpowers/plans/2026-08-21-attach-coldstart-jiggle-rearm.md +0 -797
- package/docs/superpowers/plans/2026-08-21-dashboard-keypress-lag.md +0 -366
- package/docs/superpowers/plans/2026-08-21-issue-14-needs-input-no-auto-done.md +0 -576
- package/docs/superpowers/plans/2026-08-22-jiggle-shrink-and-hold.md +0 -337
- package/docs/superpowers/plans/2026-08-22-launch-cwd-favorites.md +0 -916
- package/docs/superpowers/plans/2026-08-22-question-tool-grouping.md +0 -211
- package/docs/superpowers/plans/2026-08-22-rename-state-labels.md +0 -182
- package/docs/superpowers/plans/2026-08-22-stable-list-order.md +0 -51
- package/docs/superpowers/plans/2026-08-27-locks-acquirelock-spin.md +0 -614
- package/docs/superpowers/plans/2026-08-27-pty-runner-test-flaky.md +0 -32
- package/docs/superpowers/plans/2026-08-29-code-refs-badges.md +0 -223
- package/docs/superpowers/plans/2026-08-30-circular-navigation.md +0 -308
- package/docs/superpowers/plans/2026-08-30-post-exit-timing-fix.md +0 -37
- package/docs/superpowers/plans/2026-08-30-pty-attach-quality-debt.md +0 -311
- package/docs/superpowers/plans/2026-08-30-readme-v2.md +0 -294
- package/docs/superpowers/plans/2026-09-01-attach-detach-gate-cursor-anchor.md +0 -284
- package/docs/superpowers/plans/2026-09-02-attach-detach-editor-state.md +0 -722
- package/docs/superpowers/plans/2026-09-03-code-refs-pr-backlink-narrow.md +0 -551
- package/docs/superpowers/plans/2026-09-03-detach-gate-glyph-fallback.md +0 -146
- package/docs/superpowers/plans/2026-09-04-evidence-outputpreview.md +0 -209
- package/docs/superpowers/plans/2026-09-04-warm-host-reclaim.md +0 -796
- package/docs/superpowers/plans/2026-09-05-issue-13-drainnextfollowup-pty-probe.md +0 -114
- package/docs/superpowers/plans/2026-09-05-issue-38-windows-wezterm-ime-cursor.md +0 -73
- package/docs/superpowers/plans/2026-09-05-issue-39-truncate-codepoint-boundary.md +0 -143
- package/docs/superpowers/plans/2026-09-05-issue-61-mention-fallback-guards.md +0 -226
- package/docs/superpowers/plans/2026-09-05-issue-63-flaky-manual-completion.md +0 -87
- package/docs/superpowers/plans/2026-09-05-issue-64-changelog-release-helper.md +0 -53
- package/docs/superpowers/plans/2026-09-05-pty-host-stacking-sock-race.md +0 -731
- package/docs/superpowers/plans/2026-09-08-attach-ctrl-left-detach.md +0 -30
- package/docs/superpowers/plans/2026-09-08-dashboard-shrink-repaint.md +0 -68
- package/docs/superpowers/plans/2026-09-08-issue-11-attach-runtime-desync-heal.md +0 -917
- package/docs/superpowers/plans/2026-09-08-legacy-stale-host-recovery.md +0 -125
- package/docs/superpowers/plans/2026-09-08-spawn-async-error-swallow.md +0 -56
- package/docs/superpowers/plans/2026-09-08-stale-model-attach-guard.md +0 -96
- package/docs/superpowers/plans/2026-09-09-claimpid-blocks-replace.md +0 -267
- package/docs/superpowers/plans/2026-09-09-harden-runner-architecture.md +0 -603
- package/docs/superpowers/plans/2026-09-09-single-writer-completion.md +0 -252
- package/docs/superpowers/plans/2026-09-10-reader-consistency.md +0 -115
- package/docs/superpowers/plans/2026-09-14-attach-cursor-dectcem-gate.md +0 -469
- package/docs/superpowers/plans/2026-09-14-attach-snapshot.md +0 -92
- package/docs/superpowers/plans/2026-09-14-host-meta-orphan-lock.md +0 -771
- package/docs/superpowers/plans/2026-09-14-issue-106-terminal-frame-cognition.md +0 -299
- package/docs/superpowers/plans/2026-09-14-issue-113-foreground-preview-race.md +0 -609
- package/docs/superpowers/plans/2026-09-14-terminal-model.md +0 -145
- package/docs/superpowers/plans/2026-09-15-coordinator-pipe-root-normalize.md +0 -224
- package/docs/superpowers/plans/2026-09-15-lease-publish-eprem-reclaim.md +0 -341
- package/docs/superpowers/plans/2026-09-18-detach-anchor-reporter-endpoint.md +0 -875
- package/docs/superpowers/plans/2026-09-20-control-lifecycle.md +0 -116
- package/docs/superpowers/plans/2026-09-20-issue-121-perf-gate-out-of-coverage.md +0 -517
- package/docs/superpowers/plans/2026-09-23-a5-mid-stream-assertion.md +0 -25
- package/docs/superpowers/plans/2026-09-23-cwd-browse-tab-drill.md +0 -393
- package/docs/superpowers/plans/2026-09-23-terminal-query-forwarding.md +0 -51
- package/docs/superpowers/plans/2026-09-23-unconditional-detach-key.md +0 -58
- package/docs/superpowers/plans/2026-09-24-control-reconcile-flake.md +0 -698
- package/docs/superpowers/plans/2026-09-24-probe-attached-clients.md +0 -605
- package/docs/superpowers/plans/2026-09-29-issue-145-holding-semantic-state.md +0 -1183
- package/docs/superpowers/plans/2026-09-29-issue-145-user-verification.md +0 -66
- package/docs/superpowers/plans/2026-09-30-board-delete-key-x-y-confirm.md +0 -649
- package/docs/superpowers/plans/2026-09-30-issue-153-runner-stop-boot-window.md +0 -618
- package/docs/superpowers/plans/2026-09-30-issue-95-test-discipline.md +0 -555
- package/docs/superpowers/plans/2026-09-30-publish-dispatch-tag-check.md +0 -259
- package/docs/superpowers/plans/2026-10-06-attach-settle-scheme-delivery.md +0 -751
- package/docs/superpowers/plans/2026-10-09-issue-167-attach-middle-paste.md +0 -509
- package/docs/superpowers/plans/2026-10-09-issue-169-fullscreen-mouse-ownership.md +0 -346
- package/docs/superpowers/plans/2026-10-10-attach-delete-in-place.md +0 -832
- package/docs/superpowers/plans/2026-10-10-issue-174-selection-mirror-paste-source.md +0 -510
- package/docs/superpowers/plans/2026-10-14-issue-170-attach-reply-guard.md +0 -771
- package/docs/superpowers/plans/2026-10-14-issue-178-accept-registration-race.md +0 -1257
- package/docs/superpowers/plans/2026-10-14-issue-184-snapshot-window-contract.md +0 -657
- package/docs/superpowers/plans/2026-10-14-issue-185-attach-wheel-alt-screen.md +0 -47
- package/docs/superpowers/plans/2026-10-14-issue-187-motionless-drag.md +0 -931
- package/docs/superpowers/plans/2026-10-14-issue-189-mixed-input-segments.md +0 -823
- package/docs/superpowers/plans/2026-10-15-issue-188-x10-mouse-release.md +0 -526
- package/docs/superpowers/plans/2026-10-16-issue-198-attach-wheel-forward-fallback.md +0 -32
- package/docs/superpowers/plans/2026-10-18-attach-wheel-replay-pseudo-scrollback.md +0 -437
- package/docs/superpowers/plans/2026-10-19-issue-213-reply-guard-csi-key.md +0 -257
- package/docs/superpowers/plans/2026-10-19-issue-214-attach-forward-alt-modifier.md +0 -202
- package/docs/superpowers/plans/2026-10-21-issue-221-ask-user-question.md +0 -198
- package/docs/superpowers/plans/2026-10-23-issue-222-attach-ready.md +0 -290
- package/docs/superpowers/plans/2026-10-23-issue-225-monotonic-foreground-state.md +0 -450
- package/docs/superpowers/plans/2026-10-23-issue-229-vim-jkl.md +0 -449
- package/docs/superpowers/plans/2026-10-23-issue-232-attach-bg-only-cells.md +0 -153
- package/docs/superpowers/plans/2026-10-23-lease-heartbeat-staleness.md +0 -537
- package/docs/superpowers/plans/2026-10-23-selected-row-highlight.md +0 -405
- package/docs/superpowers/plans/2026-10-24-issue-217-windows-pid-reuse-stopping.md +0 -367
- package/docs/superpowers/plans/2026-10-24-issue-230-warm-host-background-runs.md +0 -806
- package/docs/superpowers/plans/2026-10-24-issue-238-windows-process-identity-token.md +0 -490
- package/docs/superpowers/plans/2026-10-24-issue-241-coordinator-root-exit.md +0 -461
- package/docs/superpowers/plans/2026-10-25-issue-239-coordinator-replacement-identity.md +0 -504
- package/docs/superpowers/plans/2026-10-26-issue-245-key-hints-doc-sync.md +0 -244
- package/docs/superpowers/plans/2026-10-27-issue-172-fullscreen-selection-clear.md +0 -344
- package/docs/superpowers/plans/2026-10-27-issue-252-select-key-hints.md +0 -209
- package/docs/superpowers/plans/2026-10-28-issue-255-coordinator-endpoint-zombie.md +0 -442
- package/docs/superpowers/plans/2026-10-28-issue-257-legacy-pty-runner-endpoint-cleanup.md +0 -256
- package/docs/superpowers/plans/2026-11-03-cwd-favorites-full-pool.md +0 -266
- package/docs/superpowers/specs/2026-08-15-screenlog-gc-design.md +0 -105
- package/docs/superpowers/specs/2026-08-16-attach-double-cursor-jiggle-retry-design.md +0 -142
- package/docs/superpowers/specs/2026-08-21-attach-coldstart-jiggle-rearm-design.md +0 -110
- package/docs/superpowers/specs/2026-08-21-dashboard-keypress-lag-design.md +0 -59
- package/docs/superpowers/specs/2026-08-21-issue-14-needs-input-no-auto-done-design.md +0 -109
- package/docs/superpowers/specs/2026-08-22-cwd-favorites-design.md +0 -131
- package/docs/superpowers/specs/2026-08-22-jiggle-shrink-and-hold-design.md +0 -111
- package/docs/superpowers/specs/2026-08-22-question-tool-grouping-design.md +0 -114
- package/docs/superpowers/specs/2026-08-22-rename-state-labels-design.md +0 -80
- package/docs/superpowers/specs/2026-08-27-locks-acquirelock-spin-design.md +0 -97
- package/docs/superpowers/specs/2026-08-27-pty-runner-test-flaky-design.md +0 -35
- package/docs/superpowers/specs/2026-08-29-code-refs-badges-design.md +0 -128
- package/docs/superpowers/specs/2026-08-30-circular-navigation-design.md +0 -47
- package/docs/superpowers/specs/2026-08-30-eprm-atomicwrite-race-design.md +0 -77
- package/docs/superpowers/specs/2026-08-30-post-exit-timing-fix-design.md +0 -80
- package/docs/superpowers/specs/2026-08-30-pty-attach-quality-debt-design.md +0 -105
- package/docs/superpowers/specs/2026-08-30-readme-v2-design.md +0 -115
- package/docs/superpowers/specs/2026-09-01-attach-detach-gate-cursor-anchor-design.md +0 -78
- package/docs/superpowers/specs/2026-09-02-attach-detach-editor-state-design.md +0 -120
- package/docs/superpowers/specs/2026-09-03-code-refs-pr-backlink-narrow-design.md +0 -92
- package/docs/superpowers/specs/2026-09-03-detach-gate-glyph-fallback-design.md +0 -99
- package/docs/superpowers/specs/2026-09-04-evidence-outputpreview-design.md +0 -50
- package/docs/superpowers/specs/2026-09-04-warm-host-reclaim-design.md +0 -106
- package/docs/superpowers/specs/2026-09-05-issue-13-drainnextfollowup-pty-probe-design.md +0 -64
- package/docs/superpowers/specs/2026-09-05-issue-38-windows-wezterm-ime-design.md +0 -48
- package/docs/superpowers/specs/2026-09-05-issue-39-truncate-codepoint-boundary-design.md +0 -64
- package/docs/superpowers/specs/2026-09-05-issue-61-mention-fallback-design.md +0 -71
- package/docs/superpowers/specs/2026-09-05-issue-63-flaky-manual-completion-design.md +0 -49
- package/docs/superpowers/specs/2026-09-05-issue-64-changelog-helper-design.md +0 -76
- package/docs/superpowers/specs/2026-09-05-pty-host-stacking-sock-race-design.md +0 -510
- package/docs/superpowers/specs/2026-09-07-issue-11-attach-runtime-desync-heal-design.md +0 -130
- package/docs/superpowers/specs/2026-09-08-attach-ctrl-left-detach-design.md +0 -58
- package/docs/superpowers/specs/2026-09-08-dashboard-shrink-repaint-design.md +0 -52
- package/docs/superpowers/specs/2026-09-08-legacy-stale-host-recovery-design.md +0 -87
- package/docs/superpowers/specs/2026-09-08-spawn-async-error-swallow-design.md +0 -56
- package/docs/superpowers/specs/2026-09-08-stale-model-attach-guard-design.md +0 -79
- package/docs/superpowers/specs/2026-09-09-claimpid-blocks-replace-design.md +0 -147
- package/docs/superpowers/specs/2026-09-09-harden-runner-architecture-design.md +0 -304
- package/docs/superpowers/specs/2026-09-14-attach-cursor-dectcem-gate-design.md +0 -114
- package/docs/superpowers/specs/2026-09-14-host-meta-orphan-lock-design.md +0 -120
- package/docs/superpowers/specs/2026-09-14-issue-106-terminal-frame-cognition-design.md +0 -146
- package/docs/superpowers/specs/2026-09-14-issue-113-foreground-preview-race-design.md +0 -116
- package/docs/superpowers/specs/2026-09-15-coordinator-pipe-root-normalize-design.md +0 -84
- package/docs/superpowers/specs/2026-09-15-lease-publish-eprem-reclaim-design.md +0 -92
- package/docs/superpowers/specs/2026-09-18-detach-anchor-reporter-endpoint-design.md +0 -123
- package/docs/superpowers/specs/2026-09-20-issue-121-perf-gate-out-of-coverage-design.md +0 -204
- package/docs/superpowers/specs/2026-09-23-a5-mid-stream-assertion-design.md +0 -43
- package/docs/superpowers/specs/2026-09-23-cwd-browse-tab-drill-design.md +0 -95
- package/docs/superpowers/specs/2026-09-23-terminal-query-forwarding-design.md +0 -63
- package/docs/superpowers/specs/2026-09-23-unconditional-detach-key-design.md +0 -57
- package/docs/superpowers/specs/2026-09-24-control-reconcile-flake-design.md +0 -98
- package/docs/superpowers/specs/2026-09-24-probe-attached-clients-design.md +0 -190
- package/docs/superpowers/specs/2026-09-29-issue-145-holding-semantic-state-design.md +0 -394
- package/docs/superpowers/specs/2026-09-30-board-delete-key-x-y-confirm-design.md +0 -86
- package/docs/superpowers/specs/2026-09-30-issue-153-runner-stop-boot-window-design.md +0 -75
- package/docs/superpowers/specs/2026-09-30-issue-95-test-discipline-design.md +0 -226
- package/docs/superpowers/specs/2026-09-30-publish-dispatch-tag-check-design.md +0 -129
- package/docs/superpowers/specs/2026-10-06-attach-settle-scheme-delivery-design.md +0 -196
- package/docs/superpowers/specs/2026-10-09-issue-167-attach-middle-paste-design.md +0 -125
- package/docs/superpowers/specs/2026-10-09-issue-169-fullscreen-mouse-ownership-design.md +0 -130
- package/docs/superpowers/specs/2026-10-10-attach-delete-in-place-design.md +0 -224
- package/docs/superpowers/specs/2026-10-10-issue-174-selection-mirror-paste-source-design.md +0 -154
- package/docs/superpowers/specs/2026-10-14-issue-170-attach-reply-guard-design.md +0 -195
- package/docs/superpowers/specs/2026-10-14-issue-178-accept-registration-race-design.md +0 -287
- package/docs/superpowers/specs/2026-10-14-issue-184-snapshot-window-contract-design.md +0 -288
- package/docs/superpowers/specs/2026-10-14-issue-185-attach-wheel-alt-screen-design.md +0 -126
- package/docs/superpowers/specs/2026-10-14-issue-187-motionless-drag-design.md +0 -269
- package/docs/superpowers/specs/2026-10-14-issue-189-mixed-input-segments-design.md +0 -369
- package/docs/superpowers/specs/2026-10-15-issue-188-x10-mouse-release-design.md +0 -252
- package/docs/superpowers/specs/2026-10-16-issue-198-attach-wheel-forward-fallback-design.md +0 -67
- package/docs/superpowers/specs/2026-10-18-attach-wheel-replay-pseudo-scrollback-design.md +0 -250
- package/docs/superpowers/specs/2026-10-19-issue-213-reply-guard-csi-key-design.md +0 -165
- package/docs/superpowers/specs/2026-10-19-issue-214-attach-forward-alt-modifier-design.md +0 -131
- package/docs/superpowers/specs/2026-10-20-issue-221-ask-user-question-design.md +0 -270
- package/docs/superpowers/specs/2026-10-22-issue-225-monotonic-foreground-state-design.md +0 -225
- package/docs/superpowers/specs/2026-10-23-issue-222-attach-ready-design.md +0 -204
- package/docs/superpowers/specs/2026-10-23-issue-229-vim-jkl-design.md +0 -90
- package/docs/superpowers/specs/2026-10-23-issue-232-attach-bg-only-cells-design.md +0 -146
- package/docs/superpowers/specs/2026-10-23-lease-heartbeat-staleness-design.md +0 -271
- package/docs/superpowers/specs/2026-10-23-selected-row-highlight-design.md +0 -191
- package/docs/superpowers/specs/2026-10-24-issue-217-windows-pid-reuse-stopping-design.md +0 -334
- package/docs/superpowers/specs/2026-10-24-issue-230-warm-host-background-runs-design.md +0 -283
- package/docs/superpowers/specs/2026-10-24-issue-238-windows-process-identity-token-design.md +0 -286
- package/docs/superpowers/specs/2026-10-24-issue-241-coordinator-root-exit-design.md +0 -381
- package/docs/superpowers/specs/2026-10-25-issue-239-coordinator-replacement-identity-design.md +0 -331
- package/docs/superpowers/specs/2026-10-26-issue-245-key-hints-doc-sync-design.md +0 -72
- package/docs/superpowers/specs/2026-10-27-issue-172-fullscreen-selection-clear-design.md +0 -248
- package/docs/superpowers/specs/2026-10-27-issue-252-select-key-hints-design.md +0 -95
- package/docs/superpowers/specs/2026-10-28-issue-255-coordinator-endpoint-zombie-design.md +0 -380
- package/docs/superpowers/specs/2026-10-28-issue-257-legacy-pty-runner-endpoint-cleanup-design.md +0 -197
- package/docs/superpowers/specs/2026-11-03-cwd-favorites-full-pool-design.md +0 -106
- package/scripts/budget-audit.mjs +0 -135
- package/scripts/release.mjs +0 -446
- package/scripts/release_helper.mjs +0 -309
- package/scripts/run-perf-gate.mjs +0 -40
- package/scripts/run-tests.mjs +0 -375
package/scripts/run-tests.mjs
DELETED
|
@@ -1,375 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Suite gate for issue #241: run every test file through `node --test` and fail
|
|
4
|
-
* the run when a detached state-coordinator leaked past the test process.
|
|
5
|
-
*
|
|
6
|
-
* Why this exists: every coordinator integration test creates a throwaway board
|
|
7
|
-
* root under the OS temp dir and spawns a detached coordinator. A coordinator
|
|
8
|
-
* that fails to step down stays alive forever and pins its root, so leaks
|
|
9
|
-
* accumulate on a developer box instead of showing up as a test failure.
|
|
10
|
-
*
|
|
11
|
-
* Design (spec §3.3):
|
|
12
|
-
* - Snapshot the matching coordinators BEFORE the run. Pre-existing ones are
|
|
13
|
-
* warn-only: they are not this run's fault and are never killed.
|
|
14
|
-
* - After the run, poll up to GRACE_MS for every coordinator above the snapshot
|
|
15
|
-
* to be gone (the 1 s self-exit window plus load slack).
|
|
16
|
-
* - Survivors: print pid + root, SIGTERM, wait 2 s, SIGKILL leftovers, exit 1.
|
|
17
|
-
* - A test run killed by a signal (`exitCode === null`) is never a success.
|
|
18
|
-
* - Enumeration unavailable: warn and skip the residue check (fail-open). A
|
|
19
|
-
* missing `pwsh`/`powershell`/`ps` must not turn into a red suite.
|
|
20
|
-
*
|
|
21
|
-
* Selection is deliberately narrow: the command line must contain THIS
|
|
22
|
-
* checkout's absolute `runner/state-coordinator.mjs` and its root argument must
|
|
23
|
-
* resolve under `os.tmpdir()`. Another checkout's test coordinator never
|
|
24
|
-
* matches (its script path differs), and a default production board
|
|
25
|
-
* (`~/.pi/agent/agent-board`) never matches either. Accepted limit (spec §7): a
|
|
26
|
-
* board whose `AGENT_BOARD_ROOT` points under `os.tmpdir()` and whose runner is
|
|
27
|
-
* this checkout is indistinguishable from a test coordinator, so the gate would
|
|
28
|
-
* kill that live board.
|
|
29
|
-
*
|
|
30
|
-
* Known limit (final-review m7): POSIX selection is token-based and `ps` output
|
|
31
|
-
* is unquoted, so a checkout path containing spaces splits and its coordinators
|
|
32
|
-
* are not selected (the gate silently no-ops instead of failing). win32 CIM
|
|
33
|
-
* output quotes paths with spaces, so the tokenizer handles it there.
|
|
34
|
-
*
|
|
35
|
-
* This module must stay import-side-effect free: `test/run-tests-wrapper.test.mjs`
|
|
36
|
-
* imports the pure functions and `runSuite`, and a spurious `main()` at import
|
|
37
|
-
* time would recursively start the whole suite. Only the entry-point guard at
|
|
38
|
-
* the bottom of this file may call `main()`.
|
|
39
|
-
*/
|
|
40
|
-
import { execFileSync, spawn } from "node:child_process";
|
|
41
|
-
import { readdirSync } from "node:fs";
|
|
42
|
-
import { tmpdir } from "node:os";
|
|
43
|
-
import { join, posix, resolve } from "node:path";
|
|
44
|
-
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
45
|
-
|
|
46
|
-
/** The only coordinator script this checkout's suite spawns. */
|
|
47
|
-
const COORDINATOR_RELATIVE_SCRIPT = "runner/state-coordinator.mjs";
|
|
48
|
-
const DEFAULT_GRACE_MS = 10_000;
|
|
49
|
-
const DEFAULT_POLL_MS = 100;
|
|
50
|
-
/** SIGTERM → SIGKILL pause. On win32 SIGTERM is TerminateProcess; POSIX needs the grace. */
|
|
51
|
-
const SIGKILL_DELAY_MS = 2_000;
|
|
52
|
-
const SIGTERM = "SIGTERM";
|
|
53
|
-
const SIGKILL = "SIGKILL";
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Normalize a path for comparison without depending on the host platform:
|
|
57
|
-
* forward slashes, dot segments removed, no trailing separator (a `tmpRoot` like
|
|
58
|
-
* `C:\Temp\` or `/tmp/` must select exactly like its un-suffixed form), and (on
|
|
59
|
-
* win32) case-folded. Filesystem roots keep their separator (`/` stays `/`,
|
|
60
|
-
* `C:/` stays `C:/`): stripping it would turn a drive root into the
|
|
61
|
-
* drive-relative `C:`. The platform is explicit so the unit tests can exercise
|
|
62
|
-
* win32 samples on POSIX CI.
|
|
63
|
-
* @param {string} value
|
|
64
|
-
* @param {string} platform
|
|
65
|
-
* @returns {string}
|
|
66
|
-
*/
|
|
67
|
-
function canonicalPath(value, platform) {
|
|
68
|
-
const text = String(value ?? "").replace(/\\/g, "/");
|
|
69
|
-
if (!text) return "";
|
|
70
|
-
let normalized = posix.normalize(text);
|
|
71
|
-
if (normalized.length > 1 && normalized.endsWith("/")) {
|
|
72
|
-
const trimmed = normalized.replace(/\/+$/, "");
|
|
73
|
-
normalized = trimmed.endsWith(":") ? `${trimmed}/` : trimmed;
|
|
74
|
-
}
|
|
75
|
-
return platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* True when `candidate` is `directory` itself or one of its descendants, using
|
|
80
|
-
* the already-canonical spellings. The trailing-slash check keeps a sibling
|
|
81
|
-
* such as `/tmpfoo` from matching `/tmp`; a filesystem root already ends in the
|
|
82
|
-
* separator, so `${directory}/` would become `//` and match nothing there.
|
|
83
|
-
* @param {string} candidate
|
|
84
|
-
* @param {string} directory
|
|
85
|
-
* @returns {boolean}
|
|
86
|
-
*/
|
|
87
|
-
function isUnderDirectory(candidate, directory) {
|
|
88
|
-
if (!candidate || !directory) return false;
|
|
89
|
-
if (candidate === directory) return true;
|
|
90
|
-
return candidate.startsWith(directory.endsWith("/") ? directory : `${directory}/`);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Split a process command line into argv-shaped tokens, honoring the double
|
|
95
|
-
* quotes CIM emits around paths with spaces. Node's own argv never needs
|
|
96
|
-
* backslash-escaped quotes inside a path, so this covers the coordinator shape.
|
|
97
|
-
* @param {string} commandLine
|
|
98
|
-
* @returns {string[]}
|
|
99
|
-
*/
|
|
100
|
-
function tokenizeCommandLine(commandLine) {
|
|
101
|
-
const tokens = [];
|
|
102
|
-
for (const match of commandLine.matchAll(/"([^"]*)"|(\S+)/g)) tokens.push(match[1] ?? match[2]);
|
|
103
|
-
return tokens;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Parse the platform's process-enumeration output. Never throws: unparsable
|
|
108
|
-
* output and unparsable lines are skipped.
|
|
109
|
-
* @param {string} raw
|
|
110
|
-
* @param {string} platform
|
|
111
|
-
* @returns {Array<{pid: number, commandLine: string}>}
|
|
112
|
-
*/
|
|
113
|
-
export function parseProcessList(raw, platform) {
|
|
114
|
-
const text = String(raw ?? "").trim();
|
|
115
|
-
if (!text) return [];
|
|
116
|
-
if (platform === "win32") {
|
|
117
|
-
let data;
|
|
118
|
-
try {
|
|
119
|
-
data = JSON.parse(text);
|
|
120
|
-
} catch {
|
|
121
|
-
return [];
|
|
122
|
-
}
|
|
123
|
-
// ConvertTo-Json emits a single object when exactly one process matches.
|
|
124
|
-
const rows = Array.isArray(data) ? data : [data];
|
|
125
|
-
return rows
|
|
126
|
-
.map((row) => ({ pid: Number(row?.ProcessId ?? 0), commandLine: String(row?.CommandLine ?? "") }))
|
|
127
|
-
.filter((row) => Number.isFinite(row.pid) && row.pid > 0 && row.commandLine);
|
|
128
|
-
}
|
|
129
|
-
const out = [];
|
|
130
|
-
for (const line of text.split(/\r?\n/)) {
|
|
131
|
-
const match = /^\s*(\d+)\s+(.+)$/.exec(line);
|
|
132
|
-
if (match) out.push({ pid: Number(match[1]), commandLine: match[2] });
|
|
133
|
-
}
|
|
134
|
-
return out;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Keep only coordinators this checkout's suite could have spawned: the command
|
|
139
|
-
* line runs `<repoRoot>/runner/state-coordinator.mjs` and the token after that
|
|
140
|
-
* script (the root argument, quotes stripped by the tokenizer) resolves under
|
|
141
|
-
* `tmpRoot`. Production roots and other checkouts never match.
|
|
142
|
-
* @param {{
|
|
143
|
-
* processes?: Array<{pid: number, commandLine: string}>,
|
|
144
|
-
* repoRoot: string,
|
|
145
|
-
* tmpRoot: string,
|
|
146
|
-
* platform: string,
|
|
147
|
-
* }} args
|
|
148
|
-
* @returns {Array<{pid: number, root: string}>} deterministic order by pid
|
|
149
|
-
*/
|
|
150
|
-
export function selectOrphanCoordinators({ processes, repoRoot, tmpRoot, platform }) {
|
|
151
|
-
const scriptPath = canonicalPath(`${repoRoot}/${COORDINATOR_RELATIVE_SCRIPT}`, platform);
|
|
152
|
-
const tmpPath = canonicalPath(tmpRoot, platform);
|
|
153
|
-
if (!scriptPath || !tmpPath) return [];
|
|
154
|
-
const out = [];
|
|
155
|
-
for (const proc of processes ?? []) {
|
|
156
|
-
const pid = Number(proc?.pid ?? 0);
|
|
157
|
-
if (!Number.isFinite(pid) || pid <= 0) continue;
|
|
158
|
-
const tokens = tokenizeCommandLine(String(proc?.commandLine ?? ""));
|
|
159
|
-
const scriptIndex = tokens.findIndex((token) => canonicalPath(token, platform) === scriptPath);
|
|
160
|
-
if (scriptIndex < 0) continue;
|
|
161
|
-
const root = tokens[scriptIndex + 1];
|
|
162
|
-
if (!root) continue;
|
|
163
|
-
if (!isUnderDirectory(canonicalPath(root, platform), tmpPath)) continue;
|
|
164
|
-
out.push({ pid, root });
|
|
165
|
-
}
|
|
166
|
-
return out.sort((left, right) => left.pid - right.pid);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* @param {unknown} error
|
|
171
|
-
* @returns {string}
|
|
172
|
-
*/
|
|
173
|
-
function describeError(error) {
|
|
174
|
-
return error instanceof Error ? error.message : String(error);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Orchestrate one suite run and its residue check. Every side effect is
|
|
179
|
-
* injected so the unit tests drive every branch without spawning or killing
|
|
180
|
-
* anything; the real shell lives in `main()`.
|
|
181
|
-
* @param {{
|
|
182
|
-
* spawnTest: () => Promise<{exitCode: number|null, signal?: string|null}>,
|
|
183
|
-
* listProcesses: () => Array<{pid: number, commandLine: string}>|Promise<Array<{pid: number, commandLine: string}>>,
|
|
184
|
-
* kill: (pid: number, signal: string) => void,
|
|
185
|
-
* sleep: (ms: number) => Promise<void>,
|
|
186
|
-
* log: (line: string) => void,
|
|
187
|
-
* repoRoot: string,
|
|
188
|
-
* tmpRoot: string,
|
|
189
|
-
* platform: string,
|
|
190
|
-
* graceMs?: number,
|
|
191
|
-
* pollMs?: number,
|
|
192
|
-
* now?: () => number,
|
|
193
|
-
* }} deps
|
|
194
|
-
* @returns {Promise<{exitCode: number, survivors: number[]}>}
|
|
195
|
-
*/
|
|
196
|
-
export async function runSuite(deps) {
|
|
197
|
-
const {
|
|
198
|
-
spawnTest,
|
|
199
|
-
listProcesses,
|
|
200
|
-
kill,
|
|
201
|
-
sleep,
|
|
202
|
-
log,
|
|
203
|
-
repoRoot,
|
|
204
|
-
tmpRoot,
|
|
205
|
-
platform,
|
|
206
|
-
graceMs = DEFAULT_GRACE_MS,
|
|
207
|
-
pollMs = DEFAULT_POLL_MS,
|
|
208
|
-
now = Date.now,
|
|
209
|
-
} = deps;
|
|
210
|
-
const select = (processes) => selectOrphanCoordinators({ processes, repoRoot, tmpRoot, platform });
|
|
211
|
-
|
|
212
|
-
// Snapshot first: pre-existing coordinators predate this run, so they are
|
|
213
|
-
// warned about but never killed and never fail the suite.
|
|
214
|
-
/** @type {Set<number>} */
|
|
215
|
-
const preexisting = new Set();
|
|
216
|
-
let canCheckResidue = true;
|
|
217
|
-
try {
|
|
218
|
-
for (const proc of select(await listProcesses())) preexisting.add(proc.pid);
|
|
219
|
-
} catch (error) {
|
|
220
|
-
canCheckResidue = false;
|
|
221
|
-
log(`run-tests: WARNING: cannot enumerate node processes (${describeError(error)}); skipping the coordinator residue check`);
|
|
222
|
-
}
|
|
223
|
-
if (preexisting.size > 0) {
|
|
224
|
-
log(`run-tests: WARNING: ${preexisting.size} pre-existing state-coordinator process(es) before the run; not killed, not a failure`);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
const run = await spawnTest();
|
|
228
|
-
|
|
229
|
-
// Poll for post-run residue. The last successful enumeration is kept when a
|
|
230
|
-
// later poll fails: an already observed survivor still fails the run, while
|
|
231
|
-
// never having enumerated at all stays fail-open.
|
|
232
|
-
/** @type {Array<{pid: number, root: string}>} */
|
|
233
|
-
let survivors = [];
|
|
234
|
-
let residueChecked = false;
|
|
235
|
-
if (canCheckResidue) {
|
|
236
|
-
const deadline = now() + graceMs;
|
|
237
|
-
for (;;) {
|
|
238
|
-
let current;
|
|
239
|
-
try {
|
|
240
|
-
current = select(await listProcesses());
|
|
241
|
-
residueChecked = true;
|
|
242
|
-
} catch (error) {
|
|
243
|
-
canCheckResidue = false;
|
|
244
|
-
log(`run-tests: WARNING: cannot enumerate node processes (${describeError(error)}); skipping the coordinator residue check`);
|
|
245
|
-
break;
|
|
246
|
-
}
|
|
247
|
-
survivors = current.filter((proc) => !preexisting.has(proc.pid));
|
|
248
|
-
if (survivors.length === 0) break;
|
|
249
|
-
if (now() >= deadline) break;
|
|
250
|
-
await sleep(pollMs);
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/** @type {number[]} */
|
|
255
|
-
const survivorPids = [];
|
|
256
|
-
if (survivors.length > 0) {
|
|
257
|
-
for (const proc of survivors) log(`run-tests: leaked state-coordinator pid=${proc.pid} root=${proc.root}`);
|
|
258
|
-
for (const proc of survivors) kill(proc.pid, SIGTERM);
|
|
259
|
-
await sleep(SIGKILL_DELAY_MS);
|
|
260
|
-
let leftovers = survivors;
|
|
261
|
-
if (canCheckResidue) {
|
|
262
|
-
try {
|
|
263
|
-
leftovers = select(await listProcesses()).filter((proc) => !preexisting.has(proc.pid));
|
|
264
|
-
} catch (error) {
|
|
265
|
-
log(`run-tests: WARNING: cannot enumerate node processes after SIGTERM (${describeError(error)}); sending SIGKILL to the known survivors`);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
for (const proc of leftovers) {
|
|
269
|
-
log(`run-tests: survivor pid=${proc.pid} ignored SIGTERM; sending SIGKILL`);
|
|
270
|
-
kill(proc.pid, SIGKILL);
|
|
271
|
-
}
|
|
272
|
-
survivorPids.push(...survivors.map((proc) => proc.pid));
|
|
273
|
-
} else if (residueChecked) {
|
|
274
|
-
log("run-tests: no leaked coordinators");
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
let exitCode;
|
|
278
|
-
if (run.exitCode === null) {
|
|
279
|
-
log(`run-tests: the test run ended by signal ${run.signal ?? "unknown"}; treating it as a failure`);
|
|
280
|
-
exitCode = 1;
|
|
281
|
-
} else {
|
|
282
|
-
exitCode = run.exitCode;
|
|
283
|
-
}
|
|
284
|
-
if (survivors.length > 0) exitCode = 1;
|
|
285
|
-
return { exitCode, survivors: survivorPids };
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
/**
|
|
289
|
-
* Enumerate node processes with their command lines. Throws when the platform's
|
|
290
|
-
* enumeration tool is unavailable; `runSuite` turns that into a warning and
|
|
291
|
-
* skips the residue check (fail-open).
|
|
292
|
-
* @returns {Array<{pid: number, commandLine: string}>}
|
|
293
|
-
*/
|
|
294
|
-
function listNodeProcesses() {
|
|
295
|
-
if (process.platform === "win32") {
|
|
296
|
-
// Forced UTF-8: ConvertTo-Json otherwise emits code-page text that mangles
|
|
297
|
-
// non-ASCII root paths, and CIM may quote paths containing spaces.
|
|
298
|
-
const prefix = "[Console]::OutputEncoding=[Text.Encoding]::UTF8; $OutputEncoding=[Text.Encoding]::UTF8;";
|
|
299
|
-
const query = "Get-CimInstance Win32_Process -Filter \"Name='node.exe'\" | Select-Object ProcessId,CommandLine | ConvertTo-Json -Compress";
|
|
300
|
-
const args = ["-NoProfile", "-NonInteractive", "-Command", `${prefix} ${query}`];
|
|
301
|
-
for (const shell of ["pwsh", "powershell"]) {
|
|
302
|
-
try {
|
|
303
|
-
return parseProcessList(execFileSync(shell, args, { encoding: "utf8" }), process.platform);
|
|
304
|
-
} catch (error) {
|
|
305
|
-
// Only a missing shell falls through to the next candidate; an
|
|
306
|
-
// enumeration failure is not masked by retrying another binary.
|
|
307
|
-
if (error?.code !== "ENOENT") throw error;
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
throw new Error("neither pwsh nor powershell is available");
|
|
311
|
-
}
|
|
312
|
-
// -ww matters: without unlimited width, ps truncates the command line before
|
|
313
|
-
// the root argument and a leaked coordinator silently escapes selection.
|
|
314
|
-
return parseProcessList(execFileSync("ps", ["-e", "-ww", "-o", "pid=,args="], { encoding: "utf8" }), process.platform);
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
/**
|
|
318
|
-
* Run the suite files through `node --test` with inherited stdio.
|
|
319
|
-
* @param {string} repoRoot
|
|
320
|
-
* @param {string[]} files repoRoot-relative test paths
|
|
321
|
-
* @returns {Promise<{exitCode: number|null, signal: string|null}>}
|
|
322
|
-
*/
|
|
323
|
-
function spawnSuite(repoRoot, files) {
|
|
324
|
-
return new Promise((settle) => {
|
|
325
|
-
let settled = false;
|
|
326
|
-
const finish = (value) => {
|
|
327
|
-
if (settled) return;
|
|
328
|
-
settled = true;
|
|
329
|
-
settle(value);
|
|
330
|
-
};
|
|
331
|
-
const child = spawn(process.execPath, ["--test", ...files], { cwd: repoRoot, stdio: "inherit", windowsHide: true });
|
|
332
|
-
child.on("error", (error) => {
|
|
333
|
-
console.error(`run-tests: failed to spawn the test runner: ${describeError(error)}`);
|
|
334
|
-
finish({ exitCode: 1, signal: null });
|
|
335
|
-
});
|
|
336
|
-
child.on("exit", (code, signal) => finish({ exitCode: code, signal: signal ?? null }));
|
|
337
|
-
});
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
/**
|
|
341
|
-
* Real shell: derive this checkout from the module location, enumerate the test
|
|
342
|
-
* files, run the suite, grade the residue, and set `process.exitCode`.
|
|
343
|
-
* @returns {Promise<{exitCode: number, survivors: number[]}>}
|
|
344
|
-
*/
|
|
345
|
-
export async function main() {
|
|
346
|
-
const repoRoot = fileURLToPath(new URL("..", import.meta.url));
|
|
347
|
-
// Sorted so a failing run's file ordering is stable across platforms.
|
|
348
|
-
const files = readdirSync(join(repoRoot, "test"))
|
|
349
|
-
.filter((name) => name.endsWith(".test.mjs"))
|
|
350
|
-
.sort()
|
|
351
|
-
.map((name) => join("test", name));
|
|
352
|
-
const result = await runSuite({
|
|
353
|
-
repoRoot,
|
|
354
|
-
tmpRoot: tmpdir(),
|
|
355
|
-
platform: process.platform,
|
|
356
|
-
spawnTest: () => spawnSuite(repoRoot, files),
|
|
357
|
-
listProcesses: () => listNodeProcesses(),
|
|
358
|
-
kill: (pid, signal) => {
|
|
359
|
-
try {
|
|
360
|
-
process.kill(pid, signal);
|
|
361
|
-
} catch { /* already gone */ }
|
|
362
|
-
},
|
|
363
|
-
sleep: (ms) => new Promise((resolveSleep) => setTimeout(resolveSleep, ms)),
|
|
364
|
-
log: (line) => console.log(line),
|
|
365
|
-
});
|
|
366
|
-
process.exitCode = result.exitCode;
|
|
367
|
-
return result;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
// Import safety: see the module JSDoc. `main()` must never run while a test
|
|
371
|
-
// imports the pure functions, or `node --test` would recurse into itself.
|
|
372
|
-
const invokedPath = process.argv[1];
|
|
373
|
-
if (invokedPath !== undefined && import.meta.url === pathToFileURL(resolve(invokedPath)).href) {
|
|
374
|
-
await main();
|
|
375
|
-
}
|