Files
clide/test/builtin/claude/workflow_run_test.dart
jpmschweitzerandClaude Opus 4.8 b399acedbf surface Claude Code Workflow runs in convo + sidebar (T-416)
A `Workflow` tool-use launches its multi-agent run in the background and
returns immediately; the real fan-out arrives out-of-band on stream-json
`type:"system"` task_* events (task_started / task_progress / task_updated /
task_notification) keyed by the launching tool-use id — which clide was
dropping. (Wire shape captured by two live stream-json probes; recorded on
the ticket.)

- workflow_run.dart: a pure, Flutter-free WorkflowRun/WorkflowAgent model
  that folds those events (phases, per-agent start→progress→done deltas,
  usage) into a snapshot.
- StreamJsonSession recognises the events, accumulates a
  Map<toolUseId, WorkflowRun>, and exposes `workflows` + `workflowsStream`.
- A `Workflow` tool-use with a live run renders a dedicated run card —
  phase groups, per-agent rows with spinner/check status, usage, and the
  script — falling back to the generic tool card pre-progress or on reload.
  The run breaks the activity cluster so it's always first-class (like T-342).
- The sidebar Activity tab adds a WORKFLOWS section: one row per run with its
  done/total agent count, tinted by running/done state.

Closes T-416 and the T-410 epic (all children done).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 21:44:41 +02:00

138 lines
5.6 KiB
Dart

/// Tests for the WorkflowRun model (T-416): folding stream-json `system`
/// task_* events — the exact shapes captured by the spike — into a snapshot.
library;
import 'package:clide/builtin/claude/src/workflow_run.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('isWorkflowSystemEvent', () {
test('accepts task_* system events that name a tool_use_id', () {
for (final s in kWorkflowSystemSubtypes) {
expect(isWorkflowSystemEvent({'type': 'system', 'subtype': s, 'tool_use_id': 'toolu_1'}), isTrue, reason: s);
}
});
test('rejects unrelated system subtypes and non-system events', () {
expect(isWorkflowSystemEvent({'type': 'system', 'subtype': 'init', 'tool_use_id': 'x'}), isFalse);
expect(isWorkflowSystemEvent({'type': 'system', 'subtype': 'thinking_tokens'}), isFalse);
expect(isWorkflowSystemEvent({'type': 'assistant', 'subtype': 'task_progress', 'tool_use_id': 'x'}), isFalse);
expect(isWorkflowSystemEvent({'type': 'system', 'subtype': 'task_progress'}), isFalse); // no tool_use_id
});
});
group('foldEvent — phase-less run', () {
test('task_started seeds name/description/taskId', () {
const run = WorkflowRun(toolUseId: 'toolu_1');
final r = run.foldEvent({
'type': 'system',
'subtype': 'task_started',
'task_id': 'wy01fihjt',
'tool_use_id': 'toolu_1',
'description': 'Two agents return one word each',
'workflow_name': 'parallel-words',
'prompt': 'export const meta = ...',
});
expect(r.taskId, 'wy01fihjt');
expect(r.name, 'parallel-words');
expect(r.description, 'Two agents return one word each');
expect(r.running, isTrue);
expect(r.agentCount, 0);
});
test('task_progress merges workflow_agent deltas by index', () {
var run = const WorkflowRun(toolUseId: 'toolu_1');
// First delta: two agents start, then get their agentIds + real model.
run = run.foldEvent({
'type': 'system',
'subtype': 'task_progress',
'tool_use_id': 'toolu_1',
'summary': 'Two agents return one word each',
'usage': {'total_tokens': 0, 'tool_uses': 0, 'duration_ms': 28},
'workflow_progress': [
{'type': 'workflow_agent', 'index': 1, 'label': 'alpha', 'model': 'haiku', 'state': 'start'},
{'type': 'workflow_agent', 'index': 2, 'label': 'beta', 'model': 'haiku', 'state': 'start'},
{'type': 'workflow_agent', 'index': 1, 'agentId': 'ae51341336dd3a4a0', 'model': 'claude-haiku-4-5-20251001', 'state': 'start'},
],
});
expect(run.agentCount, 2);
expect(run.agents[1]!.label, 'alpha'); // kept from earlier delta
expect(run.agents[1]!.agentId, 'ae51341336dd3a4a0'); // filled by later delta
expect(run.agents[1]!.model, 'claude-haiku-4-5-20251001'); // upgraded
expect(run.summary, 'Two agents return one word each');
// Second delta: agent 2 advances to done.
run = run.foldEvent({
'type': 'system',
'subtype': 'task_progress',
'tool_use_id': 'toolu_1',
'workflow_progress': [
{'type': 'workflow_agent', 'index': 2, 'agentId': 'a210f9290a5f5d089', 'state': 'done'},
],
});
expect(run.agents[2]!.state, WorkflowAgentState.done);
expect(run.agents[1]!.state, WorkflowAgentState.start); // untouched
expect(run.doneCount, 1);
});
test('task_updated and task_notification mark the run done', () {
var run = const WorkflowRun(toolUseId: 'toolu_1');
run = run.foldEvent({
'type': 'system',
'subtype': 'task_updated',
'tool_use_id': 'toolu_1',
'patch': {'status': 'completed', 'end_time': 1},
});
expect(run.done, isTrue);
var run2 = const WorkflowRun(toolUseId: 'toolu_1');
run2 = run2.foldEvent({
'type': 'system',
'subtype': 'task_notification',
'tool_use_id': 'toolu_1',
'status': 'completed',
'summary': 'Dynamic workflow completed',
'usage': {'total_tokens': 19306, 'tool_uses': 0, 'duration_ms': 1009},
});
expect(run2.done, isTrue);
expect(run2.summary, 'Dynamic workflow completed');
expect(run2.totalTokens, 19306);
expect(run2.durationMs, 1009);
});
});
group('foldEvent — phased run', () {
test('workflow_phase entries register phases; agents carry phase tags', () {
var run = const WorkflowRun(toolUseId: 'toolu_1');
run = run.foldEvent({
'type': 'system',
'subtype': 'task_progress',
'tool_use_id': 'toolu_1',
'workflow_progress': [
{'type': 'workflow_phase', 'index': 1, 'title': 'Scan'},
{'type': 'workflow_phase', 'index': 2, 'title': 'Fix'},
{'type': 'workflow_agent', 'index': 1, 'label': 'scan it', 'phaseIndex': 1, 'phaseTitle': 'Scan', 'model': 'haiku', 'state': 'start'},
],
});
expect(run.orderedPhases.map((p) => p.title), ['Scan', 'Fix']);
expect(run.agents[1]!.phaseIndex, 1);
expect(run.agents[1]!.phaseTitle, 'Scan');
});
});
test('orderedAgents sorts by fan-out index', () {
var run = const WorkflowRun(toolUseId: 'toolu_1');
run = run.foldEvent({
'type': 'system',
'subtype': 'task_progress',
'tool_use_id': 'toolu_1',
'workflow_progress': [
{'type': 'workflow_agent', 'index': 3, 'label': 'c', 'state': 'start'},
{'type': 'workflow_agent', 'index': 1, 'label': 'a', 'state': 'start'},
{'type': 'workflow_agent', 'index': 2, 'label': 'b', 'state': 'start'},
],
});
expect(run.orderedAgents.map((a) => a.label), ['a', 'b', 'c']);
});
}