Files
callstack__agent-device/test/output-economy/routine-workflow.test.ts
devin-ai-integration[bot] db492cbaed test(output-economy): add routine-workflow output-behavior oracle (#1190)
* test(output-economy): add routine-workflow output-behavior oracle

Add a deterministic routine-workflow measurement (#1180) that pairs
response bytes with follow-up behavior: fallback-observation count,
retry count, and whether an actionable failure preserves the session.
Refs chain across one recorded checkout session and counts derive from
the real formatters, so dropping settled-diff refs, the unchanged-
interactive tail, or a recovery handle fails the suite. Adds a matching
non-gating help-conformance next-command case. Response defaults
unchanged.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(output-economy): make routine-workflow ref-surfacing depend on rendered output

Address review on #1190:
- Drop the raw mutation-confirm/failure MCP data samples that leaked e4/e5
  and mislabeled the surface; e4/e5 now surface only from the rendered CLI
  settled-diff, so dropping added refs genuinely raises the fallback count.
- Track the failure once as its projection-invariant normalized payload
  (workflow.failure.shared.json) instead of duplicate cli/mcp raw copies.
- Reuse the shared REF_TOKEN_PATTERN from economy-metrics.ts.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(output-economy): reuse shared fixtures in routine-workflow oracle

Address review finding #2 on #1190: chain the workflow onto the shared
per-surface fixtures instead of copy-pasting them.
- orient/recheck reuse SNAPSHOT_RESULT + SNAPSHOT_DAEMON_RESULT; the
  first mutation reuses SETTLE_ADDED_REF_RESULT, so session identity and
  ref generations come from ./fixtures.ts and the two suites cannot drift.
- Only the genuinely workflow-specific pieces remain local: the unchanged
  recheck, a tail retargeted onto a settled-diff ref (SETTLE_TAIL_RESULT
  taps an unsurfaced @e6 and can't chain), the in-session timeout failure,
  and its recovered retry. routine-workflow.ts drops ~110 LOC.
- Rendered-output ref guard preserved: @e5 (settled diff) and @e7 (tail)
  surface only from formatter output; recovery semantics unchanged.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Michał Pierzchała <thymikee@gmail.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-10 15:27:24 +02:00

104 lines
4.4 KiB
TypeScript

import { describe, expect, test } from 'vitest';
import { renderRoutineWorkflow } from './routine-workflow.ts';
const { steps, error, measurement } = await renderRoutineWorkflow();
describe('routine-workflow output-behavior oracle', () => {
test('runs one recorded routine session end to end', () => {
expect(measurement.commandCount).toBe(7);
expect(measurement.steps.map((step) => step.kind)).toEqual([
'orient',
'recheck',
'mutation',
'mutation',
'read',
'failure',
'retry',
]);
expect(measurement.totalBytes).toBeGreaterThan(0);
});
test('never needs a redundant observation to reach the next target', () => {
// Every action targets a ref an earlier response already surfaced. If a
// formatter dropped the settled diff's added refs or the unchanged-
// interactive tail, the target would go unsurfaced and the agent would be
// forced to re-observe — that is what this count catches.
expect(measurement.fallbackObservationCount).toBe(0);
const targeted = measurement.steps.filter((step) => step.targetRef);
expect(targeted.length).toBeGreaterThan(0);
for (const step of targeted) {
expect(
step.targetSurfacedBy,
`${step.id} target ${step.targetRef} was not surfaced earlier`,
).toBeDefined();
}
});
test('the settled diff, not a snapshot, surfaces the post-mutation targets', () => {
const surfacedBy = Object.fromEntries(
measurement.steps
.filter((step) => step.targetRef)
.map((step) => [step.targetRef, step.targetSurfacedBy]),
);
expect(surfacedBy['@e3']).toBe('orient');
// @e5 (View receipt) exists only in the settled diff's added lines, never in
// the orienting snapshot — so its surfacer proves the rendered diff, not a
// re-observation, handed it over.
expect(surfacedBy['@e5']).toBe('mutation-confirm');
// @e7 (Continue) first appears in the removals-only settle's unchanged-
// interactive tail, the only place it shows up before it is tapped.
expect(surfacedBy['@e7']).toBe('mutation-tail');
});
test('exactly one recovery retry, driven by an actionable failure', () => {
expect(measurement.retryCount).toBe(1);
const failure = measurement.steps.find((step) => step.kind === 'failure');
const retry = measurement.steps.find((step) => step.kind === 'retry');
// The retry re-targets the same ref the failure was about; recovery stayed
// in-session instead of re-orienting.
expect(retry?.targetRef).toBe(failure?.targetRef);
});
test('the actionable failure preserves every recovery handle', () => {
expect(measurement.recoveryPreservesSession).toBe(true);
expect(measurement.recoveryFields).toEqual({
code: 'COMMAND_FAILED',
session: 'economy-fixture',
refsGeneration: 14,
retriable: true,
hint: expect.stringContaining('Retry'),
});
// Recovery identity keys on structured details, not on message text.
expect(error.details).toMatchObject({ reason: 'timeout', timeoutMs: 10_000, ref: '@e7' });
expect(error.retriable).toBe(true);
});
test('unchanged snapshot suppression keeps the prior refs valid without re-emitting the tree', () => {
const recheck = steps.find((step) => step.id === 'recheck');
const text =
recheck?.samples.cli && 'text' in recheck.samples.cli ? recheck.samples.cli.text : '';
expect(text).toContain('unchanged');
expect(text).toContain('refs are still valid');
// The suppression is strictly cheaper than the orienting snapshot it repeats.
const orientBytes = measurement.steps.find((step) => step.id === 'orient')!.bytes;
const recheckBytes = measurement.steps.find((step) => step.id === 'recheck')!.bytes;
expect(recheckBytes).toBeLessThan(orientBytes);
});
test('CLI and MCP projections of the orienting snapshot both carry the actionable refs', () => {
const orient = steps.find((step) => step.id === 'orient')!;
const cliText =
orient.samples.cli && 'text' in orient.samples.cli ? orient.samples.cli.text : '';
expect(cliText).toContain('@e3 [button] "Place order"');
const mcp =
orient.samples.mcp && 'data' in orient.samples.mcp ? orient.samples.mcp.data : undefined;
expect(mcp).toMatchObject({
isError: false,
structuredContent: { refsGeneration: 12 },
content: [{ type: 'text', text: expect.stringContaining('@e3 [button] "Place order"') }],
});
});
});