@artemiskit/core 0.3.0 → 0.4.1
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/CHANGELOG.md +17 -0
- package/README.md +4 -0
- package/dist/adapters/registry.d.ts.map +1 -1
- package/dist/adapters/types.d.ts +20 -2
- package/dist/adapters/types.d.ts.map +1 -1
- package/dist/agent-evaluation/index.d.ts +3 -0
- package/dist/agent-evaluation/index.d.ts.map +1 -0
- package/dist/agent-evaluation/scorer.d.ts +35 -0
- package/dist/agent-evaluation/scorer.d.ts.map +1 -0
- package/dist/agent-evaluation/types.d.ts +37 -0
- package/dist/agent-evaluation/types.d.ts.map +1 -0
- package/dist/artifacts/manifest.d.ts.map +1 -1
- package/dist/artifacts/types.d.ts +52 -0
- package/dist/artifacts/types.d.ts.map +1 -1
- package/dist/evaluators/index.d.ts +1 -0
- package/dist/evaluators/index.d.ts.map +1 -1
- package/dist/evaluators/json-schema.d.ts +0 -1
- package/dist/evaluators/json-schema.d.ts.map +1 -1
- package/dist/evaluators/llm-grader.d.ts +2 -0
- package/dist/evaluators/llm-grader.d.ts.map +1 -1
- package/dist/evaluators/tool-trace.d.ts +7 -0
- package/dist/evaluators/tool-trace.d.ts.map +1 -0
- package/dist/evaluators/types.d.ts +20 -0
- package/dist/evaluators/types.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20281 -13144
- package/dist/runner/executor.d.ts.map +1 -1
- package/dist/runner/runner.d.ts.map +1 -1
- package/dist/runner/types.d.ts +4 -0
- package/dist/runner/types.d.ts.map +1 -1
- package/dist/scenario/schema.d.ts +721 -63
- package/dist/scenario/schema.d.ts.map +1 -1
- package/dist/storage/local.d.ts +1 -1
- package/dist/storage/local.d.ts.map +1 -1
- package/dist/storage/supabase.d.ts +1 -1
- package/dist/storage/supabase.d.ts.map +1 -1
- package/dist/storage/types.d.ts +6 -2
- package/dist/storage/types.d.ts.map +1 -1
- package/dist/tools/fixture-executor.d.ts +10 -0
- package/dist/tools/fixture-executor.d.ts.map +1 -0
- package/dist/tools/index.d.ts +4 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/schema-validator.d.ts +10 -0
- package/dist/tools/schema-validator.d.ts.map +1 -0
- package/dist/tools/types.d.ts +50 -0
- package/dist/tools/types.d.ts.map +1 -0
- package/package.json +2 -1
- package/src/adapters/registry.ts +7 -0
- package/src/adapters/types.test.ts +21 -0
- package/src/adapters/types.ts +18 -0
- package/src/agent-evaluation/index.ts +2 -0
- package/src/agent-evaluation/scorer.test.ts +1194 -0
- package/src/agent-evaluation/scorer.ts +640 -0
- package/src/agent-evaluation/types.test.ts +27 -0
- package/src/agent-evaluation/types.ts +43 -0
- package/src/artifacts/manifest.test.ts +90 -19
- package/src/artifacts/manifest.ts +18 -5
- package/src/artifacts/types.ts +133 -0
- package/src/evaluators/index.ts +3 -0
- package/src/evaluators/json-schema.test.ts +130 -0
- package/src/evaluators/json-schema.ts +38 -63
- package/src/evaluators/llm-grader.test.ts +80 -0
- package/src/evaluators/llm-grader.ts +44 -6
- package/src/evaluators/tool-trace.test.ts +46 -0
- package/src/evaluators/tool-trace.ts +50 -0
- package/src/evaluators/types.ts +20 -0
- package/src/index.ts +6 -0
- package/src/runner/executor.test.ts +374 -0
- package/src/runner/executor.ts +349 -22
- package/src/runner/release-validation.test.ts +169 -0
- package/src/runner/runner.ts +7 -1
- package/src/runner/types.ts +4 -0
- package/src/scenario/schema.ts +56 -1
- package/src/storage/local.test.ts +24 -0
- package/src/storage/local.ts +13 -2
- package/src/storage/supabase.test.ts +111 -1
- package/src/storage/supabase.ts +26 -3
- package/src/storage/types.ts +12 -2
- package/src/tools/fixture-executor.test.ts +88 -0
- package/src/tools/fixture-executor.ts +112 -0
- package/src/tools/index.ts +3 -0
- package/src/tools/schema-validator.test.ts +32 -0
- package/src/tools/schema-validator.ts +56 -0
- package/src/tools/types.ts +80 -0
- package/adapters/openai/dist/index.js +0 -5626
|
@@ -0,0 +1,1194 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
import {
|
|
3
|
+
type AgentAcceptanceCheck,
|
|
4
|
+
type AgentArtifactCheck,
|
|
5
|
+
type AgentEvaluationEvidence,
|
|
6
|
+
type AgentTerminationStatus,
|
|
7
|
+
scoreAgentOutcome,
|
|
8
|
+
} from './scorer';
|
|
9
|
+
import type { AgentOutcome, AgentTask } from './types';
|
|
10
|
+
|
|
11
|
+
const task: AgentTask = {
|
|
12
|
+
id: 'scenario-repair',
|
|
13
|
+
fixturePath: 'fixture',
|
|
14
|
+
allowedPaths: ['scenario.yaml'],
|
|
15
|
+
allowedTools: ['workspace_read', 'workspace_patch', 'workspace_run'],
|
|
16
|
+
maxActions: 8,
|
|
17
|
+
timeoutMs: 60_000,
|
|
18
|
+
acceptanceCommands: ['akit validate scenario.yaml'],
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function createOutcome(overrides: Partial<AgentOutcome> = {}): AgentOutcome {
|
|
22
|
+
return {
|
|
23
|
+
taskId: task.id,
|
|
24
|
+
completed: true,
|
|
25
|
+
acceptancePassed: true,
|
|
26
|
+
trace: {
|
|
27
|
+
taskId: task.id,
|
|
28
|
+
actions: [],
|
|
29
|
+
changedPaths: ['scenario.yaml'],
|
|
30
|
+
startedAt: '2026-08-21T00:00:00.000Z',
|
|
31
|
+
completedAt: '2026-08-21T00:00:01.000Z',
|
|
32
|
+
},
|
|
33
|
+
finalDiff: '- type: invalid_type\n+ type: contains',
|
|
34
|
+
...overrides,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function createEvidence(overrides: Partial<AgentEvaluationEvidence> = {}): AgentEvaluationEvidence {
|
|
39
|
+
return {
|
|
40
|
+
termination: { status: 'completed' },
|
|
41
|
+
acceptanceChecks: [
|
|
42
|
+
{
|
|
43
|
+
command: 'akit validate scenario.yaml',
|
|
44
|
+
status: 'passed',
|
|
45
|
+
exitCode: 0,
|
|
46
|
+
durationMs: 25,
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
...overrides,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function withoutKey(value: Record<string, unknown>, key: string): unknown {
|
|
54
|
+
const copy = { ...value };
|
|
55
|
+
delete copy[key];
|
|
56
|
+
return copy;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function scoreRuntimeValues(...values: unknown[]) {
|
|
60
|
+
const taskValue = values.length > 0 ? values[0] : task;
|
|
61
|
+
const outcomeValue = values.length > 1 ? values[1] : createOutcome();
|
|
62
|
+
const evidenceValue = values.length > 2 ? values[2] : createEvidence();
|
|
63
|
+
return scoreAgentOutcome(
|
|
64
|
+
taskValue as AgentTask,
|
|
65
|
+
outcomeValue as AgentOutcome,
|
|
66
|
+
evidenceValue as AgentEvaluationEvidence
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function expectInfrastructureInvalid(code: string, ...values: unknown[]) {
|
|
71
|
+
let result: ReturnType<typeof scoreAgentOutcome> | undefined;
|
|
72
|
+
|
|
73
|
+
expect(() => {
|
|
74
|
+
result = scoreRuntimeValues(...values);
|
|
75
|
+
}).not.toThrow();
|
|
76
|
+
expect(result?.verdict).toBe('infrastructure_failed');
|
|
77
|
+
expect(result?.passed).toBe(false);
|
|
78
|
+
expect(result?.issues.map((issue) => issue.code)).toContain(code);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
describe('scoreAgentOutcome', () => {
|
|
82
|
+
it('passes a completed task with successful observable checks', () => {
|
|
83
|
+
const result = scoreAgentOutcome(task, createOutcome(), createEvidence());
|
|
84
|
+
|
|
85
|
+
expect(result.verdict).toBe('passed');
|
|
86
|
+
expect(result.passed).toBe(true);
|
|
87
|
+
expect(result.recoveredActionCount).toBe(0);
|
|
88
|
+
expect(result.issues).toEqual([]);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('passes with recovery after an allowed tool error when final checks pass', () => {
|
|
92
|
+
const outcome = createOutcome();
|
|
93
|
+
outcome.trace.actions = [
|
|
94
|
+
{
|
|
95
|
+
type: 'tool',
|
|
96
|
+
name: 'workspace_run',
|
|
97
|
+
status: 'error',
|
|
98
|
+
durationMs: 10,
|
|
99
|
+
summary: 'Scenario validation failed',
|
|
100
|
+
},
|
|
101
|
+
{ type: 'tool', name: 'workspace_patch', status: 'success', durationMs: 5 },
|
|
102
|
+
{ type: 'tool', name: 'workspace_run', status: 'success', durationMs: 10 },
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
const result = scoreAgentOutcome(task, outcome, createEvidence());
|
|
106
|
+
|
|
107
|
+
expect(result.verdict).toBe('passed_with_recovery');
|
|
108
|
+
expect(result.passed).toBe(true);
|
|
109
|
+
expect(result.recoveredActionCount).toBe(1);
|
|
110
|
+
expect(result.issues).toEqual([]);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('reports an infrastructure failure when the run terminates on a tool error', () => {
|
|
114
|
+
const outcome = createOutcome({ completed: false, acceptancePassed: false });
|
|
115
|
+
const evidence = createEvidence({ termination: { status: 'tool_error' } });
|
|
116
|
+
|
|
117
|
+
const result = scoreAgentOutcome(task, outcome, evidence);
|
|
118
|
+
|
|
119
|
+
expect(result.verdict).toBe('infrastructure_failed');
|
|
120
|
+
expect(result.passed).toBe(false);
|
|
121
|
+
expect(result.issues.map((issue) => issue.code)).toContain('tool-termination-error');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('fails the task when a final acceptance command fails', () => {
|
|
125
|
+
const outcome = createOutcome({ acceptancePassed: false });
|
|
126
|
+
outcome.trace.actions = [
|
|
127
|
+
{ type: 'tool', name: 'workspace_run', status: 'error', durationMs: 10 },
|
|
128
|
+
];
|
|
129
|
+
const evidence = createEvidence({
|
|
130
|
+
acceptanceChecks: [
|
|
131
|
+
{
|
|
132
|
+
command: 'akit validate scenario.yaml',
|
|
133
|
+
status: 'failed',
|
|
134
|
+
exitCode: 1,
|
|
135
|
+
durationMs: 25,
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const result = scoreAgentOutcome(task, outcome, evidence);
|
|
141
|
+
|
|
142
|
+
expect(result.verdict).toBe('task_failed');
|
|
143
|
+
expect(result.passed).toBe(false);
|
|
144
|
+
expect(result.recoveredActionCount).toBe(0);
|
|
145
|
+
expect(result.issues.map((issue) => issue.code)).toContain('acceptance-failed');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('fails the task when the workspace diff escapes the allowed paths', () => {
|
|
149
|
+
const outcome = createOutcome();
|
|
150
|
+
outcome.trace.changedPaths = ['scenario.yaml', 'README.md'];
|
|
151
|
+
|
|
152
|
+
const result = scoreAgentOutcome(task, outcome, createEvidence());
|
|
153
|
+
|
|
154
|
+
expect(result.verdict).toBe('task_failed');
|
|
155
|
+
expect(result.passed).toBe(false);
|
|
156
|
+
expect(result.issues).toContainEqual({
|
|
157
|
+
code: 'changed-path-violation',
|
|
158
|
+
message: 'Changed path is outside the allowed scope: README.md.',
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('fails the task when the sandbox rejects a prohibited action', () => {
|
|
163
|
+
const outcome = createOutcome();
|
|
164
|
+
outcome.trace.actions = [
|
|
165
|
+
{ type: 'tool', name: 'workspace_read', status: 'rejected', durationMs: 1 },
|
|
166
|
+
];
|
|
167
|
+
|
|
168
|
+
const result = scoreAgentOutcome(task, outcome, createEvidence());
|
|
169
|
+
|
|
170
|
+
expect(result.verdict).toBe('task_failed');
|
|
171
|
+
expect(result.passed).toBe(false);
|
|
172
|
+
expect(result.issues.map((issue) => issue.code)).toContain('prohibited-action');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('fails the task when a successful command action is not allowed', () => {
|
|
176
|
+
const restrictedTask = { ...task, allowedTools: ['workspace_read'] };
|
|
177
|
+
const outcome = createOutcome();
|
|
178
|
+
outcome.trace.actions = [
|
|
179
|
+
{ type: 'command', name: 'workspace_run', status: 'success', durationMs: 1 },
|
|
180
|
+
];
|
|
181
|
+
|
|
182
|
+
const result = scoreAgentOutcome(restrictedTask, outcome, createEvidence());
|
|
183
|
+
|
|
184
|
+
expect(result.verdict).toBe('task_failed');
|
|
185
|
+
expect(result.passed).toBe(false);
|
|
186
|
+
expect(result.issues.map((issue) => issue.code)).toContain('prohibited-action');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('fails the task when a successful file action is not allowed', () => {
|
|
190
|
+
const restrictedTask = { ...task, allowedTools: ['workspace_read'] };
|
|
191
|
+
const outcome = createOutcome();
|
|
192
|
+
outcome.trace.actions = [
|
|
193
|
+
{ type: 'file', name: 'workspace_patch', status: 'success', durationMs: 1 },
|
|
194
|
+
];
|
|
195
|
+
|
|
196
|
+
const result = scoreAgentOutcome(restrictedTask, outcome, createEvidence());
|
|
197
|
+
|
|
198
|
+
expect(result.verdict).toBe('task_failed');
|
|
199
|
+
expect(result.passed).toBe(false);
|
|
200
|
+
expect(result.issues.map((issue) => issue.code)).toContain('prohibited-action');
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('reports an infrastructure failure when required acceptance evidence is missing', () => {
|
|
204
|
+
const outcome = createOutcome({ acceptancePassed: false });
|
|
205
|
+
|
|
206
|
+
const result = scoreAgentOutcome(task, outcome, createEvidence({ acceptanceChecks: [] }));
|
|
207
|
+
|
|
208
|
+
expect(result.verdict).toBe('infrastructure_failed');
|
|
209
|
+
expect(result.passed).toBe(false);
|
|
210
|
+
expect(result.issues).toContainEqual({
|
|
211
|
+
code: 'acceptance-evidence-missing',
|
|
212
|
+
message: 'No acceptance evidence was recorded for: akit validate scenario.yaml.',
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('reports an infrastructure failure when the acceptance executor faults', () => {
|
|
217
|
+
const outcome = createOutcome({ acceptancePassed: false });
|
|
218
|
+
const evidence = createEvidence({
|
|
219
|
+
acceptanceChecks: [
|
|
220
|
+
{
|
|
221
|
+
command: 'akit validate scenario.yaml',
|
|
222
|
+
status: 'executor_error',
|
|
223
|
+
exitCode: null,
|
|
224
|
+
durationMs: 2,
|
|
225
|
+
},
|
|
226
|
+
],
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
const result = scoreAgentOutcome(task, outcome, evidence);
|
|
230
|
+
|
|
231
|
+
expect(result.verdict).toBe('infrastructure_failed');
|
|
232
|
+
expect(result.passed).toBe(false);
|
|
233
|
+
expect(result.issues.map((issue) => issue.code)).toContain('acceptance-executor-error');
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('fails the task when the action budget is exceeded', () => {
|
|
237
|
+
const outcome = createOutcome();
|
|
238
|
+
outcome.trace.actions = Array.from({ length: task.maxActions + 1 }, (_, index) => ({
|
|
239
|
+
type: 'tool' as const,
|
|
240
|
+
name: 'workspace_read',
|
|
241
|
+
status: 'success' as const,
|
|
242
|
+
durationMs: index,
|
|
243
|
+
}));
|
|
244
|
+
|
|
245
|
+
const result = scoreAgentOutcome(task, outcome, createEvidence());
|
|
246
|
+
|
|
247
|
+
expect(result.verdict).toBe('task_failed');
|
|
248
|
+
expect(result.issues.map((issue) => issue.code)).toContain('action-budget-exceeded');
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('fails the task when the wall-clock time budget is exceeded', () => {
|
|
252
|
+
const outcome = createOutcome();
|
|
253
|
+
outcome.trace.completedAt = '2026-08-21T00:01:00.001Z';
|
|
254
|
+
|
|
255
|
+
const result = scoreAgentOutcome(task, outcome, createEvidence());
|
|
256
|
+
|
|
257
|
+
expect(result.verdict).toBe('task_failed');
|
|
258
|
+
expect(result.issues.map((issue) => issue.code)).toContain('time-budget-exceeded');
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('fails the task when the agent terminates before completion', () => {
|
|
262
|
+
const outcome = createOutcome({ completed: false });
|
|
263
|
+
|
|
264
|
+
const result = scoreAgentOutcome(
|
|
265
|
+
task,
|
|
266
|
+
outcome,
|
|
267
|
+
createEvidence({ termination: { status: 'agent_error' } })
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
expect(result.verdict).toBe('task_failed');
|
|
271
|
+
expect(result.issues.map((issue) => issue.code)).toContain('agent-termination-error');
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('reports inconsistent completion evidence as an infrastructure failure', () => {
|
|
275
|
+
const outcome = createOutcome({ completed: false });
|
|
276
|
+
|
|
277
|
+
const result = scoreAgentOutcome(task, outcome, createEvidence());
|
|
278
|
+
|
|
279
|
+
expect(result.verdict).toBe('infrastructure_failed');
|
|
280
|
+
expect(result.issues.map((issue) => issue.code)).toContain('completion-evidence-mismatch');
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('reports inconsistent acceptance summaries as an infrastructure failure', () => {
|
|
284
|
+
const outcome = createOutcome({ acceptancePassed: false });
|
|
285
|
+
|
|
286
|
+
const result = scoreAgentOutcome(task, outcome, createEvidence());
|
|
287
|
+
|
|
288
|
+
expect(result.verdict).toBe('infrastructure_failed');
|
|
289
|
+
expect(result.issues.map((issue) => issue.code)).toContain('acceptance-evidence-mismatch');
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('reports task identity mismatches as an infrastructure failure', () => {
|
|
293
|
+
const outcome = createOutcome({ taskId: 'another-task' });
|
|
294
|
+
|
|
295
|
+
const result = scoreAgentOutcome(task, outcome, createEvidence());
|
|
296
|
+
|
|
297
|
+
expect(result.verdict).toBe('infrastructure_failed');
|
|
298
|
+
expect(result.issues.map((issue) => issue.code)).toContain('task-evidence-mismatch');
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('reports invalid trace timestamps as an infrastructure failure', () => {
|
|
302
|
+
const outcome = createOutcome();
|
|
303
|
+
outcome.trace.completedAt = 'not-a-timestamp';
|
|
304
|
+
|
|
305
|
+
const result = scoreAgentOutcome(task, outcome, createEvidence());
|
|
306
|
+
|
|
307
|
+
expect(result.verdict).toBe('infrastructure_failed');
|
|
308
|
+
expect(result.issues.map((issue) => issue.code)).toContain('trace-timestamp-invalid');
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('reports contradictory acceptance status and exit code as an infrastructure failure', () => {
|
|
312
|
+
const outcome = createOutcome({ acceptancePassed: false });
|
|
313
|
+
const evidence = createEvidence({
|
|
314
|
+
acceptanceChecks: [
|
|
315
|
+
{
|
|
316
|
+
command: 'akit validate scenario.yaml',
|
|
317
|
+
status: 'passed',
|
|
318
|
+
exitCode: 1,
|
|
319
|
+
durationMs: 25,
|
|
320
|
+
},
|
|
321
|
+
],
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
const result = scoreAgentOutcome(task, outcome, evidence);
|
|
325
|
+
|
|
326
|
+
expect(result.verdict).toBe('infrastructure_failed');
|
|
327
|
+
expect(result.issues.map((issue) => issue.code)).toContain('acceptance-evidence-invalid');
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('reports missing final diff evidence when changed paths are declared', () => {
|
|
331
|
+
for (const finalDiff of [undefined, '', ' \n']) {
|
|
332
|
+
const result = scoreAgentOutcome(task, createOutcome({ finalDiff }), createEvidence());
|
|
333
|
+
|
|
334
|
+
expect(result.verdict).toBe('infrastructure_failed');
|
|
335
|
+
expect(result.issues.map((issue) => issue.code)).toContain('final-diff-missing');
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it('requires every task-declared artifact check', () => {
|
|
340
|
+
const taskWithArtifactCheck = {
|
|
341
|
+
...task,
|
|
342
|
+
requiredArtifactChecks: ['scenario-matches-expected'],
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
const result = scoreAgentOutcome(taskWithArtifactCheck, createOutcome(), createEvidence());
|
|
346
|
+
|
|
347
|
+
expect(result.verdict).toBe('infrastructure_failed');
|
|
348
|
+
expect(result.issues.map((issue) => issue.code)).toContain('artifact-evidence-missing');
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it('fails the task when a required artifact check fails', () => {
|
|
352
|
+
const taskWithArtifactCheck = {
|
|
353
|
+
...task,
|
|
354
|
+
requiredArtifactChecks: ['scenario-matches-expected'],
|
|
355
|
+
};
|
|
356
|
+
const evidence = {
|
|
357
|
+
...createEvidence(),
|
|
358
|
+
artifactChecks: [
|
|
359
|
+
{
|
|
360
|
+
id: 'scenario-matches-expected',
|
|
361
|
+
status: 'failed' as const,
|
|
362
|
+
durationMs: 5,
|
|
363
|
+
},
|
|
364
|
+
],
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
const result = scoreAgentOutcome(taskWithArtifactCheck, createOutcome(), evidence);
|
|
368
|
+
|
|
369
|
+
expect(result.verdict).toBe('task_failed');
|
|
370
|
+
expect(result.issues.map((issue) => issue.code)).toContain('artifact-check-failed');
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('prioritizes missing acceptance evidence over a failed artifact check', () => {
|
|
374
|
+
const taskWithArtifactCheck = {
|
|
375
|
+
...task,
|
|
376
|
+
requiredArtifactChecks: ['scenario-matches-expected'],
|
|
377
|
+
};
|
|
378
|
+
const evidence = {
|
|
379
|
+
...createEvidence({ acceptanceChecks: [] }),
|
|
380
|
+
artifactChecks: [
|
|
381
|
+
{
|
|
382
|
+
id: 'scenario-matches-expected',
|
|
383
|
+
status: 'failed' as const,
|
|
384
|
+
durationMs: 5,
|
|
385
|
+
},
|
|
386
|
+
],
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
const result = scoreAgentOutcome(
|
|
390
|
+
taskWithArtifactCheck,
|
|
391
|
+
createOutcome({ acceptancePassed: false }),
|
|
392
|
+
evidence
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
expect(result.verdict).toBe('infrastructure_failed');
|
|
396
|
+
expect(result.issues.map((issue) => issue.code)).toContain('acceptance-evidence-missing');
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it('fails closed for an unknown termination status at runtime', () => {
|
|
400
|
+
const result = scoreAgentOutcome(
|
|
401
|
+
task,
|
|
402
|
+
createOutcome(),
|
|
403
|
+
createEvidence({
|
|
404
|
+
termination: { status: 'unknown' as AgentTerminationStatus },
|
|
405
|
+
})
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
expect(result.verdict).toBe('infrastructure_failed');
|
|
409
|
+
expect(result.issues.map((issue) => issue.code)).toContain('termination-evidence-invalid');
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it('fails closed for an unknown acceptance status even when the summary is false', () => {
|
|
413
|
+
const outcome = createOutcome({ acceptancePassed: false });
|
|
414
|
+
const invalidCheck: AgentAcceptanceCheck = {
|
|
415
|
+
command: 'akit validate scenario.yaml',
|
|
416
|
+
status: 'unknown' as AgentAcceptanceCheck['status'],
|
|
417
|
+
exitCode: null,
|
|
418
|
+
durationMs: 1,
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
const result = scoreAgentOutcome(
|
|
422
|
+
task,
|
|
423
|
+
outcome,
|
|
424
|
+
createEvidence({ acceptanceChecks: [invalidCheck] })
|
|
425
|
+
);
|
|
426
|
+
|
|
427
|
+
expect(result.verdict).toBe('infrastructure_failed');
|
|
428
|
+
expect(result.issues.map((issue) => issue.code)).toContain('acceptance-evidence-invalid');
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it('fails closed for any unknown artifact status at runtime', () => {
|
|
432
|
+
const invalidCheck: AgentArtifactCheck = {
|
|
433
|
+
id: 'unrequested-check',
|
|
434
|
+
status: 'unknown' as AgentArtifactCheck['status'],
|
|
435
|
+
durationMs: 1,
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
const result = scoreAgentOutcome(task, createOutcome(), {
|
|
439
|
+
...createEvidence(),
|
|
440
|
+
artifactChecks: [invalidCheck],
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
expect(result.verdict).toBe('infrastructure_failed');
|
|
444
|
+
expect(result.issues.map((issue) => issue.code)).toContain('artifact-evidence-invalid');
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
describe('runtime boundary validation', () => {
|
|
448
|
+
const sparseArray = Array(1);
|
|
449
|
+
|
|
450
|
+
for (const [name, taskValue] of [
|
|
451
|
+
['missing task', undefined],
|
|
452
|
+
['null task', null],
|
|
453
|
+
['primitive task', 1],
|
|
454
|
+
['array task', []],
|
|
455
|
+
['missing id', withoutKey(task as unknown as Record<string, unknown>, 'id')],
|
|
456
|
+
['non-string id', { ...task, id: 7 }],
|
|
457
|
+
[
|
|
458
|
+
'missing fixturePath',
|
|
459
|
+
withoutKey(task as unknown as Record<string, unknown>, 'fixturePath'),
|
|
460
|
+
],
|
|
461
|
+
['non-string fixturePath', { ...task, fixturePath: 7 }],
|
|
462
|
+
[
|
|
463
|
+
'missing allowedPaths',
|
|
464
|
+
withoutKey(task as unknown as Record<string, unknown>, 'allowedPaths'),
|
|
465
|
+
],
|
|
466
|
+
['non-array allowedPaths', { ...task, allowedPaths: 'scenario.yaml' }],
|
|
467
|
+
['sparse allowedPaths', { ...task, allowedPaths: sparseArray }],
|
|
468
|
+
['non-string allowed path', { ...task, allowedPaths: [7] }],
|
|
469
|
+
['empty allowed path', { ...task, allowedPaths: [''] }],
|
|
470
|
+
['absolute allowed path', { ...task, allowedPaths: ['/scenario.yaml'] }],
|
|
471
|
+
['traversing allowed path', { ...task, allowedPaths: ['../scenario.yaml'] }],
|
|
472
|
+
[
|
|
473
|
+
'missing allowedTools',
|
|
474
|
+
withoutKey(task as unknown as Record<string, unknown>, 'allowedTools'),
|
|
475
|
+
],
|
|
476
|
+
['non-array allowedTools', { ...task, allowedTools: 'workspace_read' }],
|
|
477
|
+
['sparse allowedTools', { ...task, allowedTools: sparseArray }],
|
|
478
|
+
['non-string allowed tool', { ...task, allowedTools: [7] }],
|
|
479
|
+
[
|
|
480
|
+
'missing acceptanceCommands',
|
|
481
|
+
withoutKey(task as unknown as Record<string, unknown>, 'acceptanceCommands'),
|
|
482
|
+
],
|
|
483
|
+
['non-array acceptanceCommands', { ...task, acceptanceCommands: 'akit validate' }],
|
|
484
|
+
['sparse acceptanceCommands', { ...task, acceptanceCommands: sparseArray }],
|
|
485
|
+
['non-string acceptance command', { ...task, acceptanceCommands: [7] }],
|
|
486
|
+
['null requiredArtifactChecks', { ...task, requiredArtifactChecks: null }],
|
|
487
|
+
['non-array requiredArtifactChecks', { ...task, requiredArtifactChecks: 'artifact' }],
|
|
488
|
+
['sparse requiredArtifactChecks', { ...task, requiredArtifactChecks: sparseArray }],
|
|
489
|
+
['non-string required artifact check', { ...task, requiredArtifactChecks: [7] }],
|
|
490
|
+
['missing maxActions', withoutKey(task as unknown as Record<string, unknown>, 'maxActions')],
|
|
491
|
+
['NaN maxActions', { ...task, maxActions: Number.NaN }],
|
|
492
|
+
['infinite maxActions', { ...task, maxActions: Number.POSITIVE_INFINITY }],
|
|
493
|
+
['negative maxActions', { ...task, maxActions: -1 }],
|
|
494
|
+
['fractional maxActions', { ...task, maxActions: 1.5 }],
|
|
495
|
+
['numeric-string maxActions', { ...task, maxActions: '8' }],
|
|
496
|
+
['missing timeoutMs', withoutKey(task as unknown as Record<string, unknown>, 'timeoutMs')],
|
|
497
|
+
['NaN timeoutMs', { ...task, timeoutMs: Number.NaN }],
|
|
498
|
+
['infinite timeoutMs', { ...task, timeoutMs: Number.POSITIVE_INFINITY }],
|
|
499
|
+
['negative timeoutMs', { ...task, timeoutMs: -1 }],
|
|
500
|
+
['numeric-string timeoutMs', { ...task, timeoutMs: '60000' }],
|
|
501
|
+
] as const) {
|
|
502
|
+
it(`rejects ${name} as an invalid task definition`, () => {
|
|
503
|
+
expectInfrastructureInvalid('task-definition-invalid', taskValue);
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
it('accepts absent or undefined required artifact checks and unknown task properties', () => {
|
|
508
|
+
const withUndefined = { ...task, requiredArtifactChecks: undefined, futureField: true };
|
|
509
|
+
|
|
510
|
+
expect(scoreRuntimeValues(task)).toEqual(scoreRuntimeValues(withUndefined));
|
|
511
|
+
expect(scoreRuntimeValues(withUndefined).verdict).toBe('passed');
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
for (const [name, outcomeValue] of [
|
|
515
|
+
['missing outcome', undefined],
|
|
516
|
+
['null outcome', null],
|
|
517
|
+
['primitive outcome', false],
|
|
518
|
+
['array outcome', []],
|
|
519
|
+
[
|
|
520
|
+
'missing taskId',
|
|
521
|
+
withoutKey(createOutcome() as unknown as Record<string, unknown>, 'taskId'),
|
|
522
|
+
],
|
|
523
|
+
['non-string taskId', { ...createOutcome(), taskId: 7 }],
|
|
524
|
+
[
|
|
525
|
+
'missing completed',
|
|
526
|
+
withoutKey(createOutcome() as unknown as Record<string, unknown>, 'completed'),
|
|
527
|
+
],
|
|
528
|
+
['non-boolean completed', { ...createOutcome(), completed: 'true' }],
|
|
529
|
+
[
|
|
530
|
+
'missing acceptancePassed',
|
|
531
|
+
withoutKey(createOutcome() as unknown as Record<string, unknown>, 'acceptancePassed'),
|
|
532
|
+
],
|
|
533
|
+
['non-boolean acceptancePassed', { ...createOutcome(), acceptancePassed: 1 }],
|
|
534
|
+
['missing trace', withoutKey(createOutcome() as unknown as Record<string, unknown>, 'trace')],
|
|
535
|
+
['null trace', { ...createOutcome(), trace: null }],
|
|
536
|
+
['primitive trace', { ...createOutcome(), trace: 'trace' }],
|
|
537
|
+
['array trace', { ...createOutcome(), trace: [] }],
|
|
538
|
+
['null error', { ...createOutcome(), error: null }],
|
|
539
|
+
['non-string error', { ...createOutcome(), error: 7 }],
|
|
540
|
+
] as const) {
|
|
541
|
+
it(`rejects ${name} as invalid outcome evidence`, () => {
|
|
542
|
+
expectInfrastructureInvalid('outcome-evidence-invalid', task, outcomeValue);
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
it('accepts an absent or undefined outcome error and unknown outcome properties', () => {
|
|
547
|
+
const withUndefined = { ...createOutcome(), error: undefined, futureField: true };
|
|
548
|
+
|
|
549
|
+
expect(scoreRuntimeValues(task, withUndefined).verdict).toBe('passed');
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
for (const [name, trace] of [
|
|
553
|
+
[
|
|
554
|
+
'missing taskId',
|
|
555
|
+
withoutKey(createOutcome().trace as unknown as Record<string, unknown>, 'taskId'),
|
|
556
|
+
],
|
|
557
|
+
['non-string taskId', { ...createOutcome().trace, taskId: 7 }],
|
|
558
|
+
[
|
|
559
|
+
'missing actions',
|
|
560
|
+
withoutKey(createOutcome().trace as unknown as Record<string, unknown>, 'actions'),
|
|
561
|
+
],
|
|
562
|
+
['non-array actions', { ...createOutcome().trace, actions: {} }],
|
|
563
|
+
[
|
|
564
|
+
'missing changedPaths',
|
|
565
|
+
withoutKey(createOutcome().trace as unknown as Record<string, unknown>, 'changedPaths'),
|
|
566
|
+
],
|
|
567
|
+
['non-array changedPaths', { ...createOutcome().trace, changedPaths: 'scenario.yaml' }],
|
|
568
|
+
['sparse changedPaths', { ...createOutcome().trace, changedPaths: sparseArray }],
|
|
569
|
+
['non-string changed path', { ...createOutcome().trace, changedPaths: [7] }],
|
|
570
|
+
] as const) {
|
|
571
|
+
it(`rejects ${name} as invalid trace evidence`, () => {
|
|
572
|
+
expectInfrastructureInvalid('trace-evidence-invalid', task, {
|
|
573
|
+
...createOutcome(),
|
|
574
|
+
trace,
|
|
575
|
+
});
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
for (const [name, action] of [
|
|
580
|
+
['sparse action', undefined],
|
|
581
|
+
['null action', null],
|
|
582
|
+
['primitive action', 'action'],
|
|
583
|
+
['array action', []],
|
|
584
|
+
['missing type', { name: 'workspace_read', status: 'success', durationMs: 1 }],
|
|
585
|
+
[
|
|
586
|
+
'unknown action type',
|
|
587
|
+
{ type: 'unknown', name: 'workspace_read', status: 'success', durationMs: 1 },
|
|
588
|
+
],
|
|
589
|
+
['missing name', { type: 'tool', status: 'success', durationMs: 1 }],
|
|
590
|
+
['non-string name', { type: 'tool', name: 7, status: 'success', durationMs: 1 }],
|
|
591
|
+
['missing status', { type: 'tool', name: 'workspace_read', durationMs: 1 }],
|
|
592
|
+
[
|
|
593
|
+
'unknown action status',
|
|
594
|
+
{ type: 'tool', name: 'workspace_read', status: 'unknown', durationMs: 1 },
|
|
595
|
+
],
|
|
596
|
+
['missing durationMs', { type: 'tool', name: 'workspace_read', status: 'success' }],
|
|
597
|
+
[
|
|
598
|
+
'NaN durationMs',
|
|
599
|
+
{ type: 'tool', name: 'workspace_read', status: 'success', durationMs: Number.NaN },
|
|
600
|
+
],
|
|
601
|
+
[
|
|
602
|
+
'infinite durationMs',
|
|
603
|
+
{
|
|
604
|
+
type: 'tool',
|
|
605
|
+
name: 'workspace_read',
|
|
606
|
+
status: 'success',
|
|
607
|
+
durationMs: Number.POSITIVE_INFINITY,
|
|
608
|
+
},
|
|
609
|
+
],
|
|
610
|
+
[
|
|
611
|
+
'negative durationMs',
|
|
612
|
+
{ type: 'tool', name: 'workspace_read', status: 'success', durationMs: -1 },
|
|
613
|
+
],
|
|
614
|
+
[
|
|
615
|
+
'numeric-string durationMs',
|
|
616
|
+
{ type: 'tool', name: 'workspace_read', status: 'success', durationMs: '1' },
|
|
617
|
+
],
|
|
618
|
+
[
|
|
619
|
+
'null summary',
|
|
620
|
+
{
|
|
621
|
+
type: 'tool',
|
|
622
|
+
name: 'workspace_read',
|
|
623
|
+
status: 'success',
|
|
624
|
+
durationMs: 1,
|
|
625
|
+
summary: null,
|
|
626
|
+
},
|
|
627
|
+
],
|
|
628
|
+
[
|
|
629
|
+
'non-string summary',
|
|
630
|
+
{
|
|
631
|
+
type: 'tool',
|
|
632
|
+
name: 'workspace_read',
|
|
633
|
+
status: 'success',
|
|
634
|
+
durationMs: 1,
|
|
635
|
+
summary: 7,
|
|
636
|
+
},
|
|
637
|
+
],
|
|
638
|
+
] as const) {
|
|
639
|
+
it(`rejects ${name} as invalid action evidence`, () => {
|
|
640
|
+
const actions = name === 'sparse action' ? sparseArray : [action];
|
|
641
|
+
expectInfrastructureInvalid('action-evidence-invalid', task, {
|
|
642
|
+
...createOutcome(),
|
|
643
|
+
trace: { ...createOutcome().trace, actions },
|
|
644
|
+
});
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
it('accepts an absent or undefined action summary and unknown action properties', () => {
|
|
649
|
+
const outcome = createOutcome();
|
|
650
|
+
outcome.trace.actions = [
|
|
651
|
+
{
|
|
652
|
+
type: 'tool',
|
|
653
|
+
name: 'workspace_read',
|
|
654
|
+
status: 'success',
|
|
655
|
+
durationMs: 1,
|
|
656
|
+
summary: undefined,
|
|
657
|
+
futureField: true,
|
|
658
|
+
} as AgentOutcome['trace']['actions'][number],
|
|
659
|
+
];
|
|
660
|
+
|
|
661
|
+
expect(scoreAgentOutcome(task, outcome, createEvidence()).verdict).toBe('passed');
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
for (const [name, evidenceValue] of [
|
|
665
|
+
['missing evidence', undefined],
|
|
666
|
+
['null evidence', null],
|
|
667
|
+
['primitive evidence', 'evidence'],
|
|
668
|
+
['array evidence', []],
|
|
669
|
+
] as const) {
|
|
670
|
+
it(`rejects ${name} as invalid top-level evaluation evidence`, () => {
|
|
671
|
+
expectInfrastructureInvalid(
|
|
672
|
+
'evaluation-evidence-invalid',
|
|
673
|
+
task,
|
|
674
|
+
createOutcome(),
|
|
675
|
+
evidenceValue
|
|
676
|
+
);
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
for (const [name, termination] of [
|
|
681
|
+
['missing termination', undefined],
|
|
682
|
+
['null termination', null],
|
|
683
|
+
['primitive termination', 'completed'],
|
|
684
|
+
['array termination', []],
|
|
685
|
+
['missing termination status', {}],
|
|
686
|
+
['unknown termination status', { status: 'unknown' }],
|
|
687
|
+
] as const) {
|
|
688
|
+
it(`rejects ${name} as invalid termination evidence`, () => {
|
|
689
|
+
const evidenceValue =
|
|
690
|
+
name === 'missing termination'
|
|
691
|
+
? withoutKey(createEvidence() as unknown as Record<string, unknown>, 'termination')
|
|
692
|
+
: { ...createEvidence(), termination };
|
|
693
|
+
expectInfrastructureInvalid(
|
|
694
|
+
'termination-evidence-invalid',
|
|
695
|
+
task,
|
|
696
|
+
createOutcome(),
|
|
697
|
+
evidenceValue
|
|
698
|
+
);
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
for (const [name, acceptanceChecks] of [
|
|
703
|
+
['missing acceptanceChecks', undefined],
|
|
704
|
+
['null acceptanceChecks', null],
|
|
705
|
+
['non-array acceptanceChecks', {}],
|
|
706
|
+
['sparse acceptanceChecks', sparseArray],
|
|
707
|
+
['null acceptance check', [null]],
|
|
708
|
+
['primitive acceptance check', ['check']],
|
|
709
|
+
['array acceptance check', [[]]],
|
|
710
|
+
['missing command', [{ status: 'passed', exitCode: 0, durationMs: 1 }]],
|
|
711
|
+
['non-string command', [{ command: 7, status: 'passed', exitCode: 0, durationMs: 1 }]],
|
|
712
|
+
['missing status', [{ command: 'akit validate scenario.yaml', exitCode: 0, durationMs: 1 }]],
|
|
713
|
+
[
|
|
714
|
+
'unknown status',
|
|
715
|
+
[
|
|
716
|
+
{
|
|
717
|
+
command: 'akit validate scenario.yaml',
|
|
718
|
+
status: 'unknown',
|
|
719
|
+
exitCode: 0,
|
|
720
|
+
durationMs: 1,
|
|
721
|
+
},
|
|
722
|
+
],
|
|
723
|
+
],
|
|
724
|
+
[
|
|
725
|
+
'missing exitCode',
|
|
726
|
+
[{ command: 'akit validate scenario.yaml', status: 'passed', durationMs: 1 }],
|
|
727
|
+
],
|
|
728
|
+
[
|
|
729
|
+
'NaN exitCode',
|
|
730
|
+
[
|
|
731
|
+
{
|
|
732
|
+
command: 'akit validate scenario.yaml',
|
|
733
|
+
status: 'failed',
|
|
734
|
+
exitCode: Number.NaN,
|
|
735
|
+
durationMs: 1,
|
|
736
|
+
},
|
|
737
|
+
],
|
|
738
|
+
],
|
|
739
|
+
[
|
|
740
|
+
'infinite exitCode',
|
|
741
|
+
[
|
|
742
|
+
{
|
|
743
|
+
command: 'akit validate scenario.yaml',
|
|
744
|
+
status: 'failed',
|
|
745
|
+
exitCode: Number.POSITIVE_INFINITY,
|
|
746
|
+
durationMs: 1,
|
|
747
|
+
},
|
|
748
|
+
],
|
|
749
|
+
],
|
|
750
|
+
[
|
|
751
|
+
'fractional exitCode',
|
|
752
|
+
[
|
|
753
|
+
{
|
|
754
|
+
command: 'akit validate scenario.yaml',
|
|
755
|
+
status: 'failed',
|
|
756
|
+
exitCode: 1.5,
|
|
757
|
+
durationMs: 1,
|
|
758
|
+
},
|
|
759
|
+
],
|
|
760
|
+
],
|
|
761
|
+
[
|
|
762
|
+
'numeric-string exitCode',
|
|
763
|
+
[
|
|
764
|
+
{
|
|
765
|
+
command: 'akit validate scenario.yaml',
|
|
766
|
+
status: 'failed',
|
|
767
|
+
exitCode: '1',
|
|
768
|
+
durationMs: 1,
|
|
769
|
+
},
|
|
770
|
+
],
|
|
771
|
+
],
|
|
772
|
+
[
|
|
773
|
+
'contradictory passed exitCode',
|
|
774
|
+
[
|
|
775
|
+
{
|
|
776
|
+
command: 'akit validate scenario.yaml',
|
|
777
|
+
status: 'passed',
|
|
778
|
+
exitCode: 1,
|
|
779
|
+
durationMs: 1,
|
|
780
|
+
},
|
|
781
|
+
],
|
|
782
|
+
],
|
|
783
|
+
[
|
|
784
|
+
'contradictory failed exitCode',
|
|
785
|
+
[
|
|
786
|
+
{
|
|
787
|
+
command: 'akit validate scenario.yaml',
|
|
788
|
+
status: 'failed',
|
|
789
|
+
exitCode: 0,
|
|
790
|
+
durationMs: 1,
|
|
791
|
+
},
|
|
792
|
+
],
|
|
793
|
+
],
|
|
794
|
+
[
|
|
795
|
+
'contradictory executor exitCode',
|
|
796
|
+
[
|
|
797
|
+
{
|
|
798
|
+
command: 'akit validate scenario.yaml',
|
|
799
|
+
status: 'executor_error',
|
|
800
|
+
exitCode: 1,
|
|
801
|
+
durationMs: 1,
|
|
802
|
+
},
|
|
803
|
+
],
|
|
804
|
+
],
|
|
805
|
+
[
|
|
806
|
+
'missing durationMs',
|
|
807
|
+
[{ command: 'akit validate scenario.yaml', status: 'passed', exitCode: 0 }],
|
|
808
|
+
],
|
|
809
|
+
[
|
|
810
|
+
'NaN durationMs',
|
|
811
|
+
[
|
|
812
|
+
{
|
|
813
|
+
command: 'akit validate scenario.yaml',
|
|
814
|
+
status: 'passed',
|
|
815
|
+
exitCode: 0,
|
|
816
|
+
durationMs: Number.NaN,
|
|
817
|
+
},
|
|
818
|
+
],
|
|
819
|
+
],
|
|
820
|
+
[
|
|
821
|
+
'infinite durationMs',
|
|
822
|
+
[
|
|
823
|
+
{
|
|
824
|
+
command: 'akit validate scenario.yaml',
|
|
825
|
+
status: 'passed',
|
|
826
|
+
exitCode: 0,
|
|
827
|
+
durationMs: Number.POSITIVE_INFINITY,
|
|
828
|
+
},
|
|
829
|
+
],
|
|
830
|
+
],
|
|
831
|
+
[
|
|
832
|
+
'negative durationMs',
|
|
833
|
+
[
|
|
834
|
+
{
|
|
835
|
+
command: 'akit validate scenario.yaml',
|
|
836
|
+
status: 'passed',
|
|
837
|
+
exitCode: 0,
|
|
838
|
+
durationMs: -1,
|
|
839
|
+
},
|
|
840
|
+
],
|
|
841
|
+
],
|
|
842
|
+
[
|
|
843
|
+
'numeric-string durationMs',
|
|
844
|
+
[
|
|
845
|
+
{
|
|
846
|
+
command: 'akit validate scenario.yaml',
|
|
847
|
+
status: 'passed',
|
|
848
|
+
exitCode: 0,
|
|
849
|
+
durationMs: '1',
|
|
850
|
+
},
|
|
851
|
+
],
|
|
852
|
+
],
|
|
853
|
+
] as const) {
|
|
854
|
+
it(`rejects ${name} as invalid acceptance evidence`, () => {
|
|
855
|
+
const evidenceValue =
|
|
856
|
+
name === 'missing acceptanceChecks'
|
|
857
|
+
? withoutKey(createEvidence() as unknown as Record<string, unknown>, 'acceptanceChecks')
|
|
858
|
+
: { ...createEvidence(), acceptanceChecks };
|
|
859
|
+
expectInfrastructureInvalid(
|
|
860
|
+
'acceptance-evidence-invalid',
|
|
861
|
+
task,
|
|
862
|
+
createOutcome(),
|
|
863
|
+
evidenceValue
|
|
864
|
+
);
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
for (const [name, artifactChecks] of [
|
|
869
|
+
['null artifactChecks', null],
|
|
870
|
+
['non-array artifactChecks', {}],
|
|
871
|
+
['sparse artifactChecks', sparseArray],
|
|
872
|
+
['null artifact check', [null]],
|
|
873
|
+
['primitive artifact check', ['check']],
|
|
874
|
+
['array artifact check', [[]]],
|
|
875
|
+
['missing id', [{ status: 'passed', durationMs: 1 }]],
|
|
876
|
+
['non-string id', [{ id: 7, status: 'passed', durationMs: 1 }]],
|
|
877
|
+
['missing status', [{ id: 'artifact', durationMs: 1 }]],
|
|
878
|
+
['unknown status', [{ id: 'artifact', status: 'unknown', durationMs: 1 }]],
|
|
879
|
+
['missing durationMs', [{ id: 'artifact', status: 'passed' }]],
|
|
880
|
+
['NaN durationMs', [{ id: 'artifact', status: 'passed', durationMs: Number.NaN }]],
|
|
881
|
+
[
|
|
882
|
+
'infinite durationMs',
|
|
883
|
+
[{ id: 'artifact', status: 'passed', durationMs: Number.POSITIVE_INFINITY }],
|
|
884
|
+
],
|
|
885
|
+
['negative durationMs', [{ id: 'artifact', status: 'passed', durationMs: -1 }]],
|
|
886
|
+
['numeric-string durationMs', [{ id: 'artifact', status: 'passed', durationMs: '1' }]],
|
|
887
|
+
] as const) {
|
|
888
|
+
it(`rejects ${name} as invalid artifact evidence`, () => {
|
|
889
|
+
expectInfrastructureInvalid('artifact-evidence-invalid', task, createOutcome(), {
|
|
890
|
+
...createEvidence(),
|
|
891
|
+
artifactChecks,
|
|
892
|
+
});
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
it('accepts absent or undefined artifact checks and unknown evidence properties', () => {
|
|
897
|
+
const withUndefined = { ...createEvidence(), artifactChecks: undefined, futureField: true };
|
|
898
|
+
|
|
899
|
+
expect(scoreRuntimeValues(task, createOutcome(), withUndefined).verdict).toBe('passed');
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
it('allows unknown object properties throughout otherwise valid runtime input', () => {
|
|
903
|
+
const outcome = createOutcome();
|
|
904
|
+
const evidence = createEvidence();
|
|
905
|
+
|
|
906
|
+
expect(
|
|
907
|
+
scoreRuntimeValues(
|
|
908
|
+
{ ...task, futureField: true },
|
|
909
|
+
{
|
|
910
|
+
...outcome,
|
|
911
|
+
futureField: true,
|
|
912
|
+
trace: {
|
|
913
|
+
...outcome.trace,
|
|
914
|
+
futureField: true,
|
|
915
|
+
actions: [
|
|
916
|
+
{
|
|
917
|
+
type: 'tool',
|
|
918
|
+
name: 'workspace_read',
|
|
919
|
+
status: 'success',
|
|
920
|
+
durationMs: 1,
|
|
921
|
+
futureField: true,
|
|
922
|
+
},
|
|
923
|
+
],
|
|
924
|
+
},
|
|
925
|
+
},
|
|
926
|
+
{
|
|
927
|
+
...evidence,
|
|
928
|
+
futureField: true,
|
|
929
|
+
termination: { ...evidence.termination, futureField: true },
|
|
930
|
+
acceptanceChecks: evidence.acceptanceChecks.map((check) => ({
|
|
931
|
+
...check,
|
|
932
|
+
futureField: true,
|
|
933
|
+
})),
|
|
934
|
+
artifactChecks: [
|
|
935
|
+
{
|
|
936
|
+
id: 'unrequested-check',
|
|
937
|
+
status: 'passed',
|
|
938
|
+
durationMs: 1,
|
|
939
|
+
futureField: true,
|
|
940
|
+
},
|
|
941
|
+
],
|
|
942
|
+
}
|
|
943
|
+
).verdict
|
|
944
|
+
).toBe('passed');
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
for (const finalDiff of [null, 1, {}, []]) {
|
|
948
|
+
it(`rejects present non-string final diff ${JSON.stringify(finalDiff)}`, () => {
|
|
949
|
+
expectInfrastructureInvalid('final-diff-invalid', task, {
|
|
950
|
+
...createOutcome(),
|
|
951
|
+
finalDiff,
|
|
952
|
+
});
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
const requiredArtifactTask: AgentTask = {
|
|
957
|
+
...task,
|
|
958
|
+
requiredArtifactChecks: ['scenario-matches-expected'],
|
|
959
|
+
};
|
|
960
|
+
const passedArtifactCheck: AgentArtifactCheck = {
|
|
961
|
+
id: 'scenario-matches-expected',
|
|
962
|
+
status: 'passed',
|
|
963
|
+
durationMs: 1,
|
|
964
|
+
};
|
|
965
|
+
const precedenceCases: Array<{
|
|
966
|
+
name: string;
|
|
967
|
+
taskValue: AgentTask;
|
|
968
|
+
outcomeValue: AgentOutcome;
|
|
969
|
+
evidenceValue: AgentEvaluationEvidence;
|
|
970
|
+
}> = [
|
|
971
|
+
{
|
|
972
|
+
name: 'task mismatch',
|
|
973
|
+
taskValue: task,
|
|
974
|
+
outcomeValue: createOutcome({ taskId: 'other-task' }),
|
|
975
|
+
evidenceValue: createEvidence(),
|
|
976
|
+
},
|
|
977
|
+
...(['agent_error', 'tool_error', 'infrastructure_error', 'timed_out'] as const).map(
|
|
978
|
+
(status) => ({
|
|
979
|
+
name: `${status} termination`,
|
|
980
|
+
taskValue: task,
|
|
981
|
+
outcomeValue: createOutcome({ completed: false, acceptancePassed: false }),
|
|
982
|
+
evidenceValue: createEvidence({ termination: { status } }),
|
|
983
|
+
})
|
|
984
|
+
),
|
|
985
|
+
{
|
|
986
|
+
name: 'completion mismatch',
|
|
987
|
+
taskValue: task,
|
|
988
|
+
outcomeValue: createOutcome({ completed: false }),
|
|
989
|
+
evidenceValue: createEvidence(),
|
|
990
|
+
},
|
|
991
|
+
{
|
|
992
|
+
name: 'artifact executor error',
|
|
993
|
+
taskValue: requiredArtifactTask,
|
|
994
|
+
outcomeValue: createOutcome(),
|
|
995
|
+
evidenceValue: createEvidence({
|
|
996
|
+
artifactChecks: [{ ...passedArtifactCheck, status: 'executor_error' }],
|
|
997
|
+
}),
|
|
998
|
+
},
|
|
999
|
+
{
|
|
1000
|
+
name: 'missing artifact evidence',
|
|
1001
|
+
taskValue: requiredArtifactTask,
|
|
1002
|
+
outcomeValue: createOutcome(),
|
|
1003
|
+
evidenceValue: createEvidence(),
|
|
1004
|
+
},
|
|
1005
|
+
{
|
|
1006
|
+
name: 'duplicate artifact evidence',
|
|
1007
|
+
taskValue: requiredArtifactTask,
|
|
1008
|
+
outcomeValue: createOutcome(),
|
|
1009
|
+
evidenceValue: createEvidence({
|
|
1010
|
+
artifactChecks: [{ ...passedArtifactCheck }, { ...passedArtifactCheck }],
|
|
1011
|
+
}),
|
|
1012
|
+
},
|
|
1013
|
+
{
|
|
1014
|
+
name: 'failed artifact check',
|
|
1015
|
+
taskValue: requiredArtifactTask,
|
|
1016
|
+
outcomeValue: createOutcome(),
|
|
1017
|
+
evidenceValue: createEvidence({
|
|
1018
|
+
artifactChecks: [{ ...passedArtifactCheck, status: 'failed' }],
|
|
1019
|
+
}),
|
|
1020
|
+
},
|
|
1021
|
+
{
|
|
1022
|
+
name: 'acceptance executor error',
|
|
1023
|
+
taskValue: task,
|
|
1024
|
+
outcomeValue: createOutcome({ acceptancePassed: false }),
|
|
1025
|
+
evidenceValue: createEvidence({
|
|
1026
|
+
acceptanceChecks: [
|
|
1027
|
+
{
|
|
1028
|
+
command: 'akit validate scenario.yaml',
|
|
1029
|
+
status: 'executor_error',
|
|
1030
|
+
exitCode: null,
|
|
1031
|
+
durationMs: 1,
|
|
1032
|
+
},
|
|
1033
|
+
],
|
|
1034
|
+
}),
|
|
1035
|
+
},
|
|
1036
|
+
{
|
|
1037
|
+
name: 'missing acceptance evidence',
|
|
1038
|
+
taskValue: task,
|
|
1039
|
+
outcomeValue: createOutcome({ acceptancePassed: false }),
|
|
1040
|
+
evidenceValue: createEvidence({ acceptanceChecks: [] }),
|
|
1041
|
+
},
|
|
1042
|
+
{
|
|
1043
|
+
name: 'duplicate acceptance evidence',
|
|
1044
|
+
taskValue: task,
|
|
1045
|
+
outcomeValue: createOutcome(),
|
|
1046
|
+
evidenceValue: createEvidence({
|
|
1047
|
+
acceptanceChecks: [
|
|
1048
|
+
{ ...createEvidence().acceptanceChecks[0] },
|
|
1049
|
+
{ ...createEvidence().acceptanceChecks[0] },
|
|
1050
|
+
],
|
|
1051
|
+
}),
|
|
1052
|
+
},
|
|
1053
|
+
{
|
|
1054
|
+
name: 'acceptance evidence mismatch',
|
|
1055
|
+
taskValue: task,
|
|
1056
|
+
outcomeValue: createOutcome({ acceptancePassed: false }),
|
|
1057
|
+
evidenceValue: createEvidence(),
|
|
1058
|
+
},
|
|
1059
|
+
{
|
|
1060
|
+
name: 'failed acceptance check',
|
|
1061
|
+
taskValue: task,
|
|
1062
|
+
outcomeValue: createOutcome({ acceptancePassed: false }),
|
|
1063
|
+
evidenceValue: createEvidence({
|
|
1064
|
+
acceptanceChecks: [
|
|
1065
|
+
{
|
|
1066
|
+
command: 'akit validate scenario.yaml',
|
|
1067
|
+
status: 'failed',
|
|
1068
|
+
exitCode: 1,
|
|
1069
|
+
durationMs: 1,
|
|
1070
|
+
},
|
|
1071
|
+
],
|
|
1072
|
+
}),
|
|
1073
|
+
},
|
|
1074
|
+
{
|
|
1075
|
+
name: 'missing final diff',
|
|
1076
|
+
taskValue: task,
|
|
1077
|
+
outcomeValue: createOutcome({ finalDiff: undefined }),
|
|
1078
|
+
evidenceValue: createEvidence(),
|
|
1079
|
+
},
|
|
1080
|
+
{
|
|
1081
|
+
name: 'changed path violation',
|
|
1082
|
+
taskValue: task,
|
|
1083
|
+
outcomeValue: createOutcome({
|
|
1084
|
+
trace: { ...createOutcome().trace, changedPaths: ['README.md'] },
|
|
1085
|
+
}),
|
|
1086
|
+
evidenceValue: createEvidence(),
|
|
1087
|
+
},
|
|
1088
|
+
{
|
|
1089
|
+
name: 'prohibited action',
|
|
1090
|
+
taskValue: task,
|
|
1091
|
+
outcomeValue: createOutcome({
|
|
1092
|
+
trace: {
|
|
1093
|
+
...createOutcome().trace,
|
|
1094
|
+
actions: [{ type: 'tool', name: 'workspace_read', status: 'rejected', durationMs: 1 }],
|
|
1095
|
+
},
|
|
1096
|
+
}),
|
|
1097
|
+
evidenceValue: createEvidence(),
|
|
1098
|
+
},
|
|
1099
|
+
{
|
|
1100
|
+
name: 'action budget violation',
|
|
1101
|
+
taskValue: task,
|
|
1102
|
+
outcomeValue: createOutcome({
|
|
1103
|
+
trace: {
|
|
1104
|
+
...createOutcome().trace,
|
|
1105
|
+
actions: Array.from({ length: task.maxActions + 1 }, () => ({
|
|
1106
|
+
type: 'tool' as const,
|
|
1107
|
+
name: 'workspace_read',
|
|
1108
|
+
status: 'success' as const,
|
|
1109
|
+
durationMs: 1,
|
|
1110
|
+
})),
|
|
1111
|
+
},
|
|
1112
|
+
}),
|
|
1113
|
+
evidenceValue: createEvidence(),
|
|
1114
|
+
},
|
|
1115
|
+
];
|
|
1116
|
+
|
|
1117
|
+
for (const [timestampName, timestampOverrides] of [
|
|
1118
|
+
['nonparseable', { completedAt: 'not-a-timestamp' }],
|
|
1119
|
+
[
|
|
1120
|
+
'out-of-order',
|
|
1121
|
+
{
|
|
1122
|
+
startedAt: '2026-08-21T00:00:01.000Z',
|
|
1123
|
+
completedAt: '2026-08-21T00:00:00.000Z',
|
|
1124
|
+
},
|
|
1125
|
+
],
|
|
1126
|
+
] as const) {
|
|
1127
|
+
for (const { name, taskValue, outcomeValue, evidenceValue } of precedenceCases) {
|
|
1128
|
+
it(`prioritizes ${timestampName} timestamps over ${name}`, () => {
|
|
1129
|
+
const result = scoreAgentOutcome(
|
|
1130
|
+
taskValue,
|
|
1131
|
+
{
|
|
1132
|
+
...outcomeValue,
|
|
1133
|
+
trace: { ...outcomeValue.trace, ...timestampOverrides },
|
|
1134
|
+
},
|
|
1135
|
+
evidenceValue
|
|
1136
|
+
);
|
|
1137
|
+
|
|
1138
|
+
expect(result.verdict).toBe('infrastructure_failed');
|
|
1139
|
+
expect(result.passed).toBe(false);
|
|
1140
|
+
expect(result.issues.map((issue) => issue.code)).toEqual(['trace-timestamp-invalid']);
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
for (const [name, timestampOverrides] of [
|
|
1146
|
+
['missing startedAt', { startedAt: undefined }],
|
|
1147
|
+
['null startedAt', { startedAt: null }],
|
|
1148
|
+
['numeric startedAt', { startedAt: 0 }],
|
|
1149
|
+
['missing completedAt', { completedAt: undefined }],
|
|
1150
|
+
['null completedAt', { completedAt: null }],
|
|
1151
|
+
['numeric completedAt', { completedAt: 0 }],
|
|
1152
|
+
[
|
|
1153
|
+
'out-of-order timestamps',
|
|
1154
|
+
{
|
|
1155
|
+
startedAt: '2026-08-21T00:00:01.000Z',
|
|
1156
|
+
completedAt: '2026-08-21T00:00:00.000Z',
|
|
1157
|
+
},
|
|
1158
|
+
],
|
|
1159
|
+
] as const) {
|
|
1160
|
+
it(`rejects ${name} as invalid trace timestamps`, () => {
|
|
1161
|
+
expectInfrastructureInvalid('trace-timestamp-invalid', task, {
|
|
1162
|
+
...createOutcome(),
|
|
1163
|
+
trace: { ...createOutcome().trace, ...timestampOverrides },
|
|
1164
|
+
});
|
|
1165
|
+
});
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
it('allows an absent final diff when no changed paths are recorded', () => {
|
|
1169
|
+
const outcome = createOutcome({ finalDiff: undefined });
|
|
1170
|
+
outcome.trace.changedPaths = [];
|
|
1171
|
+
|
|
1172
|
+
expect(scoreAgentOutcome(task, outcome, createEvidence()).verdict).toBe('passed');
|
|
1173
|
+
});
|
|
1174
|
+
|
|
1175
|
+
for (const changedPath of ['', '.', '..', '/scenario.yaml', 'C:\\scenario.yaml']) {
|
|
1176
|
+
it(`reports the invalid changed path ${JSON.stringify(changedPath)} without a truthiness gap`, () => {
|
|
1177
|
+
const result = scoreRuntimeValues(task, {
|
|
1178
|
+
...createOutcome(),
|
|
1179
|
+
trace: { ...createOutcome().trace, changedPaths: [changedPath] },
|
|
1180
|
+
});
|
|
1181
|
+
|
|
1182
|
+
expect(result.verdict).toBe('task_failed');
|
|
1183
|
+
expect(result.passed).toBe(false);
|
|
1184
|
+
expect(result.issues.map((issue) => issue.code)).toContain('changed-path-violation');
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
it('keeps well-typed task identity mismatches as task evidence mismatches', () => {
|
|
1189
|
+
const result = scoreRuntimeValues(task, createOutcome({ taskId: 'other-task' }));
|
|
1190
|
+
|
|
1191
|
+
expect(result.issues.map((issue) => issue.code)).toContain('task-evidence-mismatch');
|
|
1192
|
+
});
|
|
1193
|
+
});
|
|
1194
|
+
});
|