@principles/pd-cli 1.73.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/README.md +90 -0
- package/dist/commands/artifact.d.ts +14 -0
- package/dist/commands/artifact.d.ts.map +1 -0
- package/dist/commands/artifact.js +67 -0
- package/dist/commands/artifact.js.map +1 -0
- package/dist/commands/candidate.d.ts +83 -0
- package/dist/commands/candidate.d.ts.map +1 -0
- package/dist/commands/candidate.js +891 -0
- package/dist/commands/candidate.js.map +1 -0
- package/dist/commands/central-sync.d.ts +10 -0
- package/dist/commands/central-sync.d.ts.map +1 -0
- package/dist/commands/central-sync.js +32 -0
- package/dist/commands/central-sync.js.map +1 -0
- package/dist/commands/console.d.ts +9 -0
- package/dist/commands/console.d.ts.map +1 -0
- package/dist/commands/console.js +114 -0
- package/dist/commands/console.js.map +1 -0
- package/dist/commands/context.d.ts +7 -0
- package/dist/commands/context.d.ts.map +1 -0
- package/dist/commands/context.js +55 -0
- package/dist/commands/context.js.map +1 -0
- package/dist/commands/demo-story-a.d.ts +12 -0
- package/dist/commands/demo-story-a.d.ts.map +1 -0
- package/dist/commands/demo-story-a.js +175 -0
- package/dist/commands/demo-story-a.js.map +1 -0
- package/dist/commands/diagnose.d.ts +35 -0
- package/dist/commands/diagnose.d.ts.map +1 -0
- package/dist/commands/diagnose.js +390 -0
- package/dist/commands/diagnose.js.map +1 -0
- package/dist/commands/evolution-tasks-list.d.ts +15 -0
- package/dist/commands/evolution-tasks-list.d.ts.map +1 -0
- package/dist/commands/evolution-tasks-list.js +34 -0
- package/dist/commands/evolution-tasks-list.js.map +1 -0
- package/dist/commands/evolution-tasks-show.d.ts +14 -0
- package/dist/commands/evolution-tasks-show.d.ts.map +1 -0
- package/dist/commands/evolution-tasks-show.js +52 -0
- package/dist/commands/evolution-tasks-show.js.map +1 -0
- package/dist/commands/flow.d.ts +7 -0
- package/dist/commands/flow.d.ts.map +1 -0
- package/dist/commands/flow.js +57 -0
- package/dist/commands/flow.js.map +1 -0
- package/dist/commands/health.d.ts +16 -0
- package/dist/commands/health.d.ts.map +1 -0
- package/dist/commands/health.js +150 -0
- package/dist/commands/health.js.map +1 -0
- package/dist/commands/history.d.ts +11 -0
- package/dist/commands/history.d.ts.map +1 -0
- package/dist/commands/history.js +50 -0
- package/dist/commands/history.js.map +1 -0
- package/dist/commands/legacy-cleanup.d.ts +27 -0
- package/dist/commands/legacy-cleanup.d.ts.map +1 -0
- package/dist/commands/legacy-cleanup.js +171 -0
- package/dist/commands/legacy-cleanup.js.map +1 -0
- package/dist/commands/legacy-import.d.ts +7 -0
- package/dist/commands/legacy-import.d.ts.map +1 -0
- package/dist/commands/legacy-import.js +86 -0
- package/dist/commands/legacy-import.js.map +1 -0
- package/dist/commands/pain-record.d.ts +10 -0
- package/dist/commands/pain-record.d.ts.map +1 -0
- package/dist/commands/pain-record.js +162 -0
- package/dist/commands/pain-record.js.map +1 -0
- package/dist/commands/proven-channel-baseline.d.ts +12 -0
- package/dist/commands/proven-channel-baseline.d.ts.map +1 -0
- package/dist/commands/proven-channel-baseline.js +97 -0
- package/dist/commands/proven-channel-baseline.js.map +1 -0
- package/dist/commands/remediation-output.d.ts +40 -0
- package/dist/commands/remediation-output.d.ts.map +1 -0
- package/dist/commands/remediation-output.js +23 -0
- package/dist/commands/remediation-output.js.map +1 -0
- package/dist/commands/run.d.ts +10 -0
- package/dist/commands/run.d.ts.map +1 -0
- package/dist/commands/run.js +68 -0
- package/dist/commands/run.js.map +1 -0
- package/dist/commands/runtime-activation.d.ts +11 -0
- package/dist/commands/runtime-activation.d.ts.map +1 -0
- package/dist/commands/runtime-activation.js +150 -0
- package/dist/commands/runtime-activation.js.map +1 -0
- package/dist/commands/runtime-canary.d.ts +30 -0
- package/dist/commands/runtime-canary.d.ts.map +1 -0
- package/dist/commands/runtime-canary.js +343 -0
- package/dist/commands/runtime-canary.js.map +1 -0
- package/dist/commands/runtime-diagnostics-export.d.ts +20 -0
- package/dist/commands/runtime-diagnostics-export.d.ts.map +1 -0
- package/dist/commands/runtime-diagnostics-export.js +177 -0
- package/dist/commands/runtime-diagnostics-export.js.map +1 -0
- package/dist/commands/runtime-features.d.ts +26 -0
- package/dist/commands/runtime-features.d.ts.map +1 -0
- package/dist/commands/runtime-features.js +70 -0
- package/dist/commands/runtime-features.js.map +1 -0
- package/dist/commands/runtime-gfi-snapshot.d.ts +7 -0
- package/dist/commands/runtime-gfi-snapshot.d.ts.map +1 -0
- package/dist/commands/runtime-gfi-snapshot.js +101 -0
- package/dist/commands/runtime-gfi-snapshot.js.map +1 -0
- package/dist/commands/runtime-health-snapshot.d.ts +7 -0
- package/dist/commands/runtime-health-snapshot.d.ts.map +1 -0
- package/dist/commands/runtime-health-snapshot.js +93 -0
- package/dist/commands/runtime-health-snapshot.js.map +1 -0
- package/dist/commands/runtime-idle-trigger.d.ts +12 -0
- package/dist/commands/runtime-idle-trigger.d.ts.map +1 -0
- package/dist/commands/runtime-idle-trigger.js +102 -0
- package/dist/commands/runtime-idle-trigger.js.map +1 -0
- package/dist/commands/runtime-internalization-enqueue-successors.d.ts +9 -0
- package/dist/commands/runtime-internalization-enqueue-successors.d.ts.map +1 -0
- package/dist/commands/runtime-internalization-enqueue-successors.js +393 -0
- package/dist/commands/runtime-internalization-enqueue-successors.js.map +1 -0
- package/dist/commands/runtime-internalization-integrity-repair.d.ts +9 -0
- package/dist/commands/runtime-internalization-integrity-repair.d.ts.map +1 -0
- package/dist/commands/runtime-internalization-integrity-repair.js +54 -0
- package/dist/commands/runtime-internalization-integrity-repair.js.map +1 -0
- package/dist/commands/runtime-internalization-integrity.d.ts +7 -0
- package/dist/commands/runtime-internalization-integrity.d.ts.map +1 -0
- package/dist/commands/runtime-internalization-integrity.js +53 -0
- package/dist/commands/runtime-internalization-integrity.js.map +1 -0
- package/dist/commands/runtime-internalization-queue.d.ts +7 -0
- package/dist/commands/runtime-internalization-queue.d.ts.map +1 -0
- package/dist/commands/runtime-internalization-queue.js +85 -0
- package/dist/commands/runtime-internalization-queue.js.map +1 -0
- package/dist/commands/runtime-internalization-run-once.d.ts +12 -0
- package/dist/commands/runtime-internalization-run-once.d.ts.map +1 -0
- package/dist/commands/runtime-internalization-run-once.js +546 -0
- package/dist/commands/runtime-internalization-run-once.js.map +1 -0
- package/dist/commands/runtime-internalization-wake-once.d.ts +8 -0
- package/dist/commands/runtime-internalization-wake-once.d.ts.map +1 -0
- package/dist/commands/runtime-internalization-wake-once.js +72 -0
- package/dist/commands/runtime-internalization-wake-once.js.map +1 -0
- package/dist/commands/runtime-pain-flood-simulation.d.ts +10 -0
- package/dist/commands/runtime-pain-flood-simulation.d.ts.map +1 -0
- package/dist/commands/runtime-pain-flood-simulation.js +104 -0
- package/dist/commands/runtime-pain-flood-simulation.js.map +1 -0
- package/dist/commands/runtime-pruning.d.ts +45 -0
- package/dist/commands/runtime-pruning.d.ts.map +1 -0
- package/dist/commands/runtime-pruning.js +355 -0
- package/dist/commands/runtime-pruning.js.map +1 -0
- package/dist/commands/runtime-recovery.d.ts +9 -0
- package/dist/commands/runtime-recovery.d.ts.map +1 -0
- package/dist/commands/runtime-recovery.js +94 -0
- package/dist/commands/runtime-recovery.js.map +1 -0
- package/dist/commands/runtime-synthetic-baseline.d.ts +7 -0
- package/dist/commands/runtime-synthetic-baseline.d.ts.map +1 -0
- package/dist/commands/runtime-synthetic-baseline.js +59 -0
- package/dist/commands/runtime-synthetic-baseline.js.map +1 -0
- package/dist/commands/runtime-uat.d.ts +52 -0
- package/dist/commands/runtime-uat.d.ts.map +1 -0
- package/dist/commands/runtime-uat.js +274 -0
- package/dist/commands/runtime-uat.js.map +1 -0
- package/dist/commands/runtime.d.ts +20 -0
- package/dist/commands/runtime.d.ts.map +1 -0
- package/dist/commands/runtime.js +256 -0
- package/dist/commands/runtime.js.map +1 -0
- package/dist/commands/samples-list.d.ts +11 -0
- package/dist/commands/samples-list.d.ts.map +1 -0
- package/dist/commands/samples-list.js +37 -0
- package/dist/commands/samples-list.js.map +1 -0
- package/dist/commands/samples-review.d.ts +14 -0
- package/dist/commands/samples-review.d.ts.map +1 -0
- package/dist/commands/samples-review.js +22 -0
- package/dist/commands/samples-review.js.map +1 -0
- package/dist/commands/task.d.ts +14 -0
- package/dist/commands/task.d.ts.map +1 -0
- package/dist/commands/task.js +92 -0
- package/dist/commands/task.js.map +1 -0
- package/dist/commands/trace.d.ts +19 -0
- package/dist/commands/trace.d.ts.map +1 -0
- package/dist/commands/trace.js +154 -0
- package/dist/commands/trace.js.map +1 -0
- package/dist/commands/trajectory.d.ts +11 -0
- package/dist/commands/trajectory.d.ts.map +1 -0
- package/dist/commands/trajectory.js +47 -0
- package/dist/commands/trajectory.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +736 -0
- package/dist/index.js.map +1 -0
- package/dist/legacy/legacy-import.d.ts +15 -0
- package/dist/legacy/legacy-import.d.ts.map +1 -0
- package/dist/legacy/legacy-import.js +141 -0
- package/dist/legacy/legacy-import.js.map +1 -0
- package/dist/legacy/session-history-import.d.ts +26 -0
- package/dist/legacy/session-history-import.d.ts.map +1 -0
- package/dist/legacy/session-history-import.js +151 -0
- package/dist/legacy/session-history-import.js.map +1 -0
- package/dist/principle-tree-ledger-adapter.d.ts +12 -0
- package/dist/principle-tree-ledger-adapter.d.ts.map +1 -0
- package/dist/principle-tree-ledger-adapter.js +12 -0
- package/dist/principle-tree-ledger-adapter.js.map +1 -0
- package/dist/resolve-workspace.d.ts +12 -0
- package/dist/resolve-workspace.d.ts.map +1 -0
- package/dist/resolve-workspace.js +20 -0
- package/dist/resolve-workspace.js.map +1 -0
- package/dist/services/demo-story-a-runner.d.ts +8 -0
- package/dist/services/demo-story-a-runner.d.ts.map +1 -0
- package/dist/services/demo-story-a-runner.js +369 -0
- package/dist/services/demo-story-a-runner.js.map +1 -0
- package/dist/services/feature-flag-loader.d.ts +6 -0
- package/dist/services/feature-flag-loader.d.ts.map +1 -0
- package/dist/services/feature-flag-loader.js +54 -0
- package/dist/services/feature-flag-loader.js.map +1 -0
- package/dist/services/pain-flood-simulation-runner.d.ts +10 -0
- package/dist/services/pain-flood-simulation-runner.d.ts.map +1 -0
- package/dist/services/pain-flood-simulation-runner.js +289 -0
- package/dist/services/pain-flood-simulation-runner.js.map +1 -0
- package/dist/services/proven-channel-baseline-runner.d.ts +12 -0
- package/dist/services/proven-channel-baseline-runner.d.ts.map +1 -0
- package/dist/services/proven-channel-baseline-runner.js +114 -0
- package/dist/services/proven-channel-baseline-runner.js.map +1 -0
- package/dist/services/synthetic-baseline-runner.d.ts +8 -0
- package/dist/services/synthetic-baseline-runner.d.ts.map +1 -0
- package/dist/services/synthetic-baseline-runner.js +251 -0
- package/dist/services/synthetic-baseline-runner.js.map +1 -0
- package/package.json +35 -0
- package/src/commands/artifact.ts +82 -0
- package/src/commands/candidate.ts +1117 -0
- package/src/commands/central-sync.ts +44 -0
- package/src/commands/console.ts +121 -0
- package/src/commands/context.ts +72 -0
- package/src/commands/demo-story-a.ts +195 -0
- package/src/commands/diagnose.ts +452 -0
- package/src/commands/evolution-tasks-list.ts +44 -0
- package/src/commands/evolution-tasks-show.ts +60 -0
- package/src/commands/flow.ts +60 -0
- package/src/commands/health.ts +189 -0
- package/src/commands/history.ts +63 -0
- package/src/commands/legacy-cleanup.ts +206 -0
- package/src/commands/legacy-import.ts +104 -0
- package/src/commands/pain-record.ts +167 -0
- package/src/commands/proven-channel-baseline.ts +113 -0
- package/src/commands/remediation-output.ts +66 -0
- package/src/commands/run.ts +89 -0
- package/src/commands/runtime-activation.ts +176 -0
- package/src/commands/runtime-canary.ts +371 -0
- package/src/commands/runtime-diagnostics-export.ts +229 -0
- package/src/commands/runtime-features.ts +103 -0
- package/src/commands/runtime-gfi-snapshot.ts +135 -0
- package/src/commands/runtime-health-snapshot.ts +106 -0
- package/src/commands/runtime-internalization-enqueue-successors.ts +479 -0
- package/src/commands/runtime-internalization-integrity-repair.ts +69 -0
- package/src/commands/runtime-internalization-integrity.ts +63 -0
- package/src/commands/runtime-internalization-queue.ts +106 -0
- package/src/commands/runtime-internalization-run-once.ts +658 -0
- package/src/commands/runtime-internalization-wake-once.ts +87 -0
- package/src/commands/runtime-pain-flood-simulation.ts +121 -0
- package/src/commands/runtime-pruning.ts +438 -0
- package/src/commands/runtime-recovery.ts +107 -0
- package/src/commands/runtime-synthetic-baseline.ts +70 -0
- package/src/commands/runtime-uat.ts +339 -0
- package/src/commands/runtime.ts +281 -0
- package/src/commands/samples-list.ts +43 -0
- package/src/commands/samples-review.ts +32 -0
- package/src/commands/task.ts +130 -0
- package/src/commands/trace.ts +174 -0
- package/src/commands/trajectory.ts +64 -0
- package/src/index.ts +829 -0
- package/src/legacy/legacy-import.ts +179 -0
- package/src/legacy/session-history-import.ts +231 -0
- package/src/principle-tree-ledger-adapter.ts +13 -0
- package/src/resolve-workspace.ts +20 -0
- package/src/services/demo-story-a-runner.ts +472 -0
- package/src/services/feature-flag-loader.ts +73 -0
- package/src/services/pain-flood-simulation-runner.ts +354 -0
- package/src/services/proven-channel-baseline-runner.ts +150 -0
- package/src/services/synthetic-baseline-runner.ts +291 -0
- package/tests/commands/candidate-audit-repair.test.ts +338 -0
- package/tests/commands/candidate-intake.test.ts +589 -0
- package/tests/commands/candidate-internalization-backfill.test.ts +480 -0
- package/tests/commands/candidate-internalize.test.ts +272 -0
- package/tests/commands/candidate-route.test.ts +328 -0
- package/tests/commands/candidate-show.test.ts +95 -0
- package/tests/commands/cli-command-tree.test.ts +64 -0
- package/tests/commands/context.test.ts +114 -0
- package/tests/commands/demo-story-a.test.ts +255 -0
- package/tests/commands/diagnose.test.ts +792 -0
- package/tests/commands/health.test.ts +330 -0
- package/tests/commands/pain-record.test.ts +316 -0
- package/tests/commands/plugin-config-resolution-cutover.test.ts +220 -0
- package/tests/commands/proven-channel-baseline.test.ts +441 -0
- package/tests/commands/runtime-activation.test.ts +168 -0
- package/tests/commands/runtime-canary.test.ts +369 -0
- package/tests/commands/runtime-diagnostics-export.test.ts +170 -0
- package/tests/commands/runtime-features.test.ts +114 -0
- package/tests/commands/runtime-health-snapshot.test.ts +357 -0
- package/tests/commands/runtime-internalization-enqueue-successors.test.ts +803 -0
- package/tests/commands/runtime-internalization-integrity-repair.test.ts +169 -0
- package/tests/commands/runtime-internalization-integrity.test.ts +102 -0
- package/tests/commands/runtime-internalization-queue.test.ts +252 -0
- package/tests/commands/runtime-internalization-run-once.test.ts +1318 -0
- package/tests/commands/runtime-internalization-wake-once.test.ts +170 -0
- package/tests/commands/runtime-internalization.test.ts +52 -0
- package/tests/commands/runtime-pain-flood-simulation.test.ts +418 -0
- package/tests/commands/runtime-pruning.test.ts +693 -0
- package/tests/commands/runtime-recovery.test.ts +96 -0
- package/tests/commands/runtime-synthetic-baseline.test.ts +249 -0
- package/tests/commands/runtime-uat.test.ts +397 -0
- package/tests/commands/runtime.test.ts +262 -0
- package/tests/commands/trace.test.ts +314 -0
- package/tests/e2e/candidate-intake-e2e.test.ts +316 -0
- package/tests/services/feature-flag-loader.test.ts +207 -0
- package/tests/services/proven-channel-baseline-runner.test.ts +30 -0
- package/tsconfig.json +26 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { handleCandidateInternalize } from '../../src/commands/candidate.js';
|
|
3
|
+
|
|
4
|
+
const { mockStateManager, MockRuntimeStateManager } = vi.hoisted(() => {
|
|
5
|
+
const mockStateManager = {
|
|
6
|
+
initialize: vi.fn().mockResolvedValue(undefined),
|
|
7
|
+
getCandidate: vi.fn(),
|
|
8
|
+
getTask: vi.fn().mockResolvedValue(null),
|
|
9
|
+
createTask: vi.fn(),
|
|
10
|
+
close: vi.fn().mockResolvedValue(undefined),
|
|
11
|
+
connection: {
|
|
12
|
+
getDb: vi.fn(),
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
function MockRuntimeStateManager(this: any) {
|
|
17
|
+
return mockStateManager;
|
|
18
|
+
}
|
|
19
|
+
MockRuntimeStateManager.prototype = {};
|
|
20
|
+
|
|
21
|
+
return { mockStateManager, MockRuntimeStateManager };
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
vi.mock('@principles/core/runtime-v2', async (importOriginal) => {
|
|
25
|
+
const original = await importOriginal() as Record<string, unknown>;
|
|
26
|
+
return {
|
|
27
|
+
...original,
|
|
28
|
+
RuntimeStateManager: MockRuntimeStateManager,
|
|
29
|
+
decideInternalizationRoute: vi.fn(),
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
vi.mock('../../src/resolve-workspace.js', () => ({
|
|
34
|
+
resolveWorkspaceDir: vi.fn().mockReturnValue('/tmp/test-workspace'),
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
import { decideInternalizationRoute } from '@principles/core/runtime-v2';
|
|
38
|
+
|
|
39
|
+
const mockCandidate = (overrides: Partial<{
|
|
40
|
+
candidateId: string;
|
|
41
|
+
sourceRecommendationJson: string;
|
|
42
|
+
description: string;
|
|
43
|
+
}> = {}) => ({
|
|
44
|
+
candidateId: overrides.candidateId ?? 'cand-001',
|
|
45
|
+
artifactId: 'art-001',
|
|
46
|
+
taskId: 'task-001',
|
|
47
|
+
sourceRunId: 'run-001',
|
|
48
|
+
title: 'Test Candidate',
|
|
49
|
+
description: overrides.description ?? 'Test description',
|
|
50
|
+
confidence: 0.85,
|
|
51
|
+
status: 'active',
|
|
52
|
+
createdAt: new Date().toISOString(),
|
|
53
|
+
sourceRecommendationJson: overrides.sourceRecommendationJson ?? JSON.stringify({
|
|
54
|
+
kind: 'principle',
|
|
55
|
+
description: 'Test principle',
|
|
56
|
+
abstractedPrinciple: 'Always handle errors',
|
|
57
|
+
}),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('handleCandidateInternalize (PRI-89)', () => {
|
|
61
|
+
beforeEach(() => {
|
|
62
|
+
vi.clearAllMocks();
|
|
63
|
+
mockStateManager.getCandidate.mockResolvedValue(null);
|
|
64
|
+
mockStateManager.getTask.mockResolvedValue(null);
|
|
65
|
+
mockStateManager.createTask.mockResolvedValue({
|
|
66
|
+
taskId: 'dreamer-cand-001-prompt',
|
|
67
|
+
taskKind: 'dreamer',
|
|
68
|
+
status: 'pending',
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('valid actionable candidate creates root dreamer PI task', async () => {
|
|
73
|
+
mockStateManager.getCandidate.mockResolvedValue(mockCandidate());
|
|
74
|
+
(decideInternalizationRoute as ReturnType<typeof vi.fn>).mockReturnValue({
|
|
75
|
+
ready: true,
|
|
76
|
+
route: 'principle-ledger',
|
|
77
|
+
missingFields: [],
|
|
78
|
+
reason: 'Principle recommendation ready for ledger write path.',
|
|
79
|
+
nextAction: 'Proceed with principle-ledger intake.',
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
83
|
+
await handleCandidateInternalize({
|
|
84
|
+
candidateId: 'cand-001',
|
|
85
|
+
workspace: '/tmp/test',
|
|
86
|
+
json: true,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
expect(mockStateManager.createTask).toHaveBeenCalledOnce();
|
|
90
|
+
const createArg = (mockStateManager.createTask as ReturnType<typeof vi.fn>).mock.calls[0] as unknown[];
|
|
91
|
+
expect((createArg[0] as Record<string, unknown>).taskKind).toBe('dreamer');
|
|
92
|
+
expect((createArg[0] as Record<string, unknown>).status).toBe('pending');
|
|
93
|
+
|
|
94
|
+
const output = JSON.parse(consoleSpy.mock.calls[0]![0] as string);
|
|
95
|
+
expect(output.status).toBe('created');
|
|
96
|
+
expect(output.candidateId).toBe('cand-001');
|
|
97
|
+
expect(output.route).toBe('principle-ledger');
|
|
98
|
+
expect(output.channel).toBe('prompt');
|
|
99
|
+
expect(output.taskId).toBeDefined();
|
|
100
|
+
|
|
101
|
+
consoleSpy.mockRestore();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('repeated seed returns existing task, no duplicate', async () => {
|
|
105
|
+
mockStateManager.getCandidate.mockResolvedValue(mockCandidate());
|
|
106
|
+
(decideInternalizationRoute as ReturnType<typeof vi.fn>).mockReturnValue({
|
|
107
|
+
ready: true,
|
|
108
|
+
route: 'principle-ledger',
|
|
109
|
+
missingFields: [],
|
|
110
|
+
reason: 'Ready',
|
|
111
|
+
nextAction: 'Proceed',
|
|
112
|
+
});
|
|
113
|
+
mockStateManager.getTask.mockResolvedValue({
|
|
114
|
+
taskId: 'dreamer-cand-001-prompt',
|
|
115
|
+
taskKind: 'dreamer',
|
|
116
|
+
status: 'pending',
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
120
|
+
await handleCandidateInternalize({
|
|
121
|
+
candidateId: 'cand-001',
|
|
122
|
+
workspace: '/tmp/test',
|
|
123
|
+
json: true,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
expect(mockStateManager.createTask).not.toHaveBeenCalled();
|
|
127
|
+
const output = JSON.parse(consoleSpy.mock.calls[0]![0] as string);
|
|
128
|
+
expect(output.status).toBe('existing');
|
|
129
|
+
|
|
130
|
+
consoleSpy.mockRestore();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('--dry-run does not write database', async () => {
|
|
134
|
+
mockStateManager.getCandidate.mockResolvedValue(mockCandidate());
|
|
135
|
+
(decideInternalizationRoute as ReturnType<typeof vi.fn>).mockReturnValue({
|
|
136
|
+
ready: true,
|
|
137
|
+
route: 'principle-ledger',
|
|
138
|
+
missingFields: [],
|
|
139
|
+
reason: 'Ready',
|
|
140
|
+
nextAction: 'Proceed',
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
144
|
+
await handleCandidateInternalize({
|
|
145
|
+
candidateId: 'cand-001',
|
|
146
|
+
workspace: '/tmp/test',
|
|
147
|
+
json: true,
|
|
148
|
+
dryRun: true,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
expect(mockStateManager.createTask).not.toHaveBeenCalled();
|
|
152
|
+
const output = JSON.parse(consoleSpy.mock.calls[0]![0] as string);
|
|
153
|
+
expect(output.status).toBe('dry_run');
|
|
154
|
+
|
|
155
|
+
consoleSpy.mockRestore();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('defer/non-actionable route returns no_task_created', async () => {
|
|
159
|
+
mockStateManager.getCandidate.mockResolvedValue(mockCandidate({
|
|
160
|
+
sourceRecommendationJson: JSON.stringify({ kind: 'defer', description: 'Skip' }),
|
|
161
|
+
}));
|
|
162
|
+
(decideInternalizationRoute as ReturnType<typeof vi.fn>).mockReturnValue({
|
|
163
|
+
ready: false,
|
|
164
|
+
route: 'deferred',
|
|
165
|
+
missingFields: [],
|
|
166
|
+
reason: 'Recommendation explicitly deferred',
|
|
167
|
+
nextAction: 'No action needed',
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
171
|
+
await handleCandidateInternalize({
|
|
172
|
+
candidateId: 'cand-001',
|
|
173
|
+
workspace: '/tmp/test',
|
|
174
|
+
json: true,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
expect(mockStateManager.createTask).not.toHaveBeenCalled();
|
|
178
|
+
const output = JSON.parse(consoleSpy.mock.calls[0]![0] as string);
|
|
179
|
+
expect(output.status).toBe('no_task_created');
|
|
180
|
+
|
|
181
|
+
consoleSpy.mockRestore();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('candidate not found returns structured error', async () => {
|
|
185
|
+
mockStateManager.getCandidate.mockResolvedValue(null);
|
|
186
|
+
|
|
187
|
+
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
188
|
+
await expect(
|
|
189
|
+
handleCandidateInternalize({
|
|
190
|
+
candidateId: 'nonexistent',
|
|
191
|
+
workspace: '/tmp/test',
|
|
192
|
+
json: true,
|
|
193
|
+
})
|
|
194
|
+
).rejects.toThrow();
|
|
195
|
+
|
|
196
|
+
expect(mockStateManager.createTask).not.toHaveBeenCalled();
|
|
197
|
+
|
|
198
|
+
consoleSpy.mockRestore();
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('recommendation kind maps to correct channel', async () => {
|
|
202
|
+
mockStateManager.getCandidate.mockResolvedValue(mockCandidate({
|
|
203
|
+
sourceRecommendationJson: JSON.stringify({ kind: 'rule', description: 'Test rule', triggerPattern: 'test', action: 'do-thing' }),
|
|
204
|
+
}));
|
|
205
|
+
(decideInternalizationRoute as ReturnType<typeof vi.fn>).mockReturnValue({
|
|
206
|
+
ready: true,
|
|
207
|
+
route: 'rule-candidate',
|
|
208
|
+
missingFields: [],
|
|
209
|
+
reason: 'Rule ready',
|
|
210
|
+
nextAction: 'Proceed',
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
214
|
+
await handleCandidateInternalize({
|
|
215
|
+
candidateId: 'cand-001',
|
|
216
|
+
workspace: '/tmp/test',
|
|
217
|
+
json: true,
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const output = JSON.parse(consoleSpy.mock.calls[0]![0] as string);
|
|
221
|
+
expect(output.channel).toBe('code_tool_hook');
|
|
222
|
+
|
|
223
|
+
consoleSpy.mockRestore();
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('column fallback: sourceRecommendationJson empty but columns complete still creates task', async () => {
|
|
227
|
+
mockStateManager.getCandidate.mockResolvedValue({
|
|
228
|
+
candidateId: 'cand-fb-001',
|
|
229
|
+
artifactId: 'art-fb-001',
|
|
230
|
+
taskId: 'task-fb-001',
|
|
231
|
+
sourceRunId: 'run-fb-001',
|
|
232
|
+
title: 'Fallback Candidate',
|
|
233
|
+
description: 'Fallback desc',
|
|
234
|
+
confidence: 0.7,
|
|
235
|
+
status: 'active',
|
|
236
|
+
createdAt: new Date().toISOString(),
|
|
237
|
+
sourceRecommendationJson: undefined,
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const mockPrepare = vi.fn().mockReturnValue({
|
|
241
|
+
get: vi.fn().mockReturnValue({
|
|
242
|
+
recommendation_kind: 'principle',
|
|
243
|
+
trigger_pattern: null,
|
|
244
|
+
action: null,
|
|
245
|
+
abstracted_principle: 'Always validate inputs',
|
|
246
|
+
}),
|
|
247
|
+
});
|
|
248
|
+
mockStateManager.connection.getDb.mockReturnValue({ prepare: mockPrepare });
|
|
249
|
+
|
|
250
|
+
(decideInternalizationRoute as ReturnType<typeof vi.fn>).mockReturnValue({
|
|
251
|
+
ready: true,
|
|
252
|
+
route: 'principle-ledger',
|
|
253
|
+
missingFields: [],
|
|
254
|
+
reason: 'Principle ready (column fallback)',
|
|
255
|
+
nextAction: 'Proceed',
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
259
|
+
await handleCandidateInternalize({
|
|
260
|
+
candidateId: 'cand-fb-001',
|
|
261
|
+
workspace: '/tmp/test',
|
|
262
|
+
json: true,
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
expect(mockStateManager.createTask).toHaveBeenCalledOnce();
|
|
266
|
+
const output = JSON.parse(consoleSpy.mock.calls[0]![0] as string);
|
|
267
|
+
expect(output.status).toBe('created');
|
|
268
|
+
expect(output.candidateId).toBe('cand-fb-001');
|
|
269
|
+
|
|
270
|
+
consoleSpy.mockRestore();
|
|
271
|
+
});
|
|
272
|
+
});
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PRI-46: pd candidate route — Internalization route inspection tests
|
|
3
|
+
*
|
|
4
|
+
* Tests that handleCandidateRoute correctly loads a candidate,
|
|
5
|
+
* reconstructs the recommendation, calls decideInternalizationRoute,
|
|
6
|
+
* and outputs the decision as JSON or text.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
9
|
+
import { handleCandidateRoute } from '../../src/commands/candidate.js';
|
|
10
|
+
|
|
11
|
+
// ── Mock setup ──────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
const { mockStateManager, MockRuntimeStateManager } = vi.hoisted(() => {
|
|
14
|
+
const mockStateManager = {
|
|
15
|
+
initialize: vi.fn().mockResolvedValue(undefined),
|
|
16
|
+
getCandidate: vi.fn(),
|
|
17
|
+
close: vi.fn().mockResolvedValue(undefined),
|
|
18
|
+
connection: {
|
|
19
|
+
getDb: vi.fn(),
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function MockRuntimeStateManager(this: any) {
|
|
24
|
+
return mockStateManager;
|
|
25
|
+
}
|
|
26
|
+
MockRuntimeStateManager.prototype = {};
|
|
27
|
+
|
|
28
|
+
return { mockStateManager, MockRuntimeStateManager };
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
vi.mock('@principles/core/runtime-v2', () => ({
|
|
32
|
+
RuntimeStateManager: MockRuntimeStateManager,
|
|
33
|
+
decideInternalizationRoute: vi.fn(),
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
vi.mock('../../src/resolve-workspace.js', () => ({
|
|
37
|
+
resolveWorkspaceDir: vi.fn().mockReturnValue('/tmp/test-workspace'),
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
import { decideInternalizationRoute } from '@principles/core/runtime-v2';
|
|
41
|
+
|
|
42
|
+
// ── Test fixtures ───────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
const mockCandidate = (overrides: Partial<{
|
|
45
|
+
candidateId: string;
|
|
46
|
+
sourceRecommendationJson: string;
|
|
47
|
+
description: string;
|
|
48
|
+
}> = {}) => ({
|
|
49
|
+
candidateId: overrides.candidateId ?? 'cand-001',
|
|
50
|
+
artifactId: 'art-001',
|
|
51
|
+
taskId: 'task-001',
|
|
52
|
+
sourceRunId: 'run-001',
|
|
53
|
+
title: 'Test candidate',
|
|
54
|
+
description: overrides.description ?? 'Test recommendation',
|
|
55
|
+
confidence: 0.85,
|
|
56
|
+
sourceRecommendationJson: overrides.sourceRecommendationJson ?? JSON.stringify({
|
|
57
|
+
kind: 'rule',
|
|
58
|
+
description: 'Block force push',
|
|
59
|
+
triggerPattern: 'git\\s+push\\s+--force',
|
|
60
|
+
action: 'block and require approval',
|
|
61
|
+
}),
|
|
62
|
+
status: 'pending' as const,
|
|
63
|
+
createdAt: '2026-05-04T00:00:00.000Z',
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
let consoleLogSpy: ReturnType<typeof vi.spyOn>;
|
|
67
|
+
let consoleErrorSpy: ReturnType<typeof vi.spyOn>;
|
|
68
|
+
let exitSpy: ReturnType<typeof vi.spyOn>;
|
|
69
|
+
|
|
70
|
+
beforeEach(() => {
|
|
71
|
+
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
72
|
+
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
73
|
+
exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as () => never);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
afterEach(() => {
|
|
77
|
+
consoleLogSpy.mockRestore();
|
|
78
|
+
consoleErrorSpy.mockRestore();
|
|
79
|
+
exitSpy.mockRestore();
|
|
80
|
+
vi.clearAllMocks();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// ── Tests ───────────────────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
describe('handleCandidateRoute', () => {
|
|
86
|
+
// ── 1. Ready rule candidate ─────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
it('ready rule candidate outputs JSON with ready=true and route=rule-candidate', async () => {
|
|
89
|
+
mockStateManager.getCandidate.mockResolvedValue(mockCandidate());
|
|
90
|
+
(decideInternalizationRoute as ReturnType<typeof vi.fn>).mockReturnValue({
|
|
91
|
+
ready: true,
|
|
92
|
+
route: 'rule-candidate',
|
|
93
|
+
missingFields: [],
|
|
94
|
+
reason: 'Rule recommendation ready for candidate pipeline.',
|
|
95
|
+
nextAction: 'Proceed with rule-candidate compilation.',
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
await handleCandidateRoute({ candidateId: 'cand-001', workspace: '/tmp/ws', json: true });
|
|
99
|
+
|
|
100
|
+
expect(exitSpy).not.toHaveBeenCalledWith(1);
|
|
101
|
+
|
|
102
|
+
const jsonOutput = consoleLogSpy.mock.calls.find(call => {
|
|
103
|
+
try { JSON.parse(call[0] as string); return true; } catch { return false; }
|
|
104
|
+
});
|
|
105
|
+
expect(jsonOutput).toBeDefined();
|
|
106
|
+
const parsed = JSON.parse((jsonOutput as [string])[0]);
|
|
107
|
+
expect(parsed.candidateId).toBe('cand-001');
|
|
108
|
+
expect(parsed.recommendationKind).toBe('rule');
|
|
109
|
+
expect(parsed.route).toBe('rule-candidate');
|
|
110
|
+
expect(parsed.ready).toBe(true);
|
|
111
|
+
expect(parsed.missingFields).toEqual([]);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// ── 2. Incomplete rule candidate ────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
it('incomplete rule candidate (missing triggerPattern) outputs ready=false with missingFields', async () => {
|
|
117
|
+
mockStateManager.getCandidate.mockResolvedValue(mockCandidate({
|
|
118
|
+
sourceRecommendationJson: JSON.stringify({
|
|
119
|
+
kind: 'rule',
|
|
120
|
+
description: 'Incomplete rule',
|
|
121
|
+
action: 'block',
|
|
122
|
+
}),
|
|
123
|
+
}));
|
|
124
|
+
(decideInternalizationRoute as ReturnType<typeof vi.fn>).mockReturnValue({
|
|
125
|
+
ready: false,
|
|
126
|
+
route: 'rule-candidate',
|
|
127
|
+
missingFields: ['triggerPattern'],
|
|
128
|
+
reason: 'Rule recommendation incomplete: missing triggerPattern.',
|
|
129
|
+
nextAction: 'Re-run diagnostician with PHASE 4 taxonomy to generate missing rule fields.',
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
await handleCandidateRoute({ candidateId: 'cand-002', workspace: '/tmp/ws', json: true });
|
|
133
|
+
|
|
134
|
+
const parsed = JSON.parse(consoleLogSpy.mock.calls[0][0] as string);
|
|
135
|
+
expect(parsed.ready).toBe(false);
|
|
136
|
+
expect(parsed.missingFields).toContain('triggerPattern');
|
|
137
|
+
expect(parsed.route).toBe('rule-candidate');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// ── 3. Principle candidate ──────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
it('principle candidate with abstractedPrinciple routes to principle-ledger', async () => {
|
|
143
|
+
mockStateManager.getCandidate.mockResolvedValue(mockCandidate({
|
|
144
|
+
sourceRecommendationJson: JSON.stringify({
|
|
145
|
+
kind: 'principle',
|
|
146
|
+
description: 'Avoid mixing concerns',
|
|
147
|
+
abstractedPrinciple: 'Separate concerns into distinct modules',
|
|
148
|
+
}),
|
|
149
|
+
}));
|
|
150
|
+
(decideInternalizationRoute as ReturnType<typeof vi.fn>).mockReturnValue({
|
|
151
|
+
ready: true,
|
|
152
|
+
route: 'principle-ledger',
|
|
153
|
+
missingFields: [],
|
|
154
|
+
reason: 'Principle recommendation ready for ledger write path.',
|
|
155
|
+
nextAction: 'Proceed with principle-ledger intake.',
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
await handleCandidateRoute({ candidateId: 'cand-003', workspace: '/tmp/ws', json: true });
|
|
159
|
+
|
|
160
|
+
const parsed = JSON.parse(consoleLogSpy.mock.calls[0][0] as string);
|
|
161
|
+
expect(parsed.route).toBe('principle-ledger');
|
|
162
|
+
expect(parsed.ready).toBe(true);
|
|
163
|
+
expect(parsed.recommendationKind).toBe('principle');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// ── 4. Implementation candidate ──────────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
it('implementation candidate routes to implementation-candidate', async () => {
|
|
169
|
+
mockStateManager.getCandidate.mockResolvedValue(mockCandidate({
|
|
170
|
+
sourceRecommendationJson: JSON.stringify({
|
|
171
|
+
kind: 'implementation',
|
|
172
|
+
description: 'Auto-add error boundary to React components',
|
|
173
|
+
}),
|
|
174
|
+
}));
|
|
175
|
+
(decideInternalizationRoute as ReturnType<typeof vi.fn>).mockReturnValue({
|
|
176
|
+
ready: true,
|
|
177
|
+
route: 'implementation-candidate',
|
|
178
|
+
missingFields: [],
|
|
179
|
+
reason: 'Implementation recommendation ready for candidate pipeline.',
|
|
180
|
+
nextAction: 'Proceed with implementation-candidate intake and compilation.',
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
await handleCandidateRoute({ candidateId: 'cand-003b', workspace: '/tmp/ws', json: true });
|
|
184
|
+
|
|
185
|
+
const parsed = JSON.parse(consoleLogSpy.mock.calls[0][0] as string);
|
|
186
|
+
expect(parsed.route).toBe('implementation-candidate');
|
|
187
|
+
expect(parsed.ready).toBe(true);
|
|
188
|
+
expect(parsed.recommendationKind).toBe('implementation');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// ── 5. Prompt candidate ─────────────────────────────────────────────────
|
|
192
|
+
|
|
193
|
+
it('prompt candidate routes to prompt-injection-candidate', async () => {
|
|
194
|
+
mockStateManager.getCandidate.mockResolvedValue(mockCandidate({
|
|
195
|
+
sourceRecommendationJson: JSON.stringify({
|
|
196
|
+
kind: 'prompt',
|
|
197
|
+
description: 'Add safety reminder to system prompt',
|
|
198
|
+
}),
|
|
199
|
+
}));
|
|
200
|
+
(decideInternalizationRoute as ReturnType<typeof vi.fn>).mockReturnValue({
|
|
201
|
+
ready: true,
|
|
202
|
+
route: 'prompt-injection-candidate',
|
|
203
|
+
missingFields: [],
|
|
204
|
+
reason: 'Prompt recommendation ready for injection candidate pipeline.',
|
|
205
|
+
nextAction: 'Proceed with prompt-injection-candidate intake.',
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
await handleCandidateRoute({ candidateId: 'cand-004', workspace: '/tmp/ws', json: true });
|
|
209
|
+
|
|
210
|
+
const parsed = JSON.parse(consoleLogSpy.mock.calls[0][0] as string);
|
|
211
|
+
expect(parsed.route).toBe('prompt-injection-candidate');
|
|
212
|
+
expect(parsed.ready).toBe(true);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// ── 6. Defer candidate ──────────────────────────────────────────────────
|
|
216
|
+
|
|
217
|
+
it('defer candidate routes to deferred with ready=false', async () => {
|
|
218
|
+
mockStateManager.getCandidate.mockResolvedValue(mockCandidate({
|
|
219
|
+
sourceRecommendationJson: JSON.stringify({
|
|
220
|
+
kind: 'defer',
|
|
221
|
+
description: 'Not actionable yet',
|
|
222
|
+
}),
|
|
223
|
+
}));
|
|
224
|
+
(decideInternalizationRoute as ReturnType<typeof vi.fn>).mockReturnValue({
|
|
225
|
+
ready: false,
|
|
226
|
+
route: 'deferred',
|
|
227
|
+
missingFields: [],
|
|
228
|
+
reason: 'Recommendation explicitly deferred — no internalization action required.',
|
|
229
|
+
nextAction: 'No action needed. Re-evaluate if context changes.',
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
await handleCandidateRoute({ candidateId: 'cand-005', workspace: '/tmp/ws', json: true });
|
|
233
|
+
|
|
234
|
+
const parsed = JSON.parse(consoleLogSpy.mock.calls[0][0] as string);
|
|
235
|
+
expect(parsed.route).toBe('deferred');
|
|
236
|
+
expect(parsed.ready).toBe(false);
|
|
237
|
+
expect(parsed.reason).toContain('deferred');
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// ── 7. Unknown kind routes to deferred ──────────────────────────────────
|
|
241
|
+
|
|
242
|
+
it('unknown recommendation kind routes to deferred', async () => {
|
|
243
|
+
mockStateManager.getCandidate.mockResolvedValue(mockCandidate({
|
|
244
|
+
sourceRecommendationJson: JSON.stringify({
|
|
245
|
+
kind: 'unknown_nonsense',
|
|
246
|
+
description: 'Bad kind',
|
|
247
|
+
}),
|
|
248
|
+
}));
|
|
249
|
+
(decideInternalizationRoute as ReturnType<typeof vi.fn>).mockReturnValue({
|
|
250
|
+
ready: false,
|
|
251
|
+
route: 'deferred',
|
|
252
|
+
missingFields: [],
|
|
253
|
+
reason: 'Unrecognized recommendation kind "unknown_nonsense" — deferred to safe default.',
|
|
254
|
+
nextAction: 'Review diagnostician output for unsupported recommendation kind.',
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
await handleCandidateRoute({ candidateId: 'cand-005b', workspace: '/tmp/ws', json: true });
|
|
258
|
+
|
|
259
|
+
const parsed = JSON.parse(consoleLogSpy.mock.calls[0][0] as string);
|
|
260
|
+
expect(parsed.route).toBe('deferred');
|
|
261
|
+
expect(parsed.ready).toBe(false);
|
|
262
|
+
expect(parsed.recommendationKind).toBe('unknown_nonsense');
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// ── 8. Candidate not found ──────────────────────────────────────────────
|
|
266
|
+
|
|
267
|
+
it('candidate not found exits 1 with error message', async () => {
|
|
268
|
+
mockStateManager.getCandidate.mockResolvedValue(null);
|
|
269
|
+
|
|
270
|
+
await handleCandidateRoute({ candidateId: 'nonexistent', workspace: '/tmp/ws', json: true });
|
|
271
|
+
|
|
272
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('nonexistent'));
|
|
273
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// ── 8. Malformed source_recommendation_json uses column fallback ────────
|
|
277
|
+
|
|
278
|
+
it('malformed source_recommendation_json falls back to DB columns', async () => {
|
|
279
|
+
mockStateManager.getCandidate.mockResolvedValue(mockCandidate({
|
|
280
|
+
sourceRecommendationJson: 'not-valid-json{{{',
|
|
281
|
+
}));
|
|
282
|
+
mockStateManager.connection.getDb.mockReturnValue({
|
|
283
|
+
prepare: () => ({
|
|
284
|
+
get: () => ({
|
|
285
|
+
recommendation_kind: 'implementation',
|
|
286
|
+
trigger_pattern: null,
|
|
287
|
+
action: null,
|
|
288
|
+
abstracted_principle: null,
|
|
289
|
+
}),
|
|
290
|
+
}),
|
|
291
|
+
});
|
|
292
|
+
(decideInternalizationRoute as ReturnType<typeof vi.fn>).mockReturnValue({
|
|
293
|
+
ready: true,
|
|
294
|
+
route: 'implementation-candidate',
|
|
295
|
+
missingFields: [],
|
|
296
|
+
reason: 'Implementation recommendation ready for candidate pipeline.',
|
|
297
|
+
nextAction: 'Proceed with implementation-candidate intake and compilation.',
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
await handleCandidateRoute({ candidateId: 'cand-006', workspace: '/tmp/ws', json: true });
|
|
301
|
+
|
|
302
|
+
const parsed = JSON.parse(consoleLogSpy.mock.calls[0][0] as string);
|
|
303
|
+
expect(parsed.route).toBe('implementation-candidate');
|
|
304
|
+
expect(parsed.ready).toBe(true);
|
|
305
|
+
expect(parsed._meta?.source).toBe('column_fallback');
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// ── 9. Text output includes route/ready/missingFields ───────────────────
|
|
309
|
+
|
|
310
|
+
it('text output includes route, ready, and missingFields', async () => {
|
|
311
|
+
mockStateManager.getCandidate.mockResolvedValue(mockCandidate());
|
|
312
|
+
(decideInternalizationRoute as ReturnType<typeof vi.fn>).mockReturnValue({
|
|
313
|
+
ready: true,
|
|
314
|
+
route: 'rule-candidate',
|
|
315
|
+
missingFields: [],
|
|
316
|
+
reason: 'Rule recommendation ready for candidate pipeline.',
|
|
317
|
+
nextAction: 'Proceed with rule-candidate compilation.',
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
await handleCandidateRoute({ candidateId: 'cand-001', workspace: '/tmp/ws', json: false });
|
|
321
|
+
|
|
322
|
+
const allOutput = consoleLogSpy.mock.calls.map(call => call[0]).join('\n');
|
|
323
|
+
expect(allOutput).toContain('cand-001');
|
|
324
|
+
expect(allOutput).toContain('rule-candidate');
|
|
325
|
+
expect(allOutput).toContain('Ready: true');
|
|
326
|
+
expect(allOutput).toContain('(none)');
|
|
327
|
+
});
|
|
328
|
+
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
const { mockCandidateShow, mockResolveWorkspaceDir } = vi.hoisted(() => ({
|
|
4
|
+
mockCandidateShow: vi.fn(),
|
|
5
|
+
mockResolveWorkspaceDir: vi.fn().mockReturnValue('/fake/workspace'),
|
|
6
|
+
}));
|
|
7
|
+
|
|
8
|
+
vi.mock('../../src/resolve-workspace.js', () => ({
|
|
9
|
+
resolveWorkspaceDir: mockResolveWorkspaceDir,
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
vi.mock('../../src/commands/candidate.js', () => ({
|
|
13
|
+
handleCandidateShow: mockCandidateShow,
|
|
14
|
+
handleCandidateIntake: vi.fn(),
|
|
15
|
+
handleCandidateRoute: vi.fn(),
|
|
16
|
+
handleCandidateInternalize: vi.fn(),
|
|
17
|
+
handleCandidateAudit: vi.fn(),
|
|
18
|
+
handleCandidateInternalizationBackfill: vi.fn(),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
import { Command } from 'commander';
|
|
22
|
+
|
|
23
|
+
function createTestProgram(): Command {
|
|
24
|
+
const program = new Command();
|
|
25
|
+
program.exitOverride();
|
|
26
|
+
|
|
27
|
+
const candidateCmd = program.command('candidate');
|
|
28
|
+
|
|
29
|
+
candidateCmd
|
|
30
|
+
.command('show [candidateId]')
|
|
31
|
+
.description('Show detail for a single principle candidate')
|
|
32
|
+
.requiredOption('-w, --workspace <path>', 'Workspace directory')
|
|
33
|
+
.option('--candidate-id <id>', 'Candidate ID (alternative to positional arg)')
|
|
34
|
+
.option('--json', 'Output raw JSON')
|
|
35
|
+
.action(async (candidateId, opts) => {
|
|
36
|
+
const resolvedId = opts.candidateId ?? candidateId;
|
|
37
|
+
if (!resolvedId) {
|
|
38
|
+
throw new Error('candidate ID is required (positional or --candidate-id)');
|
|
39
|
+
}
|
|
40
|
+
if (candidateId && opts.candidateId && candidateId !== opts.candidateId) {
|
|
41
|
+
throw new Error(`conflicting candidate IDs: positional="${candidateId}", --candidate-id="${opts.candidateId}"`);
|
|
42
|
+
}
|
|
43
|
+
await mockCandidateShow({ candidateId: resolvedId, ...opts });
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
return program;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe('candidate show CLI', () => {
|
|
50
|
+
beforeEach(() => {
|
|
51
|
+
vi.clearAllMocks();
|
|
52
|
+
mockCandidateShow.mockResolvedValue(undefined);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('positional candidateId works', async () => {
|
|
56
|
+
const program = createTestProgram();
|
|
57
|
+
await program.parseAsync(['node', 'pd', 'candidate', 'show', 'c_123', '-w', '/ws']);
|
|
58
|
+
|
|
59
|
+
expect(mockCandidateShow).toHaveBeenCalledWith(
|
|
60
|
+
expect.objectContaining({ candidateId: 'c_123' }),
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('--candidate-id works', async () => {
|
|
65
|
+
const program = createTestProgram();
|
|
66
|
+
await program.parseAsync(['node', 'pd', 'candidate', 'show', '-w', '/ws', '--candidate-id', 'c_456']);
|
|
67
|
+
|
|
68
|
+
expect(mockCandidateShow).toHaveBeenCalledWith(
|
|
69
|
+
expect.objectContaining({ candidateId: 'c_456' }),
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('both positional and --candidate-id with same value works', async () => {
|
|
74
|
+
const program = createTestProgram();
|
|
75
|
+
await program.parseAsync(['node', 'pd', 'candidate', 'show', 'c_789', '-w', '/ws', '--candidate-id', 'c_789']);
|
|
76
|
+
|
|
77
|
+
expect(mockCandidateShow).toHaveBeenCalledWith(
|
|
78
|
+
expect.objectContaining({ candidateId: 'c_789' }),
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('conflicting positional and --candidate-id throws error', async () => {
|
|
83
|
+
const program = createTestProgram();
|
|
84
|
+
await expect(
|
|
85
|
+
program.parseAsync(['node', 'pd', 'candidate', 'show', 'c_a', '-w', '/ws', '--candidate-id', 'c_b']),
|
|
86
|
+
).rejects.toThrow('conflicting candidate IDs');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('neither positional nor --candidate-id throws error', async () => {
|
|
90
|
+
const program = createTestProgram();
|
|
91
|
+
await expect(
|
|
92
|
+
program.parseAsync(['node', 'pd', 'candidate', 'show', '-w', '/ws']),
|
|
93
|
+
).rejects.toThrow('candidate ID is required');
|
|
94
|
+
});
|
|
95
|
+
});
|