@principles/core 1.124.0 → 1.126.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/runtime-v2/__tests__/architecture-regression.test.js +5 -1
- package/dist/runtime-v2/__tests__/architecture-regression.test.js.map +1 -1
- package/dist/runtime-v2/__tests__/attack-e2e-pipeline-smoke.test.js +115 -21
- package/dist/runtime-v2/__tests__/attack-e2e-pipeline-smoke.test.js.map +1 -1
- package/dist/runtime-v2/__tests__/golden-path-diagnostician-e2e.test.js.map +1 -1
- package/dist/runtime-v2/__tests__/pain-signal-bridge-retried.test.js +7 -6
- package/dist/runtime-v2/__tests__/pain-signal-bridge-retried.test.js.map +1 -1
- package/dist/runtime-v2/__tests__/pain-signal-bridge-short-circuit.test.js.map +1 -1
- package/dist/runtime-v2/__tests__/pain-signal-bridge-workspace-dir.test.js.map +1 -1
- package/dist/runtime-v2/cli/diagnose.d.ts +2 -2
- package/dist/runtime-v2/cli/diagnose.d.ts.map +1 -1
- package/dist/runtime-v2/config/__tests__/pd-config-contract.test.js +3 -3
- package/dist/runtime-v2/config/__tests__/pd-config-contract.test.js.map +1 -1
- package/dist/runtime-v2/config/pd-config-defaults.js +2 -2
- package/dist/runtime-v2/config/pd-config-defaults.js.map +1 -1
- package/dist/runtime-v2/diagnostician-prompt-builder.d.ts +1 -64
- package/dist/runtime-v2/diagnostician-prompt-builder.d.ts.map +1 -1
- package/dist/runtime-v2/diagnostician-prompt-builder.js +0 -186
- package/dist/runtime-v2/diagnostician-prompt-builder.js.map +1 -1
- package/dist/runtime-v2/feature-flags/__tests__/feature-flag-contract.test.js +17 -30
- package/dist/runtime-v2/feature-flags/__tests__/feature-flag-contract.test.js.map +1 -1
- package/dist/runtime-v2/feature-flags/feature-flag-contract.d.ts.map +1 -1
- package/dist/runtime-v2/feature-flags/feature-flag-contract.js +3 -8
- package/dist/runtime-v2/feature-flags/feature-flag-contract.js.map +1 -1
- package/dist/runtime-v2/index.d.ts +8 -7
- package/dist/runtime-v2/index.d.ts.map +1 -1
- package/dist/runtime-v2/index.js +8 -5
- package/dist/runtime-v2/index.js.map +1 -1
- package/dist/runtime-v2/internalization/__tests__/diag-chain-e2e.test.js +108 -105
- package/dist/runtime-v2/internalization/__tests__/diag-chain-e2e.test.js.map +1 -1
- package/dist/runtime-v2/internalization/__tests__/runnerkind-seam.test.js +3 -3
- package/dist/runtime-v2/internalization/__tests__/runnerkind-seam.test.js.map +1 -1
- package/dist/runtime-v2/internalization/split-diagnostician-runner.d.ts.map +1 -1
- package/dist/runtime-v2/internalization/split-diagnostician-runner.js +121 -52
- package/dist/runtime-v2/internalization/split-diagnostician-runner.js.map +1 -1
- package/dist/runtime-v2/observer/__tests__/empathy-observer.real-e2e.test.js +28 -21
- package/dist/runtime-v2/observer/__tests__/empathy-observer.real-e2e.test.js.map +1 -1
- package/dist/runtime-v2/pain-signal-runtime-factory.d.ts +10 -1
- package/dist/runtime-v2/pain-signal-runtime-factory.d.ts.map +1 -1
- package/dist/runtime-v2/pain-signal-runtime-factory.js +42 -40
- package/dist/runtime-v2/pain-signal-runtime-factory.js.map +1 -1
- package/dist/runtime-v2/pain-to-principle-service.d.ts +1 -0
- package/dist/runtime-v2/pain-to-principle-service.d.ts.map +1 -1
- package/dist/runtime-v2/pain-to-principle-service.js.map +1 -1
- package/dist/runtime-v2/runner/__tests__/diagnose.test.js.map +1 -1
- package/package.json +1 -1
- package/dist/runtime-v2/__tests__/diagnostician-core-grounding.test.d.ts +0 -2
- package/dist/runtime-v2/__tests__/diagnostician-core-grounding.test.d.ts.map +0 -1
- package/dist/runtime-v2/__tests__/diagnostician-core-grounding.test.js +0 -122
- package/dist/runtime-v2/__tests__/diagnostician-core-grounding.test.js.map +0 -1
- package/dist/runtime-v2/diagnostician/__tests__/diagnostician-prompt-builder.integration.test.d.ts +0 -2
- package/dist/runtime-v2/diagnostician/__tests__/diagnostician-prompt-builder.integration.test.d.ts.map +0 -1
- package/dist/runtime-v2/diagnostician/__tests__/diagnostician-prompt-builder.integration.test.js +0 -169
- package/dist/runtime-v2/diagnostician/__tests__/diagnostician-prompt-builder.integration.test.js.map +0 -1
- package/dist/runtime-v2/diagnostician/__tests__/diagnostician-prompt-builder.test.d.ts +0 -2
- package/dist/runtime-v2/diagnostician/__tests__/diagnostician-prompt-builder.test.d.ts.map +0 -1
- package/dist/runtime-v2/diagnostician/__tests__/diagnostician-prompt-builder.test.js +0 -462
- package/dist/runtime-v2/diagnostician/__tests__/diagnostician-prompt-builder.test.js.map +0 -1
- package/dist/runtime-v2/diagnostician/__tests__/diagnostician-prompt-language.test.d.ts +0 -13
- package/dist/runtime-v2/diagnostician/__tests__/diagnostician-prompt-language.test.d.ts.map +0 -1
- package/dist/runtime-v2/diagnostician/__tests__/diagnostician-prompt-language.test.js +0 -97
- package/dist/runtime-v2/diagnostician/__tests__/diagnostician-prompt-language.test.js.map +0 -1
- package/dist/runtime-v2/runner/__tests__/diagnostician-runner.integration.test.d.ts +0 -2
- package/dist/runtime-v2/runner/__tests__/diagnostician-runner.integration.test.d.ts.map +0 -1
- package/dist/runtime-v2/runner/__tests__/diagnostician-runner.integration.test.js +0 -378
- package/dist/runtime-v2/runner/__tests__/diagnostician-runner.integration.test.js.map +0 -1
- package/dist/runtime-v2/runner/__tests__/diagnostician-runner.test.d.ts +0 -2
- package/dist/runtime-v2/runner/__tests__/diagnostician-runner.test.d.ts.map +0 -1
- package/dist/runtime-v2/runner/__tests__/diagnostician-runner.test.js +0 -682
- package/dist/runtime-v2/runner/__tests__/diagnostician-runner.test.js.map +0 -1
- package/dist/runtime-v2/runner/__tests__/diagnostician-telemetry.test.d.ts +0 -2
- package/dist/runtime-v2/runner/__tests__/diagnostician-telemetry.test.d.ts.map +0 -1
- package/dist/runtime-v2/runner/__tests__/diagnostician-telemetry.test.js +0 -286
- package/dist/runtime-v2/runner/__tests__/diagnostician-telemetry.test.js.map +0 -1
- package/dist/runtime-v2/runner/__tests__/dual-track-e2e.test.d.ts +0 -2
- package/dist/runtime-v2/runner/__tests__/dual-track-e2e.test.d.ts.map +0 -1
- package/dist/runtime-v2/runner/__tests__/dual-track-e2e.test.js +0 -320
- package/dist/runtime-v2/runner/__tests__/dual-track-e2e.test.js.map +0 -1
- package/dist/runtime-v2/runner/__tests__/lease-expiration-recovery.integration.test.d.ts +0 -2
- package/dist/runtime-v2/runner/__tests__/lease-expiration-recovery.integration.test.d.ts.map +0 -1
- package/dist/runtime-v2/runner/__tests__/lease-expiration-recovery.integration.test.js +0 -261
- package/dist/runtime-v2/runner/__tests__/lease-expiration-recovery.integration.test.js.map +0 -1
- package/dist/runtime-v2/runner/__tests__/m5-05-e2e.test.d.ts +0 -2
- package/dist/runtime-v2/runner/__tests__/m5-05-e2e.test.d.ts.map +0 -1
- package/dist/runtime-v2/runner/__tests__/m5-05-e2e.test.js +0 -405
- package/dist/runtime-v2/runner/__tests__/m5-05-e2e.test.js.map +0 -1
- package/dist/runtime-v2/runner/__tests__/m6-06-e2e.test.d.ts +0 -2
- package/dist/runtime-v2/runner/__tests__/m6-06-e2e.test.d.ts.map +0 -1
- package/dist/runtime-v2/runner/__tests__/m6-06-e2e.test.js +0 -347
- package/dist/runtime-v2/runner/__tests__/m6-06-e2e.test.js.map +0 -1
- package/dist/runtime-v2/runner/__tests__/m6-06-legacy.test.d.ts +0 -2
- package/dist/runtime-v2/runner/__tests__/m6-06-legacy.test.d.ts.map +0 -1
- package/dist/runtime-v2/runner/__tests__/m6-06-legacy.test.js +0 -186
- package/dist/runtime-v2/runner/__tests__/m6-06-legacy.test.js.map +0 -1
- package/dist/runtime-v2/runner/__tests__/m6-06-real-path.test.d.ts +0 -2
- package/dist/runtime-v2/runner/__tests__/m6-06-real-path.test.d.ts.map +0 -1
- package/dist/runtime-v2/runner/__tests__/m6-06-real-path.test.js +0 -355
- package/dist/runtime-v2/runner/__tests__/m6-06-real-path.test.js.map +0 -1
- package/dist/runtime-v2/runner/__tests__/m8-02-e2e.test.d.ts +0 -2
- package/dist/runtime-v2/runner/__tests__/m8-02-e2e.test.d.ts.map +0 -1
- package/dist/runtime-v2/runner/__tests__/m8-02-e2e.test.js +0 -486
- package/dist/runtime-v2/runner/__tests__/m8-02-e2e.test.js.map +0 -1
- package/dist/runtime-v2/runner/__tests__/m9-adapter-integration.test.d.ts +0 -2
- package/dist/runtime-v2/runner/__tests__/m9-adapter-integration.test.d.ts.map +0 -1
- package/dist/runtime-v2/runner/__tests__/m9-adapter-integration.test.js +0 -171
- package/dist/runtime-v2/runner/__tests__/m9-adapter-integration.test.js.map +0 -1
- package/dist/runtime-v2/runner/__tests__/m9-e2e.test.d.ts +0 -2
- package/dist/runtime-v2/runner/__tests__/m9-e2e.test.d.ts.map +0 -1
- package/dist/runtime-v2/runner/__tests__/m9-e2e.test.js +0 -175
- package/dist/runtime-v2/runner/__tests__/m9-e2e.test.js.map +0 -1
- package/dist/runtime-v2/runner/__tests__/max-attempts-exceeded.integration.test.d.ts +0 -2
- package/dist/runtime-v2/runner/__tests__/max-attempts-exceeded.integration.test.d.ts.map +0 -1
- package/dist/runtime-v2/runner/__tests__/max-attempts-exceeded.integration.test.js +0 -276
- package/dist/runtime-v2/runner/__tests__/max-attempts-exceeded.integration.test.js.map +0 -1
- package/dist/runtime-v2/runner/__tests__/retry-wait-recovery.integration.test.d.ts +0 -2
- package/dist/runtime-v2/runner/__tests__/retry-wait-recovery.integration.test.d.ts.map +0 -1
- package/dist/runtime-v2/runner/__tests__/retry-wait-recovery.integration.test.js +0 -272
- package/dist/runtime-v2/runner/__tests__/retry-wait-recovery.integration.test.js.map +0 -1
- package/dist/runtime-v2/runner/__tests__/start-run-input.test.d.ts +0 -2
- package/dist/runtime-v2/runner/__tests__/start-run-input.test.d.ts.map +0 -1
- package/dist/runtime-v2/runner/__tests__/start-run-input.test.js +0 -67
- package/dist/runtime-v2/runner/__tests__/start-run-input.test.js.map +0 -1
- package/dist/runtime-v2/runner/diagnostician-runner-options.d.ts +0 -57
- package/dist/runtime-v2/runner/diagnostician-runner-options.d.ts.map +0 -1
- package/dist/runtime-v2/runner/diagnostician-runner-options.js +0 -21
- package/dist/runtime-v2/runner/diagnostician-runner-options.js.map +0 -1
- package/dist/runtime-v2/runner/diagnostician-runner.d.ts +0 -89
- package/dist/runtime-v2/runner/diagnostician-runner.d.ts.map +0 -1
- package/dist/runtime-v2/runner/diagnostician-runner.js +0 -470
- package/dist/runtime-v2/runner/diagnostician-runner.js.map +0 -1
|
@@ -1,682 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DiagnosticianRunner unit tests.
|
|
3
|
-
*
|
|
4
|
-
* Covers 11 scenarios:
|
|
5
|
-
* 1. Happy path: full lifecycle succeeds
|
|
6
|
-
* 2. Polling loop: pollRun returns 'running' then 'succeeded'
|
|
7
|
-
* 3. Timeout: pollRun never reaches terminal
|
|
8
|
-
* 4. Runtime failure: pollRun returns 'failed'
|
|
9
|
-
* 5. Context build failure (transient)
|
|
10
|
-
* 6. Context build failure (permanent)
|
|
11
|
-
* 7. Validation failure
|
|
12
|
-
* 8. StartRunInput construction
|
|
13
|
-
* 9. Lease conflict
|
|
14
|
-
* 10. Max attempts exceeded
|
|
15
|
-
* 11. OpenClaw history compatibility
|
|
16
|
-
*/
|
|
17
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
18
|
-
import { PDRuntimeError } from '../../error-categories.js';
|
|
19
|
-
import { DiagnosticianRunner } from '../diagnostician-runner.js';
|
|
20
|
-
import { RunnerPhase } from '../runner-phase.js';
|
|
21
|
-
// ── Test fixtures ──────────────────────────────────────────────────────────────
|
|
22
|
-
const TASK_ID = 'task-test-001';
|
|
23
|
-
const RUN_ID = 'run-test-001';
|
|
24
|
-
const OWNER = 'test-owner';
|
|
25
|
-
const RUNTIME_KIND = 'test-double';
|
|
26
|
-
function makeTaskRecord(overrides = {}) {
|
|
27
|
-
return {
|
|
28
|
-
taskId: TASK_ID,
|
|
29
|
-
taskKind: 'diagnostician',
|
|
30
|
-
status: 'leased',
|
|
31
|
-
createdAt: '2026-04-23T00:00:00Z',
|
|
32
|
-
updatedAt: '2026-04-23T00:00:00Z',
|
|
33
|
-
leaseOwner: OWNER,
|
|
34
|
-
leaseExpiresAt: '2026-04-23T01:00:00Z',
|
|
35
|
-
attemptCount: 1,
|
|
36
|
-
maxAttempts: 3,
|
|
37
|
-
...overrides,
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
function makeRunHandle() {
|
|
41
|
-
return {
|
|
42
|
-
runId: RUN_ID,
|
|
43
|
-
runtimeKind: RUNTIME_KIND,
|
|
44
|
-
startedAt: '2026-04-23T00:00:00Z',
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
function makeContextPayload(overrides = {}) {
|
|
48
|
-
return {
|
|
49
|
-
contextId: 'ctx-001',
|
|
50
|
-
contextHash: 'abc123hash',
|
|
51
|
-
taskId: TASK_ID,
|
|
52
|
-
workspaceDir: '/test/workspace',
|
|
53
|
-
sourceRefs: ['trajectory-001'],
|
|
54
|
-
diagnosisTarget: {
|
|
55
|
-
reasonSummary: 'test diagnosis',
|
|
56
|
-
},
|
|
57
|
-
conversationWindow: [
|
|
58
|
-
{ ts: '2026-04-23T00:00:00Z', role: 'user', text: 'hello' },
|
|
59
|
-
],
|
|
60
|
-
...overrides,
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
function makeDiagnosticianOutput(overrides = {}) {
|
|
64
|
-
return {
|
|
65
|
-
valid: true,
|
|
66
|
-
diagnosisId: 'diag-001',
|
|
67
|
-
summary: 'Test diagnosis summary',
|
|
68
|
-
rootCause: 'Test root cause',
|
|
69
|
-
violatedPrinciples: [],
|
|
70
|
-
evidence: [],
|
|
71
|
-
recommendations: [],
|
|
72
|
-
confidence: 0.9,
|
|
73
|
-
...overrides,
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
function createMocks() {
|
|
77
|
-
const taskRecord = makeTaskRecord();
|
|
78
|
-
const runHandle = makeRunHandle();
|
|
79
|
-
const contextPayload = makeContextPayload();
|
|
80
|
-
const output = makeDiagnosticianOutput();
|
|
81
|
-
const mockStateManager = {
|
|
82
|
-
acquireLease: vi.fn().mockResolvedValue(taskRecord),
|
|
83
|
-
markTaskSucceeded: vi.fn().mockResolvedValue(taskRecord),
|
|
84
|
-
markTaskFailed: vi.fn().mockResolvedValue(taskRecord),
|
|
85
|
-
markTaskRetryWait: vi.fn().mockResolvedValue(taskRecord),
|
|
86
|
-
updateRunOutput: vi.fn().mockResolvedValue({}),
|
|
87
|
-
getRetryPolicy: vi.fn().mockReturnValue({
|
|
88
|
-
calculateBackoff: vi.fn().mockReturnValue(30_000),
|
|
89
|
-
shouldRetry: vi.fn().mockReturnValue(true),
|
|
90
|
-
}),
|
|
91
|
-
getRunsByTask: vi.fn().mockResolvedValue([{ runId: RUN_ID, taskId: TASK_ID }]),
|
|
92
|
-
};
|
|
93
|
-
const mockContextAssembler = {
|
|
94
|
-
assemble: vi.fn().mockResolvedValue(contextPayload),
|
|
95
|
-
};
|
|
96
|
-
const mockRuntimeAdapter = {
|
|
97
|
-
kind: vi.fn().mockReturnValue(RUNTIME_KIND),
|
|
98
|
-
getCapabilities: vi.fn(),
|
|
99
|
-
healthCheck: vi.fn(),
|
|
100
|
-
startRun: vi.fn().mockResolvedValue(runHandle),
|
|
101
|
-
pollRun: vi.fn().mockResolvedValue({
|
|
102
|
-
runId: RUN_ID,
|
|
103
|
-
status: 'succeeded',
|
|
104
|
-
}),
|
|
105
|
-
cancelRun: vi.fn().mockResolvedValue(undefined),
|
|
106
|
-
fetchOutput: vi.fn().mockResolvedValue({
|
|
107
|
-
runId: RUN_ID,
|
|
108
|
-
payload: output,
|
|
109
|
-
}),
|
|
110
|
-
fetchArtifacts: vi.fn(),
|
|
111
|
-
};
|
|
112
|
-
const mockValidator = {
|
|
113
|
-
validate: vi.fn().mockResolvedValue({
|
|
114
|
-
valid: true,
|
|
115
|
-
errors: [],
|
|
116
|
-
}),
|
|
117
|
-
};
|
|
118
|
-
const mockEventEmitter = {
|
|
119
|
-
emitTelemetry: vi.fn().mockReturnValue(true),
|
|
120
|
-
on: vi.fn(),
|
|
121
|
-
emit: vi.fn(),
|
|
122
|
-
};
|
|
123
|
-
return {
|
|
124
|
-
mockStateManager: mockStateManager,
|
|
125
|
-
mockContextAssembler: mockContextAssembler,
|
|
126
|
-
mockRuntimeAdapter: mockRuntimeAdapter,
|
|
127
|
-
mockValidator: mockValidator,
|
|
128
|
-
mockEventEmitter: mockEventEmitter,
|
|
129
|
-
taskRecord,
|
|
130
|
-
runHandle,
|
|
131
|
-
contextPayload,
|
|
132
|
-
output,
|
|
133
|
-
// Expose typed mocks for assertions
|
|
134
|
-
_stateManager: mockStateManager,
|
|
135
|
-
_contextAssembler: mockContextAssembler,
|
|
136
|
-
_runtimeAdapter: mockRuntimeAdapter,
|
|
137
|
-
_validator: mockValidator,
|
|
138
|
-
_committer: { commit: vi.fn().mockResolvedValue({ commitId: "mock-commit-id", artifactId: "mock-artifact-id", candidateCount: 0 }) },
|
|
139
|
-
_eventEmitter: mockEventEmitter,
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
function createRunner(mocks) {
|
|
143
|
-
return new DiagnosticianRunner({
|
|
144
|
-
stateManager: mocks.mockStateManager,
|
|
145
|
-
contextAssembler: mocks.mockContextAssembler,
|
|
146
|
-
runtimeAdapter: mocks.mockRuntimeAdapter,
|
|
147
|
-
eventEmitter: mocks.mockEventEmitter,
|
|
148
|
-
validator: mocks.mockValidator,
|
|
149
|
-
committer: mocks._committer,
|
|
150
|
-
}, {
|
|
151
|
-
owner: OWNER,
|
|
152
|
-
runtimeKind: RUNTIME_KIND,
|
|
153
|
-
pollIntervalMs: 100,
|
|
154
|
-
timeoutMs: 1000,
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
/** Type-safe helper to extract the first call argument from a mock. */
|
|
158
|
-
function firstCallArg(mockFn) {
|
|
159
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
160
|
-
return mockFn.mock.calls[0][0];
|
|
161
|
-
}
|
|
162
|
-
// ── Tests ──────────────────────────────────────────────────────────────────────
|
|
163
|
-
describe('DiagnosticianRunner', () => {
|
|
164
|
-
beforeEach(() => {
|
|
165
|
-
vi.useFakeTimers({ shouldAdvanceTime: true });
|
|
166
|
-
});
|
|
167
|
-
afterEach(() => {
|
|
168
|
-
vi.useRealTimers();
|
|
169
|
-
});
|
|
170
|
-
// 1. Happy path
|
|
171
|
-
it('succeeds end-to-end when all phases complete normally', async () => {
|
|
172
|
-
const mocks = createMocks();
|
|
173
|
-
const runner = createRunner(mocks);
|
|
174
|
-
const result = await runner.run(TASK_ID);
|
|
175
|
-
expect(result.status).toBe('succeeded');
|
|
176
|
-
expect(result.taskId).toBe(TASK_ID);
|
|
177
|
-
expect(result.output).toBeDefined();
|
|
178
|
-
expect(result.output?.diagnosisId).toBe('diag-001');
|
|
179
|
-
expect(result.contextHash).toBe('abc123hash');
|
|
180
|
-
expect(result.attemptCount).toBe(1);
|
|
181
|
-
expect(mocks._stateManager.acquireLease).toHaveBeenCalledWith({
|
|
182
|
-
taskId: TASK_ID,
|
|
183
|
-
owner: OWNER,
|
|
184
|
-
runtimeKind: RUNTIME_KIND,
|
|
185
|
-
});
|
|
186
|
-
expect(mocks._stateManager.markTaskSucceeded).toHaveBeenCalledWith(TASK_ID, 'commit://mock-commit-id');
|
|
187
|
-
expect(mocks._stateManager.updateRunOutput).toHaveBeenCalledWith(RUN_ID, JSON.stringify(mocks.output));
|
|
188
|
-
});
|
|
189
|
-
// 2. Polling loop
|
|
190
|
-
it('polls until terminal status is reached', async () => {
|
|
191
|
-
const mocks = createMocks();
|
|
192
|
-
// First poll: running, second poll: succeeded
|
|
193
|
-
mocks._runtimeAdapter.pollRun
|
|
194
|
-
.mockResolvedValueOnce({ runId: RUN_ID, status: 'running' })
|
|
195
|
-
.mockResolvedValueOnce({ runId: RUN_ID, status: 'succeeded' });
|
|
196
|
-
const runner = createRunner(mocks);
|
|
197
|
-
const resultPromise = runner.run(TASK_ID);
|
|
198
|
-
// Advance through the first sleep
|
|
199
|
-
await vi.advanceTimersByTimeAsync(200);
|
|
200
|
-
const result = await resultPromise;
|
|
201
|
-
expect(result.status).toBe('succeeded');
|
|
202
|
-
expect(mocks._runtimeAdapter.pollRun).toHaveBeenCalledTimes(2);
|
|
203
|
-
});
|
|
204
|
-
// 3. Timeout
|
|
205
|
-
it('cancels run and retries on timeout', async () => {
|
|
206
|
-
const mocks = createMocks();
|
|
207
|
-
// pollRun always returns 'running'
|
|
208
|
-
mocks._runtimeAdapter.pollRun.mockResolvedValue({ runId: RUN_ID, status: 'running' });
|
|
209
|
-
const runner = createRunner(mocks);
|
|
210
|
-
const resultPromise = runner.run(TASK_ID);
|
|
211
|
-
// Advance past timeout
|
|
212
|
-
await vi.advanceTimersByTimeAsync(1500);
|
|
213
|
-
const result = await resultPromise;
|
|
214
|
-
expect(mocks._runtimeAdapter.cancelRun).toHaveBeenCalledWith(RUN_ID);
|
|
215
|
-
// shouldRetry returns true by default, so it should be retried
|
|
216
|
-
expect(result.status).toBe('retried');
|
|
217
|
-
expect(result.errorCategory).toBe('timeout');
|
|
218
|
-
});
|
|
219
|
-
// 4. Runtime failure
|
|
220
|
-
it('handles runtime failure and retries if policy allows', async () => {
|
|
221
|
-
const mocks = createMocks();
|
|
222
|
-
mocks._runtimeAdapter.pollRun.mockResolvedValue({
|
|
223
|
-
runId: RUN_ID,
|
|
224
|
-
status: 'failed',
|
|
225
|
-
reason: 'agent error',
|
|
226
|
-
});
|
|
227
|
-
const runner = createRunner(mocks);
|
|
228
|
-
const result = await runner.run(TASK_ID);
|
|
229
|
-
expect(result.status).toBe('retried');
|
|
230
|
-
expect(result.errorCategory).toBe('execution_failed');
|
|
231
|
-
expect(result.failureReason).toContain('failed');
|
|
232
|
-
});
|
|
233
|
-
// 5. Context build failure (transient)
|
|
234
|
-
it('retries on transient context assembly error', async () => {
|
|
235
|
-
const mocks = createMocks();
|
|
236
|
-
mocks._contextAssembler.assemble.mockRejectedValue(new PDRuntimeError('runtime_unavailable', 'DB connection lost'));
|
|
237
|
-
const runner = createRunner(mocks);
|
|
238
|
-
const result = await runner.run(TASK_ID);
|
|
239
|
-
expect(mocks._stateManager.markTaskRetryWait).toHaveBeenCalledWith(TASK_ID, 'runtime_unavailable');
|
|
240
|
-
expect(result.status).toBe('retried');
|
|
241
|
-
expect(result.errorCategory).toBe('runtime_unavailable');
|
|
242
|
-
});
|
|
243
|
-
// 6. Context build failure (permanent)
|
|
244
|
-
it('fails permanently on workspace_invalid error', async () => {
|
|
245
|
-
const mocks = createMocks();
|
|
246
|
-
mocks._contextAssembler.assemble.mockRejectedValue(new PDRuntimeError('workspace_invalid', 'Workspace not found'));
|
|
247
|
-
const runner = createRunner(mocks);
|
|
248
|
-
const result = await runner.run(TASK_ID);
|
|
249
|
-
expect(mocks._stateManager.markTaskFailed).toHaveBeenCalledWith(TASK_ID, 'workspace_invalid');
|
|
250
|
-
expect(result.status).toBe('failed');
|
|
251
|
-
expect(result.errorCategory).toBe('workspace_invalid');
|
|
252
|
-
});
|
|
253
|
-
// 7. Validation failure
|
|
254
|
-
it('retries or fails on validation failure', async () => {
|
|
255
|
-
const mocks = createMocks();
|
|
256
|
-
mocks._validator.validate.mockResolvedValue({
|
|
257
|
-
valid: false,
|
|
258
|
-
errors: ['Missing taskId', 'Invalid confidence'],
|
|
259
|
-
errorCategory: 'output_invalid',
|
|
260
|
-
});
|
|
261
|
-
const runner = createRunner(mocks);
|
|
262
|
-
const result = await runner.run(TASK_ID);
|
|
263
|
-
// Default shouldRetry returns true
|
|
264
|
-
expect(result.status).toBe('retried');
|
|
265
|
-
expect(result.errorCategory).toBe('output_invalid');
|
|
266
|
-
expect(result.failureReason).toContain('Validation failed');
|
|
267
|
-
});
|
|
268
|
-
// 8b. StartRunInput construction with custom agentId
|
|
269
|
-
it('uses custom agentId from resolvedOptions when provided', async () => {
|
|
270
|
-
const mocks = createMocks();
|
|
271
|
-
const runner = new DiagnosticianRunner({
|
|
272
|
-
stateManager: mocks.mockStateManager,
|
|
273
|
-
contextAssembler: mocks.mockContextAssembler,
|
|
274
|
-
runtimeAdapter: mocks.mockRuntimeAdapter,
|
|
275
|
-
eventEmitter: mocks.mockEventEmitter,
|
|
276
|
-
validator: mocks.mockValidator,
|
|
277
|
-
committer: mocks._committer,
|
|
278
|
-
}, {
|
|
279
|
-
owner: OWNER,
|
|
280
|
-
runtimeKind: RUNTIME_KIND,
|
|
281
|
-
pollIntervalMs: 100,
|
|
282
|
-
timeoutMs: 1000,
|
|
283
|
-
agentId: 'custom-diagnostician',
|
|
284
|
-
});
|
|
285
|
-
await runner.run(TASK_ID);
|
|
286
|
-
const startInput = firstCallArg(mocks._runtimeAdapter.startRun);
|
|
287
|
-
expect(startInput.agentSpec.agentId).toBe('custom-diagnostician');
|
|
288
|
-
});
|
|
289
|
-
// 8c. StartRunInput uses default 'main' when agentId not provided
|
|
290
|
-
it('defaults agentSpec.agentId to main when agentId not provided', async () => {
|
|
291
|
-
const mocks = createMocks();
|
|
292
|
-
const runner = createRunner(mocks);
|
|
293
|
-
await runner.run(TASK_ID);
|
|
294
|
-
const startInput = firstCallArg(mocks._runtimeAdapter.startRun);
|
|
295
|
-
expect(startInput.agentSpec.agentId).toBe('main');
|
|
296
|
-
});
|
|
297
|
-
// 9. Lease conflict
|
|
298
|
-
it('lease_conflict returns non-mutating result without markTaskRetryWait/markTaskFailed', async () => {
|
|
299
|
-
const mocks = createMocks();
|
|
300
|
-
mocks._stateManager.acquireLease.mockRejectedValue(new PDRuntimeError('lease_conflict', 'Task already leased'));
|
|
301
|
-
const runner = createRunner(mocks);
|
|
302
|
-
const result = await runner.run(TASK_ID);
|
|
303
|
-
// lease_conflict is handled as non-mutating — no state changes
|
|
304
|
-
expect(result.status).toBe('failed');
|
|
305
|
-
expect(result.errorCategory).toBe('lease_conflict');
|
|
306
|
-
// Mutation methods must NOT be called for lease_conflict
|
|
307
|
-
expect(mocks._stateManager.markTaskRetryWait).not.toHaveBeenCalled();
|
|
308
|
-
expect(mocks._stateManager.markTaskFailed).not.toHaveBeenCalled();
|
|
309
|
-
});
|
|
310
|
-
// 9b. Post-lease errors use real leasedTask (not synthetic)
|
|
311
|
-
it('post-lease error uses real attemptCount/maxAttempts from leasedTask', async () => {
|
|
312
|
-
const mocks = createMocks();
|
|
313
|
-
// leasedTask has attemptCount=2, maxAttempts=3 (not the default 1/3)
|
|
314
|
-
mocks._stateManager.acquireLease.mockResolvedValue(makeTaskRecord({ attemptCount: 2, maxAttempts: 3 }));
|
|
315
|
-
// Make context assembly fail to trigger post-lease error path
|
|
316
|
-
mocks._contextAssembler.assemble.mockRejectedValue(new PDRuntimeError('runtime_unavailable', 'DB connection lost'));
|
|
317
|
-
const runner = createRunner(mocks);
|
|
318
|
-
const result = await runner.run(TASK_ID);
|
|
319
|
-
// Should retry (not fail permanently) because runtime_unavailable is not permanent
|
|
320
|
-
// and real leasedTask (attemptCount=2, maxAttempts=3) means shouldRetry returns true
|
|
321
|
-
expect(result.status).toBe('retried');
|
|
322
|
-
// Verify markTaskRetryWait was called with the real task's attemptCount
|
|
323
|
-
expect(mocks._stateManager.markTaskRetryWait).toHaveBeenCalledWith(TASK_ID, 'runtime_unavailable');
|
|
324
|
-
});
|
|
325
|
-
// 9c. Post-lease error with maxAttempts=1 triggers fail immediately
|
|
326
|
-
it('post-lease error with attemptCount=maxAttempts fails permanently', async () => {
|
|
327
|
-
const mocks = createMocks();
|
|
328
|
-
// leasedTask at last attempt (attemptCount=3, maxAttempts=3)
|
|
329
|
-
mocks._stateManager.acquireLease.mockResolvedValue(makeTaskRecord({ attemptCount: 3, maxAttempts: 3 }));
|
|
330
|
-
// Override shouldRetry to return false when attemptCount >= maxAttempts
|
|
331
|
-
mocks._stateManager.getRetryPolicy.mockReturnValue({
|
|
332
|
-
calculateBackoff: vi.fn().mockReturnValue(30_000),
|
|
333
|
-
shouldRetry: vi.fn().mockReturnValue(false),
|
|
334
|
-
});
|
|
335
|
-
// Make context assembly fail
|
|
336
|
-
mocks._contextAssembler.assemble.mockRejectedValue(new PDRuntimeError('runtime_unavailable', 'DB connection lost'));
|
|
337
|
-
const runner = createRunner(mocks);
|
|
338
|
-
const result = await runner.run(TASK_ID);
|
|
339
|
-
// Should fail with max_attempts_exceeded (not runtime_unavailable)
|
|
340
|
-
// because shouldRetry=false means no more retries
|
|
341
|
-
expect(result.status).toBe('failed');
|
|
342
|
-
expect(result.errorCategory).toBe('max_attempts_exceeded');
|
|
343
|
-
expect(mocks._stateManager.markTaskFailed).toHaveBeenCalledWith(TASK_ID, 'max_attempts_exceeded');
|
|
344
|
-
});
|
|
345
|
-
// 10. Max attempts exceeded
|
|
346
|
-
it('marks task failed when max attempts exceeded', async () => {
|
|
347
|
-
const mocks = createMocks();
|
|
348
|
-
// Make shouldRetry return false
|
|
349
|
-
mocks._stateManager.getRetryPolicy.mockReturnValue({
|
|
350
|
-
calculateBackoff: vi.fn().mockReturnValue(30_000),
|
|
351
|
-
shouldRetry: vi.fn().mockReturnValue(false),
|
|
352
|
-
});
|
|
353
|
-
// Trigger a failure path (runtime failure)
|
|
354
|
-
mocks._runtimeAdapter.pollRun.mockResolvedValue({
|
|
355
|
-
runId: RUN_ID,
|
|
356
|
-
status: 'failed',
|
|
357
|
-
reason: 'agent error',
|
|
358
|
-
});
|
|
359
|
-
const runner = createRunner(mocks);
|
|
360
|
-
const result = await runner.run(TASK_ID);
|
|
361
|
-
expect(mocks._stateManager.markTaskFailed).toHaveBeenCalledWith(TASK_ID, 'max_attempts_exceeded');
|
|
362
|
-
expect(result.status).toBe('failed');
|
|
363
|
-
expect(result.errorCategory).toBe('max_attempts_exceeded');
|
|
364
|
-
});
|
|
365
|
-
// 11. OpenClaw history compatibility
|
|
366
|
-
it('passes through openclaw-history context without inspection', async () => {
|
|
367
|
-
const mocks = createMocks();
|
|
368
|
-
// Context with mixed runtime_kind entries (openclaw-imported format)
|
|
369
|
-
const openClawContext = makeContextPayload({
|
|
370
|
-
sourceRefs: ['trajectory-001', 'openclaw-history-import-002'],
|
|
371
|
-
conversationWindow: [
|
|
372
|
-
{ ts: '2026-04-23T00:00:00Z', role: 'user', text: 'hello from openclaw' },
|
|
373
|
-
{ ts: '2026-04-23T00:01:00Z', role: 'assistant', text: 'response from openclaw' },
|
|
374
|
-
{ ts: '2026-04-23T00:02:00Z', role: 'tool', toolName: 'Write', toolResultSummary: 'wrote file' },
|
|
375
|
-
],
|
|
376
|
-
});
|
|
377
|
-
mocks._contextAssembler.assemble.mockResolvedValue(openClawContext);
|
|
378
|
-
const runner = createRunner(mocks);
|
|
379
|
-
const result = await runner.run(TASK_ID);
|
|
380
|
-
// Runner should NOT reject the context
|
|
381
|
-
expect(result.status).toBe('succeeded');
|
|
382
|
-
// startRun should have been called with the context serialized in inputPayload
|
|
383
|
-
const startInput = firstCallArg(mocks._runtimeAdapter.startRun);
|
|
384
|
-
const inputPayloadStr = startInput.inputPayload;
|
|
385
|
-
const inputPayload = JSON.parse(inputPayloadStr);
|
|
386
|
-
expect(inputPayload.context.conversationWindow).toHaveLength(3);
|
|
387
|
-
expect(inputPayload.context.sourceRefs).toContain('openclaw-history-import-002');
|
|
388
|
-
// contextItems should be empty (instruction embedded in inputPayload)
|
|
389
|
-
expect(startInput.contextItems).toHaveLength(0);
|
|
390
|
-
});
|
|
391
|
-
// ── Committer integration tests (m5-03 Task 5) ──────────────────────────────
|
|
392
|
-
describe('Committer integration', () => {
|
|
393
|
-
// 12. Commit before markTaskSucceeded (call order)
|
|
394
|
-
it('calls committer.commit before markTaskSucceeded', async () => {
|
|
395
|
-
const mocks = createMocks();
|
|
396
|
-
const callOrder = [];
|
|
397
|
-
const typedCommitter = mocks._committer;
|
|
398
|
-
typedCommitter.commit.mockImplementation(async () => {
|
|
399
|
-
callOrder.push('commit');
|
|
400
|
-
return { commitId: 'call-order-commit-id', artifactId: 'art-1', candidateCount: 1 };
|
|
401
|
-
});
|
|
402
|
-
mocks._stateManager.markTaskSucceeded.mockImplementation(async () => {
|
|
403
|
-
callOrder.push('markTaskSucceeded');
|
|
404
|
-
return mocks.taskRecord;
|
|
405
|
-
});
|
|
406
|
-
const runner = createRunner(mocks);
|
|
407
|
-
await runner.run(TASK_ID);
|
|
408
|
-
expect(callOrder).toEqual(['commit', 'markTaskSucceeded']);
|
|
409
|
-
});
|
|
410
|
-
// 13. resultRef uses commit:// scheme
|
|
411
|
-
it('resultRef uses commit:// scheme after commit', async () => {
|
|
412
|
-
const mocks = createMocks();
|
|
413
|
-
const typedCommitter = mocks._committer;
|
|
414
|
-
typedCommitter.commit.mockResolvedValue({
|
|
415
|
-
commitId: 'verify-commit-123',
|
|
416
|
-
artifactId: 'art-verify',
|
|
417
|
-
candidateCount: 3,
|
|
418
|
-
});
|
|
419
|
-
const runner = createRunner(mocks);
|
|
420
|
-
await runner.run(TASK_ID);
|
|
421
|
-
expect(mocks._stateManager.markTaskSucceeded).toHaveBeenCalledWith(TASK_ID, 'commit://verify-commit-123');
|
|
422
|
-
});
|
|
423
|
-
// 14. Commit failure triggers retry with artifact_commit_failed
|
|
424
|
-
it('commit failure triggers retry with artifact_commit_failed', async () => {
|
|
425
|
-
const mocks = createMocks();
|
|
426
|
-
const typedCommitter = mocks._committer;
|
|
427
|
-
typedCommitter.commit.mockRejectedValue(new PDRuntimeError('artifact_commit_failed', 'Commit failed', {}));
|
|
428
|
-
const runner = createRunner(mocks);
|
|
429
|
-
const result = await runner.run(TASK_ID);
|
|
430
|
-
expect(result.status).toBe('retried');
|
|
431
|
-
expect(result.errorCategory).toBe('artifact_commit_failed');
|
|
432
|
-
expect(mocks._stateManager.markTaskSucceeded).not.toHaveBeenCalled();
|
|
433
|
-
expect(mocks._stateManager.markTaskRetryWait).toHaveBeenCalledWith(TASK_ID, 'artifact_commit_failed');
|
|
434
|
-
});
|
|
435
|
-
// 15. Commit failure with max attempts marks task failed
|
|
436
|
-
it('commit failure with max attempts marks task failed', async () => {
|
|
437
|
-
const mocks = createMocks();
|
|
438
|
-
const typedCommitter = mocks._committer;
|
|
439
|
-
typedCommitter.commit.mockRejectedValue(new PDRuntimeError('artifact_commit_failed', 'Commit failed', {}));
|
|
440
|
-
mocks._stateManager.getRetryPolicy.mockReturnValue({
|
|
441
|
-
calculateBackoff: vi.fn().mockReturnValue(30_000),
|
|
442
|
-
shouldRetry: vi.fn().mockReturnValue(false),
|
|
443
|
-
});
|
|
444
|
-
const runner = createRunner(mocks);
|
|
445
|
-
const result = await runner.run(TASK_ID);
|
|
446
|
-
expect(result.status).toBe('failed');
|
|
447
|
-
expect(result.errorCategory).toBe('max_attempts_exceeded');
|
|
448
|
-
expect(mocks._stateManager.markTaskFailed).toHaveBeenCalledWith(TASK_ID, 'max_attempts_exceeded');
|
|
449
|
-
});
|
|
450
|
-
// 16. RunnerPhase transitions through Committing
|
|
451
|
-
it('RunnerPhase transitions through Committing during commit', async () => {
|
|
452
|
-
const mocks = createMocks();
|
|
453
|
-
const typedCommitter = mocks._committer;
|
|
454
|
-
typedCommitter.commit.mockResolvedValue({
|
|
455
|
-
commitId: 'phase-test-commit',
|
|
456
|
-
artifactId: 'art-phase',
|
|
457
|
-
candidateCount: 0,
|
|
458
|
-
});
|
|
459
|
-
const runner = createRunner(mocks);
|
|
460
|
-
const result = await runner.run(TASK_ID);
|
|
461
|
-
expect(result.status).toBe('succeeded');
|
|
462
|
-
expect(typedCommitter.commit).toHaveBeenCalledTimes(1);
|
|
463
|
-
expect(runner.currentPhase).toBe(RunnerPhase.Completed);
|
|
464
|
-
});
|
|
465
|
-
});
|
|
466
|
-
// ── M5-04: Telemetry events ───────────────────────────────────────────────
|
|
467
|
-
describe('M5-04 Telemetry events', () => {
|
|
468
|
-
// TELE-01: diagnostician_artifact_committed emitted after successful commit
|
|
469
|
-
it('emits diagnostician_artifact_committed after successful commit', async () => {
|
|
470
|
-
const mocks = createMocks();
|
|
471
|
-
const typedCommitter = mocks._committer;
|
|
472
|
-
typedCommitter.commit.mockResolvedValue({
|
|
473
|
-
commitId: 'commit-artifact-001',
|
|
474
|
-
artifactId: 'artifact-001',
|
|
475
|
-
candidateCount: 2,
|
|
476
|
-
});
|
|
477
|
-
const runner = createRunner(mocks);
|
|
478
|
-
await runner.run(TASK_ID);
|
|
479
|
-
expect(mocks._eventEmitter.emitTelemetry).toHaveBeenCalledWith(expect.objectContaining({
|
|
480
|
-
eventType: 'diagnostician_artifact_committed',
|
|
481
|
-
traceId: TASK_ID,
|
|
482
|
-
payload: expect.objectContaining({
|
|
483
|
-
commitId: 'commit-artifact-001',
|
|
484
|
-
artifactId: 'artifact-001',
|
|
485
|
-
candidateCount: 2,
|
|
486
|
-
taskId: TASK_ID,
|
|
487
|
-
runId: RUN_ID,
|
|
488
|
-
}),
|
|
489
|
-
}));
|
|
490
|
-
});
|
|
491
|
-
// TELE-02: diagnostician_artifact_commit_failed emitted when commit throws
|
|
492
|
-
it('emits diagnostician_artifact_commit_failed when commit throws', async () => {
|
|
493
|
-
const mocks = createMocks();
|
|
494
|
-
const typedCommitter = mocks._committer;
|
|
495
|
-
typedCommitter.commit.mockRejectedValue(new PDRuntimeError('artifact_commit_failed', 'Database constraint violation', {}));
|
|
496
|
-
const runner = createRunner(mocks);
|
|
497
|
-
await runner.run(TASK_ID);
|
|
498
|
-
expect(mocks._eventEmitter.emitTelemetry).toHaveBeenCalledWith(expect.objectContaining({
|
|
499
|
-
eventType: 'diagnostician_artifact_commit_failed',
|
|
500
|
-
traceId: TASK_ID,
|
|
501
|
-
payload: expect.objectContaining({
|
|
502
|
-
taskId: TASK_ID,
|
|
503
|
-
runId: RUN_ID,
|
|
504
|
-
errorCategory: 'artifact_commit_failed',
|
|
505
|
-
}),
|
|
506
|
-
}));
|
|
507
|
-
});
|
|
508
|
-
// TELE-03: principle_candidate_registered emitted per principle candidate
|
|
509
|
-
it('emits principle_candidate_registered for each principle recommendation', async () => {
|
|
510
|
-
const mocks = createMocks();
|
|
511
|
-
const typedCommitter = mocks._committer;
|
|
512
|
-
typedCommitter.commit.mockResolvedValue({
|
|
513
|
-
commitId: 'commit-candidate-001',
|
|
514
|
-
artifactId: 'artifact-001',
|
|
515
|
-
candidateCount: 2,
|
|
516
|
-
});
|
|
517
|
-
// Override output with 2 principle recommendations
|
|
518
|
-
const output = makeDiagnosticianOutput({
|
|
519
|
-
recommendations: [
|
|
520
|
-
{ kind: 'principle', description: 'Use immutable data structures' },
|
|
521
|
-
{ kind: 'principle', description: 'Prefer pure functions' },
|
|
522
|
-
{ kind: 'rule', description: 'Follow existing naming conventions' },
|
|
523
|
-
],
|
|
524
|
-
});
|
|
525
|
-
mocks._runtimeAdapter.fetchOutput = vi.fn().mockResolvedValue({
|
|
526
|
-
runId: RUN_ID,
|
|
527
|
-
payload: output,
|
|
528
|
-
});
|
|
529
|
-
const runner = createRunner(mocks);
|
|
530
|
-
await runner.run(TASK_ID);
|
|
531
|
-
// Should have 2 calls to principle_candidate_registered
|
|
532
|
-
const candidateEvents = mocks._eventEmitter.emitTelemetry.mock.calls.filter((call) => call[0]?.eventType === 'principle_candidate_registered');
|
|
533
|
-
expect(candidateEvents).toHaveLength(2);
|
|
534
|
-
const [firstEvent, secondEvent] = candidateEvents;
|
|
535
|
-
if (!firstEvent || !secondEvent) {
|
|
536
|
-
throw new Error('Expected 2 candidate events but got fewer');
|
|
537
|
-
}
|
|
538
|
-
expect(firstEvent[0]?.payload).toMatchObject({
|
|
539
|
-
commitId: 'commit-candidate-001',
|
|
540
|
-
kind: 'principle',
|
|
541
|
-
candidateIndex: 0,
|
|
542
|
-
sourceRunId: RUN_ID,
|
|
543
|
-
});
|
|
544
|
-
expect(secondEvent[0]?.payload).toMatchObject({
|
|
545
|
-
commitId: 'commit-candidate-001',
|
|
546
|
-
kind: 'principle',
|
|
547
|
-
candidateIndex: 1,
|
|
548
|
-
sourceRunId: RUN_ID,
|
|
549
|
-
});
|
|
550
|
-
});
|
|
551
|
-
// TELE-04: all events use emitTelemetry (StoreEventEmitter)
|
|
552
|
-
it('all new telemetry events use emitTelemetry, not console.log or side effects', async () => {
|
|
553
|
-
const mocks = createMocks();
|
|
554
|
-
const typedCommitter = mocks._committer;
|
|
555
|
-
typedCommitter.commit.mockResolvedValue({
|
|
556
|
-
commitId: 'commit-tele-004',
|
|
557
|
-
artifactId: 'artifact-004',
|
|
558
|
-
candidateCount: 1,
|
|
559
|
-
});
|
|
560
|
-
// Override output with 1 principle recommendation so the event fires
|
|
561
|
-
const output = makeDiagnosticianOutput({
|
|
562
|
-
recommendations: [
|
|
563
|
-
{ kind: 'principle', description: 'Test principle' },
|
|
564
|
-
],
|
|
565
|
-
});
|
|
566
|
-
mocks._runtimeAdapter.fetchOutput = vi.fn().mockResolvedValue({
|
|
567
|
-
runId: RUN_ID,
|
|
568
|
-
payload: output,
|
|
569
|
-
});
|
|
570
|
-
const runner = createRunner(mocks);
|
|
571
|
-
await runner.run(TASK_ID);
|
|
572
|
-
// All 3 new event types should go through emitTelemetry
|
|
573
|
-
const allEventTypes = mocks._eventEmitter.emitTelemetry.mock.calls.map((call) => call[0]?.eventType);
|
|
574
|
-
expect(allEventTypes).toContain('diagnostician_artifact_committed');
|
|
575
|
-
expect(allEventTypes).toContain('principle_candidate_registered');
|
|
576
|
-
});
|
|
577
|
-
});
|
|
578
|
-
// ── PRI-137: workspace_dirty as permanent error ──────────────────────────────────
|
|
579
|
-
describe('PRI-137: workspace_dirty permanent error handling', () => {
|
|
580
|
-
it('fails permanently on workspace_dirty error (never retries)', async () => {
|
|
581
|
-
const mocks = createMocks();
|
|
582
|
-
mocks._contextAssembler.assemble.mockRejectedValue(new PDRuntimeError('workspace_dirty', 'Workspace contains uncommitted changes', {
|
|
583
|
-
dirtyFiles: ['file1.ts', 'file2.ts'],
|
|
584
|
-
}));
|
|
585
|
-
const runner = createRunner(mocks);
|
|
586
|
-
const result = await runner.run(TASK_ID);
|
|
587
|
-
expect(result.status).toBe('failed');
|
|
588
|
-
expect(result.errorCategory).toBe('workspace_dirty');
|
|
589
|
-
expect(result.failureReason).toContain('workspace_dirty');
|
|
590
|
-
expect(mocks._stateManager.markTaskFailed).toHaveBeenCalledWith(TASK_ID, 'workspace_dirty');
|
|
591
|
-
expect(mocks._stateManager.markTaskRetryWait).not.toHaveBeenCalled();
|
|
592
|
-
});
|
|
593
|
-
it('workspace_dirty fails immediately even when shouldRetry returns true', async () => {
|
|
594
|
-
const mocks = createMocks();
|
|
595
|
-
mocks._contextAssembler.assemble.mockRejectedValue(new PDRuntimeError('workspace_dirty', 'Dirty workspace detected'));
|
|
596
|
-
mocks._stateManager.getRetryPolicy.mockReturnValue({
|
|
597
|
-
calculateBackoff: vi.fn().mockReturnValue(30_000),
|
|
598
|
-
shouldRetry: vi.fn().mockReturnValue(true),
|
|
599
|
-
});
|
|
600
|
-
const runner = createRunner(mocks);
|
|
601
|
-
const result = await runner.run(TASK_ID);
|
|
602
|
-
expect(result.status).toBe('failed');
|
|
603
|
-
expect(result.errorCategory).toBe('workspace_dirty');
|
|
604
|
-
expect(mocks._stateManager.markTaskRetryWait).not.toHaveBeenCalled();
|
|
605
|
-
expect(mocks._stateManager.markTaskFailed).toHaveBeenCalledWith(TASK_ID, 'workspace_dirty');
|
|
606
|
-
});
|
|
607
|
-
it('workspace_dirty is in PERMANENT_ERROR_CATEGORIES set', async () => {
|
|
608
|
-
const mocks = createMocks();
|
|
609
|
-
mocks._contextAssembler.assemble.mockRejectedValue(new PDRuntimeError('workspace_dirty', 'Workspace dirty'));
|
|
610
|
-
mocks._stateManager.getRetryPolicy.mockReturnValue({
|
|
611
|
-
calculateBackoff: vi.fn().mockReturnValue(30_000),
|
|
612
|
-
shouldRetry: vi.fn().mockReturnValue(false),
|
|
613
|
-
});
|
|
614
|
-
const runner = createRunner(mocks);
|
|
615
|
-
const result = await runner.run(TASK_ID);
|
|
616
|
-
expect(result.status).toBe('failed');
|
|
617
|
-
expect(result.errorCategory).toBe('workspace_dirty');
|
|
618
|
-
});
|
|
619
|
-
it('emits task_failed telemetry with workspace_dirty category', async () => {
|
|
620
|
-
const mocks = createMocks();
|
|
621
|
-
mocks._contextAssembler.assemble.mockRejectedValue(new PDRuntimeError('workspace_dirty', 'Uncommitted changes found'));
|
|
622
|
-
const runner = createRunner(mocks);
|
|
623
|
-
await runner.run(TASK_ID);
|
|
624
|
-
const allEventTypes = mocks._eventEmitter.emitTelemetry.mock.calls.map((call) => call[0]?.eventType);
|
|
625
|
-
expect(allEventTypes).toContain('diagnostician_task_failed');
|
|
626
|
-
const taskFailedEvents = mocks._eventEmitter.emitTelemetry.mock.calls
|
|
627
|
-
.filter((call) => call[0]?.eventType === 'diagnostician_task_failed');
|
|
628
|
-
expect(taskFailedEvents[0]?.[0]?.payload?.errorCategory).toBe('workspace_dirty');
|
|
629
|
-
});
|
|
630
|
-
it('capability_missing also fails permanently (part of permanent error set)', async () => {
|
|
631
|
-
const mocks = createMocks();
|
|
632
|
-
mocks._contextAssembler.assemble.mockRejectedValue(new PDRuntimeError('capability_missing', 'Required capability not available'));
|
|
633
|
-
const runner = createRunner(mocks);
|
|
634
|
-
const result = await runner.run(TASK_ID);
|
|
635
|
-
expect(result.status).toBe('failed');
|
|
636
|
-
expect(result.errorCategory).toBe('capability_missing');
|
|
637
|
-
expect(mocks._stateManager.markTaskFailed).toHaveBeenCalledWith(TASK_ID, 'capability_missing');
|
|
638
|
-
expect(mocks._stateManager.markTaskRetryWait).not.toHaveBeenCalled();
|
|
639
|
-
});
|
|
640
|
-
});
|
|
641
|
-
// ── ERR-001/005: Runner boundary schema validation ──────────────────────────
|
|
642
|
-
describe('Runner boundary schema validation (ERR-001/005)', () => {
|
|
643
|
-
it('rejects adapter payload that does not match DiagnosticianOutputV1Schema', async () => {
|
|
644
|
-
const mocks = createMocks();
|
|
645
|
-
// Adapter returns payload that bypasses its own validation (defense-in-depth test)
|
|
646
|
-
mocks._runtimeAdapter.fetchOutput = vi.fn().mockResolvedValue({
|
|
647
|
-
runId: RUN_ID,
|
|
648
|
-
payload: { wrong: 'shape', missing: 'all required fields' },
|
|
649
|
-
});
|
|
650
|
-
const runner = createRunner(mocks);
|
|
651
|
-
const result = await runner.run(TASK_ID);
|
|
652
|
-
// Should end up in retry_wait/output_invalid, not silently accepted
|
|
653
|
-
expect(result.status).toBe('retried');
|
|
654
|
-
expect(result.errorCategory).toBe('output_invalid');
|
|
655
|
-
});
|
|
656
|
-
it('includes evidence in output_invalid error when runner boundary rejects payload', async () => {
|
|
657
|
-
const mocks = createMocks();
|
|
658
|
-
mocks._runtimeAdapter.fetchOutput = vi.fn().mockResolvedValue({
|
|
659
|
-
runId: RUN_ID,
|
|
660
|
-
payload: { invalid: true },
|
|
661
|
-
});
|
|
662
|
-
const runner = createRunner(mocks);
|
|
663
|
-
const result = await runner.run(TASK_ID);
|
|
664
|
-
expect(result.status).toBe('retried');
|
|
665
|
-
expect(result.errorCategory).toBe('output_invalid');
|
|
666
|
-
// The retry_wait call should have been made with output_invalid
|
|
667
|
-
expect(mocks._stateManager.markTaskRetryWait).toHaveBeenCalledWith(TASK_ID, 'output_invalid');
|
|
668
|
-
});
|
|
669
|
-
it('accepts valid DiagnosticianOutputV1 payload through runner boundary', async () => {
|
|
670
|
-
const mocks = createMocks();
|
|
671
|
-
const validOutput = makeDiagnosticianOutput();
|
|
672
|
-
mocks._runtimeAdapter.fetchOutput = vi.fn().mockResolvedValue({
|
|
673
|
-
runId: RUN_ID,
|
|
674
|
-
payload: validOutput,
|
|
675
|
-
});
|
|
676
|
-
const runner = createRunner(mocks);
|
|
677
|
-
const result = await runner.run(TASK_ID);
|
|
678
|
-
expect(result.status).toBe('succeeded');
|
|
679
|
-
});
|
|
680
|
-
});
|
|
681
|
-
});
|
|
682
|
-
//# sourceMappingURL=diagnostician-runner.test.js.map
|