@principles/pd-cli 1.152.12 → 1.152.14
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/candidate.d.ts.map +1 -1
- package/dist/commands/candidate.js +34 -7
- package/dist/commands/candidate.js.map +1 -1
- package/dist/commands/diagnose.d.ts.map +1 -1
- package/dist/commands/diagnose.js +23 -7
- package/dist/commands/diagnose.js.map +1 -1
- package/dist/commands/pain-retry.d.ts.map +1 -1
- package/dist/commands/pain-retry.js +21 -7
- package/dist/commands/pain-retry.js.map +1 -1
- package/package.json +13 -7
- package/CHANGELOG.md +0 -23
- package/dist/commands/__tests__/intent-flag-wiring.test.d.ts +0 -9
- package/dist/commands/__tests__/intent-flag-wiring.test.d.ts.map +0 -1
- package/dist/commands/__tests__/intent-flag-wiring.test.js +0 -237
- package/dist/commands/__tests__/intent-flag-wiring.test.js.map +0 -1
- package/dist/commands/__tests__/legacy-cleanup.test.d.ts +0 -18
- package/dist/commands/__tests__/legacy-cleanup.test.d.ts.map +0 -1
- package/dist/commands/__tests__/legacy-cleanup.test.js +0 -467
- package/dist/commands/__tests__/legacy-cleanup.test.js.map +0 -1
- package/dist/commands/__tests__/mvp-smoke.test.d.ts +0 -15
- package/dist/commands/__tests__/mvp-smoke.test.d.ts.map +0 -1
- package/dist/commands/__tests__/mvp-smoke.test.js +0 -262
- package/dist/commands/__tests__/mvp-smoke.test.js.map +0 -1
- package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.d.ts +0 -2
- package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.d.ts.map +0 -1
- package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.js +0 -60
- package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.js.map +0 -1
- package/dist/commands/__tests__/rulecode-flag-wiring.test.d.ts +0 -21
- package/dist/commands/__tests__/rulecode-flag-wiring.test.d.ts.map +0 -1
- package/dist/commands/__tests__/rulecode-flag-wiring.test.js +0 -186
- package/dist/commands/__tests__/rulecode-flag-wiring.test.js.map +0 -1
- package/dist/commands/__tests__/rulecode-handler.test.d.ts +0 -16
- package/dist/commands/__tests__/rulecode-handler.test.d.ts.map +0 -1
- package/dist/commands/__tests__/rulecode-handler.test.js +0 -285
- package/dist/commands/__tests__/rulecode-handler.test.js.map +0 -1
- package/dist/commands/__tests__/run-rulehost-flag-wiring.test.d.ts +0 -24
- package/dist/commands/__tests__/run-rulehost-flag-wiring.test.d.ts.map +0 -1
- package/dist/commands/__tests__/run-rulehost-flag-wiring.test.js +0 -232
- package/dist/commands/__tests__/run-rulehost-flag-wiring.test.js.map +0 -1
- package/dist/commands/__tests__/runtime-activation-nextaction.test.d.ts +0 -15
- package/dist/commands/__tests__/runtime-activation-nextaction.test.d.ts.map +0 -1
- package/dist/commands/__tests__/runtime-activation-nextaction.test.js +0 -73
- package/dist/commands/__tests__/runtime-activation-nextaction.test.js.map +0 -1
- package/dist/commands/__tests__/runtime-probe-config.test.d.ts +0 -20
- package/dist/commands/__tests__/runtime-probe-config.test.d.ts.map +0 -1
- package/dist/commands/__tests__/runtime-probe-config.test.js +0 -389
- package/dist/commands/__tests__/runtime-probe-config.test.js.map +0 -1
- package/dist/commands/__tests__/telemetry-flag-wiring.test.d.ts +0 -9
- package/dist/commands/__tests__/telemetry-flag-wiring.test.d.ts.map +0 -1
- package/dist/commands/__tests__/telemetry-flag-wiring.test.js +0 -76
- package/dist/commands/__tests__/telemetry-flag-wiring.test.js.map +0 -1
- package/dist/commands/__tests__/version.test.d.ts +0 -2
- package/dist/commands/__tests__/version.test.d.ts.map +0 -1
- package/dist/commands/__tests__/version.test.js +0 -238
- package/dist/commands/__tests__/version.test.js.map +0 -1
- package/dist/commands/runtime-uat.guard.test.d.ts +0 -2
- package/dist/commands/runtime-uat.guard.test.d.ts.map +0 -1
- package/dist/commands/runtime-uat.guard.test.js +0 -153
- package/dist/commands/runtime-uat.guard.test.js.map +0 -1
- package/dist/services/__tests__/cli-output.test.d.ts +0 -18
- package/dist/services/__tests__/cli-output.test.d.ts.map +0 -1
- package/dist/services/__tests__/cli-output.test.js +0 -103
- package/dist/services/__tests__/cli-output.test.js.map +0 -1
- package/dist/services/__tests__/evaluator-runner-deps.test.d.ts +0 -2
- package/dist/services/__tests__/evaluator-runner-deps.test.d.ts.map +0 -1
- package/dist/services/__tests__/evaluator-runner-deps.test.js +0 -343
- package/dist/services/__tests__/evaluator-runner-deps.test.js.map +0 -1
- package/dist/services/__tests__/rulehost-readiness.test.d.ts +0 -2
- package/dist/services/__tests__/rulehost-readiness.test.d.ts.map +0 -1
- package/dist/services/__tests__/rulehost-readiness.test.js +0 -311
- package/dist/services/__tests__/rulehost-readiness.test.js.map +0 -1
- package/dist/services/__tests__/runtime-adapter-resolver.test.d.ts +0 -18
- package/dist/services/__tests__/runtime-adapter-resolver.test.d.ts.map +0 -1
- package/dist/services/__tests__/runtime-adapter-resolver.test.js +0 -673
- package/dist/services/__tests__/runtime-adapter-resolver.test.js.map +0 -1
- package/dist/utils/production-workspace-guard.test.d.ts +0 -2
- package/dist/utils/production-workspace-guard.test.d.ts.map +0 -1
- package/dist/utils/production-workspace-guard.test.js +0 -137
- package/dist/utils/production-workspace-guard.test.js.map +0 -1
- package/scripts/llm-dogfood.ts +0 -456
- package/scripts/migrate-illegal-expected-decision.ts +0 -252
- package/src/commands/__tests__/intent-flag-wiring.test.ts +0 -282
- package/src/commands/__tests__/legacy-cleanup.test.ts +0 -602
- package/src/commands/__tests__/mvp-smoke.test.ts +0 -300
- package/src/commands/__tests__/pain-retry-maxTokens-flag.test.ts +0 -73
- package/src/commands/__tests__/rulecode-flag-wiring.test.ts +0 -237
- package/src/commands/__tests__/rulecode-handler.test.ts +0 -369
- package/src/commands/__tests__/run-rulehost-flag-wiring.test.ts +0 -292
- package/src/commands/__tests__/runtime-activation-nextaction.test.ts +0 -81
- package/src/commands/__tests__/runtime-probe-config.test.ts +0 -432
- package/src/commands/__tests__/telemetry-flag-wiring.test.ts +0 -84
- package/src/commands/__tests__/version.test.ts +0 -251
- package/src/commands/admission-gate.ts +0 -30
- package/src/commands/artifact.ts +0 -82
- package/src/commands/build-trajectory-evidence.ts +0 -298
- package/src/commands/candidate.ts +0 -1250
- package/src/commands/codex-ingest-catchup.ts +0 -114
- package/src/commands/codex-ingest-quarantine.ts +0 -127
- package/src/commands/codex-reconcile.ts +0 -84
- package/src/commands/codex-setup.ts +0 -476
- package/src/commands/codex-worker.ts +0 -185
- package/src/commands/command-helpers.ts +0 -24
- package/src/commands/config-doctor.ts +0 -193
- package/src/commands/console.ts +0 -797
- package/src/commands/context.ts +0 -72
- package/src/commands/demo-story-a.ts +0 -241
- package/src/commands/diagnose.ts +0 -756
- package/src/commands/errors-list.ts +0 -441
- package/src/commands/evolution-tasks-list.ts +0 -48
- package/src/commands/evolution-tasks-show.ts +0 -62
- package/src/commands/flow.ts +0 -60
- package/src/commands/health-codex.ts +0 -576
- package/src/commands/health.ts +0 -315
- package/src/commands/history.ts +0 -63
- package/src/commands/intent.ts +0 -457
- package/src/commands/legacy-cleanup.ts +0 -531
- package/src/commands/legacy-import.ts +0 -104
- package/src/commands/mvp-smoke.ts +0 -160
- package/src/commands/pain-evidence.ts +0 -226
- package/src/commands/pain-list.ts +0 -239
- package/src/commands/pain-record.ts +0 -612
- package/src/commands/pain-retry.ts +0 -916
- package/src/commands/principles-stats.ts +0 -700
- package/src/commands/proven-channel-baseline.ts +0 -115
- package/src/commands/quality-scorecard.ts +0 -272
- package/src/commands/remediation-output.ts +0 -66
- package/src/commands/rulecode.ts +0 -465
- package/src/commands/run.ts +0 -89
- package/src/commands/runtime-activation.ts +0 -1629
- package/src/commands/runtime-artifact-repair.ts +0 -524
- package/src/commands/runtime-canary.ts +0 -395
- package/src/commands/runtime-compatibility-scan.ts +0 -105
- package/src/commands/runtime-diagnostics-export.ts +0 -229
- package/src/commands/runtime-features.ts +0 -153
- package/src/commands/runtime-gfi-snapshot.ts +0 -135
- package/src/commands/runtime-health-snapshot.ts +0 -108
- package/src/commands/runtime-init.ts +0 -429
- package/src/commands/runtime-internalization-enqueue-successors.ts +0 -530
- package/src/commands/runtime-internalization-integrity-repair.ts +0 -79
- package/src/commands/runtime-internalization-integrity.ts +0 -107
- package/src/commands/runtime-internalization-queue.ts +0 -135
- package/src/commands/runtime-internalization-retry.ts +0 -191
- package/src/commands/runtime-internalization-run-once.ts +0 -791
- package/src/commands/runtime-internalization-run-rulehost.ts +0 -749
- package/src/commands/runtime-internalization-wake-once.ts +0 -87
- package/src/commands/runtime-pain-flood-simulation.ts +0 -121
- package/src/commands/runtime-pruning.ts +0 -438
- package/src/commands/runtime-recovery-failed-tasks.ts +0 -258
- package/src/commands/runtime-recovery.ts +0 -109
- package/src/commands/runtime-synthetic-baseline.ts +0 -70
- package/src/commands/runtime-uat.guard.test.ts +0 -175
- package/src/commands/runtime-uat.ts +0 -380
- package/src/commands/runtime.ts +0 -473
- package/src/commands/samples-list.ts +0 -39
- package/src/commands/samples-review.ts +0 -36
- package/src/commands/task.ts +0 -271
- package/src/commands/telemetry.ts +0 -349
- package/src/commands/trace.ts +0 -174
- package/src/commands/trajectory-db-unavailable.ts +0 -19
- package/src/commands/trajectory.ts +0 -64
- package/src/commands/version.ts +0 -49
- package/src/config-reader.ts +0 -122
- package/src/index.ts +0 -1276
- package/src/legacy/legacy-import.ts +0 -179
- package/src/legacy/session-history-import.ts +0 -203
- package/src/resolve-workspace.ts +0 -106
- package/src/services/__tests__/cli-output.test.ts +0 -130
- package/src/services/__tests__/evaluator-runner-deps.test.ts +0 -382
- package/src/services/__tests__/rulehost-readiness.test.ts +0 -363
- package/src/services/__tests__/runtime-adapter-resolver.test.ts +0 -793
- package/src/services/cli-output.ts +0 -95
- package/src/services/config-doctor.ts +0 -697
- package/src/services/console-launcher.ts +0 -537
- package/src/services/demo-rule-compiler.ts +0 -40
- package/src/services/demo-story-a-runner.ts +0 -471
- package/src/services/mainline-snapshot-assembler.ts +0 -579
- package/src/services/pain-flood-simulation-runner.ts +0 -462
- package/src/services/pd-config-loader.ts +0 -433
- package/src/services/proven-channel-baseline-runner.ts +0 -157
- package/src/services/quality-scorecard/data-extractor.ts +0 -150
- package/src/services/quality-scorecard/local-evaluator.ts +0 -142
- package/src/services/quality-scorecard/strong-model-gate.ts +0 -202
- package/src/services/resolve-runtime-from-pd-config.ts +0 -230
- package/src/services/rulehost-pipeline-runner.ts +0 -1214
- package/src/services/rulehost-readiness.ts +0 -326
- package/src/services/runtime-adapter-resolver.ts +0 -364
- package/src/services/synthetic-baseline-runner.ts +0 -400
- package/src/services/version-report.ts +0 -253
- package/src/services/workspace-tool-semantics.ts +0 -26
- package/src/utils/path-security.ts +0 -96
- package/src/utils/production-workspace-guard.test.ts +0 -157
- package/src/utils/production-workspace-guard.ts +0 -210
- package/tests/bdd/cli-contract.steps.ts +0 -346
- package/tests/bdd/principles-stats.steps.ts +0 -211
- package/tests/bdd/support/gherkin-loader.ts +0 -147
- package/tests/bdd/support/repo-root.ts +0 -62
- package/tests/bdd/support/vitest-bdd.ts +0 -179
- package/tests/commands/admission-gate-coverage.test.ts +0 -79
- package/tests/commands/build-trajectory-evidence.test.ts +0 -508
- package/tests/commands/candidate-audit-repair.test.ts +0 -421
- package/tests/commands/candidate-intake.test.ts +0 -732
- package/tests/commands/candidate-internalization-backfill.test.ts +0 -548
- package/tests/commands/candidate-internalize-lineage.test.ts +0 -628
- package/tests/commands/candidate-internalize.test.ts +0 -299
- package/tests/commands/candidate-route.test.ts +0 -329
- package/tests/commands/candidate-show.test.ts +0 -95
- package/tests/commands/cli-command-tree.test.ts +0 -144
- package/tests/commands/cli-help-snapshot.test.ts +0 -155
- package/tests/commands/cli-skill-contract.test.ts +0 -121
- package/tests/commands/codex-consent-reversibility.steps.test.ts +0 -299
- package/tests/commands/codex-ingest-quarantine.test.ts +0 -136
- package/tests/commands/codex-setup.test.ts +0 -297
- package/tests/commands/codex-worker-daemon.test.ts +0 -95
- package/tests/commands/codex-worker-registration.test.ts +0 -207
- package/tests/commands/config-doctor.test.ts +0 -336
- package/tests/commands/console-launcher-edge-cases.test.ts +0 -388
- package/tests/commands/console-open.test.ts +0 -1381
- package/tests/commands/context.test.ts +0 -114
- package/tests/commands/demo-story-a.test.ts +0 -318
- package/tests/commands/diagnose.test.ts +0 -1803
- package/tests/commands/errors-list.test.ts +0 -679
- package/tests/commands/evolution-tasks-list.test.ts +0 -41
- package/tests/commands/evolution-tasks-show.test.ts +0 -46
- package/tests/commands/health-codex.test.ts +0 -349
- package/tests/commands/health-host-wiring.test.ts +0 -76
- package/tests/commands/health-reliability.test.ts +0 -99
- package/tests/commands/health.test.ts +0 -413
- package/tests/commands/intent.test.ts +0 -321
- package/tests/commands/legacy-cleanup.test.ts +0 -148
- package/tests/commands/pain-evidence.test.ts +0 -518
- package/tests/commands/pain-list.test.ts +0 -219
- package/tests/commands/pain-record-async.test.ts +0 -335
- package/tests/commands/pain-record-session-parser.test.ts +0 -228
- package/tests/commands/pain-record.test.ts +0 -851
- package/tests/commands/pain-retry.test.ts +0 -1121
- package/tests/commands/plugin-config-resolution-cutover.test.ts +0 -229
- package/tests/commands/pri-393-runtime-config-unification.test.ts +0 -443
- package/tests/commands/principles-stats-wiring.test.ts +0 -135
- package/tests/commands/principles-stats.test.ts +0 -274
- package/tests/commands/product-path-regression.test.ts +0 -86
- package/tests/commands/proven-channel-baseline.test.ts +0 -441
- package/tests/commands/rulecode.test.ts +0 -177
- package/tests/commands/run-rulehost-handler.test.ts +0 -908
- package/tests/commands/runtime-activation-approve-flag-wiring.test.ts +0 -227
- package/tests/commands/runtime-activation-deactivate-flag-wiring.test.ts +0 -167
- package/tests/commands/runtime-activation-dispatch-flag-wiring.test.ts +0 -262
- package/tests/commands/runtime-activation-governance-audit.test.ts +0 -95
- package/tests/commands/runtime-activation-list-flag-wiring.test.ts +0 -193
- package/tests/commands/runtime-activation-promote-flag-wiring.test.ts +0 -248
- package/tests/commands/runtime-activation-shadow-telemetry.test.ts +0 -130
- package/tests/commands/runtime-activation.test.ts +0 -2052
- package/tests/commands/runtime-artifact-repair-registration.test.ts +0 -74
- package/tests/commands/runtime-artifact-repair.test.ts +0 -404
- package/tests/commands/runtime-canary.test.ts +0 -430
- package/tests/commands/runtime-compatibility-scan.test.ts +0 -146
- package/tests/commands/runtime-diagnostics-export.test.ts +0 -175
- package/tests/commands/runtime-features.test.ts +0 -249
- package/tests/commands/runtime-health-snapshot.test.ts +0 -358
- package/tests/commands/runtime-init-empty-workspace.test.ts +0 -252
- package/tests/commands/runtime-init.test.ts +0 -420
- package/tests/commands/runtime-internalization-enqueue-successors.test.ts +0 -782
- package/tests/commands/runtime-internalization-integrity-repair.test.ts +0 -239
- package/tests/commands/runtime-internalization-integrity.test.ts +0 -140
- package/tests/commands/runtime-internalization-queue.test.ts +0 -380
- package/tests/commands/runtime-internalization-retry-owner-authority.test.ts +0 -431
- package/tests/commands/runtime-internalization-run-once-evaluator-parity.test.ts +0 -269
- package/tests/commands/runtime-internalization-run-once-language.test.ts +0 -303
- package/tests/commands/runtime-internalization-run-once-real-config.test.ts +0 -237
- package/tests/commands/runtime-internalization-run-once-rollout-parity.test.ts +0 -706
- package/tests/commands/runtime-internalization-run-once.test.ts +0 -1791
- package/tests/commands/runtime-internalization-wake-once.test.ts +0 -171
- package/tests/commands/runtime-internalization.test.ts +0 -53
- package/tests/commands/runtime-pain-flood-simulation.test.ts +0 -418
- package/tests/commands/runtime-pruning.test.ts +0 -694
- package/tests/commands/runtime-recovery-failed-tasks.test.ts +0 -328
- package/tests/commands/runtime-recovery.test.ts +0 -120
- package/tests/commands/runtime-synthetic-baseline.test.ts +0 -249
- package/tests/commands/runtime-uat.test.ts +0 -397
- package/tests/commands/runtime.test.ts +0 -392
- package/tests/commands/samples-list.test.ts +0 -43
- package/tests/commands/samples-review.test.ts +0 -41
- package/tests/commands/task.test.ts +0 -153
- package/tests/commands/telemetry.test.ts +0 -190
- package/tests/commands/trace.test.ts +0 -316
- package/tests/config-reader.test.ts +0 -143
- package/tests/e2e/candidate-intake-e2e.test.ts +0 -338
- package/tests/e2e/cli-full-flow.test.ts +0 -233
- package/tests/e2e/cross-package-acceptance.test.ts +0 -635
- package/tests/helpers/pd-cli-path.ts +0 -5
- package/tests/pd-cli-smoke.test.ts +0 -13
- package/tests/resolve-workspace.test.ts +0 -196
- package/tests/services/config-doctor-signal-health.test.ts +0 -109
- package/tests/services/console-launcher.test.ts +0 -114
- package/tests/services/demo-rule-compiler.test.ts +0 -242
- package/tests/services/mainline-snapshot-assembler.test.ts +0 -572
- package/tests/services/pd-config-loader.test.ts +0 -505
- package/tests/services/proven-channel-baseline-runner.test.ts +0 -30
- package/tests/services/quality-scorecard/strong-model-gate.test.ts +0 -133
- package/tests/services/resolve-runtime-from-pd-config.test.ts +0 -485
- package/tests/services/rulehost-pipeline-e2e.test.ts +0 -538
- package/tests/services/rulehost-pipeline-runner.test.ts +0 -1242
- package/tests/services/workspace-tool-semantics.test.ts +0 -76
- package/tests/utils/path-security.test.ts +0 -180
- package/tsconfig.json +0 -26
- package/vitest.config.ts +0 -10
|
@@ -1,1803 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach, afterEach, beforeAll } from 'vitest';
|
|
2
|
-
import { Command } from 'commander';
|
|
3
|
-
|
|
4
|
-
const { MockRuntimeStateManager, mockGetCandidatesByTaskId, mockUpdateCandidateStatus, mockCreateTask, mockGetTask } = vi.hoisted(() => {
|
|
5
|
-
const mockGetCandidatesByTaskId = vi.fn().mockResolvedValue([]);
|
|
6
|
-
const mockUpdateCandidateStatus = vi.fn().mockResolvedValue(undefined);
|
|
7
|
-
const mockCreateTask = vi.fn().mockResolvedValue(undefined);
|
|
8
|
-
const mockGetTask = vi.fn().mockResolvedValue({
|
|
9
|
-
taskId: 'test-task-1',
|
|
10
|
-
status: 'pending',
|
|
11
|
-
attemptCount: 0,
|
|
12
|
-
maxAttempts: 3,
|
|
13
|
-
lastError: null,
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
class MockRuntimeStateManager {
|
|
17
|
-
initialize = vi.fn().mockResolvedValue(undefined);
|
|
18
|
-
close = vi.fn().mockResolvedValue(undefined);
|
|
19
|
-
getTask = mockGetTask;
|
|
20
|
-
createTask = mockCreateTask;
|
|
21
|
-
getCandidatesByTaskId = mockGetCandidatesByTaskId;
|
|
22
|
-
updateCandidateStatus = mockUpdateCandidateStatus;
|
|
23
|
-
connection = {} as Record<string, unknown>;
|
|
24
|
-
taskStore = {};
|
|
25
|
-
runStore = {};
|
|
26
|
-
piArtifactStore = {};
|
|
27
|
-
getRetryPolicy = vi.fn().mockReturnValue({
|
|
28
|
-
shouldRetry: () => true,
|
|
29
|
-
calculateBackoff: () => 10,
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
return { MockRuntimeStateManager, mockGetCandidatesByTaskId, mockUpdateCandidateStatus, mockCreateTask, mockGetTask };
|
|
33
|
-
}, { validateType: true });
|
|
34
|
-
|
|
35
|
-
const { mockIntake, MockCandidateIntakeService } = vi.hoisted(() => {
|
|
36
|
-
const mockIntake = vi.fn();
|
|
37
|
-
function MockCandidateIntakeService(this: any) {
|
|
38
|
-
return { intake: mockIntake };
|
|
39
|
-
}
|
|
40
|
-
MockCandidateIntakeService.prototype = {};
|
|
41
|
-
return { mockIntake, MockCandidateIntakeService };
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
const { MockPrincipleTreeLedgerAdapter } = vi.hoisted(() => {
|
|
45
|
-
function MockPrincipleTreeLedgerAdapter(this: any) {
|
|
46
|
-
return {};
|
|
47
|
-
}
|
|
48
|
-
MockPrincipleTreeLedgerAdapter.prototype = {};
|
|
49
|
-
return { MockPrincipleTreeLedgerAdapter };
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
const { mockResolveRuntimeFromPdConfig } = vi.hoisted(() => {
|
|
53
|
-
const mockResolveRuntimeFromPdConfig = vi.fn().mockReturnValue({
|
|
54
|
-
result: {
|
|
55
|
-
runtimeKind: 'pi-ai',
|
|
56
|
-
provider: 'test-provider',
|
|
57
|
-
model: 'test-model',
|
|
58
|
-
apiKeyEnv: 'TEST_KEY',
|
|
59
|
-
timeoutMs: 300000,
|
|
60
|
-
agentId: 'main',
|
|
61
|
-
},
|
|
62
|
-
legacyWarnings: [],
|
|
63
|
-
configSource: '.pd/config.yaml',
|
|
64
|
-
configLoadResult: { ok: true, effective: {}, defaults: {}, legacyFilesDetected: [] },
|
|
65
|
-
});
|
|
66
|
-
return { mockResolveRuntimeFromPdConfig };
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
// BUG-1: capture runner constructor args to verify effectiveConfig wiring
|
|
70
|
-
const { diagRootCauseRunnerCtor, diagDistillerRunnerCtor, diagRouterRunnerCtor } = vi.hoisted(() => {
|
|
71
|
-
const diagRootCauseRunnerCtor = vi.fn().mockImplementation(function () { return {}; });
|
|
72
|
-
const diagDistillerRunnerCtor = vi.fn().mockImplementation(function () { return {}; });
|
|
73
|
-
const diagRouterRunnerCtor = vi.fn().mockImplementation(function () { return {}; });
|
|
74
|
-
return { diagRootCauseRunnerCtor, diagDistillerRunnerCtor, diagRouterRunnerCtor };
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
// BUG-2: capture buildDreamerSeedFromCandidate sourcePainId arg + mock resolver
|
|
78
|
-
const { buildDreamerSeedCalls, mockResolveSourcePainId } = vi.hoisted(() => {
|
|
79
|
-
const buildDreamerSeedCalls: Array<{ candidateId: string; sourcePainId?: string }> = [];
|
|
80
|
-
const mockResolveSourcePainId = vi.fn().mockResolvedValue('pain_test-source-1');
|
|
81
|
-
return { buildDreamerSeedCalls, mockResolveSourcePainId };
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
vi.mock('../../src/resolve-workspace.js', () => ({
|
|
85
|
-
resolveWorkspaceDir: vi.fn().mockReturnValue('/tmp/fake-workspace'),
|
|
86
|
-
}));
|
|
87
|
-
|
|
88
|
-
vi.mock('@principles/core/runtime-v2', () => {
|
|
89
|
-
return {
|
|
90
|
-
RuntimeStateManager: vi.fn().mockImplementation(function () {
|
|
91
|
-
return new MockRuntimeStateManager();
|
|
92
|
-
}),
|
|
93
|
-
SqliteHistoryQuery: vi.fn().mockImplementation(function () { return {}; }),
|
|
94
|
-
SqliteContextAssembler: vi.fn().mockImplementation(function () { return {}; }),
|
|
95
|
-
SqliteDiagnosticianCommitter: vi.fn().mockImplementation(function () { return {}; }),
|
|
96
|
-
SqliteTrajectoryLocator: vi.fn().mockImplementation(function () { return {}; }),
|
|
97
|
-
SqliteSourceTraceLocator: vi.fn().mockImplementation(function () { return {}; }),
|
|
98
|
-
StoreEventEmitter: vi.fn().mockImplementation(function () { return {}; }),
|
|
99
|
-
storeEmitter: { emitTelemetry: vi.fn() },
|
|
100
|
-
SplitDiagnosticianRunner: vi.fn().mockImplementation(function () { return {}; }),
|
|
101
|
-
DiagRootCauseRunner: diagRootCauseRunnerCtor,
|
|
102
|
-
DiagDistillerRunner: diagDistillerRunnerCtor,
|
|
103
|
-
DiagRouterRunner: diagRouterRunnerCtor,
|
|
104
|
-
DefaultDiagRootCauseValidator: vi.fn().mockImplementation(function () { return {}; }),
|
|
105
|
-
DefaultDiagDistillerValidator: vi.fn().mockImplementation(function () { return {}; }),
|
|
106
|
-
DisabledDiagnosticianRunner: vi.fn().mockImplementation(function () { return {}; }),
|
|
107
|
-
TestDoubleRuntimeAdapter: vi.fn().mockImplementation(function () { return {}; }),
|
|
108
|
-
OpenClawCliRuntimeAdapter: vi.fn().mockImplementation(function () { return {}; }),
|
|
109
|
-
PiAiRuntimeAdapter: vi.fn().mockImplementation(function () { return {}; }),
|
|
110
|
-
SPLIT_PIPELINE_TOTAL_TIMEOUT_MS: 300000,
|
|
111
|
-
// PRI-638: capability gate — available by default; disabled cases override this.
|
|
112
|
-
resolveDiagnosticianCapability: vi.fn((): { available: boolean; reason?: string; message?: string; nextAction?: string } => ({ available: true })),
|
|
113
|
-
PDRuntimeError: class PDRuntimeError extends Error {
|
|
114
|
-
constructor(public category: string, message: string) {
|
|
115
|
-
super(message);
|
|
116
|
-
this.name = 'PDRuntimeError';
|
|
117
|
-
}
|
|
118
|
-
},
|
|
119
|
-
CandidateIntakeService: MockCandidateIntakeService,
|
|
120
|
-
// PRI-503: admission gate mock — admit by default so existing diagnose/intake
|
|
121
|
-
// tests keep their original flow. Tests that need to assert refusal behavior
|
|
122
|
-
// can override this mock per-test.
|
|
123
|
-
evaluateCandidateAdmissionFromRecord: vi.fn().mockReturnValue({
|
|
124
|
-
decision: 'admitted',
|
|
125
|
-
reason: 'mock_admitted',
|
|
126
|
-
nextAction: 'none',
|
|
127
|
-
evidenceStatus: 'unknown',
|
|
128
|
-
}),
|
|
129
|
-
resolveRuntimeConfig: vi.fn().mockReturnValue({
|
|
130
|
-
runtimeKind: 'pi-ai',
|
|
131
|
-
provider: 'test-provider',
|
|
132
|
-
model: 'test-model',
|
|
133
|
-
apiKeyEnv: 'TEST_KEY',
|
|
134
|
-
timeoutMs: 300000,
|
|
135
|
-
agentId: 'main',
|
|
136
|
-
}),
|
|
137
|
-
isRuntimeConfigError: vi.fn().mockReturnValue(false),
|
|
138
|
-
isFeatureEnabled: vi.fn().mockReturnValue(true),
|
|
139
|
-
resolveOutputLanguage: vi.fn().mockReturnValue({ outputLanguage: 'zh-CN' }),
|
|
140
|
-
validatePdConfig: vi.fn().mockReturnValue({ valid: true, errors: [] }),
|
|
141
|
-
computeEffectivePdConfig: vi.fn().mockReturnValue({ config: {}, source: 'defaults', warnings: [] }),
|
|
142
|
-
computeFeatureFlagsFromConfig: vi.fn().mockReturnValue({}),
|
|
143
|
-
redactPdConfig: vi.fn().mockImplementation((c) => c),
|
|
144
|
-
run: vi.fn().mockResolvedValue({
|
|
145
|
-
status: 'succeeded',
|
|
146
|
-
taskId: 'test-task-1',
|
|
147
|
-
output: {
|
|
148
|
-
valid: true,
|
|
149
|
-
diagnosisId: 'diag-123',
|
|
150
|
-
taskId: 'test-task-1',
|
|
151
|
-
summary: 'Test diagnosis summary',
|
|
152
|
-
rootCause: 'Test: test root cause',
|
|
153
|
-
violatedPrinciples: [],
|
|
154
|
-
evidence: [],
|
|
155
|
-
recommendations: [{ kind: 'implementation', description: 'Test recommendation' }],
|
|
156
|
-
confidence: 0.9,
|
|
157
|
-
},
|
|
158
|
-
}),
|
|
159
|
-
status: vi.fn(),
|
|
160
|
-
PrincipleTreeLedgerAdapter: MockPrincipleTreeLedgerAdapter,
|
|
161
|
-
// Defect-004 dreamer seed mocks — minimal stubs that mirror real behavior
|
|
162
|
-
// BUG-2: capture sourcePainId arg to verify lineage wiring
|
|
163
|
-
buildDreamerSeedFromCandidate: vi.fn().mockImplementation((candidate: { candidateId: string }, opts: { route: string; ready: boolean; sourcePainId?: string }) => {
|
|
164
|
-
buildDreamerSeedCalls.push({ candidateId: candidate.candidateId, sourcePainId: opts?.sourcePainId });
|
|
165
|
-
return {
|
|
166
|
-
taskId: `dreamer-${candidate.candidateId}`,
|
|
167
|
-
taskKind: 'dreamer' as const,
|
|
168
|
-
channel: 'prompt' as const,
|
|
169
|
-
diagnosticJson: JSON.stringify({ candidateId: candidate.candidateId }),
|
|
170
|
-
status: 'pending' as const,
|
|
171
|
-
attemptCount: 0,
|
|
172
|
-
maxAttempts: 3,
|
|
173
|
-
};
|
|
174
|
-
}),
|
|
175
|
-
// PRI-720 C6: candidate-level dedup — tests assume a clean store (no
|
|
176
|
-
// pre-existing dreamer task under any channel suffix).
|
|
177
|
-
findExistingDreamerTask: vi.fn().mockResolvedValue(null),
|
|
178
|
-
CANDIDATE_KIND_TO_ROUTE: {
|
|
179
|
-
principle: 'principle-candidate',
|
|
180
|
-
rule: 'rule-candidate',
|
|
181
|
-
prompt: 'prompt-candidate',
|
|
182
|
-
},
|
|
183
|
-
ROUTE_CHANNEL_MAP: {
|
|
184
|
-
'principle-candidate': 'prompt',
|
|
185
|
-
'rule-candidate': 'code_tool_hook',
|
|
186
|
-
'prompt-candidate': 'prompt',
|
|
187
|
-
},
|
|
188
|
-
MVP_ENABLED_CHANNELS: new Set(['prompt', 'code_tool_hook', 'defer_archive']),
|
|
189
|
-
};
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
vi.mock('../../src/config-reader.js', () => ({
|
|
193
|
-
readOutputLanguageFromWorkspace: vi.fn().mockReturnValue({ outputLanguage: 'zh-CN' }),
|
|
194
|
-
}));
|
|
195
|
-
|
|
196
|
-
vi.mock('../../src/services/pd-config-loader.js', () => ({
|
|
197
|
-
loadPdConfig: vi.fn().mockReturnValue({
|
|
198
|
-
ok: true,
|
|
199
|
-
effective: { config: { featureFlags: { diagnostician_llm_degradation: true } }, source: 'file', warnings: [] },
|
|
200
|
-
defaults: { config: {}, source: 'defaults', warnings: [] },
|
|
201
|
-
}),
|
|
202
|
-
computeFlagsFromLoadResult: vi.fn().mockReturnValue({}),
|
|
203
|
-
// PRI-720: full-chain override resolution — undefined (standard topology) in tests.
|
|
204
|
-
resolvePromptFullPipelineSeedMode: vi.fn().mockReturnValue(undefined),
|
|
205
|
-
}));
|
|
206
|
-
|
|
207
|
-
vi.mock('../../src/services/resolve-runtime-from-pd-config.js', () => ({
|
|
208
|
-
resolveRuntimeFromPdConfig: mockResolveRuntimeFromPdConfig,
|
|
209
|
-
}));
|
|
210
|
-
|
|
211
|
-
// BUG-2: mock resolveSourcePainIdFromDiagnostician imported by diagnose.ts from candidate.js
|
|
212
|
-
vi.mock('../../src/commands/candidate.js', () => ({
|
|
213
|
-
resolveSourcePainIdFromDiagnostician: mockResolveSourcePainId,
|
|
214
|
-
}));
|
|
215
|
-
|
|
216
|
-
import { handleDiagnoseRun, handleDiagnoseStatus, type DiagnoseRunOptions } from '../../src/commands/diagnose.js';
|
|
217
|
-
|
|
218
|
-
const SUCCEEDED_RESULT = {
|
|
219
|
-
status: 'succeeded' as const,
|
|
220
|
-
taskId: 'test-task-1',
|
|
221
|
-
output: {
|
|
222
|
-
valid: true,
|
|
223
|
-
diagnosisId: 'diag-123',
|
|
224
|
-
taskId: 'test-task-1',
|
|
225
|
-
summary: 'Test diagnosis summary',
|
|
226
|
-
rootCause: 'Test: test root cause',
|
|
227
|
-
violatedPrinciples: [],
|
|
228
|
-
evidence: [],
|
|
229
|
-
recommendations: [
|
|
230
|
-
{ kind: 'principle', description: 'Always validate tool arguments before execution' },
|
|
231
|
-
],
|
|
232
|
-
confidence: 0.9,
|
|
233
|
-
},
|
|
234
|
-
attemptCount: 1,
|
|
235
|
-
};
|
|
236
|
-
|
|
237
|
-
// ERR-067: reset the mocked run() to a clean successful default after each test
|
|
238
|
-
// so later tests don't see leftover mockResolvedValueOnce() chains or call counts.
|
|
239
|
-
const DEFAULT_SUCCEEDED_RUN_RESULT = {
|
|
240
|
-
status: 'succeeded' as const,
|
|
241
|
-
taskId: 'test-task-1',
|
|
242
|
-
output: SUCCEEDED_RESULT.output,
|
|
243
|
-
};
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
describe('pd diagnose run --runtime routing', () => {
|
|
248
|
-
let mockResolveRuntimeConfig: ReturnType<typeof vi.fn>;
|
|
249
|
-
let mockIsRuntimeConfigError: ReturnType<typeof vi.fn>;
|
|
250
|
-
|
|
251
|
-
beforeEach(async () => {
|
|
252
|
-
vi.clearAllMocks();
|
|
253
|
-
const runtimeV2 = await import('@principles/core/runtime-v2');
|
|
254
|
-
mockResolveRuntimeConfig = vi.mocked(runtimeV2.resolveRuntimeConfig);
|
|
255
|
-
mockIsRuntimeConfigError = vi.mocked(runtimeV2.isRuntimeConfigError);
|
|
256
|
-
const { run } = runtimeV2;
|
|
257
|
-
vi.mocked(run).mockResolvedValue(SUCCEEDED_RESULT);
|
|
258
|
-
mockGetCandidatesByTaskId.mockResolvedValue([]);
|
|
259
|
-
mockUpdateCandidateStatus.mockResolvedValue(undefined);
|
|
260
|
-
mockIntake.mockReset();
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
it('CLI-01: --runtime test-double routes to TestDoubleRuntimeAdapter (regression)', async () => {
|
|
264
|
-
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
265
|
-
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
|
|
266
|
-
|
|
267
|
-
await handleDiagnoseRun({
|
|
268
|
-
taskId: 'test-task-1',
|
|
269
|
-
workspace: '/tmp/fake-workspace',
|
|
270
|
-
runtime: 'test-double',
|
|
271
|
-
json: false,
|
|
272
|
-
} as DiagnoseRunOptions);
|
|
273
|
-
|
|
274
|
-
expect(consoleSpy).toHaveBeenCalled();
|
|
275
|
-
expect(exitSpy).not.toHaveBeenCalledWith(1);
|
|
276
|
-
|
|
277
|
-
consoleSpy.mockRestore();
|
|
278
|
-
exitSpy.mockRestore();
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
it('HG-03: --runtime openclaw-cli without mode (no file config) fails via resolveRuntimeFromPdConfig', async () => {
|
|
282
|
-
mockResolveRuntimeFromPdConfig.mockReturnValueOnce({
|
|
283
|
-
result: {
|
|
284
|
-
ok: false,
|
|
285
|
-
reason: 'missing_openclaw_mode',
|
|
286
|
-
message: 'runtimeKind is openclaw-cli but no mode specified',
|
|
287
|
-
nextAction: 'Provide exactly one mode',
|
|
288
|
-
},
|
|
289
|
-
legacyWarnings: [],
|
|
290
|
-
configSource: '.pd/config.yaml',
|
|
291
|
-
configLoadResult: { ok: true, effective: {}, defaults: {}, legacyFilesDetected: [] },
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
295
|
-
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
|
|
296
|
-
|
|
297
|
-
await handleDiagnoseRun({
|
|
298
|
-
taskId: 'test-task-1',
|
|
299
|
-
workspace: '/tmp/fake-workspace',
|
|
300
|
-
runtime: 'openclaw-cli',
|
|
301
|
-
json: false,
|
|
302
|
-
} as DiagnoseRunOptions);
|
|
303
|
-
|
|
304
|
-
expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('no mode resolved'));
|
|
305
|
-
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
306
|
-
|
|
307
|
-
consoleErrorSpy.mockRestore();
|
|
308
|
-
exitSpy.mockRestore();
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
it('HG-03: both --openclaw-local and --openclaw-gateway exits with error (mutually exclusive)', async () => {
|
|
312
|
-
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
313
|
-
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
|
|
314
|
-
|
|
315
|
-
await handleDiagnoseRun({
|
|
316
|
-
taskId: 'test-task-1',
|
|
317
|
-
workspace: '/tmp/fake-workspace',
|
|
318
|
-
runtime: 'openclaw-cli',
|
|
319
|
-
openclawLocal: true,
|
|
320
|
-
openclawGateway: true,
|
|
321
|
-
json: false,
|
|
322
|
-
} as DiagnoseRunOptions);
|
|
323
|
-
|
|
324
|
-
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
325
|
-
'error: --openclaw-local and --openclaw-gateway are mutually exclusive'
|
|
326
|
-
);
|
|
327
|
-
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
328
|
-
|
|
329
|
-
consoleErrorSpy.mockRestore();
|
|
330
|
-
exitSpy.mockRestore();
|
|
331
|
-
});
|
|
332
|
-
|
|
333
|
-
it('DPB-09: openclaw-cli with file config openclawMode succeeds without CLI flag', async () => {
|
|
334
|
-
mockResolveRuntimeFromPdConfig.mockReturnValueOnce({
|
|
335
|
-
result: {
|
|
336
|
-
runtimeKind: 'openclaw-cli',
|
|
337
|
-
openclawMode: 'local',
|
|
338
|
-
timeoutMs: 300000,
|
|
339
|
-
agentId: 'main',
|
|
340
|
-
},
|
|
341
|
-
legacyWarnings: [],
|
|
342
|
-
configSource: '.pd/config.yaml',
|
|
343
|
-
configLoadResult: { ok: true, effective: {}, defaults: {}, legacyFilesDetected: [] },
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
347
|
-
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
|
|
348
|
-
|
|
349
|
-
await handleDiagnoseRun({
|
|
350
|
-
taskId: 'test-task-1',
|
|
351
|
-
workspace: '/tmp/fake-workspace',
|
|
352
|
-
runtime: 'openclaw-cli',
|
|
353
|
-
json: false,
|
|
354
|
-
} as DiagnoseRunOptions);
|
|
355
|
-
|
|
356
|
-
expect(exitSpy).not.toHaveBeenCalledWith(1);
|
|
357
|
-
|
|
358
|
-
consoleSpy.mockRestore();
|
|
359
|
-
exitSpy.mockRestore();
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
it('DPB-09: openclaw-cli flag overrides file config mode (config=gateway, flag=local → runtimeMode=local)', async () => {
|
|
363
|
-
mockResolveRuntimeFromPdConfig.mockReturnValueOnce({
|
|
364
|
-
result: {
|
|
365
|
-
runtimeKind: 'openclaw-cli',
|
|
366
|
-
openclawMode: 'gateway',
|
|
367
|
-
timeoutMs: 300000,
|
|
368
|
-
agentId: 'main',
|
|
369
|
-
},
|
|
370
|
-
legacyWarnings: [],
|
|
371
|
-
configSource: '.pd/config.yaml',
|
|
372
|
-
configLoadResult: { ok: true, effective: {}, defaults: {}, legacyFilesDetected: [] },
|
|
373
|
-
});
|
|
374
|
-
|
|
375
|
-
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
376
|
-
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
|
|
377
|
-
|
|
378
|
-
await handleDiagnoseRun({
|
|
379
|
-
taskId: 'test-task-1',
|
|
380
|
-
workspace: '/tmp/fake-workspace',
|
|
381
|
-
runtime: 'openclaw-cli',
|
|
382
|
-
openclawLocal: true,
|
|
383
|
-
json: false,
|
|
384
|
-
} as DiagnoseRunOptions);
|
|
385
|
-
|
|
386
|
-
// Flag override: config says gateway, flag says local → adapter gets local
|
|
387
|
-
const OpenClawCliMock = vi.mocked(
|
|
388
|
-
await import('@principles/core/runtime-v2').then(m => m.OpenClawCliRuntimeAdapter),
|
|
389
|
-
);
|
|
390
|
-
expect(OpenClawCliMock).toHaveBeenCalledWith(
|
|
391
|
-
expect.objectContaining({ runtimeMode: 'local' }),
|
|
392
|
-
);
|
|
393
|
-
expect(exitSpy).not.toHaveBeenCalledWith(1);
|
|
394
|
-
|
|
395
|
-
consoleSpy.mockRestore();
|
|
396
|
-
exitSpy.mockRestore();
|
|
397
|
-
});
|
|
398
|
-
|
|
399
|
-
it('DPB-09: openclaw-cli missing mode (--json) outputs JSON error', async () => {
|
|
400
|
-
mockResolveRuntimeFromPdConfig.mockReturnValueOnce({
|
|
401
|
-
result: {
|
|
402
|
-
runtimeKind: 'openclaw-cli',
|
|
403
|
-
openclawMode: undefined,
|
|
404
|
-
timeoutMs: 300000,
|
|
405
|
-
agentId: 'main',
|
|
406
|
-
},
|
|
407
|
-
legacyWarnings: [],
|
|
408
|
-
configSource: '.pd/config.yaml',
|
|
409
|
-
configLoadResult: { ok: true, effective: {}, defaults: {}, legacyFilesDetected: [] },
|
|
410
|
-
});
|
|
411
|
-
|
|
412
|
-
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
413
|
-
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
|
|
414
|
-
|
|
415
|
-
await handleDiagnoseRun({
|
|
416
|
-
taskId: 'test-task-1',
|
|
417
|
-
workspace: '/tmp/fake-workspace',
|
|
418
|
-
runtime: 'openclaw-cli',
|
|
419
|
-
json: true,
|
|
420
|
-
} as DiagnoseRunOptions);
|
|
421
|
-
|
|
422
|
-
const jsonOutput = JSON.parse(logSpy.mock.calls[0][0]);
|
|
423
|
-
expect(jsonOutput.ok).toBe(false);
|
|
424
|
-
expect(jsonOutput.reason).toBe('missing_openclaw_mode');
|
|
425
|
-
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
426
|
-
|
|
427
|
-
logSpy.mockRestore();
|
|
428
|
-
exitSpy.mockRestore();
|
|
429
|
-
});
|
|
430
|
-
|
|
431
|
-
it('CLI-04: unknown runtime kind exits with error and exit code 1', async () => {
|
|
432
|
-
const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
433
|
-
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
|
|
434
|
-
|
|
435
|
-
await handleDiagnoseRun({
|
|
436
|
-
taskId: 'test-task-1',
|
|
437
|
-
workspace: '/tmp/fake-workspace',
|
|
438
|
-
runtime: 'invalid-runtime',
|
|
439
|
-
json: true,
|
|
440
|
-
} as DiagnoseRunOptions);
|
|
441
|
-
|
|
442
|
-
expect(consoleLogSpy).toHaveBeenCalledTimes(1);
|
|
443
|
-
const jsonOutput = JSON.parse(consoleLogSpy.mock.calls[0][0] as string);
|
|
444
|
-
expect(jsonOutput.ok).toBe(false);
|
|
445
|
-
expect(jsonOutput.reason).toBe('unsupported_runtime_kind: invalid-runtime');
|
|
446
|
-
expect(jsonOutput.nextAction).toContain('openclaw-cli');
|
|
447
|
-
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
448
|
-
|
|
449
|
-
consoleLogSpy.mockRestore();
|
|
450
|
-
exitSpy.mockRestore();
|
|
451
|
-
});
|
|
452
|
-
|
|
453
|
-
it('P1: missing --runtime with no config binding refuses with missing_runtime (no test-double default)', async () => {
|
|
454
|
-
// Regression for the silent test-double default bug. Without --runtime and
|
|
455
|
-
// with no usable .pd/config.yaml binding, the command must refuse loudly
|
|
456
|
-
// (rc-9-no-silent-fallback) instead of defaulting to test-double and
|
|
457
|
-
// producing fake/failed diagnostic data in a real workspace.
|
|
458
|
-
mockResolveRuntimeFromPdConfig.mockReturnValueOnce({
|
|
459
|
-
result: { ok: false, reason: 'no_runtime_binding', message: 'no binding', nextAction: 'set runtime' },
|
|
460
|
-
legacyWarnings: [],
|
|
461
|
-
configSource: '.pd/config.yaml',
|
|
462
|
-
configLoadResult: { ok: true, effective: {}, defaults: {}, legacyFilesDetected: [] } as never,
|
|
463
|
-
});
|
|
464
|
-
mockIsRuntimeConfigError.mockReturnValueOnce(true);
|
|
465
|
-
|
|
466
|
-
const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
467
|
-
const consoleErrSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
468
|
-
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
|
|
469
|
-
|
|
470
|
-
await handleDiagnoseRun({
|
|
471
|
-
taskId: 'test-task-1',
|
|
472
|
-
workspace: '/tmp/fake-workspace',
|
|
473
|
-
// runtime intentionally omitted
|
|
474
|
-
json: true,
|
|
475
|
-
} as DiagnoseRunOptions);
|
|
476
|
-
|
|
477
|
-
// Config was consulted (not silently skipped).
|
|
478
|
-
expect(mockResolveRuntimeFromPdConfig).toHaveBeenCalledWith('/tmp/fake-workspace');
|
|
479
|
-
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
480
|
-
const jsonOutput = JSON.parse(consoleLogSpy.mock.calls[0][0] as string);
|
|
481
|
-
expect(jsonOutput.ok).toBe(false);
|
|
482
|
-
expect(jsonOutput.reason).toBe('missing_runtime');
|
|
483
|
-
expect(jsonOutput.nextAction).toContain('--runtime');
|
|
484
|
-
|
|
485
|
-
consoleLogSpy.mockRestore();
|
|
486
|
-
consoleErrSpy.mockRestore();
|
|
487
|
-
exitSpy.mockRestore();
|
|
488
|
-
});
|
|
489
|
-
|
|
490
|
-
it('P1: missing --runtime resolves kind from .pd/config.yaml (honors config, no test-double default)', async () => {
|
|
491
|
-
// When --runtime is absent but .pd/config.yaml binds openclaw-cli, the
|
|
492
|
-
// resolved kind must come from config — proving test-double is no longer
|
|
493
|
-
// the default. Mirrors DPB-09 but omits --runtime to exercise the new
|
|
494
|
-
// config-resolution path.
|
|
495
|
-
mockResolveRuntimeFromPdConfig.mockReturnValueOnce({
|
|
496
|
-
result: {
|
|
497
|
-
runtimeKind: 'openclaw-cli',
|
|
498
|
-
openclawMode: 'gateway',
|
|
499
|
-
timeoutMs: 300000,
|
|
500
|
-
agentId: 'main',
|
|
501
|
-
},
|
|
502
|
-
legacyWarnings: [],
|
|
503
|
-
configSource: '.pd/config.yaml',
|
|
504
|
-
configLoadResult: { ok: true, effective: {}, defaults: {}, legacyFilesDetected: [] } as never,
|
|
505
|
-
});
|
|
506
|
-
|
|
507
|
-
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
508
|
-
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
|
|
509
|
-
|
|
510
|
-
await handleDiagnoseRun({
|
|
511
|
-
taskId: 'test-task-1',
|
|
512
|
-
workspace: '/tmp/fake-workspace',
|
|
513
|
-
// runtime intentionally omitted — must be resolved from config
|
|
514
|
-
openclawGateway: true,
|
|
515
|
-
json: false,
|
|
516
|
-
} as DiagnoseRunOptions);
|
|
517
|
-
|
|
518
|
-
expect(mockResolveRuntimeFromPdConfig).toHaveBeenCalledWith('/tmp/fake-workspace');
|
|
519
|
-
const OpenClawCliMock = vi.mocked(
|
|
520
|
-
await import('@principles/core/runtime-v2').then(m => m.OpenClawCliRuntimeAdapter),
|
|
521
|
-
);
|
|
522
|
-
expect(OpenClawCliMock).toHaveBeenCalledWith(
|
|
523
|
-
expect.objectContaining({ runtimeMode: 'gateway' }),
|
|
524
|
-
);
|
|
525
|
-
expect(exitSpy).not.toHaveBeenCalledWith(1);
|
|
526
|
-
|
|
527
|
-
consoleSpy.mockRestore();
|
|
528
|
-
exitSpy.mockRestore();
|
|
529
|
-
});
|
|
530
|
-
|
|
531
|
-
it('DPB-09: openclaw-cli --openclaw-gateway constructs adapter with runtimeMode=gateway', async () => {
|
|
532
|
-
mockResolveRuntimeFromPdConfig.mockReturnValueOnce({
|
|
533
|
-
result: {
|
|
534
|
-
runtimeKind: 'openclaw-cli',
|
|
535
|
-
openclawMode: 'gateway',
|
|
536
|
-
timeoutMs: 300000,
|
|
537
|
-
agentId: 'main',
|
|
538
|
-
},
|
|
539
|
-
legacyWarnings: [],
|
|
540
|
-
configSource: '.pd/config.yaml',
|
|
541
|
-
configLoadResult: { ok: true, effective: {}, defaults: {}, legacyFilesDetected: [] },
|
|
542
|
-
});
|
|
543
|
-
|
|
544
|
-
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
545
|
-
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
|
|
546
|
-
|
|
547
|
-
await handleDiagnoseRun({
|
|
548
|
-
taskId: 'test-task-1',
|
|
549
|
-
workspace: '/tmp/fake-workspace',
|
|
550
|
-
runtime: 'openclaw-cli',
|
|
551
|
-
openclawGateway: true,
|
|
552
|
-
json: false,
|
|
553
|
-
} as DiagnoseRunOptions);
|
|
554
|
-
|
|
555
|
-
const OpenClawCliMock = vi.mocked(
|
|
556
|
-
await import('@principles/core/runtime-v2').then(m => m.OpenClawCliRuntimeAdapter),
|
|
557
|
-
);
|
|
558
|
-
expect(OpenClawCliMock).toHaveBeenCalledWith(
|
|
559
|
-
expect.objectContaining({ runtimeMode: 'gateway' }),
|
|
560
|
-
);
|
|
561
|
-
expect(exitSpy).not.toHaveBeenCalledWith(1);
|
|
562
|
-
|
|
563
|
-
consoleSpy.mockRestore();
|
|
564
|
-
exitSpy.mockRestore();
|
|
565
|
-
});
|
|
566
|
-
|
|
567
|
-
it('DPB-09: openclaw-cli --openclaw-local constructs adapter with runtimeMode=local', async () => {
|
|
568
|
-
mockResolveRuntimeFromPdConfig.mockReturnValueOnce({
|
|
569
|
-
result: {
|
|
570
|
-
runtimeKind: 'openclaw-cli',
|
|
571
|
-
openclawMode: 'local',
|
|
572
|
-
timeoutMs: 300000,
|
|
573
|
-
agentId: 'main',
|
|
574
|
-
},
|
|
575
|
-
legacyWarnings: [],
|
|
576
|
-
configSource: '.pd/config.yaml',
|
|
577
|
-
configLoadResult: { ok: true, effective: {}, defaults: {}, legacyFilesDetected: [] },
|
|
578
|
-
});
|
|
579
|
-
|
|
580
|
-
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
581
|
-
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
|
|
582
|
-
|
|
583
|
-
await handleDiagnoseRun({
|
|
584
|
-
taskId: 'test-task-1',
|
|
585
|
-
workspace: '/tmp/fake-workspace',
|
|
586
|
-
runtime: 'openclaw-cli',
|
|
587
|
-
openclawLocal: true,
|
|
588
|
-
json: false,
|
|
589
|
-
} as DiagnoseRunOptions);
|
|
590
|
-
|
|
591
|
-
const OpenClawCliMock = vi.mocked(
|
|
592
|
-
await import('@principles/core/runtime-v2').then(m => m.OpenClawCliRuntimeAdapter),
|
|
593
|
-
);
|
|
594
|
-
expect(OpenClawCliMock).toHaveBeenCalledWith(
|
|
595
|
-
expect.objectContaining({ runtimeMode: 'local' }),
|
|
596
|
-
);
|
|
597
|
-
expect(exitSpy).not.toHaveBeenCalledWith(1);
|
|
598
|
-
|
|
599
|
-
consoleSpy.mockRestore();
|
|
600
|
-
exitSpy.mockRestore();
|
|
601
|
-
});
|
|
602
|
-
});
|
|
603
|
-
|
|
604
|
-
describe('pd diagnose run — auto-intake after success', () => {
|
|
605
|
-
beforeEach(async () => {
|
|
606
|
-
vi.clearAllMocks();
|
|
607
|
-
const { run } = await import('@principles/core/runtime-v2');
|
|
608
|
-
vi.mocked(run).mockResolvedValue(SUCCEEDED_RESULT);
|
|
609
|
-
mockGetCandidatesByTaskId.mockResolvedValue([]);
|
|
610
|
-
mockUpdateCandidateStatus.mockResolvedValue(undefined);
|
|
611
|
-
mockIntake.mockReset();
|
|
612
|
-
});
|
|
613
|
-
|
|
614
|
-
it('INTAKE-01: successful diagnose + intake — candidates consumed, JSON includes intake evidence', async () => {
|
|
615
|
-
const candidates = [
|
|
616
|
-
{ candidateId: 'cand-1', artifactId: 'art-1', taskId: 'test-task-1', status: 'pending' },
|
|
617
|
-
{ candidateId: 'cand-2', artifactId: 'art-2', taskId: 'test-task-1', status: 'pending' },
|
|
618
|
-
];
|
|
619
|
-
mockGetCandidatesByTaskId.mockResolvedValue(candidates);
|
|
620
|
-
mockIntake
|
|
621
|
-
.mockResolvedValueOnce({ id: 'ledger-1', title: 'Principle 1', status: 'probation' })
|
|
622
|
-
.mockResolvedValueOnce({ id: 'ledger-2', title: 'Principle 2', status: 'probation' });
|
|
623
|
-
|
|
624
|
-
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
625
|
-
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
|
|
626
|
-
|
|
627
|
-
await handleDiagnoseRun({
|
|
628
|
-
taskId: 'test-task-1',
|
|
629
|
-
workspace: '/tmp/fake-workspace',
|
|
630
|
-
runtime: 'test-double',
|
|
631
|
-
json: true,
|
|
632
|
-
} as DiagnoseRunOptions);
|
|
633
|
-
|
|
634
|
-
expect(mockIntake).toHaveBeenCalledTimes(2);
|
|
635
|
-
expect(mockIntake).toHaveBeenCalledWith('cand-1');
|
|
636
|
-
expect(mockIntake).toHaveBeenCalledWith('cand-2');
|
|
637
|
-
expect(mockUpdateCandidateStatus).toHaveBeenCalledTimes(2);
|
|
638
|
-
expect(mockUpdateCandidateStatus).toHaveBeenCalledWith('cand-1', { status: 'consumed' });
|
|
639
|
-
expect(mockUpdateCandidateStatus).toHaveBeenCalledWith('cand-2', { status: 'consumed' });
|
|
640
|
-
|
|
641
|
-
const jsonOutput = consoleSpy.mock.calls.find(call => {
|
|
642
|
-
try {
|
|
643
|
-
const parsed = JSON.parse(call[0] as string);
|
|
644
|
-
return parsed.intake && Array.isArray(parsed.intake.candidates);
|
|
645
|
-
} catch { return false; }
|
|
646
|
-
});
|
|
647
|
-
expect(jsonOutput).toBeDefined();
|
|
648
|
-
const parsed = JSON.parse((jsonOutput as [string])[0]);
|
|
649
|
-
expect(parsed.intake.enabled).toBe(true);
|
|
650
|
-
expect(parsed.intake.candidates).toHaveLength(2);
|
|
651
|
-
expect(parsed.intake.candidates[0].candidateId).toBe('cand-1');
|
|
652
|
-
expect(parsed.intake.candidates[0].status).toBe('consumed');
|
|
653
|
-
expect(parsed.intake.candidates[0].ledgerEntryId).toBe('ledger-1');
|
|
654
|
-
expect(parsed.intake.candidates[1].candidateId).toBe('cand-2');
|
|
655
|
-
expect(parsed.intake.candidates[1].status).toBe('consumed');
|
|
656
|
-
expect(parsed.intake.candidates[1].ledgerEntryId).toBe('ledger-2');
|
|
657
|
-
expect(exitSpy).not.toHaveBeenCalledWith(1);
|
|
658
|
-
|
|
659
|
-
consoleSpy.mockRestore();
|
|
660
|
-
exitSpy.mockRestore();
|
|
661
|
-
});
|
|
662
|
-
|
|
663
|
-
it('INTAKE-02: successful diagnose + intake failure — exits non-zero with nextAction', async () => {
|
|
664
|
-
const candidates = [
|
|
665
|
-
{ candidateId: 'cand-ok', artifactId: 'art-1', taskId: 'test-task-1', status: 'pending' },
|
|
666
|
-
{ candidateId: 'cand-fail', artifactId: 'art-2', taskId: 'test-task-1', status: 'pending' },
|
|
667
|
-
];
|
|
668
|
-
mockGetCandidatesByTaskId.mockResolvedValue(candidates);
|
|
669
|
-
mockIntake
|
|
670
|
-
.mockResolvedValueOnce({ id: 'ledger-ok', title: 'OK', status: 'probation' })
|
|
671
|
-
.mockImplementationOnce(() => { throw new Error('Ledger write failed'); });
|
|
672
|
-
|
|
673
|
-
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
674
|
-
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
|
|
675
|
-
|
|
676
|
-
await handleDiagnoseRun({
|
|
677
|
-
taskId: 'test-task-1',
|
|
678
|
-
workspace: '/tmp/fake-workspace',
|
|
679
|
-
runtime: 'test-double',
|
|
680
|
-
json: true,
|
|
681
|
-
} as DiagnoseRunOptions);
|
|
682
|
-
|
|
683
|
-
const jsonOutput = consoleSpy.mock.calls.find(call => {
|
|
684
|
-
try {
|
|
685
|
-
const parsed = JSON.parse(call[0] as string);
|
|
686
|
-
return parsed.intake && Array.isArray(parsed.intake.candidates);
|
|
687
|
-
} catch { return false; }
|
|
688
|
-
});
|
|
689
|
-
expect(jsonOutput).toBeDefined();
|
|
690
|
-
const parsed = JSON.parse((jsonOutput as [string])[0]);
|
|
691
|
-
expect(parsed.intake.candidates).toHaveLength(2);
|
|
692
|
-
expect(parsed.intake.candidates[0].status).toBe('consumed');
|
|
693
|
-
expect(parsed.intake.candidates[1].status).toBe('intake_failed');
|
|
694
|
-
expect(parsed.intake.candidates[1].error).toContain('Ledger write failed');
|
|
695
|
-
expect(parsed.intake.candidates[1].nextAction).toContain('pd candidate intake --candidate-id cand-fail');
|
|
696
|
-
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
697
|
-
|
|
698
|
-
consoleSpy.mockRestore();
|
|
699
|
-
exitSpy.mockRestore();
|
|
700
|
-
});
|
|
701
|
-
|
|
702
|
-
it('INTAKE-03: --no-intake skips intake, candidates remain pending with advisory', async () => {
|
|
703
|
-
const candidates = [
|
|
704
|
-
{ candidateId: 'cand-1', artifactId: 'art-1', taskId: 'test-task-1', status: 'pending' },
|
|
705
|
-
];
|
|
706
|
-
mockGetCandidatesByTaskId.mockResolvedValue(candidates);
|
|
707
|
-
|
|
708
|
-
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
709
|
-
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
|
|
710
|
-
|
|
711
|
-
await handleDiagnoseRun({
|
|
712
|
-
taskId: 'test-task-1',
|
|
713
|
-
workspace: '/tmp/fake-workspace',
|
|
714
|
-
runtime: 'test-double',
|
|
715
|
-
json: true,
|
|
716
|
-
intake: false,
|
|
717
|
-
} as DiagnoseRunOptions);
|
|
718
|
-
|
|
719
|
-
expect(mockIntake).not.toHaveBeenCalled();
|
|
720
|
-
expect(mockUpdateCandidateStatus).not.toHaveBeenCalled();
|
|
721
|
-
|
|
722
|
-
const jsonOutput = consoleSpy.mock.calls.find(call => {
|
|
723
|
-
try {
|
|
724
|
-
const parsed = JSON.parse(call[0] as string);
|
|
725
|
-
return parsed.intake && Array.isArray(parsed.intake.candidates);
|
|
726
|
-
} catch { return false; }
|
|
727
|
-
});
|
|
728
|
-
expect(jsonOutput).toBeDefined();
|
|
729
|
-
const parsed = JSON.parse((jsonOutput as [string])[0]);
|
|
730
|
-
expect(parsed.intake.enabled).toBe(false);
|
|
731
|
-
expect(parsed.intake.candidates).toHaveLength(1);
|
|
732
|
-
expect(parsed.intake.candidates[0].candidateId).toBe('cand-1');
|
|
733
|
-
expect(parsed.intake.candidates[0].status).toBe('skipped');
|
|
734
|
-
expect(exitSpy).not.toHaveBeenCalledWith(1);
|
|
735
|
-
|
|
736
|
-
consoleSpy.mockRestore();
|
|
737
|
-
exitSpy.mockRestore();
|
|
738
|
-
});
|
|
739
|
-
|
|
740
|
-
it('INTAKE-04: --no-intake human-readable output shows advisory with manual intake commands', async () => {
|
|
741
|
-
const candidates = [
|
|
742
|
-
{ candidateId: 'cand-1', artifactId: 'art-1', taskId: 'test-task-1', status: 'pending' },
|
|
743
|
-
];
|
|
744
|
-
mockGetCandidatesByTaskId.mockResolvedValue(candidates);
|
|
745
|
-
|
|
746
|
-
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
747
|
-
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
|
|
748
|
-
|
|
749
|
-
await handleDiagnoseRun({
|
|
750
|
-
taskId: 'test-task-1',
|
|
751
|
-
workspace: '/tmp/fake-workspace',
|
|
752
|
-
runtime: 'test-double',
|
|
753
|
-
json: false,
|
|
754
|
-
intake: false,
|
|
755
|
-
} as DiagnoseRunOptions);
|
|
756
|
-
|
|
757
|
-
const allOutput = consoleSpy.mock.calls.map(call => call[0]).join('\n');
|
|
758
|
-
expect(allOutput).toContain('--no-intake');
|
|
759
|
-
expect(allOutput).toContain('pd candidate intake --candidate-id cand-1');
|
|
760
|
-
expect(exitSpy).not.toHaveBeenCalledWith(1);
|
|
761
|
-
|
|
762
|
-
consoleSpy.mockRestore();
|
|
763
|
-
exitSpy.mockRestore();
|
|
764
|
-
});
|
|
765
|
-
|
|
766
|
-
it('INTAKE-05: intake failure human-readable output shows nextAction', async () => {
|
|
767
|
-
const candidates = [
|
|
768
|
-
{ candidateId: 'cand-fail', artifactId: 'art-1', taskId: 'test-task-1', status: 'pending' },
|
|
769
|
-
];
|
|
770
|
-
mockGetCandidatesByTaskId.mockResolvedValue(candidates);
|
|
771
|
-
mockIntake.mockImplementation(() => { throw new Error('Ledger write failed'); });
|
|
772
|
-
|
|
773
|
-
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
774
|
-
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
|
|
775
|
-
|
|
776
|
-
await handleDiagnoseRun({
|
|
777
|
-
taskId: 'test-task-1',
|
|
778
|
-
workspace: '/tmp/fake-workspace',
|
|
779
|
-
runtime: 'test-double',
|
|
780
|
-
json: false,
|
|
781
|
-
} as DiagnoseRunOptions);
|
|
782
|
-
|
|
783
|
-
const allOutput = consoleSpy.mock.calls.map(call => call[0]).join('\n');
|
|
784
|
-
expect(allOutput).toContain('INTAKE FAILED');
|
|
785
|
-
expect(allOutput).toContain('Next action: pd candidate intake --candidate-id cand-fail');
|
|
786
|
-
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
787
|
-
|
|
788
|
-
consoleSpy.mockRestore();
|
|
789
|
-
exitSpy.mockRestore();
|
|
790
|
-
});
|
|
791
|
-
|
|
792
|
-
it('INTAKE-06: does not bypass ledger — consumed only set after intake succeeds', async () => {
|
|
793
|
-
const candidates = [
|
|
794
|
-
{ candidateId: 'cand-1', artifactId: 'art-1', taskId: 'test-task-1', status: 'pending' },
|
|
795
|
-
];
|
|
796
|
-
mockGetCandidatesByTaskId.mockResolvedValue(candidates);
|
|
797
|
-
mockIntake.mockImplementation(() => { throw new Error('Ledger write failed'); });
|
|
798
|
-
|
|
799
|
-
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
800
|
-
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
|
|
801
|
-
|
|
802
|
-
await handleDiagnoseRun({
|
|
803
|
-
taskId: 'test-task-1',
|
|
804
|
-
workspace: '/tmp/fake-workspace',
|
|
805
|
-
runtime: 'test-double',
|
|
806
|
-
json: true,
|
|
807
|
-
} as DiagnoseRunOptions);
|
|
808
|
-
|
|
809
|
-
expect(mockIntake).toHaveBeenCalledWith('cand-1');
|
|
810
|
-
expect(mockUpdateCandidateStatus).not.toHaveBeenCalled();
|
|
811
|
-
|
|
812
|
-
consoleSpy.mockRestore();
|
|
813
|
-
exitSpy.mockRestore();
|
|
814
|
-
});
|
|
815
|
-
|
|
816
|
-
it('INTAKE-07: already-consumed candidate — intake called but updateCandidateStatus skipped', async () => {
|
|
817
|
-
const candidates = [
|
|
818
|
-
{ candidateId: 'cand-consumed', artifactId: 'art-1', taskId: 'test-task-1', status: 'consumed' },
|
|
819
|
-
];
|
|
820
|
-
mockGetCandidatesByTaskId.mockResolvedValue(candidates);
|
|
821
|
-
mockIntake.mockResolvedValue({ id: 'ledger-existing', title: 'Existing', status: 'probation' });
|
|
822
|
-
|
|
823
|
-
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
824
|
-
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
|
|
825
|
-
|
|
826
|
-
await handleDiagnoseRun({
|
|
827
|
-
taskId: 'test-task-1',
|
|
828
|
-
workspace: '/tmp/fake-workspace',
|
|
829
|
-
runtime: 'test-double',
|
|
830
|
-
json: true,
|
|
831
|
-
} as DiagnoseRunOptions);
|
|
832
|
-
|
|
833
|
-
expect(mockIntake).toHaveBeenCalledWith('cand-consumed');
|
|
834
|
-
expect(mockUpdateCandidateStatus).not.toHaveBeenCalled();
|
|
835
|
-
|
|
836
|
-
const jsonOutput = consoleSpy.mock.calls.find(call => {
|
|
837
|
-
try {
|
|
838
|
-
const parsed = JSON.parse(call[0] as string);
|
|
839
|
-
return parsed.intake && Array.isArray(parsed.intake.candidates);
|
|
840
|
-
} catch { return false; }
|
|
841
|
-
});
|
|
842
|
-
const parsed = JSON.parse((jsonOutput as [string])[0]);
|
|
843
|
-
expect(parsed.intake.candidates[0].status).toBe('consumed');
|
|
844
|
-
expect(exitSpy).not.toHaveBeenCalledWith(1);
|
|
845
|
-
|
|
846
|
-
consoleSpy.mockRestore();
|
|
847
|
-
exitSpy.mockRestore();
|
|
848
|
-
});
|
|
849
|
-
|
|
850
|
-
it('INTAKE-08: no candidates produced — intake not called, output shows empty intake', async () => {
|
|
851
|
-
mockGetCandidatesByTaskId.mockResolvedValue([]);
|
|
852
|
-
|
|
853
|
-
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
854
|
-
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
|
|
855
|
-
|
|
856
|
-
await handleDiagnoseRun({
|
|
857
|
-
taskId: 'test-task-1',
|
|
858
|
-
workspace: '/tmp/fake-workspace',
|
|
859
|
-
runtime: 'test-double',
|
|
860
|
-
json: true,
|
|
861
|
-
} as DiagnoseRunOptions);
|
|
862
|
-
|
|
863
|
-
expect(mockIntake).not.toHaveBeenCalled();
|
|
864
|
-
expect(mockUpdateCandidateStatus).not.toHaveBeenCalled();
|
|
865
|
-
|
|
866
|
-
const jsonOutput = consoleSpy.mock.calls.find(call => {
|
|
867
|
-
try {
|
|
868
|
-
const parsed = JSON.parse(call[0] as string);
|
|
869
|
-
return parsed.intake;
|
|
870
|
-
} catch { return false; }
|
|
871
|
-
});
|
|
872
|
-
const parsed = JSON.parse((jsonOutput as [string])[0]);
|
|
873
|
-
expect(parsed.intake.candidates).toHaveLength(0);
|
|
874
|
-
expect(parsed.intake.enabled).toBe(true);
|
|
875
|
-
expect(exitSpy).not.toHaveBeenCalledWith(1);
|
|
876
|
-
|
|
877
|
-
consoleSpy.mockRestore();
|
|
878
|
-
exitSpy.mockRestore();
|
|
879
|
-
});
|
|
880
|
-
|
|
881
|
-
it('INTAKE-09: successful diagnose + intake — human-readable output shows consumed candidates', async () => {
|
|
882
|
-
const candidates = [
|
|
883
|
-
{ candidateId: 'cand-1', artifactId: 'art-1', taskId: 'test-task-1', status: 'pending' },
|
|
884
|
-
];
|
|
885
|
-
mockGetCandidatesByTaskId.mockResolvedValue(candidates);
|
|
886
|
-
mockIntake.mockResolvedValue({ id: 'ledger-1', title: 'Principle 1', status: 'probation' });
|
|
887
|
-
|
|
888
|
-
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
889
|
-
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
|
|
890
|
-
|
|
891
|
-
await handleDiagnoseRun({
|
|
892
|
-
taskId: 'test-task-1',
|
|
893
|
-
workspace: '/tmp/fake-workspace',
|
|
894
|
-
runtime: 'test-double',
|
|
895
|
-
json: false,
|
|
896
|
-
} as DiagnoseRunOptions);
|
|
897
|
-
|
|
898
|
-
const allOutput = consoleSpy.mock.calls.map(call => call[0]).join('\n');
|
|
899
|
-
expect(allOutput).toContain('Candidate Intake');
|
|
900
|
-
expect(allOutput).toContain('cand-1: consumed');
|
|
901
|
-
expect(allOutput).toContain('ledger-1');
|
|
902
|
-
expect(exitSpy).not.toHaveBeenCalledWith(1);
|
|
903
|
-
|
|
904
|
-
consoleSpy.mockRestore();
|
|
905
|
-
exitSpy.mockRestore();
|
|
906
|
-
});
|
|
907
|
-
|
|
908
|
-
it('INTAKE-10: --json mode outputs exactly one console.log with parseable JSON (no text header leak)', async () => {
|
|
909
|
-
mockGetCandidatesByTaskId.mockResolvedValue([]);
|
|
910
|
-
|
|
911
|
-
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
912
|
-
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
|
|
913
|
-
|
|
914
|
-
await handleDiagnoseRun({
|
|
915
|
-
taskId: 'test-task-1',
|
|
916
|
-
workspace: '/tmp/fake-workspace',
|
|
917
|
-
runtime: 'test-double',
|
|
918
|
-
json: true,
|
|
919
|
-
} as DiagnoseRunOptions);
|
|
920
|
-
|
|
921
|
-
expect(consoleSpy).toHaveBeenCalledTimes(1);
|
|
922
|
-
const rawOutput = consoleSpy.mock.calls[0][0] as string;
|
|
923
|
-
const parsed = JSON.parse(rawOutput);
|
|
924
|
-
expect(parsed.status).toBe('succeeded');
|
|
925
|
-
expect(parsed.intake).toBeDefined();
|
|
926
|
-
expect(exitSpy).not.toHaveBeenCalledWith(1);
|
|
927
|
-
|
|
928
|
-
consoleSpy.mockRestore();
|
|
929
|
-
exitSpy.mockRestore();
|
|
930
|
-
});
|
|
931
|
-
|
|
932
|
-
it('INTAKE-11: --json intake failure nextAction contains executable command with workspace', async () => {
|
|
933
|
-
const candidates = [
|
|
934
|
-
{ candidateId: 'cand-err', artifactId: 'art-1', taskId: 'test-task-1', status: 'pending' },
|
|
935
|
-
];
|
|
936
|
-
mockGetCandidatesByTaskId.mockResolvedValue(candidates);
|
|
937
|
-
mockIntake.mockImplementation(() => { throw new Error('DB locked'); });
|
|
938
|
-
|
|
939
|
-
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
940
|
-
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
|
|
941
|
-
|
|
942
|
-
await handleDiagnoseRun({
|
|
943
|
-
taskId: 'test-task-1',
|
|
944
|
-
workspace: '/tmp/fake-workspace',
|
|
945
|
-
runtime: 'test-double',
|
|
946
|
-
json: true,
|
|
947
|
-
} as DiagnoseRunOptions);
|
|
948
|
-
|
|
949
|
-
const rawOutput = consoleSpy.mock.calls[0][0] as string;
|
|
950
|
-
const parsed = JSON.parse(rawOutput);
|
|
951
|
-
const failed = parsed.intake.candidates[0];
|
|
952
|
-
expect(failed.status).toBe('intake_failed');
|
|
953
|
-
expect(failed.nextAction).toMatch(/^pd candidate intake --candidate-id cand-err --workspace "/);
|
|
954
|
-
expect(failed.nextAction).toContain('/tmp/fake-workspace');
|
|
955
|
-
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
956
|
-
|
|
957
|
-
consoleSpy.mockRestore();
|
|
958
|
-
exitSpy.mockRestore();
|
|
959
|
-
});
|
|
960
|
-
|
|
961
|
-
it('INTAKE-12: failed diagnosis does not trigger intake (process.exit stubbed)', async () => {
|
|
962
|
-
const { run } = await import('@principles/core/runtime-v2');
|
|
963
|
-
vi.mocked(run).mockResolvedValueOnce({
|
|
964
|
-
status: 'failed',
|
|
965
|
-
taskId: 'test-task-1',
|
|
966
|
-
errorCategory: 'timeout',
|
|
967
|
-
failureReason: 'LLM call timed out',
|
|
968
|
-
attemptCount: 3,
|
|
969
|
-
});
|
|
970
|
-
|
|
971
|
-
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
972
|
-
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
|
|
973
|
-
|
|
974
|
-
await handleDiagnoseRun({
|
|
975
|
-
taskId: 'test-task-1',
|
|
976
|
-
workspace: '/tmp/fake-workspace',
|
|
977
|
-
runtime: 'test-double',
|
|
978
|
-
json: true,
|
|
979
|
-
} as DiagnoseRunOptions);
|
|
980
|
-
|
|
981
|
-
expect(mockGetCandidatesByTaskId).not.toHaveBeenCalled();
|
|
982
|
-
expect(mockIntake).not.toHaveBeenCalled();
|
|
983
|
-
expect(mockUpdateCandidateStatus).not.toHaveBeenCalled();
|
|
984
|
-
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
985
|
-
|
|
986
|
-
consoleSpy.mockRestore();
|
|
987
|
-
exitSpy.mockRestore();
|
|
988
|
-
});
|
|
989
|
-
});
|
|
990
|
-
|
|
991
|
-
describe('Commander wiring for --no-intake', () => {
|
|
992
|
-
function createDiagnoseProgram(): { program: Command; capturedOpts: Record<string, unknown> } {
|
|
993
|
-
const program = new Command();
|
|
994
|
-
program.exitOverride();
|
|
995
|
-
const capturedOpts: Record<string, unknown> = {};
|
|
996
|
-
|
|
997
|
-
program
|
|
998
|
-
.command('diagnose')
|
|
999
|
-
.command('run')
|
|
1000
|
-
.option('--no-intake', 'Skip candidate intake after successful diagnosis')
|
|
1001
|
-
.option('--json', 'Output raw JSON')
|
|
1002
|
-
.action(async (opts) => {
|
|
1003
|
-
Object.assign(capturedOpts, opts);
|
|
1004
|
-
});
|
|
1005
|
-
|
|
1006
|
-
return { program, capturedOpts };
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
it('CMD-01: --no-intake accepted, sets opts.intake === false', async () => {
|
|
1010
|
-
const { program, capturedOpts } = createDiagnoseProgram();
|
|
1011
|
-
await program.parseAsync(['node', 'pd', 'diagnose', 'run', '--no-intake']);
|
|
1012
|
-
expect(capturedOpts.intake).toBe(false);
|
|
1013
|
-
});
|
|
1014
|
-
|
|
1015
|
-
it('CMD-02: default (no flag) → opts.intake === true', async () => {
|
|
1016
|
-
const { program, capturedOpts } = createDiagnoseProgram();
|
|
1017
|
-
await program.parseAsync(['node', 'pd', 'diagnose', 'run']);
|
|
1018
|
-
expect(capturedOpts.intake).toBe(true);
|
|
1019
|
-
});
|
|
1020
|
-
|
|
1021
|
-
it('CMD-03: --intake is not a valid option (Commander rejects it)', async () => {
|
|
1022
|
-
const { program } = createDiagnoseProgram();
|
|
1023
|
-
await expect(
|
|
1024
|
-
program.parseAsync(['node', 'pd', 'diagnose', 'run', '--intake'])
|
|
1025
|
-
).rejects.toThrow();
|
|
1026
|
-
});
|
|
1027
|
-
});
|
|
1028
|
-
|
|
1029
|
-
describe('pd status stalled-threshold validation', () => {
|
|
1030
|
-
it('accepts valid positive integers', async () => {
|
|
1031
|
-
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
|
|
1032
|
-
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
1033
|
-
|
|
1034
|
-
const runtimeV2 = await import('@principles/core/runtime-v2');
|
|
1035
|
-
vi.mocked(runtimeV2.status).mockResolvedValueOnce({
|
|
1036
|
-
taskId: 'test-task-1',
|
|
1037
|
-
status: 'pending',
|
|
1038
|
-
attemptCount: 0,
|
|
1039
|
-
maxAttempts: 3,
|
|
1040
|
-
lastError: null,
|
|
1041
|
-
commitId: null,
|
|
1042
|
-
artifactId: null,
|
|
1043
|
-
candidateCount: null,
|
|
1044
|
-
});
|
|
1045
|
-
|
|
1046
|
-
await handleDiagnoseStatus({
|
|
1047
|
-
taskId: 'test-task-1',
|
|
1048
|
-
stalledThreshold: '123',
|
|
1049
|
-
});
|
|
1050
|
-
|
|
1051
|
-
expect(exitSpy).not.toHaveBeenCalled();
|
|
1052
|
-
exitSpy.mockRestore();
|
|
1053
|
-
consoleSpy.mockRestore();
|
|
1054
|
-
});
|
|
1055
|
-
|
|
1056
|
-
it('rejects invalid inputs (0, negative, decimals, NaN, empty)', async () => {
|
|
1057
|
-
const invalidInputs = ['0', '-10', '1.5', 'abc', 'NaN', ''];
|
|
1058
|
-
for (const input of invalidInputs) {
|
|
1059
|
-
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
|
|
1060
|
-
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
1061
|
-
|
|
1062
|
-
await handleDiagnoseStatus({
|
|
1063
|
-
taskId: 'test-task-1',
|
|
1064
|
-
stalledThreshold: input,
|
|
1065
|
-
});
|
|
1066
|
-
|
|
1067
|
-
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
1068
|
-
expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('positive integer'));
|
|
1069
|
-
|
|
1070
|
-
exitSpy.mockRestore();
|
|
1071
|
-
consoleErrorSpy.mockRestore();
|
|
1072
|
-
}
|
|
1073
|
-
});
|
|
1074
|
-
|
|
1075
|
-
it('rejects invalid inputs in JSON mode', async () => {
|
|
1076
|
-
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
|
|
1077
|
-
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
1078
|
-
|
|
1079
|
-
await handleDiagnoseStatus({
|
|
1080
|
-
taskId: 'test-task-1',
|
|
1081
|
-
stalledThreshold: '0',
|
|
1082
|
-
json: true,
|
|
1083
|
-
});
|
|
1084
|
-
|
|
1085
|
-
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
1086
|
-
expect(consoleSpy).toHaveBeenCalled();
|
|
1087
|
-
const output = JSON.parse((consoleSpy.mock.calls[0] as string[])[0]);
|
|
1088
|
-
expect(output.ok).toBe(false);
|
|
1089
|
-
expect(output.reason).toBe('invalid_stalled_threshold');
|
|
1090
|
-
expect(output.nextAction).toContain('positive integer');
|
|
1091
|
-
|
|
1092
|
-
exitSpy.mockRestore();
|
|
1093
|
-
consoleSpy.mockRestore();
|
|
1094
|
-
});
|
|
1095
|
-
});
|
|
1096
|
-
|
|
1097
|
-
// ERR-067: 1 initial run + maxRetryLoops(10) before the safety limit converts retried to failed.
|
|
1098
|
-
const EXPECTED_MAX_RETRY_CALLS = 11;
|
|
1099
|
-
|
|
1100
|
-
describe('ERR-067: pd diagnose run retry loop for retried status', () => {
|
|
1101
|
-
beforeEach(async () => {
|
|
1102
|
-
vi.clearAllMocks();
|
|
1103
|
-
const { run } = await import('@principles/core/runtime-v2');
|
|
1104
|
-
vi.mocked(run).mockResolvedValue(DEFAULT_SUCCEEDED_RUN_RESULT);
|
|
1105
|
-
});
|
|
1106
|
-
|
|
1107
|
-
afterEach(async () => {
|
|
1108
|
-
const { run } = await import('@principles/core/runtime-v2');
|
|
1109
|
-
vi.mocked(run).mockResolvedValue(DEFAULT_SUCCEEDED_RUN_RESULT);
|
|
1110
|
-
});
|
|
1111
|
-
|
|
1112
|
-
it('ERR-067-01: retried status triggers retry loop, succeeds on second attempt', async () => {
|
|
1113
|
-
const { run } = await import('@principles/core/runtime-v2');
|
|
1114
|
-
const runMock = vi.mocked(run);
|
|
1115
|
-
runMock
|
|
1116
|
-
.mockResolvedValueOnce({
|
|
1117
|
-
status: 'retried' as const,
|
|
1118
|
-
taskId: 'test-task-1',
|
|
1119
|
-
attemptCount: 1,
|
|
1120
|
-
})
|
|
1121
|
-
.mockResolvedValueOnce(SUCCEEDED_RESULT);
|
|
1122
|
-
|
|
1123
|
-
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
1124
|
-
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
|
|
1125
|
-
|
|
1126
|
-
await handleDiagnoseRun({
|
|
1127
|
-
taskId: 'test-task-1',
|
|
1128
|
-
workspace: '/tmp/fake-workspace',
|
|
1129
|
-
runtime: 'test-double',
|
|
1130
|
-
json: false,
|
|
1131
|
-
} as DiagnoseRunOptions);
|
|
1132
|
-
|
|
1133
|
-
expect(runMock).toHaveBeenCalledTimes(2);
|
|
1134
|
-
expect(exitSpy).not.toHaveBeenCalledWith(1);
|
|
1135
|
-
|
|
1136
|
-
consoleSpy.mockRestore();
|
|
1137
|
-
exitSpy.mockRestore();
|
|
1138
|
-
runMock.mockResolvedValue(DEFAULT_SUCCEEDED_RUN_RESULT);
|
|
1139
|
-
});
|
|
1140
|
-
|
|
1141
|
-
it('ERR-067-02: retried status loops until maxRetryLoops, then converts to failed (P0-1 fix)', async () => {
|
|
1142
|
-
const { run } = await import('@principles/core/runtime-v2');
|
|
1143
|
-
const runMock = vi.mocked(run);
|
|
1144
|
-
runMock.mockResolvedValue({
|
|
1145
|
-
status: 'retried' as const,
|
|
1146
|
-
taskId: 'test-task-1',
|
|
1147
|
-
attemptCount: 1,
|
|
1148
|
-
});
|
|
1149
|
-
|
|
1150
|
-
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
1151
|
-
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
|
|
1152
|
-
|
|
1153
|
-
await handleDiagnoseRun({
|
|
1154
|
-
taskId: 'test-task-1',
|
|
1155
|
-
workspace: '/tmp/fake-workspace',
|
|
1156
|
-
runtime: 'test-double',
|
|
1157
|
-
json: false,
|
|
1158
|
-
} as DiagnoseRunOptions);
|
|
1159
|
-
|
|
1160
|
-
expect(runMock).toHaveBeenCalledTimes(EXPECTED_MAX_RETRY_CALLS);
|
|
1161
|
-
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
1162
|
-
|
|
1163
|
-
consoleSpy.mockRestore();
|
|
1164
|
-
exitSpy.mockRestore();
|
|
1165
|
-
runMock.mockResolvedValue(DEFAULT_SUCCEEDED_RUN_RESULT);
|
|
1166
|
-
});
|
|
1167
|
-
|
|
1168
|
-
it('ERR-067-03: retried with failureReason propagates to failed status (P0-1 fix)', async () => {
|
|
1169
|
-
const { run } = await import('@principles/core/runtime-v2');
|
|
1170
|
-
const runMock = vi.mocked(run);
|
|
1171
|
-
runMock.mockResolvedValue({
|
|
1172
|
-
status: 'retried' as const,
|
|
1173
|
-
taskId: 'test-task-1',
|
|
1174
|
-
attemptCount: 1,
|
|
1175
|
-
failureReason: 'Schema validation failed',
|
|
1176
|
-
});
|
|
1177
|
-
|
|
1178
|
-
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
1179
|
-
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
|
|
1180
|
-
|
|
1181
|
-
await handleDiagnoseRun({
|
|
1182
|
-
taskId: 'test-task-1',
|
|
1183
|
-
workspace: '/tmp/fake-workspace',
|
|
1184
|
-
runtime: 'test-double',
|
|
1185
|
-
json: true,
|
|
1186
|
-
} as DiagnoseRunOptions);
|
|
1187
|
-
|
|
1188
|
-
expect(runMock).toHaveBeenCalledTimes(EXPECTED_MAX_RETRY_CALLS);
|
|
1189
|
-
|
|
1190
|
-
const jsonOutput = consoleSpy.mock.calls.find(call => {
|
|
1191
|
-
try {
|
|
1192
|
-
const parsed = JSON.parse(call[0] as string);
|
|
1193
|
-
return parsed.status === 'failed';
|
|
1194
|
-
} catch { return false; }
|
|
1195
|
-
});
|
|
1196
|
-
expect(jsonOutput).toBeDefined();
|
|
1197
|
-
const parsed = JSON.parse((jsonOutput as [string])[0]);
|
|
1198
|
-
expect(parsed.status).toBe('failed');
|
|
1199
|
-
expect(parsed.failureReason).toContain('Max retry loops');
|
|
1200
|
-
expect(parsed.failureReason).toContain('Schema validation failed');
|
|
1201
|
-
|
|
1202
|
-
consoleSpy.mockRestore();
|
|
1203
|
-
exitSpy.mockRestore();
|
|
1204
|
-
runMock.mockResolvedValue(DEFAULT_SUCCEEDED_RUN_RESULT);
|
|
1205
|
-
});
|
|
1206
|
-
|
|
1207
|
-
it('ERR-067-04: succeeded status does NOT trigger retry loop', async () => {
|
|
1208
|
-
const { run } = await import('@principles/core/runtime-v2');
|
|
1209
|
-
const runMock = vi.mocked(run);
|
|
1210
|
-
runMock.mockResolvedValue(SUCCEEDED_RESULT);
|
|
1211
|
-
|
|
1212
|
-
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
1213
|
-
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
|
|
1214
|
-
|
|
1215
|
-
await handleDiagnoseRun({
|
|
1216
|
-
taskId: 'test-task-1',
|
|
1217
|
-
workspace: '/tmp/fake-workspace',
|
|
1218
|
-
runtime: 'test-double',
|
|
1219
|
-
json: false,
|
|
1220
|
-
} as DiagnoseRunOptions);
|
|
1221
|
-
|
|
1222
|
-
expect(runMock).toHaveBeenCalledTimes(1);
|
|
1223
|
-
expect(exitSpy).not.toHaveBeenCalledWith(1);
|
|
1224
|
-
|
|
1225
|
-
consoleSpy.mockRestore();
|
|
1226
|
-
exitSpy.mockRestore();
|
|
1227
|
-
runMock.mockResolvedValue(DEFAULT_SUCCEEDED_RUN_RESULT);
|
|
1228
|
-
});
|
|
1229
|
-
|
|
1230
|
-
it('ERR-067-05: failed status does NOT trigger retry loop', async () => {
|
|
1231
|
-
const { run } = await import('@principles/core/runtime-v2');
|
|
1232
|
-
const runMock = vi.mocked(run);
|
|
1233
|
-
runMock.mockResolvedValue({
|
|
1234
|
-
status: 'failed' as const,
|
|
1235
|
-
taskId: 'test-task-1',
|
|
1236
|
-
attemptCount: 3,
|
|
1237
|
-
failureReason: 'Max attempts exceeded',
|
|
1238
|
-
});
|
|
1239
|
-
|
|
1240
|
-
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
1241
|
-
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
|
|
1242
|
-
|
|
1243
|
-
await handleDiagnoseRun({
|
|
1244
|
-
taskId: 'test-task-1',
|
|
1245
|
-
workspace: '/tmp/fake-workspace',
|
|
1246
|
-
runtime: 'test-double',
|
|
1247
|
-
json: false,
|
|
1248
|
-
} as DiagnoseRunOptions);
|
|
1249
|
-
|
|
1250
|
-
expect(runMock).toHaveBeenCalledTimes(1);
|
|
1251
|
-
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
1252
|
-
|
|
1253
|
-
consoleSpy.mockRestore();
|
|
1254
|
-
exitSpy.mockRestore();
|
|
1255
|
-
runMock.mockResolvedValue(DEFAULT_SUCCEEDED_RUN_RESULT);
|
|
1256
|
-
});
|
|
1257
|
-
});
|
|
1258
|
-
|
|
1259
|
-
// Defect-004 Part 2: dreamer seed after intake
|
|
1260
|
-
describe('Defect-004: pd diagnose run — dreamer seed after intake', () => {
|
|
1261
|
-
beforeEach(async () => {
|
|
1262
|
-
vi.clearAllMocks();
|
|
1263
|
-
const { run } = await import('@principles/core/runtime-v2');
|
|
1264
|
-
vi.mocked(run).mockResolvedValue(SUCCEEDED_RESULT);
|
|
1265
|
-
mockGetCandidatesByTaskId.mockResolvedValue([]);
|
|
1266
|
-
mockUpdateCandidateStatus.mockResolvedValue(undefined);
|
|
1267
|
-
mockCreateTask.mockResolvedValue(undefined);
|
|
1268
|
-
// For dreamer seed tests, getTask should return null (dreamer task doesn't exist yet)
|
|
1269
|
-
// so createTask gets called. Individual tests can override this if needed.
|
|
1270
|
-
mockGetTask.mockResolvedValue(null);
|
|
1271
|
-
mockIntake.mockReset();
|
|
1272
|
-
mockIntake.mockResolvedValue({ id: 'ledger-1', title: 'P1', status: 'probation' });
|
|
1273
|
-
});
|
|
1274
|
-
|
|
1275
|
-
it('DREAMER-01: principle candidate — dreamer task created via createTask', async () => {
|
|
1276
|
-
const candidates = [
|
|
1277
|
-
{ candidateId: 'cand-1', artifactId: 'art-1', taskId: 'test-task-1', status: 'pending', recommendationKind: 'principle' },
|
|
1278
|
-
];
|
|
1279
|
-
mockGetCandidatesByTaskId.mockResolvedValue(candidates);
|
|
1280
|
-
|
|
1281
|
-
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
1282
|
-
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
|
|
1283
|
-
|
|
1284
|
-
await handleDiagnoseRun({
|
|
1285
|
-
taskId: 'test-task-1',
|
|
1286
|
-
workspace: '/tmp/fake-workspace',
|
|
1287
|
-
runtime: 'test-double',
|
|
1288
|
-
json: true,
|
|
1289
|
-
} as DiagnoseRunOptions);
|
|
1290
|
-
|
|
1291
|
-
// Verify createTask was called for the dreamer task
|
|
1292
|
-
expect(mockCreateTask).toHaveBeenCalled();
|
|
1293
|
-
const createCall = mockCreateTask.mock.calls[0]?.[0];
|
|
1294
|
-
expect(createCall?.taskKind).toBe('dreamer');
|
|
1295
|
-
expect(createCall?.taskId).toBe('dreamer-cand-1');
|
|
1296
|
-
expect(createCall?.status).toBe('pending');
|
|
1297
|
-
|
|
1298
|
-
consoleSpy.mockRestore();
|
|
1299
|
-
exitSpy.mockRestore();
|
|
1300
|
-
});
|
|
1301
|
-
|
|
1302
|
-
it('DREAMER-02: defer candidate — no dreamer task created', async () => {
|
|
1303
|
-
const candidates = [
|
|
1304
|
-
{ candidateId: 'cand-defer', artifactId: 'art-1', taskId: 'test-task-1', status: 'pending', recommendationKind: 'defer' },
|
|
1305
|
-
];
|
|
1306
|
-
mockGetCandidatesByTaskId.mockResolvedValue(candidates);
|
|
1307
|
-
|
|
1308
|
-
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
1309
|
-
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
|
|
1310
|
-
|
|
1311
|
-
await handleDiagnoseRun({
|
|
1312
|
-
taskId: 'test-task-1',
|
|
1313
|
-
workspace: '/tmp/fake-workspace',
|
|
1314
|
-
runtime: 'test-double',
|
|
1315
|
-
json: true,
|
|
1316
|
-
} as DiagnoseRunOptions);
|
|
1317
|
-
|
|
1318
|
-
// No dreamer task should be created for defer candidates
|
|
1319
|
-
expect(mockCreateTask).not.toHaveBeenCalled();
|
|
1320
|
-
|
|
1321
|
-
consoleSpy.mockRestore();
|
|
1322
|
-
exitSpy.mockRestore();
|
|
1323
|
-
});
|
|
1324
|
-
|
|
1325
|
-
it('DREAMER-03: implementation candidate — no dreamer task created', async () => {
|
|
1326
|
-
const candidates = [
|
|
1327
|
-
{ candidateId: 'cand-impl', artifactId: 'art-1', taskId: 'test-task-1', status: 'pending', recommendationKind: 'implementation' },
|
|
1328
|
-
];
|
|
1329
|
-
mockGetCandidatesByTaskId.mockResolvedValue(candidates);
|
|
1330
|
-
|
|
1331
|
-
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
1332
|
-
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
|
|
1333
|
-
|
|
1334
|
-
await handleDiagnoseRun({
|
|
1335
|
-
taskId: 'test-task-1',
|
|
1336
|
-
workspace: '/tmp/fake-workspace',
|
|
1337
|
-
runtime: 'test-double',
|
|
1338
|
-
json: true,
|
|
1339
|
-
} as DiagnoseRunOptions);
|
|
1340
|
-
|
|
1341
|
-
// No dreamer task should be created for implementation candidates
|
|
1342
|
-
expect(mockCreateTask).not.toHaveBeenCalled();
|
|
1343
|
-
|
|
1344
|
-
consoleSpy.mockRestore();
|
|
1345
|
-
exitSpy.mockRestore();
|
|
1346
|
-
});
|
|
1347
|
-
|
|
1348
|
-
it('DREAMER-04: --no-intake skips dreamer seed', async () => {
|
|
1349
|
-
const candidates = [
|
|
1350
|
-
{ candidateId: 'cand-1', artifactId: 'art-1', taskId: 'test-task-1', status: 'pending', recommendationKind: 'principle' },
|
|
1351
|
-
];
|
|
1352
|
-
mockGetCandidatesByTaskId.mockResolvedValue(candidates);
|
|
1353
|
-
|
|
1354
|
-
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
1355
|
-
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
|
|
1356
|
-
|
|
1357
|
-
await handleDiagnoseRun({
|
|
1358
|
-
taskId: 'test-task-1',
|
|
1359
|
-
workspace: '/tmp/fake-workspace',
|
|
1360
|
-
runtime: 'test-double',
|
|
1361
|
-
json: true,
|
|
1362
|
-
intake: false,
|
|
1363
|
-
} as DiagnoseRunOptions);
|
|
1364
|
-
|
|
1365
|
-
// No dreamer task should be created when intake is disabled
|
|
1366
|
-
expect(mockCreateTask).not.toHaveBeenCalled();
|
|
1367
|
-
|
|
1368
|
-
consoleSpy.mockRestore();
|
|
1369
|
-
exitSpy.mockRestore();
|
|
1370
|
-
});
|
|
1371
|
-
|
|
1372
|
-
it('DREAMER-05: dreamer seed failure surfaces in intake results but does not fail the command', async () => {
|
|
1373
|
-
const candidates = [
|
|
1374
|
-
{ candidateId: 'cand-1', artifactId: 'art-1', taskId: 'test-task-1', status: 'pending', recommendationKind: 'principle' },
|
|
1375
|
-
];
|
|
1376
|
-
mockGetCandidatesByTaskId.mockResolvedValue(candidates);
|
|
1377
|
-
// Make createTask throw to simulate seed failure
|
|
1378
|
-
mockCreateTask.mockRejectedValueOnce(new Error('DB write failed'));
|
|
1379
|
-
|
|
1380
|
-
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
1381
|
-
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
|
|
1382
|
-
|
|
1383
|
-
await handleDiagnoseRun({
|
|
1384
|
-
taskId: 'test-task-1',
|
|
1385
|
-
workspace: '/tmp/fake-workspace',
|
|
1386
|
-
runtime: 'test-double',
|
|
1387
|
-
json: true,
|
|
1388
|
-
} as DiagnoseRunOptions);
|
|
1389
|
-
|
|
1390
|
-
// Command should still succeed (exit 0) — dreamer seed failure is best-effort
|
|
1391
|
-
expect(exitSpy).not.toHaveBeenCalledWith(1);
|
|
1392
|
-
|
|
1393
|
-
// JSON output should include dreamer_seed_failed status
|
|
1394
|
-
const jsonOutput = consoleSpy.mock.calls.find(call => {
|
|
1395
|
-
try {
|
|
1396
|
-
const parsed = JSON.parse(call[0] as string);
|
|
1397
|
-
return parsed.intake && Array.isArray(parsed.intake.candidates);
|
|
1398
|
-
} catch { return false; }
|
|
1399
|
-
});
|
|
1400
|
-
expect(jsonOutput).toBeDefined();
|
|
1401
|
-
const parsed = JSON.parse((jsonOutput as [string])[0]);
|
|
1402
|
-
const seedResult = parsed.intake.candidates.find((c: { candidateId: string; status: string }) => c.candidateId === 'cand-1' && c.status === 'dreamer_seed_failed');
|
|
1403
|
-
expect(seedResult).toBeDefined();
|
|
1404
|
-
expect(seedResult.error).toContain('DB write failed');
|
|
1405
|
-
|
|
1406
|
-
consoleSpy.mockRestore();
|
|
1407
|
-
exitSpy.mockRestore();
|
|
1408
|
-
});
|
|
1409
|
-
});
|
|
1410
|
-
|
|
1411
|
-
// DEFECT-005 (PRI-514): A single defer candidate's intake_failed must NOT poison
|
|
1412
|
-
// the dreamer seed loop for other successfully-consumed candidates. Previously
|
|
1413
|
-
// `intakeFailed` was a global flag gating the ENTIRE dreamer seed loop — one
|
|
1414
|
-
// defer candidate (admission gate deferred) set it true, skipping dreamer seed
|
|
1415
|
-
// for ALL candidates including ones that succeeded intake (EP-03 / ERR-089
|
|
1416
|
-
// sibling-branch defect: one branch's failure punished all sibling branches).
|
|
1417
|
-
describe('DEFECT-005 (PRI-514): defer intake_failed must not poison other candidates dreamer seed', () => {
|
|
1418
|
-
beforeEach(async () => {
|
|
1419
|
-
vi.clearAllMocks();
|
|
1420
|
-
const { run, evaluateCandidateAdmissionFromRecord } = await import('@principles/core/runtime-v2');
|
|
1421
|
-
vi.mocked(run).mockResolvedValue(SUCCEEDED_RESULT);
|
|
1422
|
-
mockGetCandidatesByTaskId.mockResolvedValue([]);
|
|
1423
|
-
mockUpdateCandidateStatus.mockResolvedValue(undefined);
|
|
1424
|
-
mockCreateTask.mockResolvedValue(undefined);
|
|
1425
|
-
mockGetTask.mockResolvedValue(null);
|
|
1426
|
-
mockIntake.mockReset();
|
|
1427
|
-
mockIntake.mockResolvedValue({ id: 'ledger-1', title: 'P1', status: 'probation' });
|
|
1428
|
-
// Default: admit all candidates. Individual tests override for defer.
|
|
1429
|
-
vi.mocked(evaluateCandidateAdmissionFromRecord).mockReturnValue({
|
|
1430
|
-
decision: 'admitted',
|
|
1431
|
-
reason: 'mock_admitted',
|
|
1432
|
-
nextAction: 'none',
|
|
1433
|
-
evidenceStatus: 'unknown',
|
|
1434
|
-
});
|
|
1435
|
-
});
|
|
1436
|
-
|
|
1437
|
-
// Restore the factory-default admission decision so later describe blocks
|
|
1438
|
-
// (which do `vi.clearAllMocks()` but do not re-set the implementation) see
|
|
1439
|
-
// `admitted` again. Without this, a `mockReturnValue({deferred})` from
|
|
1440
|
-
// DEFECT-005-B would leak into BUG-2 and refuse all its candidates.
|
|
1441
|
-
afterEach(async () => {
|
|
1442
|
-
const { evaluateCandidateAdmissionFromRecord } = await import('@principles/core/runtime-v2');
|
|
1443
|
-
vi.mocked(evaluateCandidateAdmissionFromRecord).mockReturnValue({
|
|
1444
|
-
decision: 'admitted',
|
|
1445
|
-
reason: 'mock_admitted',
|
|
1446
|
-
nextAction: 'none',
|
|
1447
|
-
evidenceStatus: 'unknown',
|
|
1448
|
-
});
|
|
1449
|
-
});
|
|
1450
|
-
|
|
1451
|
-
it('DEFECT-005-A: defer deferred + principle/rule/prompt consumed — principle/rule/prompt still dreamer_seeded', async () => {
|
|
1452
|
-
// Admission gate defers ONLY defer candidates; admits everything else.
|
|
1453
|
-
const { evaluateCandidateAdmissionFromRecord } = await import('@principles/core/runtime-v2');
|
|
1454
|
-
vi.mocked(evaluateCandidateAdmissionFromRecord).mockImplementation((record) => {
|
|
1455
|
-
if (record.recommendationKind === 'defer') {
|
|
1456
|
-
return {
|
|
1457
|
-
decision: 'deferred',
|
|
1458
|
-
reason: 'recommendation_kind_defer_not_actionable',
|
|
1459
|
-
nextAction: 'review_defer_disposition_manually',
|
|
1460
|
-
evidenceStatus: 'unknown',
|
|
1461
|
-
};
|
|
1462
|
-
}
|
|
1463
|
-
return {
|
|
1464
|
-
decision: 'admitted',
|
|
1465
|
-
reason: 'mock_admitted',
|
|
1466
|
-
nextAction: 'none',
|
|
1467
|
-
evidenceStatus: 'unknown',
|
|
1468
|
-
};
|
|
1469
|
-
});
|
|
1470
|
-
|
|
1471
|
-
const candidates = [
|
|
1472
|
-
{ candidateId: 'cand-defer', artifactId: 'art-d', taskId: 'test-task-1', status: 'pending', recommendationKind: 'defer', confidence: 0.9 },
|
|
1473
|
-
{ candidateId: 'cand-principle', artifactId: 'art-p', taskId: 'test-task-1', status: 'pending', recommendationKind: 'principle', confidence: 0.9 },
|
|
1474
|
-
{ candidateId: 'cand-rule', artifactId: 'art-r', taskId: 'test-task-1', status: 'pending', recommendationKind: 'rule', confidence: 0.9 },
|
|
1475
|
-
{ candidateId: 'cand-prompt', artifactId: 'art-pp', taskId: 'test-task-1', status: 'pending', recommendationKind: 'prompt', confidence: 0.9 },
|
|
1476
|
-
{ candidateId: 'cand-impl', artifactId: 'art-i', taskId: 'test-task-1', status: 'pending', recommendationKind: 'implementation', confidence: 0.9 },
|
|
1477
|
-
];
|
|
1478
|
-
mockGetCandidatesByTaskId.mockResolvedValue(candidates);
|
|
1479
|
-
|
|
1480
|
-
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
1481
|
-
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
|
|
1482
|
-
|
|
1483
|
-
await handleDiagnoseRun({
|
|
1484
|
-
taskId: 'test-task-1',
|
|
1485
|
-
workspace: '/tmp/fake-workspace',
|
|
1486
|
-
runtime: 'test-double',
|
|
1487
|
-
json: true,
|
|
1488
|
-
} as DiagnoseRunOptions);
|
|
1489
|
-
|
|
1490
|
-
const jsonOutput = consoleSpy.mock.calls.find(call => {
|
|
1491
|
-
try {
|
|
1492
|
-
const parsed = JSON.parse(call[0] as string);
|
|
1493
|
-
return parsed.intake && Array.isArray(parsed.intake.candidates);
|
|
1494
|
-
} catch { return false; }
|
|
1495
|
-
});
|
|
1496
|
-
expect(jsonOutput).toBeDefined();
|
|
1497
|
-
const parsed = JSON.parse((jsonOutput as [string])[0]);
|
|
1498
|
-
const results = parsed.intake.candidates as Array<{ candidateId: string; status: string }>;
|
|
1499
|
-
|
|
1500
|
-
// defer candidate: intake_failed (admission gate deferred) — correct.
|
|
1501
|
-
// intakeResults may carry multiple entries per candidate (intake + seed),
|
|
1502
|
-
// so we match on BOTH candidateId and status (same pattern as DREAMER-05).
|
|
1503
|
-
const deferFailed = results.find(r => r.candidateId === 'cand-defer' && r.status === 'intake_failed');
|
|
1504
|
-
expect(deferFailed).toBeDefined();
|
|
1505
|
-
|
|
1506
|
-
// principle/rule/prompt: dreamer_seeded — MUST NOT be poisoned by defer's intake_failed
|
|
1507
|
-
const principleSeeded = results.find(r => r.candidateId === 'cand-principle' && r.status === 'dreamer_seeded');
|
|
1508
|
-
expect(principleSeeded).toBeDefined();
|
|
1509
|
-
|
|
1510
|
-
const ruleSeeded = results.find(r => r.candidateId === 'cand-rule' && r.status === 'dreamer_seeded');
|
|
1511
|
-
expect(ruleSeeded).toBeDefined();
|
|
1512
|
-
|
|
1513
|
-
const promptSeeded = results.find(r => r.candidateId === 'cand-prompt' && r.status === 'dreamer_seeded');
|
|
1514
|
-
expect(promptSeeded).toBeDefined();
|
|
1515
|
-
|
|
1516
|
-
// 3 dreamer tasks created (principle + rule + prompt).
|
|
1517
|
-
// defer was refused at the gate (no intake, no seed).
|
|
1518
|
-
// implementation is skipped by the dreamer seed loop (`if kind === 'implementation' continue`).
|
|
1519
|
-
expect(mockCreateTask).toHaveBeenCalledTimes(3);
|
|
1520
|
-
|
|
1521
|
-
consoleSpy.mockRestore();
|
|
1522
|
-
exitSpy.mockRestore();
|
|
1523
|
-
});
|
|
1524
|
-
|
|
1525
|
-
it('DEFECT-005-B: all candidates intake_failed — 0 dreamer_seeded (boundary)', async () => {
|
|
1526
|
-
// Refuse everything via admission gate
|
|
1527
|
-
const { evaluateCandidateAdmissionFromRecord } = await import('@principles/core/runtime-v2');
|
|
1528
|
-
vi.mocked(evaluateCandidateAdmissionFromRecord).mockReturnValue({
|
|
1529
|
-
decision: 'deferred',
|
|
1530
|
-
reason: 'mock_deferred',
|
|
1531
|
-
nextAction: 'none',
|
|
1532
|
-
evidenceStatus: 'unknown',
|
|
1533
|
-
});
|
|
1534
|
-
|
|
1535
|
-
const candidates = [
|
|
1536
|
-
{ candidateId: 'cand-1', artifactId: 'art-1', taskId: 'test-task-1', status: 'pending', recommendationKind: 'principle', confidence: 0.9 },
|
|
1537
|
-
{ candidateId: 'cand-2', artifactId: 'art-2', taskId: 'test-task-1', status: 'pending', recommendationKind: 'rule', confidence: 0.9 },
|
|
1538
|
-
];
|
|
1539
|
-
mockGetCandidatesByTaskId.mockResolvedValue(candidates);
|
|
1540
|
-
|
|
1541
|
-
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
1542
|
-
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
|
|
1543
|
-
|
|
1544
|
-
await handleDiagnoseRun({
|
|
1545
|
-
taskId: 'test-task-1',
|
|
1546
|
-
workspace: '/tmp/fake-workspace',
|
|
1547
|
-
runtime: 'test-double',
|
|
1548
|
-
json: true,
|
|
1549
|
-
} as DiagnoseRunOptions);
|
|
1550
|
-
|
|
1551
|
-
// No dreamer tasks should be created when no candidate was consumed
|
|
1552
|
-
expect(mockCreateTask).not.toHaveBeenCalled();
|
|
1553
|
-
|
|
1554
|
-
const jsonOutput = consoleSpy.mock.calls.find(call => {
|
|
1555
|
-
try {
|
|
1556
|
-
const parsed = JSON.parse(call[0] as string);
|
|
1557
|
-
return parsed.intake && Array.isArray(parsed.intake.candidates);
|
|
1558
|
-
} catch { return false; }
|
|
1559
|
-
});
|
|
1560
|
-
expect(jsonOutput).toBeDefined();
|
|
1561
|
-
const parsed = JSON.parse((jsonOutput as [string])[0]);
|
|
1562
|
-
const results = parsed.intake.candidates as Array<{ candidateId: string; status: string }>;
|
|
1563
|
-
expect(results.every(r => r.status === 'intake_failed')).toBe(true);
|
|
1564
|
-
|
|
1565
|
-
consoleSpy.mockRestore();
|
|
1566
|
-
exitSpy.mockRestore();
|
|
1567
|
-
});
|
|
1568
|
-
});
|
|
1569
|
-
|
|
1570
|
-
// BUG-1 (PRI-442): effectiveConfig must be passed to split-pipeline runners
|
|
1571
|
-
// so that ADR-0019 LLM rate-limit degradation (isDegradationEnabled) can fire.
|
|
1572
|
-
// Without this wiring, isDegradationEnabled() always returns false because
|
|
1573
|
-
// this.config.effectiveConfig is undefined. EP-02: production path wiring.
|
|
1574
|
-
describe('BUG-1 (PRI-442): effectiveConfig wiring to split-pipeline runners', () => {
|
|
1575
|
-
beforeEach(async () => {
|
|
1576
|
-
vi.clearAllMocks();
|
|
1577
|
-
const { run } = await import('@principles/core/runtime-v2');
|
|
1578
|
-
vi.mocked(run).mockResolvedValue(SUCCEEDED_RESULT);
|
|
1579
|
-
mockGetCandidatesByTaskId.mockResolvedValue([]);
|
|
1580
|
-
mockUpdateCandidateStatus.mockResolvedValue(undefined);
|
|
1581
|
-
mockIntake.mockReset();
|
|
1582
|
-
});
|
|
1583
|
-
|
|
1584
|
-
it('passes effectiveConfig to DiagRootCauseRunner when split pipeline is enabled', async () => {
|
|
1585
|
-
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
1586
|
-
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
|
|
1587
|
-
|
|
1588
|
-
await handleDiagnoseRun({
|
|
1589
|
-
taskId: 'test-task-1',
|
|
1590
|
-
workspace: '/tmp/fake-workspace',
|
|
1591
|
-
runtime: 'test-double',
|
|
1592
|
-
json: true,
|
|
1593
|
-
} as DiagnoseRunOptions);
|
|
1594
|
-
|
|
1595
|
-
expect(diagRootCauseRunnerCtor).toHaveBeenCalled();
|
|
1596
|
-
const optionsArg = diagRootCauseRunnerCtor.mock.calls[0]?.[1];
|
|
1597
|
-
expect(optionsArg?.effectiveConfig).toBeDefined();
|
|
1598
|
-
expect(optionsArg?.effectiveConfig).toEqual(
|
|
1599
|
-
expect.objectContaining({ config: expect.anything(), source: 'file' }),
|
|
1600
|
-
);
|
|
1601
|
-
|
|
1602
|
-
consoleSpy.mockRestore();
|
|
1603
|
-
exitSpy.mockRestore();
|
|
1604
|
-
});
|
|
1605
|
-
|
|
1606
|
-
it('passes effectiveConfig to DiagDistillerRunner and DiagRouterRunner', async () => {
|
|
1607
|
-
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
1608
|
-
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
|
|
1609
|
-
|
|
1610
|
-
await handleDiagnoseRun({
|
|
1611
|
-
taskId: 'test-task-1',
|
|
1612
|
-
workspace: '/tmp/fake-workspace',
|
|
1613
|
-
runtime: 'test-double',
|
|
1614
|
-
json: true,
|
|
1615
|
-
} as DiagnoseRunOptions);
|
|
1616
|
-
|
|
1617
|
-
const distillerOptions = diagDistillerRunnerCtor.mock.calls[0]?.[1];
|
|
1618
|
-
expect(distillerOptions?.effectiveConfig).toBeDefined();
|
|
1619
|
-
|
|
1620
|
-
const routerOptions = diagRouterRunnerCtor.mock.calls[0]?.[1];
|
|
1621
|
-
expect(routerOptions?.effectiveConfig).toBeDefined();
|
|
1622
|
-
|
|
1623
|
-
consoleSpy.mockRestore();
|
|
1624
|
-
exitSpy.mockRestore();
|
|
1625
|
-
});
|
|
1626
|
-
|
|
1627
|
-
// CR-2 (CodeRabbit P2): loadPdConfig mock only covered ok:true. The
|
|
1628
|
-
// ok:false fallback branch (effectiveConfig = defaults) was untested.
|
|
1629
|
-
// This test locks in the fallback behavior + asserts rc-9 observability.
|
|
1630
|
-
it('warns and falls back to defaults when configLoadResult.ok is false', async () => {
|
|
1631
|
-
// Override the module-level mock for this test only
|
|
1632
|
-
const { loadPdConfig } = await import('../../src/services/pd-config-loader.js');
|
|
1633
|
-
vi.mocked(loadPdConfig).mockReturnValueOnce({
|
|
1634
|
-
ok: false,
|
|
1635
|
-
source: 'malformed',
|
|
1636
|
-
configPath: '/tmp/fake-workspace/.pd/config.yaml',
|
|
1637
|
-
errors: [
|
|
1638
|
-
{ path: 'featureFlags.diagnostician_llm_degradation', reason: 'expected boolean, got string', nextAction: 'Fix the value to be a boolean' },
|
|
1639
|
-
],
|
|
1640
|
-
defaults: { config: {}, source: 'defaults', warnings: [] },
|
|
1641
|
-
warnings: [],
|
|
1642
|
-
legacyFilesDetected: [],
|
|
1643
|
-
legacyFileNextActions: [],
|
|
1644
|
-
});
|
|
1645
|
-
|
|
1646
|
-
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
1647
|
-
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
1648
|
-
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
|
|
1649
|
-
|
|
1650
|
-
await handleDiagnoseRun({
|
|
1651
|
-
taskId: 'test-task-1',
|
|
1652
|
-
workspace: '/tmp/fake-workspace',
|
|
1653
|
-
runtime: 'test-double',
|
|
1654
|
-
json: true,
|
|
1655
|
-
} as DiagnoseRunOptions);
|
|
1656
|
-
|
|
1657
|
-
// Assert warning was emitted (rc-9: no silent fallback)
|
|
1658
|
-
expect(warnSpy).toHaveBeenCalledWith(
|
|
1659
|
-
expect.stringContaining('[pd diagnose]'),
|
|
1660
|
-
);
|
|
1661
|
-
expect(warnSpy).toHaveBeenCalledWith(
|
|
1662
|
-
expect.stringContaining('malformed'),
|
|
1663
|
-
);
|
|
1664
|
-
|
|
1665
|
-
// Assert runners still received effectiveConfig (the defaults-based one)
|
|
1666
|
-
expect(diagRootCauseRunnerCtor).toHaveBeenCalled();
|
|
1667
|
-
const optionsArg = diagRootCauseRunnerCtor.mock.calls[0]?.[1];
|
|
1668
|
-
expect(optionsArg?.effectiveConfig).toBeDefined();
|
|
1669
|
-
expect(optionsArg?.effectiveConfig).toEqual(
|
|
1670
|
-
expect.objectContaining({ source: 'defaults' }),
|
|
1671
|
-
);
|
|
1672
|
-
|
|
1673
|
-
warnSpy.mockRestore();
|
|
1674
|
-
consoleSpy.mockRestore();
|
|
1675
|
-
exitSpy.mockRestore();
|
|
1676
|
-
});
|
|
1677
|
-
});
|
|
1678
|
-
|
|
1679
|
-
// BUG-2 (PRI-442): sourcePainId must be resolved from the diagnostician task
|
|
1680
|
-
// chain and passed to buildDreamerSeedFromCandidate. Without this, the dreamer
|
|
1681
|
-
// task's diagnosticJson omits sourcePainId, breaking run-rulehost lineage.
|
|
1682
|
-
// rc-6: lineage consistency. ERR-004: never invent lineage.
|
|
1683
|
-
describe('BUG-2 (PRI-442): sourcePainId resolution for dreamer seed', () => {
|
|
1684
|
-
beforeEach(async () => {
|
|
1685
|
-
vi.clearAllMocks();
|
|
1686
|
-
buildDreamerSeedCalls.length = 0;
|
|
1687
|
-
const { run } = await import('@principles/core/runtime-v2');
|
|
1688
|
-
vi.mocked(run).mockResolvedValue(SUCCEEDED_RESULT);
|
|
1689
|
-
mockGetCandidatesByTaskId.mockResolvedValue([]);
|
|
1690
|
-
mockUpdateCandidateStatus.mockResolvedValue(undefined);
|
|
1691
|
-
mockCreateTask.mockResolvedValue(undefined);
|
|
1692
|
-
mockGetTask.mockResolvedValue(null);
|
|
1693
|
-
mockIntake.mockReset();
|
|
1694
|
-
mockIntake.mockResolvedValue({ id: 'ledger-1', title: 'P1', status: 'probation' });
|
|
1695
|
-
mockResolveSourcePainId.mockResolvedValue('pain_test-source-1');
|
|
1696
|
-
});
|
|
1697
|
-
|
|
1698
|
-
it('passes resolved sourcePainId to buildDreamerSeedFromCandidate (lineage consistency)', async () => {
|
|
1699
|
-
const candidates = [
|
|
1700
|
-
{ candidateId: 'cand-1', artifactId: 'art-1', taskId: 'test-task-1', status: 'pending', recommendationKind: 'principle' },
|
|
1701
|
-
];
|
|
1702
|
-
mockGetCandidatesByTaskId.mockResolvedValue(candidates);
|
|
1703
|
-
|
|
1704
|
-
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
1705
|
-
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
|
|
1706
|
-
|
|
1707
|
-
await handleDiagnoseRun({
|
|
1708
|
-
taskId: 'test-task-1',
|
|
1709
|
-
workspace: '/tmp/fake-workspace',
|
|
1710
|
-
runtime: 'test-double',
|
|
1711
|
-
json: true,
|
|
1712
|
-
} as DiagnoseRunOptions);
|
|
1713
|
-
|
|
1714
|
-
// resolveSourcePainIdFromDiagnostician should have been called
|
|
1715
|
-
expect(mockResolveSourcePainId).toHaveBeenCalled();
|
|
1716
|
-
// buildDreamerSeedFromCandidate should have received the resolved sourcePainId
|
|
1717
|
-
expect(buildDreamerSeedCalls.length).toBeGreaterThan(0);
|
|
1718
|
-
expect(buildDreamerSeedCalls[0].sourcePainId).toBe('pain_test-source-1');
|
|
1719
|
-
|
|
1720
|
-
consoleSpy.mockRestore();
|
|
1721
|
-
exitSpy.mockRestore();
|
|
1722
|
-
});
|
|
1723
|
-
});
|
|
1724
|
-
|
|
1725
|
-
// ── PRI-638: unified capability-disabled semantics ───────────────────────────
|
|
1726
|
-
//
|
|
1727
|
-
// The CLI owns no kill switch of its own: it reads the canonical authority
|
|
1728
|
-
// (internalAgents.agents.diagnostician.enabled) through the same resolver the
|
|
1729
|
-
// runtime factory uses. Owner-disabled must come out as a structured
|
|
1730
|
-
// `capability_disabled` result — never as missing_runtime / config failure —
|
|
1731
|
-
// with no adapter constructed and no provider contacted.
|
|
1732
|
-
|
|
1733
|
-
describe('PRI-638: pd diagnose run when Diagnostician capability is disabled', () => {
|
|
1734
|
-
beforeEach(async () => {
|
|
1735
|
-
vi.clearAllMocks();
|
|
1736
|
-
const runtimeV2 = await import('@principles/core/runtime-v2');
|
|
1737
|
-
vi.mocked(runtimeV2.resolveDiagnosticianCapability).mockReturnValue({
|
|
1738
|
-
available: false,
|
|
1739
|
-
reason: 'capability_disabled',
|
|
1740
|
-
message: "Agent 'diagnostician' is disabled",
|
|
1741
|
-
nextAction: "Enable agent 'diagnostician' in .pd/config.yaml internalAgents.agents.diagnostician.enabled",
|
|
1742
|
-
});
|
|
1743
|
-
});
|
|
1744
|
-
|
|
1745
|
-
afterEach(async () => {
|
|
1746
|
-
const runtimeV2 = await import('@principles/core/runtime-v2');
|
|
1747
|
-
vi.mocked(runtimeV2.resolveDiagnosticianCapability).mockReset();
|
|
1748
|
-
});
|
|
1749
|
-
|
|
1750
|
-
it('DIAG-638-01: --json emits a structured capability_disabled result and exits 1', async () => {
|
|
1751
|
-
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
1752
|
-
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
|
|
1753
|
-
|
|
1754
|
-
await handleDiagnoseRun({
|
|
1755
|
-
taskId: 'diag_task-1',
|
|
1756
|
-
workspace: '/tmp/fake-workspace',
|
|
1757
|
-
runtime: 'test-double',
|
|
1758
|
-
json: true,
|
|
1759
|
-
} as DiagnoseRunOptions);
|
|
1760
|
-
|
|
1761
|
-
const jsonLine = logSpy.mock.calls
|
|
1762
|
-
.map((c) => String(c[0]))
|
|
1763
|
-
.find((line) => line.trim().startsWith('{'));
|
|
1764
|
-
expect(jsonLine).toBeDefined();
|
|
1765
|
-
const parsed = JSON.parse(jsonLine as string);
|
|
1766
|
-
expect(parsed.reason).toBe('capability_disabled');
|
|
1767
|
-
expect(parsed.nextAction).toContain('internalAgents.agents.diagnostician.enabled');
|
|
1768
|
-
expect(parsed.message).toContain('disabled');
|
|
1769
|
-
|
|
1770
|
-
// Kill switch fires before any runtime machinery: no adapter, no runner.
|
|
1771
|
-
const runtimeV2 = await import('@principles/core/runtime-v2');
|
|
1772
|
-
expect(runtimeV2.TestDoubleRuntimeAdapter).not.toHaveBeenCalled();
|
|
1773
|
-
expect(runtimeV2.SplitDiagnosticianRunner).not.toHaveBeenCalled();
|
|
1774
|
-
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
1775
|
-
|
|
1776
|
-
logSpy.mockRestore();
|
|
1777
|
-
exitSpy.mockRestore();
|
|
1778
|
-
});
|
|
1779
|
-
|
|
1780
|
-
it('DIAG-638-02: human-readable output names the reason and the recovery action', async () => {
|
|
1781
|
-
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
1782
|
-
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
|
|
1783
|
-
|
|
1784
|
-
await handleDiagnoseRun({
|
|
1785
|
-
taskId: 'diag_task-1',
|
|
1786
|
-
workspace: '/tmp/fake-workspace',
|
|
1787
|
-
runtime: 'test-double',
|
|
1788
|
-
json: false,
|
|
1789
|
-
} as DiagnoseRunOptions);
|
|
1790
|
-
|
|
1791
|
-
const out = errSpy.mock.calls.map((c) => String(c[0])).join('\n');
|
|
1792
|
-
expect(out).toContain('diagnostician');
|
|
1793
|
-
expect(out).toContain('capability_disabled');
|
|
1794
|
-
expect(out).toContain('internalAgents.agents.diagnostician.enabled');
|
|
1795
|
-
|
|
1796
|
-
const runtimeV2b = await import('@principles/core/runtime-v2');
|
|
1797
|
-
expect(runtimeV2b.TestDoubleRuntimeAdapter).not.toHaveBeenCalled();
|
|
1798
|
-
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
1799
|
-
|
|
1800
|
-
errSpy.mockRestore();
|
|
1801
|
-
exitSpy.mockRestore();
|
|
1802
|
-
});
|
|
1803
|
-
});
|