@pareto4/pp-core 1.22.0 → 1.23.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/bin/install.js +205 -28
- package/commands/pp-help.md +2 -1
- package/gemini-extension.json +1 -1
- package/hooks/dist/pp-check-update-worker.js +1 -1
- package/hooks/dist/pp-context-monitor.js +3 -3
- package/hooks/dist/pp-statusline.js +88 -14
- package/hooks/dist/pp-update-banner.js +44 -4
- package/hooks/dist/pp-workflow-guard.js +3 -3
- package/hooks/pp-check-update-worker.js +1 -1
- package/hooks/pp-context-monitor.js +3 -3
- package/hooks/pp-statusline.js +88 -14
- package/hooks/pp-update-banner.js +44 -4
- package/hooks/pp-workflow-guard.js +3 -3
- package/package.json +1 -1
- package/pp-core/bin/lib/audience-command.cjs +271 -0
- package/pp-core/bin/lib/audience.cjs +293 -0
- package/pp-core/bin/lib/commands.cjs +26 -17
- package/pp-core/bin/lib/discovery-charter.cjs +13 -1
- package/pp-core/bin/lib/discovery-input-check.cjs +11 -11
- package/pp-core/bin/lib/discovery-session-check.cjs +7 -7
- package/pp-core/bin/lib/discovery-status.cjs +419 -0
- package/pp-core/bin/lib/explain-command.cjs +48 -0
- package/pp-core/bin/lib/gate-card.cjs +97 -0
- package/pp-core/bin/lib/hook-messages.cjs +132 -0
- package/pp-core/bin/lib/init-contract.cjs +116 -76
- package/pp-core/bin/lib/init-interactive.cjs +110 -49
- package/pp-core/bin/lib/init.cjs +44 -3
- package/pp-core/bin/lib/io.cjs +33 -0
- package/pp-core/bin/lib/message-labels.cjs +75 -0
- package/pp-core/bin/lib/messages/core.cjs +20 -0
- package/pp-core/bin/lib/messages/discovery.cjs +267 -0
- package/pp-core/bin/lib/messages/gate.cjs +257 -0
- package/pp-core/bin/lib/messages/hook.cjs +114 -0
- package/pp-core/bin/lib/messages/init.cjs +513 -0
- package/pp-core/bin/lib/messages/progress.cjs +110 -0
- package/pp-core/bin/lib/messages.cjs +208 -0
- package/pp-core/bin/lib/vault-command-router.cjs +16 -3
- package/pp-core/bin/lib/violation-render.cjs +78 -0
- package/pp-core/bin/pp-tools.cjs +68 -23
- package/pp-core/bin/shared/config-schema.manifest.json +1 -0
- package/pp-core/references/audience-moments.md +119 -0
- package/pp-core/references/audience.md +114 -0
- package/pp-core/references/discovery-charter.md +2 -2
- package/pp-core/templates/project-charter.md +3 -4
- package/pp-core/workflows/add-phase.md +2 -0
- package/pp-core/workflows/add-tests.md +2 -0
- package/pp-core/workflows/add-todo.md +2 -0
- package/pp-core/workflows/ai-integration-phase.md +2 -0
- package/pp-core/workflows/audit-milestone.md +2 -0
- package/pp-core/workflows/autonomous.md +2 -0
- package/pp-core/workflows/check-todos.md +2 -0
- package/pp-core/workflows/code-review-fix.md +2 -0
- package/pp-core/workflows/code-review.md +2 -0
- package/pp-core/workflows/complete-milestone.md +10 -1
- package/pp-core/workflows/discovery-sprint/charter.md +3 -2
- package/pp-core/workflows/discovery-sprint/prefill.md +3 -0
- package/pp-core/workflows/discovery-sprint/prototype.md +14 -0
- package/pp-core/workflows/discovery-sprint/sessions/01-kick-off.md +4 -1
- package/pp-core/workflows/discovery-sprint/sessions/13-wrap-up.md +1 -1
- package/pp-core/workflows/discovery-sprint/vault-context.md +15 -0
- package/pp-core/workflows/discovery-sprint.md +14 -0
- package/pp-core/workflows/discovery-to-build.md +8 -2
- package/pp-core/workflows/discuss-phase-assumptions.md +2 -0
- package/pp-core/workflows/discuss-phase.md +1 -1
- package/pp-core/workflows/edit-phase.md +2 -0
- package/pp-core/workflows/eval-review.md +2 -0
- package/pp-core/workflows/execute-phase/steps/check-query-gate.md +25 -1
- package/pp-core/workflows/execute-phase/steps/vault-fetch.md +1 -0
- package/pp-core/workflows/execute-phase/steps/vault-load-gate.md +2 -0
- package/pp-core/workflows/execute-phase.md +1 -1
- package/pp-core/workflows/execute-plan.md +2 -0
- package/pp-core/workflows/extract-learnings.md +2 -0
- package/pp-core/workflows/help/modes/business.md +56 -0
- package/pp-core/workflows/help.md +12 -2
- package/pp-core/workflows/import.md +10 -1
- package/pp-core/workflows/ingest-docs.md +2 -0
- package/pp-core/workflows/init.md +17 -1
- package/pp-core/workflows/insert-phase.md +2 -0
- package/pp-core/workflows/list-workspaces.md +2 -0
- package/pp-core/workflows/manager.md +2 -0
- package/pp-core/workflows/map-codebase.md +2 -0
- package/pp-core/workflows/milestone-summary.md +2 -0
- package/pp-core/workflows/new-milestone.md +10 -1
- package/pp-core/workflows/new-project.md +12 -2
- package/pp-core/workflows/new-workspace.md +2 -0
- package/pp-core/workflows/next.md +8 -0
- package/pp-core/workflows/plan-milestone-gaps.md +10 -1
- package/pp-core/workflows/plan-phase.md +2 -1
- package/pp-core/workflows/plan-review-convergence.md +1 -1
- package/pp-core/workflows/progress.md +10 -0
- package/pp-core/workflows/quick.md +11 -1
- package/pp-core/workflows/remove-phase.md +2 -0
- package/pp-core/workflows/remove-workspace.md +2 -0
- package/pp-core/workflows/resume-project.md +25 -0
- package/pp-core/workflows/review.md +2 -0
- package/pp-core/workflows/scan.md +2 -0
- package/pp-core/workflows/secure-phase.md +2 -0
- package/pp-core/workflows/settings.md +41 -0
- package/pp-core/workflows/ship.md +2 -0
- package/pp-core/workflows/spec-phase.md +1 -1
- package/pp-core/workflows/ui-phase.md +2 -0
- package/pp-core/workflows/ui-review.md +2 -0
- package/pp-core/workflows/ultraplan-phase.md +2 -0
- package/pp-core/workflows/validate-phase.md +2 -0
- package/pp-core/workflows/vault-review.md +2 -0
- package/pp-core/workflows/verify-phase.md +2 -0
- package/pp-core/workflows/verify-work.md +2 -0
- package/scripts/affected-tests-lib.cjs +26 -12
- package/scripts/ci-release-only.cjs +158 -0
- package/scripts/live-runs/driver.cjs +326 -0
- package/scripts/live-runs/run.cjs +906 -0
- package/scripts/live-runs/scenarios.cjs +326 -0
- package/scripts/live-runs/scratch-project.cjs +132 -0
- package/scripts/live-runs/scrub.cjs +291 -0
- package/scripts/live-runs/turn-guard.cjs +107 -0
- package/scripts/live-runs/walk-setup.cjs +198 -0
- package/scripts/run-tests.cjs +151 -135
- package/scripts/test-env.cjs +64 -0
package/scripts/run-tests.cjs
CHANGED
|
@@ -33,6 +33,7 @@ const { readdirSync } = require('fs');
|
|
|
33
33
|
const { join, basename } = require('path');
|
|
34
34
|
const { execFileSync, spawnSync } = require('child_process');
|
|
35
35
|
const { ExitError, runMain } = require('./lib/cli-exit.cjs');
|
|
36
|
+
const { prepareSuiteEnv } = require('./test-env.cjs');
|
|
36
37
|
|
|
37
38
|
const SUITES = ['all', 'unit', 'integration', 'install', 'security', 'slow'];
|
|
38
39
|
|
|
@@ -459,145 +460,160 @@ function main() {
|
|
|
459
460
|
// exports GSD_WORKSTREAM, which would redirect fixture STATE.md reads away from
|
|
460
461
|
// each <tmp>/.planning and silently diverge from the clean CI/Docker env. Strip
|
|
461
462
|
// them so the local runner matches CI; tests that need them set them explicitly.
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
//
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
// Windows has significantly higher per-subprocess overhead than Linux/macOS:
|
|
478
|
-
// - Windows Defender scans each spawned process on first execution, adding
|
|
479
|
-
// latency proportional to the number of concurrent spawns.
|
|
480
|
-
// - NTFS has higher file-system latency under concurrent access compared to
|
|
481
|
-
// ext4/APFS, which amplifies contention when multiple test chunks run in
|
|
482
|
-
// parallel and all read/write the same fixture directories.
|
|
483
|
-
// Reducing to 2 halves the peak concurrent subprocess count on Windows and
|
|
484
|
-
// keeps per-chunk wall-clock time well within the 20m CI job cap.
|
|
485
|
-
//
|
|
486
|
-
// Operator override via TEST_CONCURRENCY env var for local debugging.
|
|
487
|
-
const defaultConcurrency = process.platform === 'win32' ? 2 : 4;
|
|
488
|
-
const concurrency = process.env.TEST_CONCURRENCY
|
|
489
|
-
? `--test-concurrency=${process.env.TEST_CONCURRENCY}`
|
|
490
|
-
: `--test-concurrency=${defaultConcurrency}`;
|
|
491
|
-
|
|
492
|
-
// Windows `CreateProcess` caps the full command line at 32,767 chars
|
|
493
|
-
// (lpCommandLine). With 500+ test paths the spawn fails instantly with no
|
|
494
|
-
// test output. Linux/macOS allow ~2 MB (ARG_MAX) so unchunked spawns are
|
|
495
|
-
// fine there. Split into chunks sized for the tightest target so behavior
|
|
496
|
-
// is identical across platforms. (#3597)
|
|
497
|
-
// Operator override (also used by tests to force chunking with short paths).
|
|
498
|
-
const MAX_CMDLINE_CHARS = process.env.RUN_TESTS_MAX_CMDLINE_CHARS
|
|
499
|
-
? Number(process.env.RUN_TESTS_MAX_CMDLINE_CHARS)
|
|
500
|
-
: 28000; // headroom below the 32,767 Windows ceiling
|
|
501
|
-
const MAX_FILES_PER_CHUNK = process.env.RUN_TESTS_MAX_FILES_PER_CHUNK
|
|
502
|
-
? Number(process.env.RUN_TESTS_MAX_FILES_PER_CHUNK)
|
|
503
|
-
: 180;
|
|
504
|
-
|
|
505
|
-
// node:test does not exit until the event loop drains. A unit test that leaks
|
|
506
|
-
// an open handle (un-terminated Worker, un-killed child_process, ref'd timer)
|
|
507
|
-
// makes a chunk's `node --test` child hang ~150s on Windows AFTER its last test
|
|
508
|
-
// prints; two such stalls push the windows full lane past its 20m cap and the
|
|
509
|
-
// job is CANCELLED with no failed step — a false-negative gate (#1051, recurrence
|
|
510
|
-
// of #869). --test-force-exit (Node >=22) exits the runner once all tests
|
|
511
|
-
// finish regardless of lingering handles — but it exits before the reporter has
|
|
512
|
-
// written everything, so it DROPS reported tests: a 180-file chunk reported
|
|
513
|
-
// 4,688 / 4,726 / 4,705 tests with it and 4,759 every time without it (Linux,
|
|
514
|
-
// 1.5 CPUs, 2026-11-10), and on GitHub runners it cut whole chunk summaries
|
|
515
|
-
// (PR #31/#32). The leaking tests were fixed at the source and no CI lane runs
|
|
516
|
-
// Windows any more, so it is now OPT-IN: RUN_TESTS_FORCE_EXIT=1. The per-chunk
|
|
517
|
-
// timeout below still bounds a genuine hang, loudly.
|
|
518
|
-
const nodeMajor = Number(process.versions.node.split('.')[0]);
|
|
519
|
-
const forceExit = nodeMajor >= 22 && process.env.RUN_TESTS_FORCE_EXIT === '1';
|
|
520
|
-
|
|
521
|
-
const FIXED_OVERHEAD = process.execPath.length + '--test'.length + concurrency.length + (forceExit ? '--test-force-exit'.length + 1 : 0) + 8;
|
|
522
|
-
const chunks = [];
|
|
523
|
-
let current = [];
|
|
524
|
-
let currentLen = FIXED_OVERHEAD;
|
|
525
|
-
for (const file of selected) {
|
|
526
|
-
const add = file.length + 1; // +1 for the inter-arg separator
|
|
527
|
-
if (
|
|
528
|
-
current.length > 0 &&
|
|
529
|
-
(currentLen + add > MAX_CMDLINE_CHARS || current.length >= MAX_FILES_PER_CHUNK)
|
|
530
|
-
) {
|
|
531
|
-
chunks.push(current);
|
|
532
|
-
current = [];
|
|
533
|
-
currentLen = FIXED_OVERHEAD;
|
|
534
|
-
}
|
|
535
|
-
current.push(file);
|
|
536
|
-
currentLen += add;
|
|
537
|
-
}
|
|
538
|
-
if (current.length > 0) chunks.push(current);
|
|
539
|
-
|
|
540
|
-
// A chunk that still hangs (a leak the backstop somehow misses, or a wedged
|
|
541
|
-
// subprocess) must fail loudly rather than silently burn the job's wall-clock
|
|
542
|
-
// budget until the CI runner cancels the whole job. Default 10 min per chunk:
|
|
543
|
-
// well above a healthy chunk (~4-5 min on the windows lane) but below the 20m
|
|
544
|
-
// job cap. Operator/test override via RUN_TESTS_CHUNK_TIMEOUT_MS.
|
|
545
|
-
const chunkTimeoutMs = process.env.RUN_TESTS_CHUNK_TIMEOUT_MS
|
|
546
|
-
? Number(process.env.RUN_TESTS_CHUNK_TIMEOUT_MS)
|
|
547
|
-
: 600000;
|
|
548
|
-
|
|
549
|
-
let firstFailureExit = 0;
|
|
550
|
-
for (let i = 0; i < chunks.length; i++) {
|
|
551
|
-
if (chunks.length > 1) {
|
|
552
|
-
console.error(`run-tests: chunk ${i + 1}/${chunks.length} — ${chunks[i].length} files`);
|
|
553
|
-
}
|
|
554
|
-
const chunkFiles = chunks[i].map(f => f.split(/[\\/]/).pop()).join(' ');
|
|
555
|
-
// stdout is captured, not inherited, so the chunk's node:test summary can be
|
|
556
|
-
// checked; it is written through unchanged once the chunk ends. stderr stays
|
|
557
|
-
// live. A chunk can exit 0 with its output cut off and no summary — on CI
|
|
558
|
-
// (2026-10-28) two of five chunks did, and ~1,000 tests never reported while
|
|
559
|
-
// the job stayed green — so a missing summary fails the run.
|
|
560
|
-
const result = spawnSync(
|
|
561
|
-
process.execPath,
|
|
562
|
-
['--test', ...(forceExit ? ['--test-force-exit'] : []), concurrency, ...chunks[i]],
|
|
563
|
-
{
|
|
564
|
-
stdio: ['inherit', 'pipe', 'inherit'],
|
|
565
|
-
env: { ...process.env },
|
|
566
|
-
timeout: chunkTimeoutMs,
|
|
567
|
-
maxBuffer: 1024 * 1024 * 1024,
|
|
568
|
-
},
|
|
463
|
+
// The same goes for the person's audience setting (AUD-04): PP_AUDIENCE and the
|
|
464
|
+
// person file (`~/.gsd/person.json`, rooted at GSD_HOME || home) are stripped /
|
|
465
|
+
// redirected so a developer whose own audience is business gets CI's results.
|
|
466
|
+
// Tests that need a setting pass their own GSD_HOME / PP_AUDIENCE.
|
|
467
|
+
// The same preparation backs `npm run test:affected` (scripts/test-env.cjs).
|
|
468
|
+
const suiteEnv = prepareSuiteEnv(process.env);
|
|
469
|
+
|
|
470
|
+
try {
|
|
471
|
+
// Log selected files to stderr for CI / harness-test visibility.
|
|
472
|
+
// node:test default reporter doesn't echo filenames, so this gives
|
|
473
|
+
// operators a single stable line they can grep.
|
|
474
|
+
console.error(
|
|
475
|
+
`run-tests: suite="${suite || 'all'}" files=${selected.length}: ${selected
|
|
476
|
+
.map(f => f.split(/[\\/]/).pop())
|
|
477
|
+
.join(' ')}`,
|
|
569
478
|
);
|
|
570
|
-
|
|
571
|
-
//
|
|
572
|
-
//
|
|
573
|
-
//
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
479
|
+
|
|
480
|
+
// Default concurrency: 4 on Linux/macOS, 2 on Windows.
|
|
481
|
+
//
|
|
482
|
+
// Windows has significantly higher per-subprocess overhead than Linux/macOS:
|
|
483
|
+
// - Windows Defender scans each spawned process on first execution, adding
|
|
484
|
+
// latency proportional to the number of concurrent spawns.
|
|
485
|
+
// - NTFS has higher file-system latency under concurrent access compared to
|
|
486
|
+
// ext4/APFS, which amplifies contention when multiple test chunks run in
|
|
487
|
+
// parallel and all read/write the same fixture directories.
|
|
488
|
+
// Reducing to 2 halves the peak concurrent subprocess count on Windows and
|
|
489
|
+
// keeps per-chunk wall-clock time well within the 20m CI job cap.
|
|
490
|
+
//
|
|
491
|
+
// Operator override via TEST_CONCURRENCY env var for local debugging.
|
|
492
|
+
const defaultConcurrency = process.platform === 'win32' ? 2 : 4;
|
|
493
|
+
const concurrency = process.env.TEST_CONCURRENCY
|
|
494
|
+
? `--test-concurrency=${process.env.TEST_CONCURRENCY}`
|
|
495
|
+
: `--test-concurrency=${defaultConcurrency}`;
|
|
496
|
+
|
|
497
|
+
// Windows `CreateProcess` caps the full command line at 32,767 chars
|
|
498
|
+
// (lpCommandLine). With 500+ test paths the spawn fails instantly with no
|
|
499
|
+
// test output. Linux/macOS allow ~2 MB (ARG_MAX) so unchunked spawns are
|
|
500
|
+
// fine there. Split into chunks sized for the tightest target so behavior
|
|
501
|
+
// is identical across platforms. (#3597)
|
|
502
|
+
// Operator override (also used by tests to force chunking with short paths).
|
|
503
|
+
const MAX_CMDLINE_CHARS = process.env.RUN_TESTS_MAX_CMDLINE_CHARS
|
|
504
|
+
? Number(process.env.RUN_TESTS_MAX_CMDLINE_CHARS)
|
|
505
|
+
: 28000; // headroom below the 32,767 Windows ceiling
|
|
506
|
+
const MAX_FILES_PER_CHUNK = process.env.RUN_TESTS_MAX_FILES_PER_CHUNK
|
|
507
|
+
? Number(process.env.RUN_TESTS_MAX_FILES_PER_CHUNK)
|
|
508
|
+
: 180;
|
|
509
|
+
|
|
510
|
+
// node:test does not exit until the event loop drains. A unit test that leaks
|
|
511
|
+
// an open handle (un-terminated Worker, un-killed child_process, ref'd timer)
|
|
512
|
+
// makes a chunk's `node --test` child hang ~150s on Windows AFTER its last test
|
|
513
|
+
// prints; two such stalls push the windows full lane past its 20m cap and the
|
|
514
|
+
// job is CANCELLED with no failed step — a false-negative gate (#1051, recurrence
|
|
515
|
+
// of #869). --test-force-exit (Node >=22) exits the runner once all tests
|
|
516
|
+
// finish regardless of lingering handles — but it exits before the reporter has
|
|
517
|
+
// written everything, so it DROPS reported tests: a 180-file chunk reported
|
|
518
|
+
// 4,688 / 4,726 / 4,705 tests with it and 4,759 every time without it (Linux,
|
|
519
|
+
// 1.5 CPUs, 2026-11-10), and on GitHub runners it cut whole chunk summaries
|
|
520
|
+
// (PR #31/#32). The leaking tests were fixed at the source and no CI lane runs
|
|
521
|
+
// Windows any more, so it is now OPT-IN: RUN_TESTS_FORCE_EXIT=1. The per-chunk
|
|
522
|
+
// timeout below still bounds a genuine hang, loudly.
|
|
523
|
+
const nodeMajor = Number(process.versions.node.split('.')[0]);
|
|
524
|
+
const forceExit = nodeMajor >= 22 && process.env.RUN_TESTS_FORCE_EXIT === '1';
|
|
525
|
+
|
|
526
|
+
const FIXED_OVERHEAD = process.execPath.length + '--test'.length + concurrency.length + (forceExit ? '--test-force-exit'.length + 1 : 0) + 8;
|
|
527
|
+
const chunks = [];
|
|
528
|
+
let current = [];
|
|
529
|
+
let currentLen = FIXED_OVERHEAD;
|
|
530
|
+
for (const file of selected) {
|
|
531
|
+
const add = file.length + 1; // +1 for the inter-arg separator
|
|
532
|
+
if (
|
|
533
|
+
current.length > 0 &&
|
|
534
|
+
(currentLen + add > MAX_CMDLINE_CHARS || current.length >= MAX_FILES_PER_CHUNK)
|
|
535
|
+
) {
|
|
536
|
+
chunks.push(current);
|
|
537
|
+
current = [];
|
|
538
|
+
currentLen = FIXED_OVERHEAD;
|
|
539
|
+
}
|
|
540
|
+
current.push(file);
|
|
541
|
+
currentLen += add;
|
|
542
|
+
}
|
|
543
|
+
if (current.length > 0) chunks.push(current);
|
|
544
|
+
|
|
545
|
+
// A chunk that still hangs (a leak the backstop somehow misses, or a wedged
|
|
546
|
+
// subprocess) must fail loudly rather than silently burn the job's wall-clock
|
|
547
|
+
// budget until the CI runner cancels the whole job. Default 10 min per chunk:
|
|
548
|
+
// well above a healthy chunk (~4-5 min on the windows lane) but below the 20m
|
|
549
|
+
// job cap. Operator/test override via RUN_TESTS_CHUNK_TIMEOUT_MS.
|
|
550
|
+
const chunkTimeoutMs = process.env.RUN_TESTS_CHUNK_TIMEOUT_MS
|
|
551
|
+
? Number(process.env.RUN_TESTS_CHUNK_TIMEOUT_MS)
|
|
552
|
+
: 600000;
|
|
553
|
+
|
|
554
|
+
let firstFailureExit = 0;
|
|
555
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
556
|
+
if (chunks.length > 1) {
|
|
557
|
+
console.error(`run-tests: chunk ${i + 1}/${chunks.length} — ${chunks[i].length} files`);
|
|
558
|
+
}
|
|
559
|
+
const chunkFiles = chunks[i].map(f => f.split(/[\\/]/).pop()).join(' ');
|
|
560
|
+
// stdout is captured, not inherited, so the chunk's node:test summary can be
|
|
561
|
+
// checked; it is written through unchanged once the chunk ends. stderr stays
|
|
562
|
+
// live. A chunk can exit 0 with its output cut off and no summary — on CI
|
|
563
|
+
// (2026-10-28) two of five chunks did, and ~1,000 tests never reported while
|
|
564
|
+
// the job stayed green — so a missing summary fails the run.
|
|
565
|
+
const result = spawnSync(
|
|
566
|
+
process.execPath,
|
|
567
|
+
['--test', ...(forceExit ? ['--test-force-exit'] : []), concurrency, ...chunks[i]],
|
|
568
|
+
{
|
|
569
|
+
stdio: ['inherit', 'pipe', 'inherit'],
|
|
570
|
+
env: { ...process.env },
|
|
571
|
+
timeout: chunkTimeoutMs,
|
|
572
|
+
maxBuffer: 1024 * 1024 * 1024,
|
|
573
|
+
},
|
|
593
574
|
);
|
|
594
|
-
|
|
575
|
+
if (result.stdout && result.stdout.length > 0) process.stdout.write(result.stdout);
|
|
576
|
+
// When the per-chunk timeout fires, spawnSync kills the child and reports
|
|
577
|
+
// error.code === 'ETIMEDOUT' (POSIX) and/or a kill signal (platform-dependent).
|
|
578
|
+
// Check both so detection holds on Windows and POSIX.
|
|
579
|
+
const timedOut = (result.error && result.error.code === 'ETIMEDOUT') ||
|
|
580
|
+
(result.signal !== null && result.status === null && !result.error);
|
|
581
|
+
let code = result.status === null ? 1 : result.status;
|
|
582
|
+
if (result.error && result.error.code === 'ETIMEDOUT') code = 1;
|
|
583
|
+
if (timedOut) {
|
|
584
|
+
console.error(
|
|
585
|
+
`run-tests: chunk ${i + 1}/${chunks.length} exceeded the per-chunk timeout ` +
|
|
586
|
+
`of ${chunkTimeoutMs}ms and was killed — a test in this chunk is likely leaking ` +
|
|
587
|
+
`an open handle (un-terminated Worker, un-killed child process, or ref'd timer) ` +
|
|
588
|
+
`so node --test never exits. Files: ${chunkFiles}`,
|
|
589
|
+
);
|
|
590
|
+
} else if (result.error) {
|
|
591
|
+
console.error(`run-tests: chunk ${i + 1}/${chunks.length} could not run: ${result.error.message}`);
|
|
592
|
+
code = 1;
|
|
593
|
+
} else if (code === 0 && !hasTestSummary(result.stdout)) {
|
|
594
|
+
console.error(
|
|
595
|
+
`run-tests: chunk ${i + 1}/${chunks.length} exited without a test summary ` +
|
|
596
|
+
`(no "ℹ tests N" / "ℹ fail N" lines) — its output was cut off, so some of its ` +
|
|
597
|
+
`tests never reported. Files: ${chunkFiles}`,
|
|
598
|
+
);
|
|
599
|
+
code = 1;
|
|
600
|
+
}
|
|
601
|
+
// Run every chunk so the operator sees all failures in one pass; report
|
|
602
|
+
// the first non-zero exit at the end.
|
|
603
|
+
if (code !== 0 && firstFailureExit === 0) firstFailureExit = code;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// A test that wrote the shared suite person file would poison every file that
|
|
607
|
+
// runs after it, so it fails the run loudly instead of causing flakiness.
|
|
608
|
+
const leak = suiteEnv.leakMessage();
|
|
609
|
+
if (leak) {
|
|
610
|
+
console.error(`run-tests: ${leak}`);
|
|
611
|
+
return firstFailureExit !== 0 ? firstFailureExit : 1;
|
|
595
612
|
}
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
613
|
+
if (firstFailureExit !== 0) return firstFailureExit;
|
|
614
|
+
} finally {
|
|
615
|
+
suiteEnv.cleanup();
|
|
599
616
|
}
|
|
600
|
-
if (firstFailureExit !== 0) return firstFailureExit;
|
|
601
617
|
}
|
|
602
618
|
|
|
603
619
|
/**
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared test-suite environment preparation (Phase 39 D-16, WR-04).
|
|
5
|
+
*
|
|
6
|
+
* Both entry points that spawn `node --test` — `scripts/run-tests.cjs`
|
|
7
|
+
* (`npm test`) and `scripts/affected-tests-lib.cjs#runNodeTestFiles`
|
|
8
|
+
* (`npm run test:affected`) — call this, so a developer's own shell never
|
|
9
|
+
* changes what the suite sees:
|
|
10
|
+
*
|
|
11
|
+
* - GSD_PROJECT / GSD_WORKSTREAM would redirect fixture `.planning` reads;
|
|
12
|
+
* - CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS changes agent dispatch;
|
|
13
|
+
* - PP_AUDIENCE and the person file (`~/.gsd/person.json`, rooted at
|
|
14
|
+
* GSD_HOME || home) would change the resolved audience (AUD-04).
|
|
15
|
+
*
|
|
16
|
+
* The person file's home is redirected to a fresh throwaway directory for the
|
|
17
|
+
* whole run. A test that writes the person file there would poison every file
|
|
18
|
+
* that runs after it, so `leakMessage()` reports it and the caller fails the
|
|
19
|
+
* run. Tests that need a setting pass their own GSD_HOME / PP_AUDIENCE.
|
|
20
|
+
*
|
|
21
|
+
* Kept out of `scripts/lib/` on purpose: the installer copies every file in
|
|
22
|
+
* `scripts/lib/` into each user's config dir, and this is a dev-only helper.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const fs = require('node:fs');
|
|
26
|
+
const os = require('node:os');
|
|
27
|
+
const path = require('node:path');
|
|
28
|
+
|
|
29
|
+
const STRIPPED_VARS = Object.freeze([
|
|
30
|
+
'GSD_PROJECT',
|
|
31
|
+
'GSD_WORKSTREAM',
|
|
32
|
+
'CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS',
|
|
33
|
+
'PP_AUDIENCE',
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Strip the session variables from `env` (mutated in place) and point its
|
|
38
|
+
* GSD_HOME at a new temporary suite home.
|
|
39
|
+
*
|
|
40
|
+
* @param {NodeJS.ProcessEnv} [env=process.env]
|
|
41
|
+
* @returns {{ env: NodeJS.ProcessEnv, home: string, leakMessage: () => string|null, cleanup: () => void }}
|
|
42
|
+
*/
|
|
43
|
+
function prepareSuiteEnv(env = process.env) {
|
|
44
|
+
for (const name of STRIPPED_VARS) delete env[name];
|
|
45
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'pp-suite-home-'));
|
|
46
|
+
env.GSD_HOME = home;
|
|
47
|
+
const personFile = path.join(home, '.gsd', 'person.json');
|
|
48
|
+
return {
|
|
49
|
+
env,
|
|
50
|
+
home,
|
|
51
|
+
leakMessage() {
|
|
52
|
+
if (!fs.existsSync(personFile)) return null;
|
|
53
|
+
return (
|
|
54
|
+
`a test wrote the person file in the shared suite home (${personFile}) — ` +
|
|
55
|
+
'every test that runs audience set/answer or installs with --audience must pass its own GSD_HOME'
|
|
56
|
+
);
|
|
57
|
+
},
|
|
58
|
+
cleanup() {
|
|
59
|
+
fs.rmSync(home, { recursive: true, force: true });
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = { prepareSuiteEnv, STRIPPED_VARS };
|