@principles/pd-cli 1.133.0 → 1.135.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/dist/commands/__tests__/intent-flag-wiring.test.js +74 -3
- package/dist/commands/__tests__/intent-flag-wiring.test.js.map +1 -1
- package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.d.ts +2 -0
- package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.d.ts.map +1 -0
- package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.js +60 -0
- package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.js.map +1 -0
- package/dist/commands/__tests__/rulecode-flag-wiring.test.js +11 -4
- package/dist/commands/__tests__/rulecode-flag-wiring.test.js.map +1 -1
- package/dist/commands/__tests__/run-rulehost-flag-wiring.test.js +9 -0
- package/dist/commands/__tests__/run-rulehost-flag-wiring.test.js.map +1 -1
- package/dist/commands/admission-gate.d.ts +24 -0
- package/dist/commands/admission-gate.d.ts.map +1 -0
- package/dist/commands/admission-gate.js +29 -0
- package/dist/commands/admission-gate.js.map +1 -0
- package/dist/commands/candidate.d.ts.map +1 -1
- package/dist/commands/candidate.js +72 -6
- package/dist/commands/candidate.js.map +1 -1
- package/dist/commands/console.d.ts.map +1 -1
- package/dist/commands/console.js +42 -0
- package/dist/commands/console.js.map +1 -1
- package/dist/commands/demo-story-a.d.ts.map +1 -1
- package/dist/commands/demo-story-a.js +4 -2
- package/dist/commands/demo-story-a.js.map +1 -1
- package/dist/commands/diagnose.d.ts.map +1 -1
- package/dist/commands/diagnose.js +155 -8
- package/dist/commands/diagnose.js.map +1 -1
- package/dist/commands/errors-list.d.ts +25 -0
- package/dist/commands/errors-list.d.ts.map +1 -0
- package/dist/commands/errors-list.js +368 -0
- package/dist/commands/errors-list.js.map +1 -0
- package/dist/commands/health-codex.d.ts +7 -0
- package/dist/commands/health-codex.d.ts.map +1 -0
- package/dist/commands/health-codex.js +209 -0
- package/dist/commands/health-codex.js.map +1 -0
- package/dist/commands/health.d.ts +24 -0
- package/dist/commands/health.d.ts.map +1 -1
- package/dist/commands/health.js +48 -0
- package/dist/commands/health.js.map +1 -1
- package/dist/commands/intent.d.ts +21 -4
- package/dist/commands/intent.d.ts.map +1 -1
- package/dist/commands/intent.js +88 -27
- package/dist/commands/intent.js.map +1 -1
- package/dist/commands/pain-evidence.d.ts +6 -0
- package/dist/commands/pain-evidence.d.ts.map +1 -1
- package/dist/commands/pain-evidence.js +6 -0
- package/dist/commands/pain-evidence.js.map +1 -1
- package/dist/commands/pain-record.d.ts.map +1 -1
- package/dist/commands/pain-record.js +21 -0
- package/dist/commands/pain-record.js.map +1 -1
- package/dist/commands/pain-retry.d.ts +4 -0
- package/dist/commands/pain-retry.d.ts.map +1 -1
- package/dist/commands/pain-retry.js +327 -32
- package/dist/commands/pain-retry.js.map +1 -1
- package/dist/commands/proven-channel-baseline.d.ts.map +1 -1
- package/dist/commands/proven-channel-baseline.js +4 -2
- package/dist/commands/proven-channel-baseline.js.map +1 -1
- package/dist/commands/runtime-activation.d.ts +30 -0
- package/dist/commands/runtime-activation.d.ts.map +1 -1
- package/dist/commands/runtime-activation.js +567 -11
- package/dist/commands/runtime-activation.js.map +1 -1
- package/dist/commands/runtime-canary.d.ts.map +1 -1
- package/dist/commands/runtime-canary.js +32 -7
- package/dist/commands/runtime-canary.js.map +1 -1
- package/dist/commands/runtime-health-snapshot.d.ts.map +1 -1
- package/dist/commands/runtime-health-snapshot.js +4 -2
- package/dist/commands/runtime-health-snapshot.js.map +1 -1
- package/dist/commands/runtime-init.d.ts +43 -0
- package/dist/commands/runtime-init.d.ts.map +1 -0
- package/dist/commands/runtime-init.js +264 -0
- package/dist/commands/runtime-init.js.map +1 -0
- package/dist/commands/runtime-internalization-context-trace.d.ts +25 -0
- package/dist/commands/runtime-internalization-context-trace.d.ts.map +1 -0
- package/dist/commands/runtime-internalization-context-trace.js +374 -0
- package/dist/commands/runtime-internalization-context-trace.js.map +1 -0
- package/dist/commands/runtime-internalization-enqueue-successors.d.ts.map +1 -1
- package/dist/commands/runtime-internalization-enqueue-successors.js +3 -1
- package/dist/commands/runtime-internalization-enqueue-successors.js.map +1 -1
- package/dist/commands/runtime-internalization-integrity.d.ts.map +1 -1
- package/dist/commands/runtime-internalization-integrity.js +4 -2
- package/dist/commands/runtime-internalization-integrity.js.map +1 -1
- package/dist/commands/runtime-internalization-run-once.d.ts.map +1 -1
- package/dist/commands/runtime-internalization-run-once.js +20 -5
- package/dist/commands/runtime-internalization-run-once.js.map +1 -1
- package/dist/commands/runtime-internalization-run-rulehost.d.ts +1 -0
- package/dist/commands/runtime-internalization-run-rulehost.d.ts.map +1 -1
- package/dist/commands/runtime-internalization-run-rulehost.js +149 -6
- package/dist/commands/runtime-internalization-run-rulehost.js.map +1 -1
- package/dist/commands/runtime-uat.d.ts.map +1 -1
- package/dist/commands/runtime-uat.guard.test.js +13 -15
- package/dist/commands/runtime-uat.guard.test.js.map +1 -1
- package/dist/commands/runtime-uat.js +4 -3
- package/dist/commands/runtime-uat.js.map +1 -1
- package/dist/config-reader.js +1 -1
- package/dist/config-reader.js.map +1 -1
- package/dist/index.js +54 -78
- package/dist/index.js.map +1 -1
- package/dist/services/__tests__/evaluator-runner-deps.test.d.ts +2 -0
- package/dist/services/__tests__/evaluator-runner-deps.test.d.ts.map +1 -0
- package/dist/services/__tests__/evaluator-runner-deps.test.js +304 -0
- package/dist/services/__tests__/evaluator-runner-deps.test.js.map +1 -0
- package/dist/services/__tests__/rulehost-readiness.test.js.map +1 -1
- package/dist/services/__tests__/runtime-adapter-resolver.test.js.map +1 -1
- package/dist/services/config-doctor.d.ts.map +1 -1
- package/dist/services/config-doctor.js +9 -2
- package/dist/services/config-doctor.js.map +1 -1
- package/dist/services/console-launcher.d.ts +6 -0
- package/dist/services/console-launcher.d.ts.map +1 -1
- package/dist/services/console-launcher.js.map +1 -1
- package/dist/services/pain-flood-simulation-runner.d.ts.map +1 -1
- package/dist/services/pain-flood-simulation-runner.js +9 -0
- package/dist/services/pain-flood-simulation-runner.js.map +1 -1
- package/dist/services/pd-config-loader.js +2 -2
- package/dist/services/pd-config-loader.js.map +1 -1
- package/dist/services/proven-channel-baseline-runner.d.ts.map +1 -1
- package/dist/services/proven-channel-baseline-runner.js +10 -2
- package/dist/services/proven-channel-baseline-runner.js.map +1 -1
- package/dist/services/quality-scorecard/data-extractor.js.map +1 -1
- package/dist/services/resolve-runtime-from-pd-config.js.map +1 -1
- package/dist/services/rulehost-pipeline-runner.d.ts +77 -1
- package/dist/services/rulehost-pipeline-runner.d.ts.map +1 -1
- package/dist/services/rulehost-pipeline-runner.js +111 -7
- package/dist/services/rulehost-pipeline-runner.js.map +1 -1
- package/dist/services/rulehost-readiness.js.map +1 -1
- package/dist/services/runtime-adapter-resolver.d.ts +1 -0
- package/dist/services/runtime-adapter-resolver.d.ts.map +1 -1
- package/dist/services/runtime-adapter-resolver.js +3 -0
- package/dist/services/runtime-adapter-resolver.js.map +1 -1
- package/dist/services/synthetic-baseline-runner.d.ts.map +1 -1
- package/dist/services/synthetic-baseline-runner.js +11 -0
- package/dist/services/synthetic-baseline-runner.js.map +1 -1
- package/dist/utils/production-workspace-guard.d.ts +4 -4
- package/dist/utils/production-workspace-guard.d.ts.map +1 -1
- package/dist/utils/production-workspace-guard.js +19 -16
- package/dist/utils/production-workspace-guard.js.map +1 -1
- package/dist/utils/production-workspace-guard.test.js +54 -19
- package/dist/utils/production-workspace-guard.test.js.map +1 -1
- package/package.json +8 -5
- package/src/commands/__tests__/intent-flag-wiring.test.ts +88 -3
- package/src/commands/__tests__/pain-retry-maxTokens-flag.test.ts +73 -0
- package/src/commands/__tests__/rulecode-flag-wiring.test.ts +31 -24
- package/src/commands/__tests__/run-rulehost-flag-wiring.test.ts +12 -0
- package/src/commands/admission-gate.ts +30 -0
- package/src/commands/candidate.ts +74 -6
- package/src/commands/console.ts +41 -0
- package/src/commands/demo-story-a.ts +4 -2
- package/src/commands/diagnose.ts +158 -10
- package/src/commands/errors-list.ts +440 -0
- package/src/commands/health-codex.ts +249 -0
- package/src/commands/health.ts +60 -0
- package/src/commands/intent.ts +98 -29
- package/src/commands/pain-evidence.ts +6 -0
- package/src/commands/pain-record.ts +21 -0
- package/src/commands/pain-retry.ts +353 -35
- package/src/commands/proven-channel-baseline.ts +4 -2
- package/src/commands/runtime-activation.ts +657 -12
- package/src/commands/runtime-canary.ts +31 -5
- package/src/commands/runtime-health-snapshot.ts +4 -2
- package/src/commands/runtime-init.ts +305 -0
- package/src/commands/runtime-internalization-context-trace.ts +445 -0
- package/src/commands/runtime-internalization-enqueue-successors.ts +3 -1
- package/src/commands/runtime-internalization-integrity.ts +4 -2
- package/src/commands/runtime-internalization-run-once.ts +20 -5
- package/src/commands/runtime-internalization-run-rulehost.ts +159 -6
- package/src/commands/runtime-uat.guard.test.ts +14 -16
- package/src/commands/runtime-uat.ts +4 -3
- package/src/config-reader.ts +1 -1
- package/src/index.ts +69 -78
- package/src/services/__tests__/evaluator-runner-deps.test.ts +341 -0
- package/src/services/config-doctor.ts +9 -2
- package/src/services/console-launcher.ts +6 -0
- package/src/services/pain-flood-simulation-runner.ts +9 -0
- package/src/services/pd-config-loader.ts +2 -2
- package/src/services/proven-channel-baseline-runner.ts +10 -3
- package/src/services/rulehost-pipeline-runner.ts +157 -6
- package/src/services/runtime-adapter-resolver.ts +4 -0
- package/src/services/synthetic-baseline-runner.ts +11 -0
- package/src/utils/production-workspace-guard.test.ts +62 -20
- package/src/utils/production-workspace-guard.ts +19 -16
- package/tests/bdd/cli-contract.steps.ts +343 -0
- package/tests/bdd/context-trace.steps.ts +229 -0
- package/tests/bdd/support/gherkin-loader.ts +147 -0
- package/tests/bdd/support/repo-root.ts +62 -0
- package/tests/bdd/support/vitest-bdd.ts +179 -0
- package/tests/commands/admission-gate-coverage.test.ts +79 -0
- package/tests/commands/candidate-audit-repair.test.ts +77 -0
- package/tests/commands/candidate-intake.test.ts +155 -10
- package/tests/commands/candidate-internalization-backfill.test.ts +30 -0
- package/tests/commands/cli-help-snapshot.test.ts +15 -1
- package/tests/commands/console-open.test.ts +229 -32
- package/tests/commands/diagnose.test.ts +619 -14
- package/tests/commands/errors-list.test.ts +678 -0
- package/tests/commands/health-codex.test.ts +243 -0
- package/tests/commands/health-host-wiring.test.ts +76 -0
- package/tests/commands/intent.test.ts +4 -4
- package/tests/commands/pain-record.test.ts +80 -0
- package/tests/commands/pain-retry.test.ts +94 -3
- package/tests/commands/run-rulehost-handler.test.ts +133 -0
- package/tests/commands/runtime-activation-approve-flag-wiring.test.ts +227 -0
- package/tests/commands/runtime-activation-deactivate-flag-wiring.test.ts +167 -0
- package/tests/commands/runtime-activation-dispatch-flag-wiring.test.ts +262 -0
- package/tests/commands/runtime-activation-list-flag-wiring.test.ts +193 -0
- package/tests/commands/runtime-activation-promote-flag-wiring.test.ts +222 -0
- package/tests/commands/runtime-activation.test.ts +1090 -5
- package/tests/commands/runtime-canary.test.ts +50 -0
- package/tests/commands/runtime-features.test.ts +2 -0
- package/tests/commands/runtime-init-empty-workspace.test.ts +234 -0
- package/tests/commands/runtime-init.test.ts +346 -0
- package/tests/commands/runtime-internalization-context-trace.test.ts +110 -0
- package/tests/commands/runtime-internalization.test.ts +2 -2
- package/tests/config-reader.test.ts +6 -5
- package/tests/e2e/cross-package-acceptance.test.ts +55 -13
- package/tests/services/mainline-snapshot-assembler.test.ts +6 -3
- package/tests/services/resolve-runtime-from-pd-config.test.ts +2 -0
- package/tests/services/rulehost-pipeline-runner.test.ts +1 -1
- package/vitest.config.ts +1 -1
|
@@ -72,6 +72,19 @@ vi.mock('@principles/core/runtime-v2', () => ({
|
|
|
72
72
|
RuntimeStateManager: MockRuntimeStateManager,
|
|
73
73
|
resolveOutputLanguage: vi.fn().mockReturnValue({ outputLanguage: 'zh-CN' }),
|
|
74
74
|
PrincipleTreeLedgerAdapter: MockPrincipleTreeLedgerAdapter,
|
|
75
|
+
evaluateCandidateAdmissionFromRecord: vi.fn((candidate: { recommendationKind: string; confidence: number | null }) => {
|
|
76
|
+
// Mirror the real implementation for test purposes
|
|
77
|
+
if (candidate.recommendationKind === 'defer') {
|
|
78
|
+
return { decision: 'deferred', reason: 'recommendation_kind_defer_not_actionable', nextAction: 'review_defer_disposition_manually', evidenceStatus: 'unknown' };
|
|
79
|
+
}
|
|
80
|
+
if (candidate.confidence === null) {
|
|
81
|
+
return { decision: 'needs_evidence', reason: 'confidence_missing_on_candidate_record', nextAction: 're_run_diagnosis_to_populate_confidence_or_manual_review', evidenceStatus: 'unknown' };
|
|
82
|
+
}
|
|
83
|
+
if (candidate.confidence < 0.5) {
|
|
84
|
+
return { decision: 'needs_evidence', reason: `confidence_below_threshold:${candidate.confidence.toFixed(2)}<0.5`, nextAction: 'provide_additional_evidence_or_manual_review', evidenceStatus: 'unknown' };
|
|
85
|
+
}
|
|
86
|
+
return { decision: 'admitted', reason: 'cli_partial_check_passed_confidence_and_kind', nextAction: 'none', evidenceStatus: 'unknown' };
|
|
87
|
+
}),
|
|
75
88
|
}));
|
|
76
89
|
|
|
77
90
|
vi.mock('../../src/resolve-workspace.js', () => ({
|
|
@@ -348,29 +361,37 @@ describe('pd candidate intake', () => {
|
|
|
348
361
|
|
|
349
362
|
// Test 4: Non-existent candidate
|
|
350
363
|
it('Test 4 (non-existent candidate): throws CANDIDATE_NOT_FOUND error', async () => {
|
|
351
|
-
//
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
'CANDIDATE_NOT_FOUND',
|
|
356
|
-
{ candidateId: 'invalid-id' }
|
|
357
|
-
);
|
|
358
|
-
});
|
|
364
|
+
// PRI-442 Stage 4: getCandidate now returns null before service.intake is called.
|
|
365
|
+
// The CLI exits with "Candidate not found" instead of relying on service.intake
|
|
366
|
+
// to throw CANDIDATE_NOT_FOUND.
|
|
367
|
+
mockStateManager.getCandidate.mockResolvedValue(null);
|
|
359
368
|
|
|
360
369
|
await handleCandidateIntake({
|
|
361
370
|
candidateId: 'invalid-id',
|
|
362
371
|
workspace: '/tmp/test-workspace',
|
|
363
372
|
});
|
|
364
373
|
|
|
365
|
-
// Verify error output contains
|
|
374
|
+
// Verify error output contains "Candidate not found"
|
|
366
375
|
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
367
|
-
expect.stringContaining('
|
|
376
|
+
expect.stringContaining('Candidate not found')
|
|
368
377
|
);
|
|
369
378
|
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
370
379
|
});
|
|
371
380
|
|
|
372
381
|
// Test 5: Invalid candidateId (empty string)
|
|
373
382
|
it('Test 5 (invalid candidateId): empty string throws INPUT_INVALID error', async () => {
|
|
383
|
+
// PRI-442 Stage 4: getCandidate is called before service.intake, so we need
|
|
384
|
+
// to mock it to return a valid candidate so service.intake is reached.
|
|
385
|
+
const mockCandidate = {
|
|
386
|
+
candidateId: '',
|
|
387
|
+
artifactId: 'artifact-1',
|
|
388
|
+
taskId: 'task-1',
|
|
389
|
+
title: 'Test',
|
|
390
|
+
description: 'Test',
|
|
391
|
+
status: 'pending',
|
|
392
|
+
};
|
|
393
|
+
mockStateManager.getCandidate.mockResolvedValue(mockCandidate);
|
|
394
|
+
|
|
374
395
|
// Setup: service.intake throws INPUT_INVALID for empty candidateId
|
|
375
396
|
mockService.intake.mockImplementation(() => {
|
|
376
397
|
throw new MockCandidateIntakeError(
|
|
@@ -475,6 +496,18 @@ describe('pd candidate intake', () => {
|
|
|
475
496
|
|
|
476
497
|
// Test 7: Generic error (non-CandidateIntakeError, covers line 247)
|
|
477
498
|
it('Test 7 (generic error): handles non-CandidateIntakeError errors', async () => {
|
|
499
|
+
// PRI-442 Stage 4: getCandidate is called before service.intake, so we need
|
|
500
|
+
// to mock it to return a valid candidate so service.intake is reached.
|
|
501
|
+
const mockCandidate = {
|
|
502
|
+
candidateId: 'valid-id',
|
|
503
|
+
artifactId: 'artifact-1',
|
|
504
|
+
taskId: 'task-1',
|
|
505
|
+
title: 'Test',
|
|
506
|
+
description: 'Test',
|
|
507
|
+
status: 'pending',
|
|
508
|
+
};
|
|
509
|
+
mockStateManager.getCandidate.mockResolvedValue(mockCandidate);
|
|
510
|
+
|
|
478
511
|
// Setup: service.intake throws a generic Error (not CandidateIntakeError)
|
|
479
512
|
mockService.intake.mockImplementation(() => {
|
|
480
513
|
throw new Error('Some unexpected error');
|
|
@@ -584,4 +617,116 @@ describe('pd candidate intake', () => {
|
|
|
584
617
|
mockStateManager.connection = originalConnection;
|
|
585
618
|
}
|
|
586
619
|
});
|
|
620
|
+
|
|
621
|
+
// PRI-442 Stage 4: admission gate bypass tests
|
|
622
|
+
describe('admission gate bypass prevention (PRI-442 Stage 4)', () => {
|
|
623
|
+
it('refuses intake when recommendationKind is defer', async () => {
|
|
624
|
+
const mockCandidate = {
|
|
625
|
+
candidateId: 'defer-cand',
|
|
626
|
+
artifactId: 'artifact-1',
|
|
627
|
+
taskId: 'task-1',
|
|
628
|
+
title: 'Deferred candidate',
|
|
629
|
+
description: 'Should not be intakeable',
|
|
630
|
+
status: 'pending',
|
|
631
|
+
recommendationKind: 'defer',
|
|
632
|
+
confidence: 0.9,
|
|
633
|
+
};
|
|
634
|
+
mockStateManager.getCandidate.mockResolvedValue(mockCandidate);
|
|
635
|
+
|
|
636
|
+
await handleCandidateIntake({
|
|
637
|
+
candidateId: 'defer-cand',
|
|
638
|
+
workspace: '/tmp/test-workspace',
|
|
639
|
+
json: true,
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
// Should NOT call service.intake
|
|
643
|
+
expect(mockService.intake).not.toHaveBeenCalled();
|
|
644
|
+
// Should output refused status
|
|
645
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
646
|
+
expect.stringContaining('"status": "refused"')
|
|
647
|
+
);
|
|
648
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
649
|
+
expect.stringContaining('"admissionDecision": "deferred"')
|
|
650
|
+
);
|
|
651
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
it('refuses intake when confidence is below threshold', async () => {
|
|
655
|
+
const mockCandidate = {
|
|
656
|
+
candidateId: 'low-conf-cand',
|
|
657
|
+
artifactId: 'artifact-1',
|
|
658
|
+
taskId: 'task-1',
|
|
659
|
+
title: 'Low confidence candidate',
|
|
660
|
+
description: 'Should not be intakeable',
|
|
661
|
+
status: 'pending',
|
|
662
|
+
recommendationKind: 'principle',
|
|
663
|
+
confidence: 0.3,
|
|
664
|
+
};
|
|
665
|
+
mockStateManager.getCandidate.mockResolvedValue(mockCandidate);
|
|
666
|
+
|
|
667
|
+
await handleCandidateIntake({
|
|
668
|
+
candidateId: 'low-conf-cand',
|
|
669
|
+
workspace: '/tmp/test-workspace',
|
|
670
|
+
json: false,
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
expect(mockService.intake).not.toHaveBeenCalled();
|
|
674
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
675
|
+
expect.stringContaining('Admission gate refused')
|
|
676
|
+
);
|
|
677
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
678
|
+
expect.stringContaining('needs_evidence')
|
|
679
|
+
);
|
|
680
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
it('refuses intake when confidence is null (rc-3 fail loud)', async () => {
|
|
684
|
+
const mockCandidate = {
|
|
685
|
+
candidateId: 'null-conf-cand',
|
|
686
|
+
artifactId: 'artifact-1',
|
|
687
|
+
taskId: 'task-1',
|
|
688
|
+
title: 'Null confidence candidate',
|
|
689
|
+
description: 'Should not be intakeable',
|
|
690
|
+
status: 'pending',
|
|
691
|
+
recommendationKind: 'principle',
|
|
692
|
+
confidence: null,
|
|
693
|
+
};
|
|
694
|
+
mockStateManager.getCandidate.mockResolvedValue(mockCandidate);
|
|
695
|
+
|
|
696
|
+
await handleCandidateIntake({
|
|
697
|
+
candidateId: 'null-conf-cand',
|
|
698
|
+
workspace: '/tmp/test-workspace',
|
|
699
|
+
json: true,
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
expect(mockService.intake).not.toHaveBeenCalled();
|
|
703
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
704
|
+
expect.stringContaining('confidence_missing_on_candidate_record')
|
|
705
|
+
);
|
|
706
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
it('allows intake when candidate passes admission gate', async () => {
|
|
710
|
+
const mockCandidate = {
|
|
711
|
+
candidateId: 'admitted-cand',
|
|
712
|
+
artifactId: 'artifact-1',
|
|
713
|
+
taskId: 'task-1',
|
|
714
|
+
title: 'Admitted candidate',
|
|
715
|
+
description: 'Should be intakeable',
|
|
716
|
+
status: 'pending',
|
|
717
|
+
recommendationKind: 'principle',
|
|
718
|
+
confidence: 0.8,
|
|
719
|
+
};
|
|
720
|
+
mockStateManager.getCandidate.mockResolvedValue(mockCandidate);
|
|
721
|
+
mockService.intake.mockResolvedValue({ id: 'entry-1', title: 'Test', text: 'Test', status: 'probation' });
|
|
722
|
+
|
|
723
|
+
await handleCandidateIntake({
|
|
724
|
+
candidateId: 'admitted-cand',
|
|
725
|
+
workspace: '/tmp/test-workspace',
|
|
726
|
+
json: true,
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
expect(mockService.intake).toHaveBeenCalledWith('admitted-cand');
|
|
730
|
+
});
|
|
731
|
+
});
|
|
587
732
|
});
|
|
@@ -477,6 +477,36 @@ describe('Commander wiring for backfill --include-pending', () => {
|
|
|
477
477
|
expect(output.details.totalPending).toBe(1);
|
|
478
478
|
});
|
|
479
479
|
|
|
480
|
+
// PRI-442 Stage 4: admission gate bypass prevention for backfill --include-pending
|
|
481
|
+
it('--include-pending refuses deferred candidate (admission gate bypass prevention)', async () => {
|
|
482
|
+
mockPrepareAll.mockImplementation((sql: string) => {
|
|
483
|
+
if (sql.includes("'consumed'")) return [];
|
|
484
|
+
if (sql.includes("'pending'")) return [{ candidate_id: 'cand-defer-1' }];
|
|
485
|
+
return [];
|
|
486
|
+
});
|
|
487
|
+
mockGetCandidate.mockResolvedValue({
|
|
488
|
+
candidateId: 'cand-defer-1',
|
|
489
|
+
taskId: 'diag-task-cand-defer-1',
|
|
490
|
+
description: 'deferred candidate',
|
|
491
|
+
recommendationKind: 'defer',
|
|
492
|
+
confidence: 0.9,
|
|
493
|
+
sourceRecommendationJson: JSON.stringify({ kind: 'defer', description: 'deferred' }),
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
const program = createBackfillTestProgram();
|
|
497
|
+
await program.parseAsync(['node', 'pd', 'candidate', 'internalization', 'backfill', '--workspace', WS, '--include-pending', '--confirm', '--json']);
|
|
498
|
+
|
|
499
|
+
const output = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
500
|
+
// Should NOT have called intake for the deferred candidate
|
|
501
|
+
expect(mockIntake).not.toHaveBeenCalled();
|
|
502
|
+
// Should have an intake_failed result with admission gate reason
|
|
503
|
+
const failedResult = output.details.results.find(
|
|
504
|
+
(r: { status: string; reason: string }) => r.status === 'intake_failed' && r.reason.includes('Admission gate refused'),
|
|
505
|
+
);
|
|
506
|
+
expect(failedResult).toBeDefined();
|
|
507
|
+
expect(output.details.intakeFailed).toBe(1);
|
|
508
|
+
});
|
|
509
|
+
|
|
480
510
|
it('--dry-run -> RuntimeStateManager readonly=true', async () => {
|
|
481
511
|
const program = createBackfillTestProgram();
|
|
482
512
|
await program.parseAsync(['node', 'pd', 'candidate', 'internalization', 'backfill', '--workspace', WS, '--dry-run', '--json']);
|
|
@@ -86,7 +86,6 @@ describe('PRI-455: pd runtime --help shows only MVP owner subcommands', () => {
|
|
|
86
86
|
'canary',
|
|
87
87
|
'synthetic',
|
|
88
88
|
'uat',
|
|
89
|
-
'internalization',
|
|
90
89
|
'recovery',
|
|
91
90
|
'pruning',
|
|
92
91
|
'diagnostics',
|
|
@@ -133,3 +132,18 @@ describe('PRI-455: hidden commands still function (de-surface, not delete)', ()
|
|
|
133
132
|
expect(output).toContain('--apply');
|
|
134
133
|
});
|
|
135
134
|
});
|
|
135
|
+
|
|
136
|
+
describe('pd demo first-principle alias (Fix-24)', () => {
|
|
137
|
+
// cli-7-test-wiring: verify the alias resolves to the same command as story-a.
|
|
138
|
+
it('pd demo first-principle --help works and matches story-a', () => {
|
|
139
|
+
const aliasOutput = runPdHelp(['demo', 'first-principle', '--help']);
|
|
140
|
+
const storyAOutput = runPdHelp(['demo', 'story-a', '--help']);
|
|
141
|
+
|
|
142
|
+
// Both should show the same options
|
|
143
|
+
expect(aliasOutput).toContain('--workspace');
|
|
144
|
+
expect(aliasOutput).toContain('--json');
|
|
145
|
+
expect(aliasOutput).toContain('--channels');
|
|
146
|
+
// Alias output should be identical to story-a output
|
|
147
|
+
expect(aliasOutput).toBe(storyAOutput);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
@@ -496,11 +496,22 @@ describe('CLI command wiring (pd console open)', () => {
|
|
|
496
496
|
workspaceRoot = path.resolve(__dirname, '../../../..');
|
|
497
497
|
cliPath = path.join(workspaceRoot, 'packages', 'pd-cli', 'dist', 'index.js');
|
|
498
498
|
tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'pd-console-open-test-'));
|
|
499
|
-
//
|
|
500
|
-
|
|
501
|
-
|
|
499
|
+
// Isolated fake HOME so the tests never read from — or rmSync-destroy — a
|
|
500
|
+
// real ~/.openclaw install on developer machines. runPd() points the CLI
|
|
501
|
+
// child at this home via __PD_CONSOLE_TEST_FAKE_HOME.
|
|
502
|
+
const fakeHome = path.join(tmp, 'home');
|
|
503
|
+
process.env.__PD_CONSOLE_TEST_FAKE_HOME = fakeHome;
|
|
504
|
+
// Fake a console install: dir + ESM package.json + dist/server.js with a
|
|
505
|
+
// minimal HTTP server. The package.json "type": "module" makes the fake
|
|
506
|
+
// server's module system deterministic regardless of whether a real
|
|
507
|
+
// extension-root package.json exists above the console dir.
|
|
508
|
+
const consoleDir = path.join(fakeHome, '.openclaw', 'extensions', 'principles-disciple', 'console');
|
|
509
|
+
fs.mkdirSync(path.join(consoleDir, 'dist', 'web'), { recursive: true });
|
|
510
|
+
fs.writeFileSync(path.join(consoleDir, 'package.json'), JSON.stringify({ name: 'fake-pd-console', version: '0.0.0', type: 'module' }, null, 2));
|
|
511
|
+
// EP-06 regression guard: dist/web/index.html must exist (PR #1169 fix)
|
|
512
|
+
fs.writeFileSync(path.join(consoleDir, 'dist', 'web', 'index.html'), '<!DOCTYPE html><html></html>');
|
|
502
513
|
fs.writeFileSync(path.join(consoleDir, 'dist', 'server.js'), `
|
|
503
|
-
|
|
514
|
+
import http from 'node:http';
|
|
504
515
|
const args = process.argv.slice(2);
|
|
505
516
|
const portIdx = args.indexOf('--port');
|
|
506
517
|
const port = portIdx >= 0 ? parseInt(args[portIdx + 1]) : 3100;
|
|
@@ -522,11 +533,8 @@ describe('CLI command wiring (pd console open)', () => {
|
|
|
522
533
|
});
|
|
523
534
|
|
|
524
535
|
afterEach(() => {
|
|
536
|
+
delete process.env.__PD_CONSOLE_TEST_FAKE_HOME;
|
|
525
537
|
try { fs.rmSync(tmp, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
526
|
-
try {
|
|
527
|
-
const consoleDir = path.join(os.homedir(), '.openclaw', 'extensions', 'principles-disciple', 'console');
|
|
528
|
-
fs.rmSync(consoleDir, { recursive: true, force: true });
|
|
529
|
-
} catch { /* ignore */ }
|
|
530
538
|
});
|
|
531
539
|
|
|
532
540
|
it('console open subcommand is registered (pd console open --help)', () => {
|
|
@@ -569,6 +577,58 @@ describe('CLI command wiring (pd console open)', () => {
|
|
|
569
577
|
expect(parsed).toHaveProperty('browserOpened');
|
|
570
578
|
}, 10_000);
|
|
571
579
|
|
|
580
|
+
it('pd console open --json fresh spawn includes a positive integer serverPid (PRI-526)', async () => {
|
|
581
|
+
// Async stream-parse + guaranteed teardown. execFileSync patterns are
|
|
582
|
+
// unreliable here on Windows: the CLI keeps running after printing JSON
|
|
583
|
+
// (fresh spawn) or may unexpectedly take another branch, and killing via
|
|
584
|
+
// timeout can leave the grandchild server holding pipe handles.
|
|
585
|
+
let run: CliJsonRun | undefined;
|
|
586
|
+
try {
|
|
587
|
+
run = await runPdUntilJson(
|
|
588
|
+
['console', 'open', '--workspace', tmp, '--port', '49390', '--json', '--no-browser'],
|
|
589
|
+
workspaceRoot,
|
|
590
|
+
);
|
|
591
|
+
if (!isRecord(run.parsed)) throw new Error('CLI JSON output was not an object');
|
|
592
|
+
expect(run.parsed.status).toBe('started');
|
|
593
|
+
expect(run.parsed.reused).toBe(false);
|
|
594
|
+
const serverPid = run.parsed.serverPid;
|
|
595
|
+
expect(typeof serverPid === 'number' && Number.isInteger(serverPid) && serverPid > 0).toBe(true);
|
|
596
|
+
} finally {
|
|
597
|
+
await teardownCliTree(run);
|
|
598
|
+
}
|
|
599
|
+
}, 20_000);
|
|
600
|
+
|
|
601
|
+
it('pd console open --json reused path does NOT include serverPid (PRI-526)', async () => {
|
|
602
|
+
// Stand up a fake healthy console in-process, then point the CLI at its
|
|
603
|
+
// port: planConsoleLaunch probes /api/health → 200 → reused, no spawn.
|
|
604
|
+
const server = http.createServer((req, res) => {
|
|
605
|
+
if (req.url === '/api/health') {
|
|
606
|
+
res.statusCode = 200;
|
|
607
|
+
res.end(JSON.stringify({ success: true }));
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
res.statusCode = 404;
|
|
611
|
+
res.end();
|
|
612
|
+
});
|
|
613
|
+
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', () => resolve()));
|
|
614
|
+
const addr = server.address();
|
|
615
|
+
if (typeof addr !== 'object' || !addr) throw new Error('no addr');
|
|
616
|
+
let run: CliJsonRun | undefined;
|
|
617
|
+
try {
|
|
618
|
+
run = await runPdUntilJson(
|
|
619
|
+
['console', 'open', '--workspace', tmp, '--port', String(addr.port), '--json', '--no-browser'],
|
|
620
|
+
workspaceRoot,
|
|
621
|
+
);
|
|
622
|
+
if (!isRecord(run.parsed)) throw new Error('CLI JSON output was not an object');
|
|
623
|
+
expect(run.parsed.status).toBe('reused');
|
|
624
|
+
expect(run.parsed.reused).toBe(true);
|
|
625
|
+
expect(run.parsed).not.toHaveProperty('serverPid');
|
|
626
|
+
} finally {
|
|
627
|
+
await teardownCliTree(run);
|
|
628
|
+
server.close();
|
|
629
|
+
}
|
|
630
|
+
}, 20_000);
|
|
631
|
+
|
|
572
632
|
it('pd console open --port 99999 --json returns a structured failure (invalid port)', () => {
|
|
573
633
|
const out = runPd(['console', 'open', '--workspace', tmp, '--port', '99999', '--json', '--no-browser'], workspaceRoot);
|
|
574
634
|
const parsed = JSON.parse(out);
|
|
@@ -666,11 +726,22 @@ describe('CLI command wiring (pd console open)', () => {
|
|
|
666
726
|
|
|
667
727
|
it('sets browserOpened: false when browser fails to open', async () => {
|
|
668
728
|
const { handleConsoleOpen } = await import('../../src/commands/console.js');
|
|
669
|
-
|
|
729
|
+
|
|
670
730
|
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => {}) as any);
|
|
671
731
|
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
672
732
|
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
673
733
|
|
|
734
|
+
// The in-process handler resolves the console dir via getConsoleDir(),
|
|
735
|
+
// which reads HOME/USERPROFILE directly. Point them at the isolated
|
|
736
|
+
// fake home — a clean CI runner has no real ~/.openclaw console, and
|
|
737
|
+
// relying on one (as this test implicitly did before) makes the result
|
|
738
|
+
// machine-dependent.
|
|
739
|
+
const fakeHome = process.env.__PD_CONSOLE_TEST_FAKE_HOME ?? '';
|
|
740
|
+
const savedHome = process.env.HOME;
|
|
741
|
+
const savedUserProfile = process.env.USERPROFILE;
|
|
742
|
+
process.env.HOME = fakeHome;
|
|
743
|
+
process.env.USERPROFILE = fakeHome;
|
|
744
|
+
|
|
674
745
|
const mockChild = new EventEmitter() as any;
|
|
675
746
|
mockChild.unref = vi.fn();
|
|
676
747
|
let spawnCalled = false;
|
|
@@ -698,23 +769,29 @@ describe('CLI command wiring (pd console open)', () => {
|
|
|
698
769
|
};
|
|
699
770
|
};
|
|
700
771
|
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
772
|
+
try {
|
|
773
|
+
await handleConsoleOpen({
|
|
774
|
+
workspace: tmp,
|
|
775
|
+
json: false,
|
|
776
|
+
});
|
|
705
777
|
|
|
706
|
-
|
|
778
|
+
await new Promise(resolve => setTimeout(resolve, 150));
|
|
707
779
|
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
expect(loggedOutput).not.toContain('Browser opened');
|
|
713
|
-
expect(loggedOutput).toContain('Open http://127.0.0.1:3100 in your browser');
|
|
780
|
+
const loggedOutput = logSpy.mock.calls.map(c => c.join(' ')).join('\n');
|
|
781
|
+
expect(exitSpy).not.toHaveBeenCalled();
|
|
782
|
+
expect(spawnCalled).toBe(true);
|
|
714
783
|
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
784
|
+
expect(loggedOutput).not.toContain('Browser opened');
|
|
785
|
+
expect(loggedOutput).toContain('Open http://127.0.0.1:3100 in your browser');
|
|
786
|
+
} finally {
|
|
787
|
+
if (savedHome === undefined) delete process.env.HOME;
|
|
788
|
+
else process.env.HOME = savedHome;
|
|
789
|
+
if (savedUserProfile === undefined) delete process.env.USERPROFILE;
|
|
790
|
+
else process.env.USERPROFILE = savedUserProfile;
|
|
791
|
+
exitSpy.mockRestore();
|
|
792
|
+
logSpy.mockRestore();
|
|
793
|
+
errorSpy.mockRestore();
|
|
794
|
+
}
|
|
718
795
|
});
|
|
719
796
|
});
|
|
720
797
|
|
|
@@ -752,6 +829,13 @@ describe('CLI command wiring (pd console open)', () => {
|
|
|
752
829
|
it('[::1] is accepted and normalized to ::1 (not refused)', () => {
|
|
753
830
|
// May spawn a long-lived server, use timeout.
|
|
754
831
|
const out = runPd(['console', 'open', '--workspace', tmp, '--host', '[::1]', '--json', '--no-browser'], workspaceRoot, 8_000);
|
|
832
|
+
if (out.trim() === '') {
|
|
833
|
+
// The CLI was still in its 15s ready-poll when execFileSync timed out.
|
|
834
|
+
// Known environment limitation: on Windows hosts where IPv6 loopback
|
|
835
|
+
// connections are refused (EACCES), the health probe on ::1 never
|
|
836
|
+
// succeeds. CI (Linux) is unaffected.
|
|
837
|
+
throw new Error('CLI produced no JSON within 8s — ::1 health probe never succeeded (IPv6 loopback may be blocked on this machine)');
|
|
838
|
+
}
|
|
755
839
|
const parsed = JSON.parse(out);
|
|
756
840
|
// Should NOT be refused — [::1] is loopback
|
|
757
841
|
expect(parsed.status).not.toBe('refused');
|
|
@@ -759,22 +843,30 @@ describe('CLI command wiring (pd console open)', () => {
|
|
|
759
843
|
expect(parsed.host).toBe('::1');
|
|
760
844
|
}, 10_000);
|
|
761
845
|
});
|
|
846
|
+
|
|
847
|
+
describe('EP-06 regression guard: dist/web/index.html check (PR #1169)', () => {
|
|
848
|
+
it('returns console_web_ui_missing when dist/web/index.html does not exist', () => {
|
|
849
|
+
// beforeEach creates the fake console dir WITH dist/web/index.html.
|
|
850
|
+
// Remove it to simulate a corrupted install (the PR #1169 regression).
|
|
851
|
+
const fakeHome = process.env.__PD_CONSOLE_TEST_FAKE_HOME ?? '';
|
|
852
|
+
const webIndex = path.join(fakeHome, '.openclaw', 'extensions', 'principles-disciple', 'console', 'dist', 'web', 'index.html');
|
|
853
|
+
fs.rmSync(webIndex, { force: true });
|
|
854
|
+
|
|
855
|
+
const out = runPd(['console', 'open', '--workspace', tmp, '--json', '--no-browser'], workspaceRoot, 5_000);
|
|
856
|
+
const parsed = JSON.parse(out);
|
|
857
|
+
expect(parsed.status).toBe('failed');
|
|
858
|
+
expect(parsed.reason).toBe('console_web_ui_missing');
|
|
859
|
+
expect(parsed.nextAction).toMatch(/create-principles-disciple/);
|
|
860
|
+
}, 10_000);
|
|
861
|
+
});
|
|
762
862
|
});
|
|
763
863
|
|
|
764
864
|
function runPd(args: string[], cwd: string, timeoutMs?: number): string {
|
|
765
865
|
try {
|
|
766
|
-
const env: Record<string, string> = { ...process.env };
|
|
767
|
-
if (!args.includes('--workspace') && !args.includes('--help') && !args.includes('-h')) {
|
|
768
|
-
env.USERPROFILE = '/nonexistent';
|
|
769
|
-
env.HOME = '/nonexistent';
|
|
770
|
-
env.HOMEPATH = '/nonexistent';
|
|
771
|
-
env.HOMEDRIVE = '/nonexistent';
|
|
772
|
-
delete env.PD_WORKSPACE_DIR;
|
|
773
|
-
}
|
|
774
866
|
return execFileSync('node', [getBuiltPdCliPath(), ...args], {
|
|
775
867
|
encoding: 'utf8',
|
|
776
868
|
cwd,
|
|
777
|
-
env,
|
|
869
|
+
env: buildChildEnv(),
|
|
778
870
|
timeout: timeoutMs,
|
|
779
871
|
});
|
|
780
872
|
} catch (err: unknown) {
|
|
@@ -787,3 +879,108 @@ function runPd(args: string[], cwd: string, timeoutMs?: number): string {
|
|
|
787
879
|
throw err;
|
|
788
880
|
}
|
|
789
881
|
}
|
|
882
|
+
|
|
883
|
+
/** Env for spawned CLI processes: isolated fake HOME, no workspace env leaks. */
|
|
884
|
+
function buildChildEnv(): Record<string, string> {
|
|
885
|
+
const env = { ...process.env } as Record<string, string>;
|
|
886
|
+
const fakeHome = process.env.__PD_CONSOLE_TEST_FAKE_HOME;
|
|
887
|
+
if (fakeHome) {
|
|
888
|
+
env.USERPROFILE = fakeHome;
|
|
889
|
+
env.HOME = fakeHome;
|
|
890
|
+
delete env.HOMEPATH;
|
|
891
|
+
delete env.HOMEDRIVE;
|
|
892
|
+
delete env.PD_WORKSPACE_DIR;
|
|
893
|
+
}
|
|
894
|
+
return env;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
/** Runtime type guard for unknown CLI JSON output (rc-1/rc-2). */
|
|
898
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
899
|
+
return typeof value === 'object' && value !== null;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// ─── Async CLI runner with guaranteed teardown (PRI-526 tests) ───────────────
|
|
903
|
+
|
|
904
|
+
interface CliJsonRun {
|
|
905
|
+
parsed: unknown;
|
|
906
|
+
child: childProcessModule.ChildProcess;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
/**
|
|
910
|
+
* Spawn the CLI and resolve as soon as stdout contains one parseable JSON
|
|
911
|
+
* object. Fails fast (bounded timeout / early exit) with the collected
|
|
912
|
+
* stdout in the error message. The caller MUST call teardownCliTree(run)
|
|
913
|
+
* in a finally block — the CLI process may still be alive after the JSON
|
|
914
|
+
* is emitted (fresh-spawn mode keeps the console server attached).
|
|
915
|
+
*/
|
|
916
|
+
async function runPdUntilJson(args: string[], cwd: string, timeoutMs = 12_000): Promise<CliJsonRun> {
|
|
917
|
+
const child = childProcessModule.spawn('node', [getBuiltPdCliPath(), ...args], {
|
|
918
|
+
cwd,
|
|
919
|
+
env: buildChildEnv(),
|
|
920
|
+
});
|
|
921
|
+
let stdout = '';
|
|
922
|
+
child.stdout?.on('data', (d: Buffer) => { stdout += d.toString('utf8'); });
|
|
923
|
+
|
|
924
|
+
const parsed: unknown = await new Promise<unknown>((resolve, reject) => {
|
|
925
|
+
let done = false;
|
|
926
|
+
const fail = (msg: string): void => {
|
|
927
|
+
if (done) return;
|
|
928
|
+
done = true;
|
|
929
|
+
reject(new Error(`${msg}; stdout so far: ${stdout.slice(0, 400)}`));
|
|
930
|
+
};
|
|
931
|
+
const timer = setTimeout(() => fail('timed out waiting for CLI JSON'), timeoutMs);
|
|
932
|
+
const tryParse = (): boolean => {
|
|
933
|
+
const trimmed = stdout.trim();
|
|
934
|
+
if (!trimmed.startsWith('{')) return false;
|
|
935
|
+
try {
|
|
936
|
+
const value = JSON.parse(trimmed) as unknown;
|
|
937
|
+
done = true;
|
|
938
|
+
clearTimeout(timer);
|
|
939
|
+
resolve(value);
|
|
940
|
+
return true;
|
|
941
|
+
} catch {
|
|
942
|
+
return false; // partial pretty-printed JSON — wait for more chunks
|
|
943
|
+
}
|
|
944
|
+
};
|
|
945
|
+
if (tryParse()) return;
|
|
946
|
+
const onData = (): void => {
|
|
947
|
+
if (tryParse()) child.stdout?.off('data', onData);
|
|
948
|
+
};
|
|
949
|
+
child.stdout?.on('data', onData);
|
|
950
|
+
child.on('error', (err: Error) => fail(`CLI spawn error: ${err.message}`));
|
|
951
|
+
child.on('exit', (code: number | null) => fail(`CLI exited with code ${code} before emitting JSON`));
|
|
952
|
+
});
|
|
953
|
+
|
|
954
|
+
return { parsed, child };
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
/**
|
|
958
|
+
* Deterministic teardown of a runPdUntilJson run: kill the reported console
|
|
959
|
+
* server PID (grandchild) first so the CLI can exit on its own, then wait a
|
|
960
|
+
* bounded time, then force-kill the whole CLI tree. Safe to call multiple
|
|
961
|
+
* times and with undefined.
|
|
962
|
+
*/
|
|
963
|
+
async function teardownCliTree(run: CliJsonRun | undefined): Promise<void> {
|
|
964
|
+
if (!run) return;
|
|
965
|
+
const record = isRecord(run.parsed) ? run.parsed : undefined;
|
|
966
|
+
const serverPid = record !== undefined && typeof record.serverPid === 'number' ? record.serverPid : undefined;
|
|
967
|
+
if (serverPid !== undefined) {
|
|
968
|
+
try { process.kill(serverPid); } catch { /* already gone */ }
|
|
969
|
+
}
|
|
970
|
+
await new Promise<void>((resolve) => {
|
|
971
|
+
const t = setTimeout(() => { killTreeForce(run.child); resolve(); }, 4_000);
|
|
972
|
+
run.child.once('exit', () => { clearTimeout(t); resolve(); });
|
|
973
|
+
});
|
|
974
|
+
killTreeForce(run.child);
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
/**
|
|
978
|
+
* Last-resort kill of the CLI child process. The console server grandchild is
|
|
979
|
+
* reaped separately via its reported serverPid (see teardownCliTree), so a
|
|
980
|
+
* plain signal kill suffices — no shell/argv command wrapper needed.
|
|
981
|
+
*/
|
|
982
|
+
function killTreeForce(child: childProcessModule.ChildProcess): void {
|
|
983
|
+
try {
|
|
984
|
+
child.kill('SIGKILL');
|
|
985
|
+
} catch { /* already gone */ }
|
|
986
|
+
}
|