@artemiskit/core 0.4.2 → 0.5.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.
@@ -55,7 +55,7 @@ describe('createRunManifest', () => {
55
55
  endTime,
56
56
  });
57
57
 
58
- expect(manifest.version).toBe('1.1');
58
+ expect(manifest.version).toBe('1.3');
59
59
  expect(manifest.project).toBe('test-project');
60
60
  expect(manifest.run_id).toBeTruthy();
61
61
  expect(manifest.run_id.length).toBe(12);
@@ -163,6 +163,56 @@ describe('createRunManifest', () => {
163
163
  expect(manifest.resolved_config?.source.model).toBe('config');
164
164
  });
165
165
 
166
+ test('retains a validated workload identity when provided', () => {
167
+ const manifest = createRunManifest({
168
+ project: 'test-project',
169
+ config: { scenario: 'test-scenario', provider: 'openai' },
170
+ workloadIdentity: {
171
+ schema_version: '1',
172
+ workload: {
173
+ schema_version: '1',
174
+ algorithm: 'sha256',
175
+ digest: 'a'.repeat(64),
176
+ },
177
+ rubric: {
178
+ schema_version: '1',
179
+ algorithm: 'sha256',
180
+ digest: 'b'.repeat(64),
181
+ },
182
+ },
183
+ cases: mockCases,
184
+ startTime: new Date(),
185
+ endTime: new Date(),
186
+ });
187
+
188
+ expect(manifest.workload_identity?.workload.digest).toBe('a'.repeat(64));
189
+ expect(() => assertRunManifestIntegrity(manifest)).not.toThrow();
190
+ });
191
+
192
+ test('retains target and evaluator execution provenance separately', () => {
193
+ const manifest = createRunManifest({
194
+ project: 'test-project',
195
+ config: { scenario: 'test-scenario', provider: 'openai' },
196
+ executionProvenance: {
197
+ schema_version: '1',
198
+ target: {
199
+ provider: 'openai',
200
+ requested_models: ['gpt-requested'],
201
+ observed_models: ['gpt-observed'],
202
+ generation: { temperature: 0, max_tokens: 100, seed: 42 },
203
+ },
204
+ evaluator: { models: ['judge-model'] },
205
+ },
206
+ cases: mockCases,
207
+ startTime: new Date(),
208
+ endTime: new Date(),
209
+ });
210
+
211
+ expect(manifest.execution_provenance?.target.observed_models).toEqual(['gpt-observed']);
212
+ expect(manifest.execution_provenance?.evaluator?.models).toEqual(['judge-model']);
213
+ expect(() => assertRunManifestIntegrity(manifest)).not.toThrow();
214
+ });
215
+
166
216
  test('includes provenance information', () => {
167
217
  const manifest = createRunManifest({
168
218
  project: 'test-project',
@@ -273,5 +323,21 @@ describe('createRunManifest', () => {
273
323
  ],
274
324
  };
275
325
  expect(() => assertRunManifestIntegrity(malformed)).toThrow('invalid evidence validation');
326
+
327
+ expect(() =>
328
+ assertRunManifestIntegrity({ ...historical, workload_identity: { schema_version: '1' } })
329
+ ).toThrow('malformed workload identity');
330
+ expect(() =>
331
+ assertRunManifestIntegrity({
332
+ ...historical,
333
+ execution_provenance: { schema_version: '1', target: { provider: '' } },
334
+ })
335
+ ).toThrow('malformed execution provenance');
336
+ expect(() =>
337
+ assertRunManifestIntegrity({
338
+ ...historical,
339
+ cases: [{ ...historical.cases[0], target: { provider: '', observed_models: ['x'] } }],
340
+ })
341
+ ).toThrow('malformed target evidence');
276
342
  });
277
343
  });
@@ -9,11 +9,13 @@ import { getGitInfo } from '../provenance/git';
9
9
  import type {
10
10
  CaseResult,
11
11
  CostEstimateInfo,
12
+ ExecutionProvenance,
12
13
  ManifestRedactionInfo,
13
14
  ResolvedConfig,
14
15
  RunConfig,
15
16
  RunManifest,
16
17
  RunMetrics,
18
+ WorkloadIdentity,
17
19
  } from './types';
18
20
  import { getCaseEvaluationStatus } from './types';
19
21
 
@@ -24,6 +26,8 @@ export function createRunManifest(options: {
24
26
  project: string;
25
27
  config: RunConfig;
26
28
  resolvedConfig?: ResolvedConfig;
29
+ workloadIdentity?: WorkloadIdentity;
30
+ executionProvenance?: ExecutionProvenance;
27
31
  cases: CaseResult[];
28
32
  startTime: Date;
29
33
  endTime: Date;
@@ -35,6 +39,8 @@ export function createRunManifest(options: {
35
39
  project,
36
40
  config,
37
41
  resolvedConfig,
42
+ workloadIdentity,
43
+ executionProvenance,
38
44
  cases,
39
45
  startTime,
40
46
  endTime,
@@ -50,7 +56,7 @@ export function createRunManifest(options: {
50
56
  const environment = getEnvironmentInfo();
51
57
 
52
58
  return {
53
- version: '1.1',
59
+ version: '1.3',
54
60
  run_id: nanoid(12),
55
61
  project,
56
62
  start_time: startTime.toISOString(),
@@ -58,6 +64,8 @@ export function createRunManifest(options: {
58
64
  duration_ms: endTime.getTime() - startTime.getTime(),
59
65
  config,
60
66
  resolved_config: resolvedConfig,
67
+ workload_identity: workloadIdentity,
68
+ execution_provenance: executionProvenance,
61
69
  metrics,
62
70
  git,
63
71
  provenance: {
@@ -42,6 +42,56 @@ export interface ManifestRedactionInfo {
42
42
  };
43
43
  }
44
44
 
45
+ // ============================================================================
46
+ // Reproducible Evidence Types
47
+ // ============================================================================
48
+
49
+ /** A versioned SHA-256 digest of canonical, redacted assessment material. */
50
+ export interface ContentIdentity {
51
+ schema_version: '1';
52
+ algorithm: 'sha256';
53
+ digest: string;
54
+ }
55
+
56
+ /**
57
+ * Separates the scenario workload from the criteria used to judge it.
58
+ *
59
+ * The digests prove matching declared, sanitized inputs. They are not a
60
+ * signature or an attestation of provider behaviour.
61
+ */
62
+ export interface WorkloadIdentity {
63
+ schema_version: '1';
64
+ workload: ContentIdentity;
65
+ rubric: ContentIdentity;
66
+ }
67
+
68
+ /** Bounded target identity captured from a case execution. */
69
+ export interface CaseTargetEvidence {
70
+ provider: string;
71
+ requested_model?: string;
72
+ /** Model identifiers returned by the target provider during this case. */
73
+ observed_models?: string[];
74
+ }
75
+
76
+ /** Requested and observed execution configuration for a complete run. */
77
+ export interface ExecutionProvenance {
78
+ schema_version: '1';
79
+ target: {
80
+ provider: string;
81
+ requested_models?: string[];
82
+ observed_models?: string[];
83
+ generation?: {
84
+ temperature?: number;
85
+ max_tokens?: number;
86
+ seed?: number;
87
+ };
88
+ };
89
+ /** Judge/evaluator model identities, never combined with target identity. */
90
+ evaluator?: {
91
+ models?: string[];
92
+ };
93
+ }
94
+
45
95
  // ============================================================================
46
96
  // Case Result Types
47
97
  // ============================================================================
@@ -101,6 +151,8 @@ export interface CaseResult {
101
151
  error?: string;
102
152
  /** Sanitized evaluator evidence; arbitrary evaluator details are never stored here. */
103
153
  evidence?: CaseEvaluationEvidence;
154
+ /** Requested and observed target identity for this case. */
155
+ target?: CaseTargetEvidence;
104
156
  /** Redaction information for this case */
105
157
  redaction?: CaseRedactionInfo;
106
158
  /** Ordered tool activity captured for an enabled tool loop. */
@@ -277,6 +329,10 @@ export interface RunManifest {
277
329
  config: RunConfig;
278
330
  /** Resolved configuration with full provider details and source tracking */
279
331
  resolved_config?: ResolvedConfig;
332
+ /** Versioned identities for the declared workload and evaluation rubric. */
333
+ workload_identity?: WorkloadIdentity;
334
+ /** Requested and observed target/evaluator configuration for this run. */
335
+ execution_provenance?: ExecutionProvenance;
280
336
  metrics: RunMetrics;
281
337
  git: GitInfo;
282
338
  provenance: ProvenanceInfo;
@@ -323,6 +379,13 @@ export function assertRunManifestIntegrity(manifest: unknown): asserts manifest
323
379
  throw new Error('Invalid run manifest: expected an object with a cases array');
324
380
  }
325
381
 
382
+ if (manifest.workload_identity !== undefined) {
383
+ assertWorkloadIdentity(manifest.workload_identity);
384
+ }
385
+ if (manifest.execution_provenance !== undefined) {
386
+ assertExecutionProvenance(manifest.execution_provenance);
387
+ }
388
+
326
389
  for (const [index, caseResult] of manifest.cases.entries()) {
327
390
  if (!isRecord(caseResult)) {
328
391
  throw new Error(`Invalid run manifest: case ${index} is not an object`);
@@ -341,9 +404,85 @@ export function assertRunManifestIntegrity(manifest: unknown): asserts manifest
341
404
  if (caseResult.evidence !== undefined) {
342
405
  assertCaseEvaluationEvidence(caseResult.evidence, index);
343
406
  }
407
+ if (caseResult.target !== undefined) {
408
+ assertCaseTargetEvidence(caseResult.target);
409
+ }
344
410
  }
345
411
  }
346
412
 
413
+ function assertCaseTargetEvidence(target: unknown): void {
414
+ if (
415
+ !isRecord(target) ||
416
+ typeof target.provider !== 'string' ||
417
+ target.provider.length === 0 ||
418
+ target.provider.length > 100 ||
419
+ (target.requested_model !== undefined &&
420
+ (typeof target.requested_model !== 'string' || target.requested_model.length > 200)) ||
421
+ !isBoundedStringList(target.observed_models)
422
+ ) {
423
+ throw new Error('Invalid run manifest: malformed target evidence');
424
+ }
425
+ }
426
+
427
+ function assertExecutionProvenance(provenance: unknown): void {
428
+ if (!isRecord(provenance) || provenance.schema_version !== '1' || !isRecord(provenance.target)) {
429
+ throw new Error('Invalid run manifest: malformed execution provenance');
430
+ }
431
+ const target = provenance.target;
432
+ if (
433
+ typeof target.provider !== 'string' ||
434
+ target.provider.length === 0 ||
435
+ target.provider.length > 100 ||
436
+ !isBoundedStringList(target.requested_models) ||
437
+ !isBoundedStringList(target.observed_models) ||
438
+ (target.generation !== undefined && !isGenerationConfig(target.generation))
439
+ ) {
440
+ throw new Error('Invalid run manifest: malformed execution provenance');
441
+ }
442
+ if (provenance.evaluator !== undefined) {
443
+ if (!isRecord(provenance.evaluator) || !isBoundedStringList(provenance.evaluator.models)) {
444
+ throw new Error('Invalid run manifest: malformed execution provenance');
445
+ }
446
+ }
447
+ }
448
+
449
+ function isBoundedStringList(value: unknown): boolean {
450
+ return (
451
+ value === undefined ||
452
+ (Array.isArray(value) &&
453
+ value.length <= 100 &&
454
+ value.every((item) => typeof item === 'string' && item.length > 0 && item.length <= 200))
455
+ );
456
+ }
457
+
458
+ function isGenerationConfig(value: unknown): boolean {
459
+ if (!isRecord(value)) return false;
460
+ return [value.temperature, value.max_tokens, value.seed].every(
461
+ (item) => item === undefined || (typeof item === 'number' && Number.isFinite(item))
462
+ );
463
+ }
464
+
465
+ function assertWorkloadIdentity(identity: unknown): void {
466
+ if (
467
+ !isRecord(identity) ||
468
+ identity.schema_version !== '1' ||
469
+ !isContentIdentity(identity.workload) ||
470
+ !isContentIdentity(identity.rubric)
471
+ ) {
472
+ throw new Error('Invalid run manifest: malformed workload identity');
473
+ }
474
+ }
475
+
476
+ function isContentIdentity(value: unknown): boolean {
477
+ return (
478
+ isRecord(value) &&
479
+ value.schema_version === '1' &&
480
+ value.algorithm === 'sha256' &&
481
+ typeof value.digest === 'string' &&
482
+ /^[a-f0-9]{64}$/.test(value.digest)
483
+ );
484
+ }
485
+
347
486
  function assertCaseEvaluationEvidence(evidence: unknown, caseIndex: number): void {
348
487
  if (
349
488
  !isRecord(evidence) ||
@@ -0,0 +1,59 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { createExecutionProvenance } from './execution-provenance';
3
+
4
+ describe('createExecutionProvenance', () => {
5
+ test('keeps requested, observed target, and evaluator model identities separate', () => {
6
+ const provenance = createExecutionProvenance({
7
+ provider: 'openai',
8
+ requestedModel: 'gpt-requested',
9
+ temperature: 0.2,
10
+ maxTokens: 100,
11
+ seed: 7,
12
+ cases: [
13
+ {
14
+ id: 'one',
15
+ ok: true,
16
+ score: 1,
17
+ matcherType: 'exact',
18
+ latencyMs: 1,
19
+ tokens: { prompt: 1, completion: 1, total: 2 },
20
+ prompt: 'prompt',
21
+ response: 'response',
22
+ expected: { type: 'exact', value: 'response', caseSensitive: true },
23
+ tags: [],
24
+ target: {
25
+ provider: 'openai',
26
+ requested_model: 'gpt-requested',
27
+ observed_models: ['gpt-observed'],
28
+ },
29
+ evidence: { evaluator: 'llm_grader', model: 'judge-model' },
30
+ },
31
+ ],
32
+ });
33
+
34
+ expect(provenance).toEqual({
35
+ schema_version: '1',
36
+ target: {
37
+ provider: 'openai',
38
+ requested_models: ['gpt-requested'],
39
+ observed_models: ['gpt-observed'],
40
+ generation: { temperature: 0.2, max_tokens: 100, seed: 7 },
41
+ },
42
+ evaluator: { models: ['judge-model'] },
43
+ });
44
+ });
45
+
46
+ test('records an unavailable observed identity without inventing one', () => {
47
+ const provenance = createExecutionProvenance({
48
+ provider: 'custom',
49
+ requestedModel: 'requested-model',
50
+ cases: [],
51
+ });
52
+
53
+ expect(provenance.target).toEqual({
54
+ provider: 'custom',
55
+ requested_models: ['requested-model'],
56
+ });
57
+ expect(provenance.evaluator).toBeUndefined();
58
+ });
59
+ });
@@ -0,0 +1,53 @@
1
+ /** Bounded execution provenance derived from declared and observed run evidence. */
2
+
3
+ import type { CaseResult, ExecutionProvenance } from '../artifacts/types';
4
+
5
+ export function createExecutionProvenance(options: {
6
+ provider: string;
7
+ requestedModel?: string;
8
+ temperature?: number;
9
+ maxTokens?: number;
10
+ seed?: number;
11
+ cases: CaseResult[];
12
+ }): ExecutionProvenance {
13
+ const requestedModels = uniqueStrings([
14
+ options.requestedModel,
15
+ ...options.cases.map((caseResult) => caseResult.target?.requested_model),
16
+ ]);
17
+ const observedModels = uniqueStrings(
18
+ options.cases.flatMap((caseResult) => caseResult.target?.observed_models ?? [])
19
+ );
20
+ const evaluatorModels = uniqueStrings(
21
+ options.cases.map((caseResult) => caseResult.evidence?.model)
22
+ );
23
+ const generation = omitUndefined({
24
+ temperature: options.temperature,
25
+ max_tokens: options.maxTokens,
26
+ seed: options.seed,
27
+ });
28
+
29
+ return {
30
+ schema_version: '1',
31
+ target: {
32
+ provider: boundedString(options.provider, 100) ?? 'unknown',
33
+ ...(requestedModels.length ? { requested_models: requestedModels } : {}),
34
+ ...(observedModels.length ? { observed_models: observedModels } : {}),
35
+ ...(Object.keys(generation).length ? { generation } : {}),
36
+ },
37
+ ...(evaluatorModels.length ? { evaluator: { models: evaluatorModels } } : {}),
38
+ };
39
+ }
40
+
41
+ function uniqueStrings(values: unknown[]): string[] {
42
+ return [...new Set(values.map((value) => boundedString(value, 200)).filter(Boolean))] as string[];
43
+ }
44
+
45
+ function boundedString(value: unknown, maxLength: number): string | undefined {
46
+ return typeof value === 'string' && value.length > 0 ? value.slice(0, maxLength) : undefined;
47
+ }
48
+
49
+ function omitUndefined<T extends Record<string, number | undefined>>(value: T): Partial<T> {
50
+ return Object.fromEntries(
51
+ Object.entries(value).filter(([, item]) => item !== undefined)
52
+ ) as Partial<T>;
53
+ }
@@ -4,3 +4,5 @@
4
4
 
5
5
  export { getGitInfo } from './git';
6
6
  export { getEnvironmentInfo } from './environment';
7
+ export { createWorkloadIdentity } from './workload-identity';
8
+ export { createExecutionProvenance } from './execution-provenance';
@@ -0,0 +1,69 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { validateScenario } from '../scenario';
3
+ import { createWorkloadIdentity } from './workload-identity';
4
+
5
+ const baseScenario = () =>
6
+ validateScenario({
7
+ name: 'Customer-service policy test',
8
+ version: '2026-09',
9
+ providerConfig: { apiKey: 'sk-live-very-secret-value' },
10
+ setup: {
11
+ systemPrompt: 'Use token=internal-secret only for this fixture.',
12
+ },
13
+ cases: [
14
+ {
15
+ id: 'refund-policy',
16
+ prompt: 'Can I receive a refund?',
17
+ expected: {
18
+ type: 'llm_grader',
19
+ rubric: 'Approve only eligible refunds.',
20
+ strict: true,
21
+ threshold: 0.8,
22
+ },
23
+ },
24
+ ],
25
+ });
26
+
27
+ describe('createWorkloadIdentity', () => {
28
+ test('is deterministic for equivalent declared scenario material', () => {
29
+ const first = createWorkloadIdentity(baseScenario());
30
+ const second = createWorkloadIdentity(baseScenario());
31
+
32
+ expect(first).toEqual(second);
33
+ expect(first.workload.digest).toHaveLength(64);
34
+ expect(first.rubric.digest).toHaveLength(64);
35
+ });
36
+
37
+ test('separates workload changes from rubric changes', () => {
38
+ const base = baseScenario();
39
+ const changedPrompt = structuredClone(base);
40
+ changedPrompt.cases[0].prompt = 'Can I receive a replacement?';
41
+ const changedRubric = structuredClone(base);
42
+ changedRubric.cases[0].expected = {
43
+ type: 'llm_grader',
44
+ rubric: 'Approve only eligible replacements.',
45
+ strict: true,
46
+ threshold: 0.8,
47
+ };
48
+
49
+ const baseIdentity = createWorkloadIdentity(base);
50
+ const promptIdentity = createWorkloadIdentity(changedPrompt);
51
+ const rubricIdentity = createWorkloadIdentity(changedRubric);
52
+
53
+ expect(promptIdentity.workload.digest).not.toBe(baseIdentity.workload.digest);
54
+ expect(promptIdentity.rubric.digest).toBe(baseIdentity.rubric.digest);
55
+ expect(rubricIdentity.workload.digest).toBe(baseIdentity.workload.digest);
56
+ expect(rubricIdentity.rubric.digest).not.toBe(baseIdentity.rubric.digest);
57
+ });
58
+
59
+ test('excludes sensitive configuration and recognized secret text before hashing', () => {
60
+ const first = baseScenario();
61
+ first.cases[0].metadata = { author: 'ArtemisKit', authToken: 'fixture-token-one' };
62
+ const second = structuredClone(first);
63
+ second.providerConfig = { apiKey: 'sk-live-another-secret-value' };
64
+ second.setup = { systemPrompt: 'Use token=different-secret only for this fixture.' };
65
+ second.cases[0].metadata = { author: 'ArtemisKit', authToken: 'fixture-token-two' };
66
+
67
+ expect(createWorkloadIdentity(second)).toEqual(createWorkloadIdentity(first));
68
+ });
69
+ });
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Canonical, redacted workload identities for reproducible assessment evidence.
3
+ */
4
+
5
+ import { createHash } from 'node:crypto';
6
+ import type { WorkloadIdentity } from '../artifacts/types';
7
+ import { redactText, resolvePatterns } from '../redaction/redactor';
8
+ import { DEFAULT_REDACTION_PATTERNS } from '../redaction/types';
9
+ import type { Scenario } from '../scenario/schema';
10
+
11
+ const SENSITIVE_KEY = /(?:api[-_]?key|authorization|auth|credential|password|secret|token)$/i;
12
+ const REDACTED_SECRET = '[REDACTED_SECRET]';
13
+
14
+ /**
15
+ * Create independent identities for the declared workload and its evaluation
16
+ * criteria. Input is sorted, redacted, and hashed locally; raw material is
17
+ * never retained in the identity artifact.
18
+ */
19
+ export function createWorkloadIdentity(scenario: Scenario): WorkloadIdentity {
20
+ const patterns = resolvePatterns([
21
+ ...DEFAULT_REDACTION_PATTERNS,
22
+ ...(scenario.redaction?.patterns ?? []),
23
+ ]).map(({ regex }) => regex);
24
+
25
+ const workload = {
26
+ name: scenario.name,
27
+ version: scenario.version,
28
+ description: scenario.description,
29
+ tags: scenario.tags,
30
+ variables: scenario.variables,
31
+ setup: scenario.setup,
32
+ cases: scenario.cases.map((testCase) => ({
33
+ id: testCase.id,
34
+ name: testCase.name,
35
+ description: testCase.description,
36
+ prompt: testCase.prompt,
37
+ tags: testCase.tags,
38
+ metadata: testCase.metadata,
39
+ timeout: testCase.timeout,
40
+ retries: testCase.retries,
41
+ variables: testCase.variables,
42
+ })),
43
+ };
44
+
45
+ const rubric = {
46
+ cases: scenario.cases.map((testCase) => ({
47
+ id: testCase.id,
48
+ expected: testCase.expected,
49
+ })),
50
+ };
51
+
52
+ return {
53
+ schema_version: '1',
54
+ workload: createContentIdentity(workload, patterns),
55
+ rubric: createContentIdentity(rubric, patterns),
56
+ };
57
+ }
58
+
59
+ function createContentIdentity(value: unknown, patterns: RegExp[]) {
60
+ const canonical = JSON.stringify(canonicalize(value, patterns));
61
+ return {
62
+ schema_version: '1' as const,
63
+ algorithm: 'sha256' as const,
64
+ digest: createHash('sha256').update(canonical).digest('hex'),
65
+ };
66
+ }
67
+
68
+ function canonicalize(value: unknown, patterns: RegExp[], key?: string): unknown {
69
+ if (key && SENSITIVE_KEY.test(key)) return REDACTED_SECRET;
70
+
71
+ if (typeof value === 'string') {
72
+ return redactText(value, patterns, REDACTED_SECRET).text;
73
+ }
74
+
75
+ if (Array.isArray(value)) {
76
+ return value.map((item) => canonicalize(item, patterns));
77
+ }
78
+
79
+ if (value && typeof value === 'object') {
80
+ return Object.fromEntries(
81
+ Object.entries(value)
82
+ .filter(([, nestedValue]) => nestedValue !== undefined)
83
+ .sort(([left], [right]) => left.localeCompare(right))
84
+ .map(([nestedKey, nestedValue]) => [
85
+ nestedKey,
86
+ canonicalize(nestedValue, patterns, nestedKey),
87
+ ])
88
+ );
89
+ }
90
+
91
+ return value;
92
+ }
@@ -170,6 +170,11 @@ describe('executeCase tool loop', () => {
170
170
  expect(result.error).toBe('TOOL_EXECUTOR_REQUIRED');
171
171
  expect(result.latencyMs).toBe(4);
172
172
  expect(result.tokens).toEqual({ prompt: 7, completion: 2, total: 9 });
173
+ expect(result.target).toEqual({
174
+ provider: 'ling',
175
+ requested_model: 'Ling-3.0-flash',
176
+ observed_models: ['Ling-3.0-flash'],
177
+ });
173
178
  });
174
179
 
175
180
  it('retains prior generation metrics when a later generation rejects', async () => {
@@ -201,6 +206,7 @@ describe('executeCase tool loop', () => {
201
206
  steps: 1,
202
207
  terminationReason: 'tool_error',
203
208
  });
209
+ expect(result.target?.observed_models).toEqual(['Ling-3.0-flash']);
204
210
  });
205
211
 
206
212
  it('retains prior generation metrics when a later generation times out', async () => {
@@ -304,7 +310,11 @@ describe('executeCase measurement integrity', () => {
304
310
  status: 'error',
305
311
  response: '',
306
312
  error: 'provider unavailable',
313
+ target: {
314
+ provider: 'test',
315
+ },
307
316
  });
317
+ expect(result.target?.observed_models).toBeUndefined();
308
318
  });
309
319
 
310
320
  it('retains only the bounded evidence contract rather than evaluator details', async () => {