use cv_testkit::{jsonl, World};
use std::fs;
fn write_workflow(w: &World, sid: &str, run: &str, state: &serde_json::Value, script: &str) {
let wdir = w.proj.join(sid).join("workflows");
let sdir = wdir.join("scripts");
fs::create_dir_all(&sdir).unwrap();
fs::write(wdir.join(format!("{run}.json")), serde_json::to_string(state).unwrap()).unwrap();
let name = state["workflowName"].as_str().unwrap_or("workflow");
fs::write(sdir.join(format!("{name}-wf_{run}.js")), script).unwrap();
}
fn write_workflow_agent(w: &World, sid: &str, run: &str, agent_id: &str, lines: &[serde_json::Value]) {
let dir = w.proj.join(sid).join("subagents").join("workflows").join(run);
fs::create_dir_all(&dir).unwrap();
fs::write(dir.join(format!("agent-{agent_id}.jsonl")), jsonl(lines)).unwrap();
fs::write(
dir.join(format!("agent-{agent_id}.meta.json")),
r#"{"agentType":"workflow-subagent"}"#,
)
.unwrap();
}
fn user(uuid: &str, parent: Option<&str>, text: &str) -> serde_json::Value {
serde_json::json!({
"type": "user", "uuid": uuid, "parentUuid": parent, "sessionId": "s",
"timestamp": "2026-06-07T12:00:00Z", "cwd": "/work/proj",
"message": {"role": "user", "content": text}
})
}
fn assistant_tools(uuid: &str, ts: &str, blocks: serde_json::Value) -> serde_json::Value {
serde_json::json!({
"type": "assistant", "uuid": uuid, "sessionId": "s", "timestamp": ts,
"message": {"role": "assistant", "model": "claude-test", "content": blocks}
})
}
fn tool_use(id: &str, name: &str, input: serde_json::Value) -> serde_json::Value {
serde_json::json!({"type": "tool_use", "id": id, "name": name, "input": input})
}
fn boundary(uuid: &str, parent: &str, trigger: &str, pre: u64) -> serde_json::Value {
serde_json::json!({
"type": "system", "subtype": "compact_boundary", "uuid": uuid, "parentUuid": parent,
"sessionId": "s", "timestamp": "2026-06-07T12:00:00Z",
"content": "Conversation compacted",
"compactMetadata": {"trigger": trigger, "preTokens": pre, "durationMs": 90000}
})
}
fn compact_summary(uuid: &str, parent: &str, body: &str) -> serde_json::Value {
serde_json::json!({
"type": "user", "uuid": uuid, "parentUuid": parent, "isCompactSummary": true,
"sessionId": "s", "timestamp": "2026-06-07T12:00:00Z",
"message": {"role": "user", "content": body}
})
}
fn build_world(tag: &str) -> World {
let w = World::new(tag);
let sid = "fsess";
w.write_session(
sid,
&[
user("u0", None, "start the work"),
assistant_tools(
"a0",
"2026-06-07T12:00:01Z",
serde_json::json!([
tool_use("t1", "Bash", serde_json::json!({"command": "cargo build"})),
tool_use("t2", "Workflow", serde_json::json!({"script": "wf"})),
]),
),
user("u1", Some("a0"), "more"),
boundary("b1", "u1", "manual", 980000),
compact_summary("s1", "b1", "SUMMARY ONE: the early context"),
user("u2", Some("s1"), "keep going"),
assistant_tools(
"a1",
"2026-06-07T12:05:00Z",
serde_json::json!([tool_use(
"t3",
"Edit",
serde_json::json!({"file_path": "x.rs", "old_string": "a", "new_string": "b"})
)]),
),
boundary("b2", "a1", "auto", 750000),
compact_summary("s2", "b2", "SUMMARY TWO: the middle context"),
user("u3", Some("s2"), "finish up"),
],
);
let run = "wf_test-0001";
write_workflow(
&w,
sid,
run,
&serde_json::json!({
"runId": run,
"workflowName": "demo-workflow",
"status": "completed",
"summary": "demo run did the two phases",
"defaultModel": "claude-test",
"agentCount": 2,
"totalTokens": 12345,
"totalToolCalls": 7,
"durationMs": 600000,
"scriptPath": "/work/proj/fsess/workflows/scripts/demo-workflow-wf_test-0001.js",
"script": "export const meta = { name: 'demo-workflow' }\n// the driving script body",
"phases": [
{"title": "Phase-A", "detail": "build the thing"},
{"title": "Phase-B", "detail": "verify the thing"}
],
"workflowProgress": [
{"type": "workflow_phase", "index": 1, "title": "Phase-A"},
{"type": "workflow_phase", "index": 2, "title": "Phase-B"},
{"type": "workflow_agent", "index": 1, "label": "builder", "phaseIndex": 1,
"agentId": "aBUILD", "model": "claude-test", "state": "done",
"tokens": 8000, "toolCalls": 5, "durationMs": 400000,
"promptPreview": "build it", "resultPreview": "{\"built\":true}"},
{"type": "workflow_agent", "index": 2, "label": "verifier", "phaseIndex": 2,
"agentId": "aVERIFY", "model": "claude-test", "state": "error",
"tokens": 4345, "toolCalls": 2, "error": "found a bug",
"promptPreview": "verify it", "resultPreview": "{\"ok\":false}"}
]
}),
"export const meta = { name: 'demo-workflow', phases: [] }\n// the driving script body",
);
write_workflow_agent(
&w,
sid,
run,
"aBUILD",
&[
user("wu0", None, "build it"),
assistant_tools(
"wa0",
"2026-06-07T12:01:00Z",
serde_json::json!([
tool_use("b1", "Bash", serde_json::json!({"command": "make"})),
tool_use("b2", "Read", serde_json::json!({"file_path": "src/lib.rs"})),
tool_use(
"b3",
"Edit",
serde_json::json!({"file_path": "src/lib.rs", "old_string": "x", "new_string": "y"})
),
]),
),
],
);
write_workflow_agent(
&w,
sid,
run,
"aVERIFY",
&[
user("vu0", None, "verify it"),
assistant_tools(
"va0",
"2026-06-07T12:02:00Z",
serde_json::json!([tool_use("v1", "Bash", serde_json::json!({"command": "cargo test"}))]),
),
],
);
w
}
#[test]
fn show_kind_compaction_lists_boundaries_and_summaries() {
let w = build_world("compaction");
let (out, _) = w.cv_ok(&["show", "fsess", "--kind", "compaction_boundary,compaction_summary"]);
assert!(out.contains("compacted 2 time(s)"), "got:\n{out}");
assert!(out.contains("manual"), "trigger surfaced:\n{out}");
assert!(out.contains("auto"), "second trigger surfaced:\n{out}");
assert!(out.contains("SUMMARY ONE"), "first summary head:\n{out}");
let (full, _) = w.cv_ok(&[
"show",
"fsess",
"--kind",
"compaction_boundary,compaction_summary",
"--summaries",
]);
assert!(full.contains("SUMMARY ONE: the early context"));
assert!(full.contains("SUMMARY TWO: the middle context"));
let (js, _) = w.cv_ok(&[
"show",
"fsess",
"--kind",
"compaction_boundary,compaction_summary",
"--json",
]);
let v: serde_json::Value = serde_json::from_str(&js).unwrap();
assert_eq!(v.as_array().unwrap().len(), 2);
assert!(v[0].get("pre_compaction_span").is_some(), "span in json:\n{js}");
assert_eq!(v[0]["trigger"], "manual");
assert_eq!(v[0]["pre_tokens"], 980000);
}
#[test]
fn show_pre_compaction_windows_the_lost_span() {
let w = build_world("precompact");
let (out, err) = w.cv_ok(&["show", "fsess", "--pre-compaction", "1"]);
assert!(err.contains("pre-compaction #1 of 2"), "banner:\n{err}");
assert!(
err.contains("messages 0..3"),
"computed span (the 0.11 `A..B` grammar):\n{err}"
);
assert!(!err.contains("messages 0-3"), "the old `A-B` span must be gone:\n{err}");
assert!(out.contains("start the work"), "pre-span body:\n{out}");
assert!(
!out.contains("finish up"),
"must not leak post-compaction content:\n{out}"
);
let (_, err2) = w.cv_ok(&["show", "fsess", "--pre-compaction", "2"]);
assert!(err2.contains("pre-compaction #2 of 2"), "{err2}");
let (ok, _, _, err3) = w.cv(&["show", "fsess", "--pre-compaction", "9"]);
assert!(!ok && err3.contains("no compaction #9"), "{err3}");
}
#[test]
fn workflow_renders_phase_tree_and_lists() {
let w = build_world("workflow");
let (list, _) = w.cv_ok(&["workflow", "fsess"]);
assert!(list.contains("1 workflow(s)"), "got:\n{list}");
assert!(list.contains("demo-workflow"), "{list}");
assert!(list.contains("2 phase(s)") && list.contains("2 agent(s)"), "{list}");
let (tree, _) = w.cv_ok(&["workflow", "fsess", "wf_test"]);
assert!(tree.contains("workflow demo-workflow"), "{tree}");
assert!(tree.contains("12,345 tokens"), "grouped token total:\n{tree}");
assert!(tree.contains("phase 1 · Phase-A"), "{tree}");
assert!(tree.contains("build the thing"), "phase detail:\n{tree}");
assert!(tree.contains("builder"), "agent label:\n{tree}");
assert!(
tree.contains("✓ builder") && tree.contains("✗ verifier"),
"state glyphs:\n{tree}"
);
assert!(tree.contains("found a bug"), "agent error surfaced:\n{tree}");
assert!(tree.contains("\"built\":true"), "result preview:\n{tree}");
let (scr, _) = w.cv_ok(&["workflow", "fsess", "wf_test", "--script"]);
assert!(scr.contains("the driving script body"), "script body:\n{scr}");
for args in [
&["workflow", "fsess", "--script"][..],
&["workflow", "fsess", "--results"][..],
&["workflow", "fsess", "--revive"][..],
&["workflow", "fsess", "--follow"][..],
&["workflow", "fsess", "wf_test", "--revive-all"][..],
&["workflow", "fsess", "wf_test", "--revive", "--json"][..],
&["workflow", "fsess", "wf_test", "--revive", "--follow"][..],
] {
let (_, code, _, err) = w.cv(args);
assert_eq!(code, 2, "{args:?}: {err}");
}
let (js, _) = w.cv_ok(&["workflow", "fsess", "wf_test", "--json"]);
let v: serde_json::Value = serde_json::from_str(&js).unwrap();
assert_eq!(v["name"], "demo-workflow");
assert_eq!(v["phases"].as_array().unwrap().len(), 2);
assert_eq!(v["phases"][0]["agents"][0]["agent_id"], "aBUILD");
}
#[test]
fn tools_cross_agent_queries() {
let w = build_world("tools");
let (agg, _) = w.cv_ok(&["tools", "fsess"]);
assert!(agg.contains("orchestrator + forest"), "{agg}");
assert!(agg.contains("Bash"), "{agg}");
let (which, _) = w.cv_ok(&["tools", "fsess", "--tool", "Bash"]);
assert!(which.contains("3 agent(s) used \"Bash\""), "got:\n{which}");
let (one, _) = w.cv_ok(&["tools", "fsess", "--agent", "aBUILD"]);
assert!(one.contains("tools used by aBUILD"), "{one}");
assert!(
one.contains("Bash") && one.contains("Read") && one.contains("Edit"),
"{one}"
);
let (across, _) = w.cv_ok(&["tools", "fsess", "--workflow", "wf_test", "--across"]);
assert!(across.contains("aBUILD") && across.contains("aVERIFY"), "{across}");
assert!(
!across.contains("orchestrator"),
"workflow view excludes orchestrator:\n{across}"
);
let (tl, _) = w.cv_ok(&["tools", "fsess", "--timeline"]);
assert!(tl.contains("tool call(s) (chronological)"), "{tl}");
assert!(tl.contains("orch") && tl.contains("aBUILD"), "agents tagged:\n{tl}");
}