Skip to content

Commit dcd0319

Browse files
shivamhwpclaude
andcommitted
test(orchestration-v2): replay a Claude workflow end to end
The adapter's workflow projection was covered in isolation and the panel derivation was covered against synthetic agents, but nothing drove real workflow frames through the orchestrator to the projection the client receives. Adds a claude_workflow replay fixture: a local_workflow task with two phases and two members, asserted on the resulting projection. Pins what the panel groups by — that members claim declared phases, that their membership points at the coordinator that actually reached the projection, and that the coordinator's usage still covers its members so subtracting theirs cannot go negative. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent a54d55a commit dcd0319

5 files changed

Lines changed: 137 additions & 0 deletions

File tree

apps/server/src/orchestration-v2/ProviderEventIngestor.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,8 @@ layer("ProviderEventIngestorV2", (it) => {
303303
maxAttempts: 3,
304304
retryDelayMs: 2_000,
305305
});
306+
assert.isNotNull(errorItem.startedAt);
307+
if (errorItem.startedAt === null) return;
306308
assert.equal(
307309
DateTime.toEpochMillis(errorItem.startedAt),
308310
DateTime.toEpochMillis(retryStartedAt),
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{"type": "transcript_start", "provider": "claudeAgent", "protocol": "claude-agent-sdk.query", "version": "0.2.111", "scenario": "claude_workflow", "metadata": {"prompts": ["Run a two-phase workflow, then answer exactly: claude workflow fixture complete"], "model": "claude-sonnet-4-6", "nativeSessionId": "3c7f21a0-9d4e-4b21-8f60-000000000201", "queryMode": "streaming", "tools": "claude_code", "permissionMode": "bypassPermissions", "generatedBy": "hand-authored-workflow-observability"}}
2+
{"type": "expect_outbound", "label": "query.open", "frame": {"type": "query.open", "options": {"model": "claude-sonnet-4-6", "tools": {"type": "preset", "preset": "claude_code"}, "permissionMode": "bypassPermissions", "allowDangerouslySkipPermissions": true, "sessionId": "3c7f21a0-9d4e-4b21-8f60-000000000201"}}}
3+
{"type": "expect_outbound", "label": "prompt.offer:1", "frame": {"type": "prompt.offer", "message": {"type": "user", "message": {"role": "user", "content": "Run a two-phase workflow, then answer exactly: claude workflow fixture complete"}, "parent_tool_use_id": null}}}
4+
{"type": "emit_inbound", "label": "system", "frame": {"type": "system", "subtype": "init", "agents": [], "apiKeySource": "none", "claude_code_version": "2.1.183", "cwd": "/tmp/claude-replay-claude_workflow", "tools": [], "mcp_servers": [], "model": "claude-sonnet-4-6", "permissionMode": "bypassPermissions", "slash_commands": [], "output_style": "default", "skills": [], "plugins": [], "fast_mode_state": "off", "uuid": "af3bf4cc-1385-4e19-a5fa-fcbe2a0ca201", "session_id": "3c7f21a0-9d4e-4b21-8f60-000000000201"}}
5+
{"type": "emit_inbound", "label": "assistant:commentary", "frame": {"type": "assistant", "message": {"model": "claude-sonnet-4-6", "id": "msg_01claudeworkflow1", "type": "message", "role": "assistant", "content": [{"type": "text", "text": "Starting the two-phase workflow."}], "stop_reason": null, "stop_sequence": null, "stop_details": null, "usage": {"input_tokens": 1, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0, "output_tokens": 1, "service_tier": "standard", "inference_geo": "not_available"}, "context_management": null}, "parent_tool_use_id": null, "session_id": "3c7f21a0-9d4e-4b21-8f60-000000000201", "uuid": "97898e28-ee4e-4fcf-80c3-5c790c502201"}}
6+
{"type": "emit_inbound", "label": "task_started:local_workflow", "frame": {"type": "system", "subtype": "task_started", "task_id": "wf7ka2mq1", "tool_use_id": "toolu_01ClaudeWorkflowFixture01", "description": "Research and implement", "task_type": "local_workflow", "prompt": "Research the parser, then implement the fix.", "uuid": "4873c95b-1aa9-44a5-9783-dc56dac73201", "session_id": "3c7f21a0-9d4e-4b21-8f60-000000000201"}}
7+
{"type": "emit_inbound", "label": "task_progress:phase-1", "frame": {"type": "system", "subtype": "task_progress", "task_id": "wf7ka2mq1", "tool_use_id": "toolu_01ClaudeWorkflowFixture01", "description": "Research phase running", "usage": {"input_tokens": 300, "output_tokens": 200, "total_tokens": 500, "tool_uses": 4}, "workflow_progress": [{"type": "workflow_phase", "index": 0, "title": "Research"}, {"type": "workflow_phase", "index": 1, "title": "Implement"}, {"type": "workflow_agent", "index": 0, "label": "Researcher", "state": "active", "phaseIndex": 0, "attempt": 0, "model": "claude-sonnet-4-6", "lastToolName": "Read", "tokens": 200, "toolCalls": 2}], "uuid": "4873c95b-1aa9-44a5-9783-dc56dac73202", "session_id": "3c7f21a0-9d4e-4b21-8f60-000000000201"}}
8+
{"type": "emit_inbound", "label": "task_progress:phase-2", "frame": {"type": "system", "subtype": "task_progress", "task_id": "wf7ka2mq1", "tool_use_id": "toolu_01ClaudeWorkflowFixture01", "description": "Implement phase running", "usage": {"input_tokens": 700, "output_tokens": 500, "total_tokens": 1200, "tool_uses": 9}, "workflow_progress": [{"type": "workflow_phase", "index": 0, "title": "Research"}, {"type": "workflow_phase", "index": 1, "title": "Implement"}, {"type": "workflow_agent", "index": 0, "label": "Researcher", "state": "done", "phaseIndex": 0, "attempt": 0, "model": "claude-sonnet-4-6", "lastToolName": "Read", "tokens": 400, "toolCalls": 3}, {"type": "workflow_agent", "index": 1, "label": "Implementer", "state": "active", "phase_index": 1, "attempt": 0, "model": "claude-opus-4-1", "last_tool_name": "Edit", "tokens": 500, "tool_calls": 4}], "uuid": "4873c95b-1aa9-44a5-9783-dc56dac73203", "session_id": "3c7f21a0-9d4e-4b21-8f60-000000000201"}}
9+
{"type": "emit_inbound", "label": "task_notification:local_workflow", "frame": {"type": "system", "subtype": "task_notification", "task_id": "wf7ka2mq1", "tool_use_id": "toolu_01ClaudeWorkflowFixture01", "status": "completed", "output_file": "", "summary": "Workflow finished both phases.", "usage": {"input_tokens": 900, "output_tokens": 600, "total_tokens": 1500, "tool_uses": 11}, "uuid": "4873c95b-1aa9-44a5-9783-dc56dac73204", "session_id": "3c7f21a0-9d4e-4b21-8f60-000000000201"}}
10+
{"type": "emit_inbound", "label": "assistant:commentary", "frame": {"type": "assistant", "message": {"model": "claude-sonnet-4-6", "id": "msg_01claudeworkflow2", "type": "message", "role": "assistant", "content": [{"type": "text", "text": "claude workflow fixture complete"}], "stop_reason": null, "stop_sequence": null, "stop_details": null, "usage": {"input_tokens": 1, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0, "output_tokens": 1, "service_tier": "standard", "inference_geo": "not_available"}, "context_management": null}, "parent_tool_use_id": null, "session_id": "3c7f21a0-9d4e-4b21-8f60-000000000201", "uuid": "89be9e65-097b-47e8-849b-be723386b201"}}
11+
{"type": "emit_inbound", "label": "result", "frame": {"type": "result", "subtype": "success", "is_error": false, "api_error_status": null, "duration_ms": 4000, "duration_api_ms": 3500, "num_turns": 2, "result": "claude workflow fixture complete", "stop_reason": "end_turn", "session_id": "3c7f21a0-9d4e-4b21-8f60-000000000201", "total_cost_usd": 0.001, "usage": {"input_tokens": 2, "cache_creation_input_tokens": 0, "cache_read_input_tokens": 0, "output_tokens": 2, "server_tool_use": {"web_search_requests": 0, "web_fetch_requests": 0}, "service_tier": "standard", "cache_creation": {"ephemeral_1h_input_tokens": 0, "ephemeral_5m_input_tokens": 0}, "inference_geo": "", "iterations": [{"input_tokens": 1, "output_tokens": 1, "cache_read_input_tokens": 0, "cache_creation_input_tokens": 0, "cache_creation": {"ephemeral_5m_input_tokens": 0, "ephemeral_1h_input_tokens": 0}, "type": "message"}], "speed": "standard"}, "modelUsage": {"claude-sonnet-4-6": {"inputTokens": 2, "outputTokens": 2, "cacheReadInputTokens": 0, "cacheCreationInputTokens": 0, "webSearchRequests": 0, "costUSD": 0.001, "contextWindow": 200000, "maxOutputTokens": 32000}}, "permission_denials": [], "terminal_reason": "completed", "fast_mode_state": "off", "uuid": "89be9e65-097b-47e8-849b-be723386b202"}}
12+
{"type": "runtime_exit", "status": "success"}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import type { OrchestratorFixtureInput } from "../shared.ts";
2+
3+
export const CLAUDE_WORKFLOW_PROMPT =
4+
"Run a two-phase workflow, then answer exactly: claude workflow fixture complete";
5+
6+
/**
7+
* End-to-end cover for the Claude workflow coordinator. The adapter-level
8+
* projection of phases and workers is unit-tested, and the panel derivation is
9+
* unit-tested against synthetic agents — nothing joined the two. This drives
10+
* real workflow frames through the orchestrator so the coordinator, its
11+
* phases, and its members are asserted on the projection the client actually
12+
* receives, then feeds that projection to the panel derivation.
13+
*/
14+
export function claudeWorkflowInput(): OrchestratorFixtureInput {
15+
return {
16+
steps: [{ type: "message", text: CLAUDE_WORKFLOW_PROMPT }],
17+
};
18+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { assert } from "@effect/vitest";
2+
import type { ProviderReplayTranscript } from "@t3tools/contracts";
3+
4+
import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts";
5+
import {
6+
assertBaseProjection,
7+
assertSemanticProjectionIntegrity,
8+
assertUserMessagesInclude,
9+
projectionFor,
10+
} from "../shared.ts";
11+
import { CLAUDE_WORKFLOW_PROMPT } from "./input.ts";
12+
13+
export function assertClaudeWorkflowOutput(
14+
result: OrchestratorV2ScenarioResult,
15+
transcript: ProviderReplayTranscript,
16+
) {
17+
assertBaseProjection({ result, transcript, runCount: 1, runStatuses: ["completed"] });
18+
19+
const projection = projectionFor(result, transcript.scenario);
20+
assertSemanticProjectionIntegrity(projection);
21+
assertUserMessagesInclude(projection, [CLAUDE_WORKFLOW_PROMPT]);
22+
23+
const coordinator = projection.subagents.find((subagent) => subagent.kind === "workflow");
24+
assert.isDefined(coordinator, "the workflow must reach the projection as a coordinator");
25+
assert.deepEqual(coordinator?.role, { name: "workflow-coordinator", source: "app_default" });
26+
assert.deepEqual(coordinator?.workflow?.phases, [
27+
{ index: 0, title: "Research" },
28+
{ index: 1, title: "Implement" },
29+
]);
30+
31+
const members = projection.subagents
32+
.filter((subagent) => subagent.kind === "workflow_agent")
33+
.toSorted(
34+
(left, right) =>
35+
(left.workflowMembership?.agentIndex ?? 0) - (right.workflowMembership?.agentIndex ?? 0),
36+
);
37+
assert.lengthOf(members, 2, "both workflow members must reach the projection");
38+
assert.deepEqual(
39+
members.map((member) => [
40+
member.title,
41+
member.status,
42+
member.workflowMembership?.phaseIndex,
43+
member.model,
44+
member.usage?.totalTokens,
45+
]),
46+
[
47+
["Researcher", "completed", 0, "claude-sonnet-4-6", 400],
48+
["Implementer", "running", 1, "claude-opus-4-1", 500],
49+
],
50+
);
51+
// Membership must point at the coordinator that actually reached the
52+
// projection, not at an id the adapter invented independently.
53+
assert.deepEqual(
54+
[...new Set(members.map((member) => member.workflowMembership?.workflowSubagentId))],
55+
[coordinator?.id],
56+
);
57+
58+
// The panel groups by these three fields, so pin the values it reads rather
59+
// than only the ones the coordinator carries. The derivation itself is
60+
// covered in client-runtime; the schema is what joins the two sides, so what
61+
// matters here is that the server emits a shape that groups cleanly:
62+
// every member claims a declared phase, and no member is left ungrouped.
63+
const declaredPhases = new Set((coordinator?.workflow?.phases ?? []).map((phase) => phase.index));
64+
assert.deepEqual(
65+
members.map((member) => declaredPhases.has(member.workflowMembership?.phaseIndex ?? -1)),
66+
[true, true],
67+
);
68+
assert.lengthOf(
69+
projection.subagents.filter(
70+
(subagent) => subagent.kind === "workflow_agent" && subagent.workflowMembership === null,
71+
),
72+
0,
73+
"a member without membership would render outside its workflow",
74+
);
75+
// The coordinator's usage already covers its members, so the panel subtracts
76+
// theirs from it. That only stays non-negative if the provider's totals are
77+
// carried through unmodified.
78+
const memberTotal = members.reduce(
79+
(total, member) => total + (member.usage?.totalTokens ?? 0),
80+
0,
81+
);
82+
assert.isAtLeast(coordinator?.usage?.totalTokens ?? 0, memberTotal);
83+
84+
const assistantTexts = projection.turnItems.flatMap((item) =>
85+
item.type === "assistant_message" ? [item.text] : [],
86+
);
87+
assert.deepEqual(assistantTexts, [
88+
"Starting the two-phase workflow.",
89+
"claude workflow fixture complete",
90+
]);
91+
}

apps/server/src/orchestration-v2/testkit/fixtures/index.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { claudeIdleResumeInput } from "./claude_idle_resume/input.ts";
44
import { assertClaudeIdleResumeOutput } from "./claude_idle_resume/output.ts";
55
import { claudeLocalBashTaskInput } from "./claude_local_bash_task/input.ts";
66
import { assertClaudeLocalBashTaskOutput } from "./claude_local_bash_task/output.ts";
7+
import { claudeWorkflowInput } from "./claude_workflow/input.ts";
8+
import { assertClaudeWorkflowOutput } from "./claude_workflow/output.ts";
79
import { claudeResultIsErrorInput } from "./claude_result_is_error/input.ts";
810
import { assertClaudeResultIsErrorOutput } from "./claude_result_is_error/output.ts";
911
import { grokSubagentLineageInput } from "./grok_subagent_lineage/input.ts";
@@ -99,6 +101,18 @@ export const ORCHESTRATOR_REPLAY_FIXTURES = [
99101
},
100102
],
101103
},
104+
{
105+
name: "claude_workflow",
106+
buildInput: claudeWorkflowInput,
107+
providers: [
108+
{
109+
driver: ProviderDriverKind.make("claudeAgent"),
110+
transcriptFile: new URL("./claude_workflow/claude_transcript.ndjson", import.meta.url),
111+
modelSelection: CLAUDE_MODEL_SELECTION,
112+
assertOutput: assertClaudeWorkflowOutput,
113+
},
114+
],
115+
},
102116
{
103117
name: "claude_idle_resume",
104118
buildInput: claudeIdleResumeInput,

0 commit comments

Comments
 (0)