@principles/pd-cli 1.132.0 → 1.134.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/__tests__/intent-flag-wiring.test.js +74 -3
- package/dist/commands/__tests__/intent-flag-wiring.test.js.map +1 -1
- package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.d.ts +2 -0
- package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.d.ts.map +1 -0
- package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.js +60 -0
- package/dist/commands/__tests__/pain-retry-maxTokens-flag.test.js.map +1 -0
- package/dist/commands/__tests__/rulecode-flag-wiring.test.js +11 -4
- package/dist/commands/__tests__/rulecode-flag-wiring.test.js.map +1 -1
- package/dist/commands/__tests__/run-rulehost-flag-wiring.test.js +9 -0
- package/dist/commands/__tests__/run-rulehost-flag-wiring.test.js.map +1 -1
- package/dist/commands/admission-gate.d.ts +24 -0
- package/dist/commands/admission-gate.d.ts.map +1 -0
- package/dist/commands/admission-gate.js +29 -0
- package/dist/commands/admission-gate.js.map +1 -0
- package/dist/commands/candidate.d.ts.map +1 -1
- package/dist/commands/candidate.js +72 -6
- package/dist/commands/candidate.js.map +1 -1
- package/dist/commands/console.d.ts.map +1 -1
- package/dist/commands/console.js +40 -0
- package/dist/commands/console.js.map +1 -1
- package/dist/commands/demo-story-a.d.ts.map +1 -1
- package/dist/commands/demo-story-a.js +4 -2
- package/dist/commands/demo-story-a.js.map +1 -1
- package/dist/commands/diagnose.d.ts.map +1 -1
- package/dist/commands/diagnose.js +155 -8
- package/dist/commands/diagnose.js.map +1 -1
- package/dist/commands/errors-list.d.ts +25 -0
- package/dist/commands/errors-list.d.ts.map +1 -0
- package/dist/commands/errors-list.js +368 -0
- package/dist/commands/errors-list.js.map +1 -0
- package/dist/commands/health-codex.d.ts +7 -0
- package/dist/commands/health-codex.d.ts.map +1 -0
- package/dist/commands/health-codex.js +209 -0
- package/dist/commands/health-codex.js.map +1 -0
- package/dist/commands/health.d.ts +24 -0
- package/dist/commands/health.d.ts.map +1 -1
- package/dist/commands/health.js +48 -0
- package/dist/commands/health.js.map +1 -1
- package/dist/commands/intent.d.ts +21 -4
- package/dist/commands/intent.d.ts.map +1 -1
- package/dist/commands/intent.js +88 -27
- package/dist/commands/intent.js.map +1 -1
- package/dist/commands/pain-evidence.d.ts +6 -0
- package/dist/commands/pain-evidence.d.ts.map +1 -1
- package/dist/commands/pain-evidence.js +6 -0
- package/dist/commands/pain-evidence.js.map +1 -1
- package/dist/commands/pain-record.d.ts.map +1 -1
- package/dist/commands/pain-record.js +21 -0
- package/dist/commands/pain-record.js.map +1 -1
- package/dist/commands/pain-retry.d.ts +4 -0
- package/dist/commands/pain-retry.d.ts.map +1 -1
- package/dist/commands/pain-retry.js +327 -32
- package/dist/commands/pain-retry.js.map +1 -1
- package/dist/commands/proven-channel-baseline.d.ts.map +1 -1
- package/dist/commands/proven-channel-baseline.js +4 -2
- package/dist/commands/proven-channel-baseline.js.map +1 -1
- package/dist/commands/runtime-activation.d.ts +30 -0
- package/dist/commands/runtime-activation.d.ts.map +1 -1
- package/dist/commands/runtime-activation.js +567 -11
- package/dist/commands/runtime-activation.js.map +1 -1
- package/dist/commands/runtime-canary.d.ts.map +1 -1
- package/dist/commands/runtime-canary.js +32 -7
- package/dist/commands/runtime-canary.js.map +1 -1
- package/dist/commands/runtime-health-snapshot.d.ts.map +1 -1
- package/dist/commands/runtime-health-snapshot.js +4 -2
- package/dist/commands/runtime-health-snapshot.js.map +1 -1
- package/dist/commands/runtime-init.d.ts +43 -0
- package/dist/commands/runtime-init.d.ts.map +1 -0
- package/dist/commands/runtime-init.js +264 -0
- package/dist/commands/runtime-init.js.map +1 -0
- package/dist/commands/runtime-internalization-context-trace.d.ts +25 -0
- package/dist/commands/runtime-internalization-context-trace.d.ts.map +1 -0
- package/dist/commands/runtime-internalization-context-trace.js +374 -0
- package/dist/commands/runtime-internalization-context-trace.js.map +1 -0
- package/dist/commands/runtime-internalization-enqueue-successors.d.ts.map +1 -1
- package/dist/commands/runtime-internalization-enqueue-successors.js +3 -1
- package/dist/commands/runtime-internalization-enqueue-successors.js.map +1 -1
- package/dist/commands/runtime-internalization-integrity.d.ts.map +1 -1
- package/dist/commands/runtime-internalization-integrity.js +4 -2
- package/dist/commands/runtime-internalization-integrity.js.map +1 -1
- package/dist/commands/runtime-internalization-run-once.d.ts.map +1 -1
- package/dist/commands/runtime-internalization-run-once.js +20 -5
- package/dist/commands/runtime-internalization-run-once.js.map +1 -1
- package/dist/commands/runtime-internalization-run-rulehost.d.ts +1 -0
- package/dist/commands/runtime-internalization-run-rulehost.d.ts.map +1 -1
- package/dist/commands/runtime-internalization-run-rulehost.js +149 -6
- package/dist/commands/runtime-internalization-run-rulehost.js.map +1 -1
- package/dist/commands/runtime-uat.d.ts.map +1 -1
- package/dist/commands/runtime-uat.guard.test.js +13 -15
- package/dist/commands/runtime-uat.guard.test.js.map +1 -1
- package/dist/commands/runtime-uat.js +4 -3
- package/dist/commands/runtime-uat.js.map +1 -1
- package/dist/config-reader.js +1 -1
- package/dist/config-reader.js.map +1 -1
- package/dist/index.js +54 -78
- package/dist/index.js.map +1 -1
- package/dist/services/__tests__/evaluator-runner-deps.test.d.ts +2 -0
- package/dist/services/__tests__/evaluator-runner-deps.test.d.ts.map +1 -0
- package/dist/services/__tests__/evaluator-runner-deps.test.js +304 -0
- package/dist/services/__tests__/evaluator-runner-deps.test.js.map +1 -0
- package/dist/services/__tests__/rulehost-readiness.test.js.map +1 -1
- package/dist/services/__tests__/runtime-adapter-resolver.test.js.map +1 -1
- package/dist/services/config-doctor.d.ts.map +1 -1
- package/dist/services/config-doctor.js +9 -2
- package/dist/services/config-doctor.js.map +1 -1
- package/dist/services/pain-flood-simulation-runner.d.ts.map +1 -1
- package/dist/services/pain-flood-simulation-runner.js +9 -0
- package/dist/services/pain-flood-simulation-runner.js.map +1 -1
- package/dist/services/pd-config-loader.js +2 -2
- package/dist/services/pd-config-loader.js.map +1 -1
- package/dist/services/proven-channel-baseline-runner.d.ts.map +1 -1
- package/dist/services/proven-channel-baseline-runner.js +10 -2
- package/dist/services/proven-channel-baseline-runner.js.map +1 -1
- package/dist/services/quality-scorecard/data-extractor.js.map +1 -1
- package/dist/services/resolve-runtime-from-pd-config.js.map +1 -1
- package/dist/services/rulehost-pipeline-runner.d.ts +77 -1
- package/dist/services/rulehost-pipeline-runner.d.ts.map +1 -1
- package/dist/services/rulehost-pipeline-runner.js +111 -7
- package/dist/services/rulehost-pipeline-runner.js.map +1 -1
- package/dist/services/rulehost-readiness.js.map +1 -1
- package/dist/services/runtime-adapter-resolver.d.ts +1 -0
- package/dist/services/runtime-adapter-resolver.d.ts.map +1 -1
- package/dist/services/runtime-adapter-resolver.js +3 -0
- package/dist/services/runtime-adapter-resolver.js.map +1 -1
- package/dist/services/synthetic-baseline-runner.d.ts.map +1 -1
- package/dist/services/synthetic-baseline-runner.js +11 -0
- package/dist/services/synthetic-baseline-runner.js.map +1 -1
- package/dist/utils/production-workspace-guard.d.ts +4 -4
- package/dist/utils/production-workspace-guard.d.ts.map +1 -1
- package/dist/utils/production-workspace-guard.js +19 -16
- package/dist/utils/production-workspace-guard.js.map +1 -1
- package/dist/utils/production-workspace-guard.test.js +54 -19
- package/dist/utils/production-workspace-guard.test.js.map +1 -1
- package/package.json +8 -5
- package/scripts/migrate-illegal-expected-decision.ts +252 -0
- package/src/commands/__tests__/intent-flag-wiring.test.ts +88 -3
- package/src/commands/__tests__/pain-retry-maxTokens-flag.test.ts +73 -0
- package/src/commands/__tests__/rulecode-flag-wiring.test.ts +31 -24
- package/src/commands/__tests__/run-rulehost-flag-wiring.test.ts +12 -0
- package/src/commands/admission-gate.ts +30 -0
- package/src/commands/candidate.ts +74 -6
- package/src/commands/console.ts +40 -0
- package/src/commands/demo-story-a.ts +4 -2
- package/src/commands/diagnose.ts +158 -10
- package/src/commands/errors-list.ts +440 -0
- package/src/commands/health-codex.ts +249 -0
- package/src/commands/health.ts +60 -0
- package/src/commands/intent.ts +98 -29
- package/src/commands/pain-evidence.ts +6 -0
- package/src/commands/pain-record.ts +21 -0
- package/src/commands/pain-retry.ts +353 -35
- package/src/commands/proven-channel-baseline.ts +4 -2
- package/src/commands/runtime-activation.ts +657 -12
- package/src/commands/runtime-canary.ts +31 -5
- package/src/commands/runtime-health-snapshot.ts +4 -2
- package/src/commands/runtime-init.ts +305 -0
- package/src/commands/runtime-internalization-context-trace.ts +445 -0
- package/src/commands/runtime-internalization-enqueue-successors.ts +3 -1
- package/src/commands/runtime-internalization-integrity.ts +4 -2
- package/src/commands/runtime-internalization-run-once.ts +20 -5
- package/src/commands/runtime-internalization-run-rulehost.ts +159 -6
- package/src/commands/runtime-uat.guard.test.ts +14 -16
- package/src/commands/runtime-uat.ts +4 -3
- package/src/config-reader.ts +1 -1
- package/src/index.ts +69 -78
- package/src/services/__tests__/evaluator-runner-deps.test.ts +341 -0
- package/src/services/config-doctor.ts +9 -2
- package/src/services/pain-flood-simulation-runner.ts +9 -0
- package/src/services/pd-config-loader.ts +2 -2
- package/src/services/proven-channel-baseline-runner.ts +10 -3
- package/src/services/rulehost-pipeline-runner.ts +157 -6
- package/src/services/runtime-adapter-resolver.ts +4 -0
- package/src/services/synthetic-baseline-runner.ts +11 -0
- package/src/utils/production-workspace-guard.test.ts +62 -20
- package/src/utils/production-workspace-guard.ts +19 -16
- package/tests/bdd/cli-contract.steps.ts +343 -0
- package/tests/bdd/context-trace.steps.ts +229 -0
- package/tests/bdd/support/gherkin-loader.ts +147 -0
- package/tests/bdd/support/repo-root.ts +62 -0
- package/tests/bdd/support/vitest-bdd.ts +179 -0
- package/tests/commands/admission-gate-coverage.test.ts +79 -0
- package/tests/commands/candidate-audit-repair.test.ts +77 -0
- package/tests/commands/candidate-intake.test.ts +155 -10
- package/tests/commands/candidate-internalization-backfill.test.ts +30 -0
- package/tests/commands/cli-help-snapshot.test.ts +15 -1
- package/tests/commands/console-open.test.ts +19 -0
- package/tests/commands/diagnose.test.ts +619 -14
- package/tests/commands/errors-list.test.ts +678 -0
- package/tests/commands/health-codex.test.ts +243 -0
- package/tests/commands/health-host-wiring.test.ts +76 -0
- package/tests/commands/intent.test.ts +4 -4
- package/tests/commands/pain-record.test.ts +80 -0
- package/tests/commands/pain-retry.test.ts +94 -3
- package/tests/commands/run-rulehost-handler.test.ts +133 -0
- package/tests/commands/runtime-activation-approve-flag-wiring.test.ts +227 -0
- package/tests/commands/runtime-activation-deactivate-flag-wiring.test.ts +167 -0
- package/tests/commands/runtime-activation-dispatch-flag-wiring.test.ts +262 -0
- package/tests/commands/runtime-activation-list-flag-wiring.test.ts +193 -0
- package/tests/commands/runtime-activation-promote-flag-wiring.test.ts +222 -0
- package/tests/commands/runtime-activation.test.ts +1090 -5
- package/tests/commands/runtime-canary.test.ts +50 -0
- package/tests/commands/runtime-features.test.ts +2 -0
- package/tests/commands/runtime-init-empty-workspace.test.ts +234 -0
- package/tests/commands/runtime-init.test.ts +346 -0
- package/tests/commands/runtime-internalization-context-trace.test.ts +110 -0
- package/tests/commands/runtime-internalization.test.ts +2 -2
- package/tests/config-reader.test.ts +6 -5
- package/tests/e2e/cross-package-acceptance.test.ts +55 -13
- package/tests/services/mainline-snapshot-assembler.test.ts +6 -3
- package/tests/services/resolve-runtime-from-pd-config.test.ts +2 -0
- package/tests/services/rulehost-pipeline-runner.test.ts +1 -1
- package/vitest.config.ts +1 -1
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach, afterEach, beforeAll } from 'vitest';
|
|
2
2
|
import { Command } from 'commander';
|
|
3
3
|
|
|
4
|
-
const { MockRuntimeStateManager, mockGetCandidatesByTaskId, mockUpdateCandidateStatus } = vi.hoisted(() => {
|
|
4
|
+
const { MockRuntimeStateManager, mockGetCandidatesByTaskId, mockUpdateCandidateStatus, mockCreateTask, mockGetTask } = vi.hoisted(() => {
|
|
5
5
|
const mockGetCandidatesByTaskId = vi.fn().mockResolvedValue([]);
|
|
6
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
|
+
});
|
|
7
15
|
|
|
8
16
|
class MockRuntimeStateManager {
|
|
9
17
|
initialize = vi.fn().mockResolvedValue(undefined);
|
|
10
18
|
close = vi.fn().mockResolvedValue(undefined);
|
|
11
|
-
getTask =
|
|
12
|
-
|
|
13
|
-
status: 'pending',
|
|
14
|
-
attemptCount: 0,
|
|
15
|
-
maxAttempts: 3,
|
|
16
|
-
lastError: null,
|
|
17
|
-
});
|
|
19
|
+
getTask = mockGetTask;
|
|
20
|
+
createTask = mockCreateTask;
|
|
18
21
|
getCandidatesByTaskId = mockGetCandidatesByTaskId;
|
|
19
22
|
updateCandidateStatus = mockUpdateCandidateStatus;
|
|
20
23
|
connection = {} as Record<string, unknown>;
|
|
@@ -26,7 +29,7 @@ const { MockRuntimeStateManager, mockGetCandidatesByTaskId, mockUpdateCandidateS
|
|
|
26
29
|
calculateBackoff: () => 10,
|
|
27
30
|
});
|
|
28
31
|
}
|
|
29
|
-
return { MockRuntimeStateManager, mockGetCandidatesByTaskId, mockUpdateCandidateStatus };
|
|
32
|
+
return { MockRuntimeStateManager, mockGetCandidatesByTaskId, mockUpdateCandidateStatus, mockCreateTask, mockGetTask };
|
|
30
33
|
}, { validateType: true });
|
|
31
34
|
|
|
32
35
|
const { mockIntake, MockCandidateIntakeService } = vi.hoisted(() => {
|
|
@@ -63,6 +66,21 @@ const { mockResolveRuntimeFromPdConfig } = vi.hoisted(() => {
|
|
|
63
66
|
return { mockResolveRuntimeFromPdConfig };
|
|
64
67
|
});
|
|
65
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
|
+
|
|
66
84
|
vi.mock('../../src/resolve-workspace.js', () => ({
|
|
67
85
|
resolveWorkspaceDir: vi.fn().mockReturnValue('/tmp/fake-workspace'),
|
|
68
86
|
}));
|
|
@@ -80,9 +98,9 @@ vi.mock('@principles/core/runtime-v2', () => {
|
|
|
80
98
|
StoreEventEmitter: vi.fn().mockImplementation(function () { return {}; }),
|
|
81
99
|
storeEmitter: { emitTelemetry: vi.fn() },
|
|
82
100
|
SplitDiagnosticianRunner: vi.fn().mockImplementation(function () { return {}; }),
|
|
83
|
-
DiagRootCauseRunner:
|
|
84
|
-
DiagDistillerRunner:
|
|
85
|
-
DiagRouterRunner:
|
|
101
|
+
DiagRootCauseRunner: diagRootCauseRunnerCtor,
|
|
102
|
+
DiagDistillerRunner: diagDistillerRunnerCtor,
|
|
103
|
+
DiagRouterRunner: diagRouterRunnerCtor,
|
|
86
104
|
DefaultDiagRootCauseValidator: vi.fn().mockImplementation(function () { return {}; }),
|
|
87
105
|
DefaultDiagDistillerValidator: vi.fn().mockImplementation(function () { return {}; }),
|
|
88
106
|
DisabledDiagnosticianRunner: vi.fn().mockImplementation(function () { return {}; }),
|
|
@@ -97,6 +115,15 @@ vi.mock('@principles/core/runtime-v2', () => {
|
|
|
97
115
|
}
|
|
98
116
|
},
|
|
99
117
|
CandidateIntakeService: MockCandidateIntakeService,
|
|
118
|
+
// PRI-503: admission gate mock — admit by default so existing diagnose/intake
|
|
119
|
+
// tests keep their original flow. Tests that need to assert refusal behavior
|
|
120
|
+
// can override this mock per-test.
|
|
121
|
+
evaluateCandidateAdmissionFromRecord: vi.fn().mockReturnValue({
|
|
122
|
+
decision: 'admitted',
|
|
123
|
+
reason: 'mock_admitted',
|
|
124
|
+
nextAction: 'none',
|
|
125
|
+
evidenceStatus: 'unknown',
|
|
126
|
+
}),
|
|
100
127
|
resolveRuntimeConfig: vi.fn().mockReturnValue({
|
|
101
128
|
runtimeKind: 'pi-ai',
|
|
102
129
|
provider: 'test-provider',
|
|
@@ -123,12 +150,37 @@ vi.mock('@principles/core/runtime-v2', () => {
|
|
|
123
150
|
rootCause: 'Test: test root cause',
|
|
124
151
|
violatedPrinciples: [],
|
|
125
152
|
evidence: [],
|
|
126
|
-
recommendations: [],
|
|
153
|
+
recommendations: [{ kind: 'implementation', description: 'Test recommendation' }],
|
|
127
154
|
confidence: 0.9,
|
|
128
155
|
},
|
|
129
156
|
}),
|
|
130
157
|
status: vi.fn(),
|
|
131
158
|
PrincipleTreeLedgerAdapter: MockPrincipleTreeLedgerAdapter,
|
|
159
|
+
// Defect-004 dreamer seed mocks — minimal stubs that mirror real behavior
|
|
160
|
+
// BUG-2: capture sourcePainId arg to verify lineage wiring
|
|
161
|
+
buildDreamerSeedFromCandidate: vi.fn().mockImplementation((candidate: { candidateId: string }, opts: { route: string; ready: boolean; sourcePainId?: string }) => {
|
|
162
|
+
buildDreamerSeedCalls.push({ candidateId: candidate.candidateId, sourcePainId: opts?.sourcePainId });
|
|
163
|
+
return {
|
|
164
|
+
taskId: `dreamer-${candidate.candidateId}`,
|
|
165
|
+
taskKind: 'dreamer' as const,
|
|
166
|
+
channel: 'prompt' as const,
|
|
167
|
+
diagnosticJson: JSON.stringify({ candidateId: candidate.candidateId }),
|
|
168
|
+
status: 'pending' as const,
|
|
169
|
+
attemptCount: 0,
|
|
170
|
+
maxAttempts: 3,
|
|
171
|
+
};
|
|
172
|
+
}),
|
|
173
|
+
CANDIDATE_KIND_TO_ROUTE: {
|
|
174
|
+
principle: 'principle-candidate',
|
|
175
|
+
rule: 'rule-candidate',
|
|
176
|
+
prompt: 'prompt-candidate',
|
|
177
|
+
},
|
|
178
|
+
ROUTE_CHANNEL_MAP: {
|
|
179
|
+
'principle-candidate': 'prompt',
|
|
180
|
+
'rule-candidate': 'code_tool_hook',
|
|
181
|
+
'prompt-candidate': 'prompt',
|
|
182
|
+
},
|
|
183
|
+
MVP_ENABLED_CHANNELS: new Set(['prompt', 'code_tool_hook', 'defer_archive']),
|
|
132
184
|
};
|
|
133
185
|
});
|
|
134
186
|
|
|
@@ -137,7 +189,11 @@ vi.mock('../../src/config-reader.js', () => ({
|
|
|
137
189
|
}));
|
|
138
190
|
|
|
139
191
|
vi.mock('../../src/services/pd-config-loader.js', () => ({
|
|
140
|
-
loadPdConfig: vi.fn().mockReturnValue({
|
|
192
|
+
loadPdConfig: vi.fn().mockReturnValue({
|
|
193
|
+
ok: true,
|
|
194
|
+
effective: { config: { featureFlags: { diagnostician_llm_degradation: true } }, source: 'file', warnings: [] },
|
|
195
|
+
defaults: { config: {}, source: 'defaults', warnings: [] },
|
|
196
|
+
}),
|
|
141
197
|
computeFlagsFromLoadResult: vi.fn().mockReturnValue({}),
|
|
142
198
|
}));
|
|
143
199
|
|
|
@@ -145,6 +201,11 @@ vi.mock('../../src/services/resolve-runtime-from-pd-config.js', () => ({
|
|
|
145
201
|
resolveRuntimeFromPdConfig: mockResolveRuntimeFromPdConfig,
|
|
146
202
|
}));
|
|
147
203
|
|
|
204
|
+
// BUG-2: mock resolveSourcePainIdFromDiagnostician imported by diagnose.ts from candidate.js
|
|
205
|
+
vi.mock('../../src/commands/candidate.js', () => ({
|
|
206
|
+
resolveSourcePainIdFromDiagnostician: mockResolveSourcePainId,
|
|
207
|
+
}));
|
|
208
|
+
|
|
148
209
|
import { handleDiagnoseRun, handleDiagnoseStatus, type DiagnoseRunOptions } from '../../src/commands/diagnose.js';
|
|
149
210
|
|
|
150
211
|
const SUCCEEDED_RESULT = {
|
|
@@ -382,6 +443,84 @@ describe('pd diagnose run --runtime routing', () => {
|
|
|
382
443
|
exitSpy.mockRestore();
|
|
383
444
|
});
|
|
384
445
|
|
|
446
|
+
it('P1: missing --runtime with no config binding refuses with missing_runtime (no test-double default)', async () => {
|
|
447
|
+
// Regression for the silent test-double default bug. Without --runtime and
|
|
448
|
+
// with no usable .pd/config.yaml binding, the command must refuse loudly
|
|
449
|
+
// (rc-9-no-silent-fallback) instead of defaulting to test-double and
|
|
450
|
+
// producing fake/failed diagnostic data in a real workspace.
|
|
451
|
+
mockResolveRuntimeFromPdConfig.mockReturnValueOnce({
|
|
452
|
+
result: { ok: false, reason: 'no_runtime_binding', message: 'no binding', nextAction: 'set runtime' },
|
|
453
|
+
legacyWarnings: [],
|
|
454
|
+
configSource: '.pd/config.yaml',
|
|
455
|
+
configLoadResult: { ok: true, effective: {}, defaults: {}, legacyFilesDetected: [] } as never,
|
|
456
|
+
});
|
|
457
|
+
mockIsRuntimeConfigError.mockReturnValueOnce(true);
|
|
458
|
+
|
|
459
|
+
const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
460
|
+
const consoleErrSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
461
|
+
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
|
|
462
|
+
|
|
463
|
+
await handleDiagnoseRun({
|
|
464
|
+
taskId: 'test-task-1',
|
|
465
|
+
workspace: '/tmp/fake-workspace',
|
|
466
|
+
// runtime intentionally omitted
|
|
467
|
+
json: true,
|
|
468
|
+
} as DiagnoseRunOptions);
|
|
469
|
+
|
|
470
|
+
// Config was consulted (not silently skipped).
|
|
471
|
+
expect(mockResolveRuntimeFromPdConfig).toHaveBeenCalledWith('/tmp/fake-workspace');
|
|
472
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
473
|
+
const jsonOutput = JSON.parse(consoleLogSpy.mock.calls[0][0] as string);
|
|
474
|
+
expect(jsonOutput.ok).toBe(false);
|
|
475
|
+
expect(jsonOutput.reason).toBe('missing_runtime');
|
|
476
|
+
expect(jsonOutput.nextAction).toContain('--runtime');
|
|
477
|
+
|
|
478
|
+
consoleLogSpy.mockRestore();
|
|
479
|
+
consoleErrSpy.mockRestore();
|
|
480
|
+
exitSpy.mockRestore();
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
it('P1: missing --runtime resolves kind from .pd/config.yaml (honors config, no test-double default)', async () => {
|
|
484
|
+
// When --runtime is absent but .pd/config.yaml binds openclaw-cli, the
|
|
485
|
+
// resolved kind must come from config — proving test-double is no longer
|
|
486
|
+
// the default. Mirrors DPB-09 but omits --runtime to exercise the new
|
|
487
|
+
// config-resolution path.
|
|
488
|
+
mockResolveRuntimeFromPdConfig.mockReturnValueOnce({
|
|
489
|
+
result: {
|
|
490
|
+
runtimeKind: 'openclaw-cli',
|
|
491
|
+
openclawMode: 'gateway',
|
|
492
|
+
timeoutMs: 300000,
|
|
493
|
+
agentId: 'main',
|
|
494
|
+
},
|
|
495
|
+
legacyWarnings: [],
|
|
496
|
+
configSource: '.pd/config.yaml',
|
|
497
|
+
configLoadResult: { ok: true, effective: {}, defaults: {}, legacyFilesDetected: [] } as never,
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
501
|
+
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
|
|
502
|
+
|
|
503
|
+
await handleDiagnoseRun({
|
|
504
|
+
taskId: 'test-task-1',
|
|
505
|
+
workspace: '/tmp/fake-workspace',
|
|
506
|
+
// runtime intentionally omitted — must be resolved from config
|
|
507
|
+
openclawGateway: true,
|
|
508
|
+
json: false,
|
|
509
|
+
} as DiagnoseRunOptions);
|
|
510
|
+
|
|
511
|
+
expect(mockResolveRuntimeFromPdConfig).toHaveBeenCalledWith('/tmp/fake-workspace');
|
|
512
|
+
const OpenClawCliMock = vi.mocked(
|
|
513
|
+
await import('@principles/core/runtime-v2').then(m => m.OpenClawCliRuntimeAdapter),
|
|
514
|
+
);
|
|
515
|
+
expect(OpenClawCliMock).toHaveBeenCalledWith(
|
|
516
|
+
expect.objectContaining({ runtimeMode: 'gateway' }),
|
|
517
|
+
);
|
|
518
|
+
expect(exitSpy).not.toHaveBeenCalledWith(1);
|
|
519
|
+
|
|
520
|
+
consoleSpy.mockRestore();
|
|
521
|
+
exitSpy.mockRestore();
|
|
522
|
+
});
|
|
523
|
+
|
|
385
524
|
it('DPB-09: openclaw-cli --openclaw-gateway constructs adapter with runtimeMode=gateway', async () => {
|
|
386
525
|
mockResolveRuntimeFromPdConfig.mockReturnValueOnce({
|
|
387
526
|
result: {
|
|
@@ -1109,3 +1248,469 @@ describe('ERR-067: pd diagnose run retry loop for retried status', () => {
|
|
|
1109
1248
|
runMock.mockResolvedValue(DEFAULT_SUCCEEDED_RUN_RESULT);
|
|
1110
1249
|
});
|
|
1111
1250
|
});
|
|
1251
|
+
|
|
1252
|
+
// Defect-004 Part 2: dreamer seed after intake
|
|
1253
|
+
describe('Defect-004: pd diagnose run — dreamer seed after intake', () => {
|
|
1254
|
+
beforeEach(async () => {
|
|
1255
|
+
vi.clearAllMocks();
|
|
1256
|
+
const { run } = await import('@principles/core/runtime-v2');
|
|
1257
|
+
vi.mocked(run).mockResolvedValue(SUCCEEDED_RESULT);
|
|
1258
|
+
mockGetCandidatesByTaskId.mockResolvedValue([]);
|
|
1259
|
+
mockUpdateCandidateStatus.mockResolvedValue(undefined);
|
|
1260
|
+
mockCreateTask.mockResolvedValue(undefined);
|
|
1261
|
+
// For dreamer seed tests, getTask should return null (dreamer task doesn't exist yet)
|
|
1262
|
+
// so createTask gets called. Individual tests can override this if needed.
|
|
1263
|
+
mockGetTask.mockResolvedValue(null);
|
|
1264
|
+
mockIntake.mockReset();
|
|
1265
|
+
mockIntake.mockResolvedValue({ id: 'ledger-1', title: 'P1', status: 'probation' });
|
|
1266
|
+
});
|
|
1267
|
+
|
|
1268
|
+
it('DREAMER-01: principle candidate — dreamer task created via createTask', async () => {
|
|
1269
|
+
const candidates = [
|
|
1270
|
+
{ candidateId: 'cand-1', artifactId: 'art-1', taskId: 'test-task-1', status: 'pending', recommendationKind: 'principle' },
|
|
1271
|
+
];
|
|
1272
|
+
mockGetCandidatesByTaskId.mockResolvedValue(candidates);
|
|
1273
|
+
|
|
1274
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
1275
|
+
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
|
|
1276
|
+
|
|
1277
|
+
await handleDiagnoseRun({
|
|
1278
|
+
taskId: 'test-task-1',
|
|
1279
|
+
workspace: '/tmp/fake-workspace',
|
|
1280
|
+
runtime: 'test-double',
|
|
1281
|
+
json: true,
|
|
1282
|
+
} as DiagnoseRunOptions);
|
|
1283
|
+
|
|
1284
|
+
// Verify createTask was called for the dreamer task
|
|
1285
|
+
expect(mockCreateTask).toHaveBeenCalled();
|
|
1286
|
+
const createCall = mockCreateTask.mock.calls[0]?.[0];
|
|
1287
|
+
expect(createCall?.taskKind).toBe('dreamer');
|
|
1288
|
+
expect(createCall?.taskId).toBe('dreamer-cand-1');
|
|
1289
|
+
expect(createCall?.status).toBe('pending');
|
|
1290
|
+
|
|
1291
|
+
consoleSpy.mockRestore();
|
|
1292
|
+
exitSpy.mockRestore();
|
|
1293
|
+
});
|
|
1294
|
+
|
|
1295
|
+
it('DREAMER-02: defer candidate — no dreamer task created', async () => {
|
|
1296
|
+
const candidates = [
|
|
1297
|
+
{ candidateId: 'cand-defer', artifactId: 'art-1', taskId: 'test-task-1', status: 'pending', recommendationKind: 'defer' },
|
|
1298
|
+
];
|
|
1299
|
+
mockGetCandidatesByTaskId.mockResolvedValue(candidates);
|
|
1300
|
+
|
|
1301
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
1302
|
+
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
|
|
1303
|
+
|
|
1304
|
+
await handleDiagnoseRun({
|
|
1305
|
+
taskId: 'test-task-1',
|
|
1306
|
+
workspace: '/tmp/fake-workspace',
|
|
1307
|
+
runtime: 'test-double',
|
|
1308
|
+
json: true,
|
|
1309
|
+
} as DiagnoseRunOptions);
|
|
1310
|
+
|
|
1311
|
+
// No dreamer task should be created for defer candidates
|
|
1312
|
+
expect(mockCreateTask).not.toHaveBeenCalled();
|
|
1313
|
+
|
|
1314
|
+
consoleSpy.mockRestore();
|
|
1315
|
+
exitSpy.mockRestore();
|
|
1316
|
+
});
|
|
1317
|
+
|
|
1318
|
+
it('DREAMER-03: implementation candidate — no dreamer task created', async () => {
|
|
1319
|
+
const candidates = [
|
|
1320
|
+
{ candidateId: 'cand-impl', artifactId: 'art-1', taskId: 'test-task-1', status: 'pending', recommendationKind: 'implementation' },
|
|
1321
|
+
];
|
|
1322
|
+
mockGetCandidatesByTaskId.mockResolvedValue(candidates);
|
|
1323
|
+
|
|
1324
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
1325
|
+
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
|
|
1326
|
+
|
|
1327
|
+
await handleDiagnoseRun({
|
|
1328
|
+
taskId: 'test-task-1',
|
|
1329
|
+
workspace: '/tmp/fake-workspace',
|
|
1330
|
+
runtime: 'test-double',
|
|
1331
|
+
json: true,
|
|
1332
|
+
} as DiagnoseRunOptions);
|
|
1333
|
+
|
|
1334
|
+
// No dreamer task should be created for implementation candidates
|
|
1335
|
+
expect(mockCreateTask).not.toHaveBeenCalled();
|
|
1336
|
+
|
|
1337
|
+
consoleSpy.mockRestore();
|
|
1338
|
+
exitSpy.mockRestore();
|
|
1339
|
+
});
|
|
1340
|
+
|
|
1341
|
+
it('DREAMER-04: --no-intake skips dreamer seed', async () => {
|
|
1342
|
+
const candidates = [
|
|
1343
|
+
{ candidateId: 'cand-1', artifactId: 'art-1', taskId: 'test-task-1', status: 'pending', recommendationKind: 'principle' },
|
|
1344
|
+
];
|
|
1345
|
+
mockGetCandidatesByTaskId.mockResolvedValue(candidates);
|
|
1346
|
+
|
|
1347
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
1348
|
+
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
|
|
1349
|
+
|
|
1350
|
+
await handleDiagnoseRun({
|
|
1351
|
+
taskId: 'test-task-1',
|
|
1352
|
+
workspace: '/tmp/fake-workspace',
|
|
1353
|
+
runtime: 'test-double',
|
|
1354
|
+
json: true,
|
|
1355
|
+
intake: false,
|
|
1356
|
+
} as DiagnoseRunOptions);
|
|
1357
|
+
|
|
1358
|
+
// No dreamer task should be created when intake is disabled
|
|
1359
|
+
expect(mockCreateTask).not.toHaveBeenCalled();
|
|
1360
|
+
|
|
1361
|
+
consoleSpy.mockRestore();
|
|
1362
|
+
exitSpy.mockRestore();
|
|
1363
|
+
});
|
|
1364
|
+
|
|
1365
|
+
it('DREAMER-05: dreamer seed failure surfaces in intake results but does not fail the command', async () => {
|
|
1366
|
+
const candidates = [
|
|
1367
|
+
{ candidateId: 'cand-1', artifactId: 'art-1', taskId: 'test-task-1', status: 'pending', recommendationKind: 'principle' },
|
|
1368
|
+
];
|
|
1369
|
+
mockGetCandidatesByTaskId.mockResolvedValue(candidates);
|
|
1370
|
+
// Make createTask throw to simulate seed failure
|
|
1371
|
+
mockCreateTask.mockRejectedValueOnce(new Error('DB write failed'));
|
|
1372
|
+
|
|
1373
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
1374
|
+
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
|
|
1375
|
+
|
|
1376
|
+
await handleDiagnoseRun({
|
|
1377
|
+
taskId: 'test-task-1',
|
|
1378
|
+
workspace: '/tmp/fake-workspace',
|
|
1379
|
+
runtime: 'test-double',
|
|
1380
|
+
json: true,
|
|
1381
|
+
} as DiagnoseRunOptions);
|
|
1382
|
+
|
|
1383
|
+
// Command should still succeed (exit 0) — dreamer seed failure is best-effort
|
|
1384
|
+
expect(exitSpy).not.toHaveBeenCalledWith(1);
|
|
1385
|
+
|
|
1386
|
+
// JSON output should include dreamer_seed_failed status
|
|
1387
|
+
const jsonOutput = consoleSpy.mock.calls.find(call => {
|
|
1388
|
+
try {
|
|
1389
|
+
const parsed = JSON.parse(call[0] as string);
|
|
1390
|
+
return parsed.intake && Array.isArray(parsed.intake.candidates);
|
|
1391
|
+
} catch { return false; }
|
|
1392
|
+
});
|
|
1393
|
+
expect(jsonOutput).toBeDefined();
|
|
1394
|
+
const parsed = JSON.parse((jsonOutput as [string])[0]);
|
|
1395
|
+
const seedResult = parsed.intake.candidates.find((c: { candidateId: string; status: string }) => c.candidateId === 'cand-1' && c.status === 'dreamer_seed_failed');
|
|
1396
|
+
expect(seedResult).toBeDefined();
|
|
1397
|
+
expect(seedResult.error).toContain('DB write failed');
|
|
1398
|
+
|
|
1399
|
+
consoleSpy.mockRestore();
|
|
1400
|
+
exitSpy.mockRestore();
|
|
1401
|
+
});
|
|
1402
|
+
});
|
|
1403
|
+
|
|
1404
|
+
// DEFECT-005 (PRI-514): A single defer candidate's intake_failed must NOT poison
|
|
1405
|
+
// the dreamer seed loop for other successfully-consumed candidates. Previously
|
|
1406
|
+
// `intakeFailed` was a global flag gating the ENTIRE dreamer seed loop — one
|
|
1407
|
+
// defer candidate (admission gate deferred) set it true, skipping dreamer seed
|
|
1408
|
+
// for ALL candidates including ones that succeeded intake (EP-03 / ERR-089
|
|
1409
|
+
// sibling-branch defect: one branch's failure punished all sibling branches).
|
|
1410
|
+
describe('DEFECT-005 (PRI-514): defer intake_failed must not poison other candidates dreamer seed', () => {
|
|
1411
|
+
beforeEach(async () => {
|
|
1412
|
+
vi.clearAllMocks();
|
|
1413
|
+
const { run, evaluateCandidateAdmissionFromRecord } = await import('@principles/core/runtime-v2');
|
|
1414
|
+
vi.mocked(run).mockResolvedValue(SUCCEEDED_RESULT);
|
|
1415
|
+
mockGetCandidatesByTaskId.mockResolvedValue([]);
|
|
1416
|
+
mockUpdateCandidateStatus.mockResolvedValue(undefined);
|
|
1417
|
+
mockCreateTask.mockResolvedValue(undefined);
|
|
1418
|
+
mockGetTask.mockResolvedValue(null);
|
|
1419
|
+
mockIntake.mockReset();
|
|
1420
|
+
mockIntake.mockResolvedValue({ id: 'ledger-1', title: 'P1', status: 'probation' });
|
|
1421
|
+
// Default: admit all candidates. Individual tests override for defer.
|
|
1422
|
+
vi.mocked(evaluateCandidateAdmissionFromRecord).mockReturnValue({
|
|
1423
|
+
decision: 'admitted',
|
|
1424
|
+
reason: 'mock_admitted',
|
|
1425
|
+
nextAction: 'none',
|
|
1426
|
+
evidenceStatus: 'unknown',
|
|
1427
|
+
});
|
|
1428
|
+
});
|
|
1429
|
+
|
|
1430
|
+
// Restore the factory-default admission decision so later describe blocks
|
|
1431
|
+
// (which do `vi.clearAllMocks()` but do not re-set the implementation) see
|
|
1432
|
+
// `admitted` again. Without this, a `mockReturnValue({deferred})` from
|
|
1433
|
+
// DEFECT-005-B would leak into BUG-2 and refuse all its candidates.
|
|
1434
|
+
afterEach(async () => {
|
|
1435
|
+
const { evaluateCandidateAdmissionFromRecord } = await import('@principles/core/runtime-v2');
|
|
1436
|
+
vi.mocked(evaluateCandidateAdmissionFromRecord).mockReturnValue({
|
|
1437
|
+
decision: 'admitted',
|
|
1438
|
+
reason: 'mock_admitted',
|
|
1439
|
+
nextAction: 'none',
|
|
1440
|
+
evidenceStatus: 'unknown',
|
|
1441
|
+
});
|
|
1442
|
+
});
|
|
1443
|
+
|
|
1444
|
+
it('DEFECT-005-A: defer deferred + principle/rule/prompt consumed — principle/rule/prompt still dreamer_seeded', async () => {
|
|
1445
|
+
// Admission gate defers ONLY defer candidates; admits everything else.
|
|
1446
|
+
const { evaluateCandidateAdmissionFromRecord } = await import('@principles/core/runtime-v2');
|
|
1447
|
+
vi.mocked(evaluateCandidateAdmissionFromRecord).mockImplementation((record) => {
|
|
1448
|
+
if (record.recommendationKind === 'defer') {
|
|
1449
|
+
return {
|
|
1450
|
+
decision: 'deferred',
|
|
1451
|
+
reason: 'recommendation_kind_defer_not_actionable',
|
|
1452
|
+
nextAction: 'review_defer_disposition_manually',
|
|
1453
|
+
evidenceStatus: 'unknown',
|
|
1454
|
+
};
|
|
1455
|
+
}
|
|
1456
|
+
return {
|
|
1457
|
+
decision: 'admitted',
|
|
1458
|
+
reason: 'mock_admitted',
|
|
1459
|
+
nextAction: 'none',
|
|
1460
|
+
evidenceStatus: 'unknown',
|
|
1461
|
+
};
|
|
1462
|
+
});
|
|
1463
|
+
|
|
1464
|
+
const candidates = [
|
|
1465
|
+
{ candidateId: 'cand-defer', artifactId: 'art-d', taskId: 'test-task-1', status: 'pending', recommendationKind: 'defer', confidence: 0.9 },
|
|
1466
|
+
{ candidateId: 'cand-principle', artifactId: 'art-p', taskId: 'test-task-1', status: 'pending', recommendationKind: 'principle', confidence: 0.9 },
|
|
1467
|
+
{ candidateId: 'cand-rule', artifactId: 'art-r', taskId: 'test-task-1', status: 'pending', recommendationKind: 'rule', confidence: 0.9 },
|
|
1468
|
+
{ candidateId: 'cand-prompt', artifactId: 'art-pp', taskId: 'test-task-1', status: 'pending', recommendationKind: 'prompt', confidence: 0.9 },
|
|
1469
|
+
{ candidateId: 'cand-impl', artifactId: 'art-i', taskId: 'test-task-1', status: 'pending', recommendationKind: 'implementation', confidence: 0.9 },
|
|
1470
|
+
];
|
|
1471
|
+
mockGetCandidatesByTaskId.mockResolvedValue(candidates);
|
|
1472
|
+
|
|
1473
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
1474
|
+
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
|
|
1475
|
+
|
|
1476
|
+
await handleDiagnoseRun({
|
|
1477
|
+
taskId: 'test-task-1',
|
|
1478
|
+
workspace: '/tmp/fake-workspace',
|
|
1479
|
+
runtime: 'test-double',
|
|
1480
|
+
json: true,
|
|
1481
|
+
} as DiagnoseRunOptions);
|
|
1482
|
+
|
|
1483
|
+
const jsonOutput = consoleSpy.mock.calls.find(call => {
|
|
1484
|
+
try {
|
|
1485
|
+
const parsed = JSON.parse(call[0] as string);
|
|
1486
|
+
return parsed.intake && Array.isArray(parsed.intake.candidates);
|
|
1487
|
+
} catch { return false; }
|
|
1488
|
+
});
|
|
1489
|
+
expect(jsonOutput).toBeDefined();
|
|
1490
|
+
const parsed = JSON.parse((jsonOutput as [string])[0]);
|
|
1491
|
+
const results = parsed.intake.candidates as Array<{ candidateId: string; status: string }>;
|
|
1492
|
+
|
|
1493
|
+
// defer candidate: intake_failed (admission gate deferred) — correct.
|
|
1494
|
+
// intakeResults may carry multiple entries per candidate (intake + seed),
|
|
1495
|
+
// so we match on BOTH candidateId and status (same pattern as DREAMER-05).
|
|
1496
|
+
const deferFailed = results.find(r => r.candidateId === 'cand-defer' && r.status === 'intake_failed');
|
|
1497
|
+
expect(deferFailed).toBeDefined();
|
|
1498
|
+
|
|
1499
|
+
// principle/rule/prompt: dreamer_seeded — MUST NOT be poisoned by defer's intake_failed
|
|
1500
|
+
const principleSeeded = results.find(r => r.candidateId === 'cand-principle' && r.status === 'dreamer_seeded');
|
|
1501
|
+
expect(principleSeeded).toBeDefined();
|
|
1502
|
+
|
|
1503
|
+
const ruleSeeded = results.find(r => r.candidateId === 'cand-rule' && r.status === 'dreamer_seeded');
|
|
1504
|
+
expect(ruleSeeded).toBeDefined();
|
|
1505
|
+
|
|
1506
|
+
const promptSeeded = results.find(r => r.candidateId === 'cand-prompt' && r.status === 'dreamer_seeded');
|
|
1507
|
+
expect(promptSeeded).toBeDefined();
|
|
1508
|
+
|
|
1509
|
+
// 3 dreamer tasks created (principle + rule + prompt).
|
|
1510
|
+
// defer was refused at the gate (no intake, no seed).
|
|
1511
|
+
// implementation is skipped by the dreamer seed loop (`if kind === 'implementation' continue`).
|
|
1512
|
+
expect(mockCreateTask).toHaveBeenCalledTimes(3);
|
|
1513
|
+
|
|
1514
|
+
consoleSpy.mockRestore();
|
|
1515
|
+
exitSpy.mockRestore();
|
|
1516
|
+
});
|
|
1517
|
+
|
|
1518
|
+
it('DEFECT-005-B: all candidates intake_failed — 0 dreamer_seeded (boundary)', async () => {
|
|
1519
|
+
// Refuse everything via admission gate
|
|
1520
|
+
const { evaluateCandidateAdmissionFromRecord } = await import('@principles/core/runtime-v2');
|
|
1521
|
+
vi.mocked(evaluateCandidateAdmissionFromRecord).mockReturnValue({
|
|
1522
|
+
decision: 'deferred',
|
|
1523
|
+
reason: 'mock_deferred',
|
|
1524
|
+
nextAction: 'none',
|
|
1525
|
+
evidenceStatus: 'unknown',
|
|
1526
|
+
});
|
|
1527
|
+
|
|
1528
|
+
const candidates = [
|
|
1529
|
+
{ candidateId: 'cand-1', artifactId: 'art-1', taskId: 'test-task-1', status: 'pending', recommendationKind: 'principle', confidence: 0.9 },
|
|
1530
|
+
{ candidateId: 'cand-2', artifactId: 'art-2', taskId: 'test-task-1', status: 'pending', recommendationKind: 'rule', confidence: 0.9 },
|
|
1531
|
+
];
|
|
1532
|
+
mockGetCandidatesByTaskId.mockResolvedValue(candidates);
|
|
1533
|
+
|
|
1534
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
1535
|
+
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
|
|
1536
|
+
|
|
1537
|
+
await handleDiagnoseRun({
|
|
1538
|
+
taskId: 'test-task-1',
|
|
1539
|
+
workspace: '/tmp/fake-workspace',
|
|
1540
|
+
runtime: 'test-double',
|
|
1541
|
+
json: true,
|
|
1542
|
+
} as DiagnoseRunOptions);
|
|
1543
|
+
|
|
1544
|
+
// No dreamer tasks should be created when no candidate was consumed
|
|
1545
|
+
expect(mockCreateTask).not.toHaveBeenCalled();
|
|
1546
|
+
|
|
1547
|
+
const jsonOutput = consoleSpy.mock.calls.find(call => {
|
|
1548
|
+
try {
|
|
1549
|
+
const parsed = JSON.parse(call[0] as string);
|
|
1550
|
+
return parsed.intake && Array.isArray(parsed.intake.candidates);
|
|
1551
|
+
} catch { return false; }
|
|
1552
|
+
});
|
|
1553
|
+
expect(jsonOutput).toBeDefined();
|
|
1554
|
+
const parsed = JSON.parse((jsonOutput as [string])[0]);
|
|
1555
|
+
const results = parsed.intake.candidates as Array<{ candidateId: string; status: string }>;
|
|
1556
|
+
expect(results.every(r => r.status === 'intake_failed')).toBe(true);
|
|
1557
|
+
|
|
1558
|
+
consoleSpy.mockRestore();
|
|
1559
|
+
exitSpy.mockRestore();
|
|
1560
|
+
});
|
|
1561
|
+
});
|
|
1562
|
+
|
|
1563
|
+
// BUG-1 (PRI-442): effectiveConfig must be passed to split-pipeline runners
|
|
1564
|
+
// so that ADR-0019 LLM rate-limit degradation (isDegradationEnabled) can fire.
|
|
1565
|
+
// Without this wiring, isDegradationEnabled() always returns false because
|
|
1566
|
+
// this.config.effectiveConfig is undefined. EP-02: production path wiring.
|
|
1567
|
+
describe('BUG-1 (PRI-442): effectiveConfig wiring to split-pipeline runners', () => {
|
|
1568
|
+
beforeEach(async () => {
|
|
1569
|
+
vi.clearAllMocks();
|
|
1570
|
+
const { run } = await import('@principles/core/runtime-v2');
|
|
1571
|
+
vi.mocked(run).mockResolvedValue(SUCCEEDED_RESULT);
|
|
1572
|
+
mockGetCandidatesByTaskId.mockResolvedValue([]);
|
|
1573
|
+
mockUpdateCandidateStatus.mockResolvedValue(undefined);
|
|
1574
|
+
mockIntake.mockReset();
|
|
1575
|
+
});
|
|
1576
|
+
|
|
1577
|
+
it('passes effectiveConfig to DiagRootCauseRunner when split pipeline is enabled', async () => {
|
|
1578
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
1579
|
+
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
|
|
1580
|
+
|
|
1581
|
+
await handleDiagnoseRun({
|
|
1582
|
+
taskId: 'test-task-1',
|
|
1583
|
+
workspace: '/tmp/fake-workspace',
|
|
1584
|
+
runtime: 'test-double',
|
|
1585
|
+
json: true,
|
|
1586
|
+
} as DiagnoseRunOptions);
|
|
1587
|
+
|
|
1588
|
+
expect(diagRootCauseRunnerCtor).toHaveBeenCalled();
|
|
1589
|
+
const optionsArg = diagRootCauseRunnerCtor.mock.calls[0]?.[1];
|
|
1590
|
+
expect(optionsArg?.effectiveConfig).toBeDefined();
|
|
1591
|
+
expect(optionsArg?.effectiveConfig).toEqual(
|
|
1592
|
+
expect.objectContaining({ config: expect.anything(), source: 'file' }),
|
|
1593
|
+
);
|
|
1594
|
+
|
|
1595
|
+
consoleSpy.mockRestore();
|
|
1596
|
+
exitSpy.mockRestore();
|
|
1597
|
+
});
|
|
1598
|
+
|
|
1599
|
+
it('passes effectiveConfig to DiagDistillerRunner and DiagRouterRunner', async () => {
|
|
1600
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
1601
|
+
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
|
|
1602
|
+
|
|
1603
|
+
await handleDiagnoseRun({
|
|
1604
|
+
taskId: 'test-task-1',
|
|
1605
|
+
workspace: '/tmp/fake-workspace',
|
|
1606
|
+
runtime: 'test-double',
|
|
1607
|
+
json: true,
|
|
1608
|
+
} as DiagnoseRunOptions);
|
|
1609
|
+
|
|
1610
|
+
const distillerOptions = diagDistillerRunnerCtor.mock.calls[0]?.[1];
|
|
1611
|
+
expect(distillerOptions?.effectiveConfig).toBeDefined();
|
|
1612
|
+
|
|
1613
|
+
const routerOptions = diagRouterRunnerCtor.mock.calls[0]?.[1];
|
|
1614
|
+
expect(routerOptions?.effectiveConfig).toBeDefined();
|
|
1615
|
+
|
|
1616
|
+
consoleSpy.mockRestore();
|
|
1617
|
+
exitSpy.mockRestore();
|
|
1618
|
+
});
|
|
1619
|
+
|
|
1620
|
+
// CR-2 (CodeRabbit P2): loadPdConfig mock only covered ok:true. The
|
|
1621
|
+
// ok:false fallback branch (effectiveConfig = defaults) was untested.
|
|
1622
|
+
// This test locks in the fallback behavior + asserts rc-9 observability.
|
|
1623
|
+
it('warns and falls back to defaults when configLoadResult.ok is false', async () => {
|
|
1624
|
+
// Override the module-level mock for this test only
|
|
1625
|
+
const { loadPdConfig } = await import('../../src/services/pd-config-loader.js');
|
|
1626
|
+
vi.mocked(loadPdConfig).mockReturnValueOnce({
|
|
1627
|
+
ok: false,
|
|
1628
|
+
source: 'malformed',
|
|
1629
|
+
configPath: '/tmp/fake-workspace/.pd/config.yaml',
|
|
1630
|
+
errors: [
|
|
1631
|
+
{ path: 'featureFlags.diagnostician_llm_degradation', reason: 'expected boolean, got string', nextAction: 'Fix the value to be a boolean' },
|
|
1632
|
+
],
|
|
1633
|
+
defaults: { config: {}, source: 'defaults', warnings: [] },
|
|
1634
|
+
warnings: [],
|
|
1635
|
+
legacyFilesDetected: [],
|
|
1636
|
+
legacyFileNextActions: [],
|
|
1637
|
+
});
|
|
1638
|
+
|
|
1639
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
1640
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
1641
|
+
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
|
|
1642
|
+
|
|
1643
|
+
await handleDiagnoseRun({
|
|
1644
|
+
taskId: 'test-task-1',
|
|
1645
|
+
workspace: '/tmp/fake-workspace',
|
|
1646
|
+
runtime: 'test-double',
|
|
1647
|
+
json: true,
|
|
1648
|
+
} as DiagnoseRunOptions);
|
|
1649
|
+
|
|
1650
|
+
// Assert warning was emitted (rc-9: no silent fallback)
|
|
1651
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
1652
|
+
expect.stringContaining('[pd diagnose]'),
|
|
1653
|
+
);
|
|
1654
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
1655
|
+
expect.stringContaining('malformed'),
|
|
1656
|
+
);
|
|
1657
|
+
|
|
1658
|
+
// Assert runners still received effectiveConfig (the defaults-based one)
|
|
1659
|
+
expect(diagRootCauseRunnerCtor).toHaveBeenCalled();
|
|
1660
|
+
const optionsArg = diagRootCauseRunnerCtor.mock.calls[0]?.[1];
|
|
1661
|
+
expect(optionsArg?.effectiveConfig).toBeDefined();
|
|
1662
|
+
expect(optionsArg?.effectiveConfig).toEqual(
|
|
1663
|
+
expect.objectContaining({ source: 'defaults' }),
|
|
1664
|
+
);
|
|
1665
|
+
|
|
1666
|
+
warnSpy.mockRestore();
|
|
1667
|
+
consoleSpy.mockRestore();
|
|
1668
|
+
exitSpy.mockRestore();
|
|
1669
|
+
});
|
|
1670
|
+
});
|
|
1671
|
+
|
|
1672
|
+
// BUG-2 (PRI-442): sourcePainId must be resolved from the diagnostician task
|
|
1673
|
+
// chain and passed to buildDreamerSeedFromCandidate. Without this, the dreamer
|
|
1674
|
+
// task's diagnosticJson omits sourcePainId, breaking run-rulehost lineage.
|
|
1675
|
+
// rc-6: lineage consistency. ERR-004: never invent lineage.
|
|
1676
|
+
describe('BUG-2 (PRI-442): sourcePainId resolution for dreamer seed', () => {
|
|
1677
|
+
beforeEach(async () => {
|
|
1678
|
+
vi.clearAllMocks();
|
|
1679
|
+
buildDreamerSeedCalls.length = 0;
|
|
1680
|
+
const { run } = await import('@principles/core/runtime-v2');
|
|
1681
|
+
vi.mocked(run).mockResolvedValue(SUCCEEDED_RESULT);
|
|
1682
|
+
mockGetCandidatesByTaskId.mockResolvedValue([]);
|
|
1683
|
+
mockUpdateCandidateStatus.mockResolvedValue(undefined);
|
|
1684
|
+
mockCreateTask.mockResolvedValue(undefined);
|
|
1685
|
+
mockGetTask.mockResolvedValue(null);
|
|
1686
|
+
mockIntake.mockReset();
|
|
1687
|
+
mockIntake.mockResolvedValue({ id: 'ledger-1', title: 'P1', status: 'probation' });
|
|
1688
|
+
mockResolveSourcePainId.mockResolvedValue('pain_test-source-1');
|
|
1689
|
+
});
|
|
1690
|
+
|
|
1691
|
+
it('passes resolved sourcePainId to buildDreamerSeedFromCandidate (lineage consistency)', async () => {
|
|
1692
|
+
const candidates = [
|
|
1693
|
+
{ candidateId: 'cand-1', artifactId: 'art-1', taskId: 'test-task-1', status: 'pending', recommendationKind: 'principle' },
|
|
1694
|
+
];
|
|
1695
|
+
mockGetCandidatesByTaskId.mockResolvedValue(candidates);
|
|
1696
|
+
|
|
1697
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
1698
|
+
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
|
|
1699
|
+
|
|
1700
|
+
await handleDiagnoseRun({
|
|
1701
|
+
taskId: 'test-task-1',
|
|
1702
|
+
workspace: '/tmp/fake-workspace',
|
|
1703
|
+
runtime: 'test-double',
|
|
1704
|
+
json: true,
|
|
1705
|
+
} as DiagnoseRunOptions);
|
|
1706
|
+
|
|
1707
|
+
// resolveSourcePainIdFromDiagnostician should have been called
|
|
1708
|
+
expect(mockResolveSourcePainId).toHaveBeenCalled();
|
|
1709
|
+
// buildDreamerSeedFromCandidate should have received the resolved sourcePainId
|
|
1710
|
+
expect(buildDreamerSeedCalls.length).toBeGreaterThan(0);
|
|
1711
|
+
expect(buildDreamerSeedCalls[0].sourcePainId).toBe('pain_test-source-1');
|
|
1712
|
+
|
|
1713
|
+
consoleSpy.mockRestore();
|
|
1714
|
+
exitSpy.mockRestore();
|
|
1715
|
+
});
|
|
1716
|
+
});
|