use super::*;
#[test]
fn brief_with_unreviewed_discovered_files_is_blocked_with_structured_metadata() {
let workspace_root =
std::env::temp_dir().join(format!("tandem-brief-coverage-{}", uuid::Uuid::new_v4()));
std::fs::create_dir_all(workspace_root.join("docs")).expect("create workspace");
std::fs::write(
workspace_root.join("docs/one.md"),
"# One\nsource content\n",
)
.expect("write one");
std::fs::write(
workspace_root.join("docs/two.md"),
"# Two\nsource content\n",
)
.expect("write two");
let snapshot = automation_workspace_root_file_snapshot(
workspace_root.to_str().expect("workspace root string"),
);
let node = AutomationFlowNode {
knowledge: tandem_orchestrator::KnowledgeBinding::default(),
node_id: "research".to_string(),
agent_id: "agent-a".to_string(),
objective: "Research".to_string(),
depends_on: Vec::new(),
input_refs: Vec::new(),
output_contract: Some(AutomationFlowOutputContract {
kind: "brief".to_string(),
validator: None,
enforcement: None,
schema: None,
summary_guidance: None,
}),
retry_policy: None,
timeout_ms: None,
max_tool_calls: None,
stage_kind: None,
gate: None,
metadata: Some(json!({
"builder": {
"output_path": "marketing-brief.md",
"web_research_expected": false
}
})),
};
let brief_text = "# Marketing Brief\n\n## Workspace source audit\nPrepared from workspace sources.\n\n### Files Reviewed\n| Local Path | Evidence Summary |\n|---|---|\n| `docs/one.md` | Core source reviewed |\n\n### Files Not Reviewed\n| Local Path | Reason |\n|---|---|\n| `docs/extra.md` | Out of scope for this run |\n\n### Web Sources Reviewed\n| URL | Status | Notes |\n|---|---|---|\n| https://example.com | Fetched | Confirmed live |\n\n## Campaign goal\nClarify positioning.\n\n## Target audience\n- Operators.\n\n## Core pain points\n- Coordination overhead.\n\n## Positioning angle\nTandem centralizes orchestration.\n\n## Competitor context\nLocal-only comparison for this run.\n\n## Proof points with citations\n1. Supported from docs/source.md. Source note: https://example.com/reference\n\n## Likely objections\n- Proof depth.\n\n## Channel considerations\n- Landing page.\n\n## Recommended message hierarchy\n1. Problem\n2. Promise\n".to_string();
std::fs::write(workspace_root.join("marketing-brief.md"), &brief_text).expect("seed brief");
let mut session = Session::new(
Some("coverage mismatch".to_string()),
Some(
workspace_root
.to_str()
.expect("workspace root string")
.to_string(),
),
);
session.messages.push(tandem_types::Message::new(
MessageRole::Assistant,
vec![
MessagePart::ToolInvocation {
tool: "glob".to_string(),
args: json!({"pattern":"docs/**/*.md"}),
result: Some(json!({"output": format!(
"{}\n{}",
workspace_root.join("docs/one.md").display(),
workspace_root.join("docs/two.md").display()
)})),
error: None,
},
MessagePart::ToolInvocation {
tool: "read".to_string(),
args: json!({"path":"docs/one.md"}),
result: Some(json!({"ok": true})),
error: None,
},
MessagePart::ToolInvocation {
tool: "write".to_string(),
args: json!({"path":"marketing-brief.md","content":brief_text}),
result: Some(json!({"ok": true})),
error: None,
},
],
));
let tool_telemetry = summarize_automation_tool_activity(
&node,
&session,
&["glob".to_string(), "read".to_string(), "write".to_string()],
);
let (_accepted_output, metadata, rejected) = validate_automation_artifact_output(
&node,
&session,
workspace_root.to_str().expect("workspace root string"),
"Done\n\n{\"status\":\"completed\"}",
&tool_telemetry,
None,
Some(("marketing-brief.md".to_string(), brief_text)),
&snapshot,
);
assert_eq!(
rejected.as_deref(),
Some(
"research completed without covering or explicitly skipping relevant discovered files"
)
);
assert_eq!(
metadata
.get("unreviewed_relevant_paths")
.and_then(Value::as_array)
.map(|values| values.len()),
Some(1)
);
assert!(metadata
.get("unmet_requirements")
.and_then(Value::as_array)
.is_some_and(|values| values
.iter()
.any(|value| value.as_str() == Some("relevant_files_not_reviewed_or_skipped"))));
let _ = std::fs::remove_dir_all(workspace_root);
}
#[test]
fn research_brief_without_source_coverage_flag_gets_semantic_block_reason_and_needs_repair() {
let workspace_root = std::env::temp_dir().join(format!(
"tandem-research-no-coverage-{}",
uuid::Uuid::new_v4()
));
std::fs::create_dir_all(&workspace_root).expect("create workspace");
let brief_text =
"# Marketing Brief\n\n## Workspace source audit\nPrepared from workspace sources.\n"
.to_string();
std::fs::write(workspace_root.join("marketing-brief.md"), &brief_text).expect("seed brief");
let node = AutomationFlowNode {
knowledge: tandem_orchestrator::KnowledgeBinding::default(),
node_id: "research-brief".to_string(),
agent_id: "researcher".to_string(),
objective: "Write marketing brief".to_string(),
depends_on: Vec::new(),
input_refs: Vec::new(),
output_contract: Some(AutomationFlowOutputContract {
kind: "brief".to_string(),
validator: None,
enforcement: None,
schema: None,
summary_guidance: None,
}),
retry_policy: None,
timeout_ms: None,
max_tool_calls: None,
stage_kind: None,
gate: None,
metadata: Some(json!({
"builder": {
"output_path": "marketing-brief.md",
"web_research_expected": true
}
})),
};
let mut session = Session::new(
Some("research-no-coverage".to_string()),
Some(
workspace_root
.to_str()
.expect("workspace root string")
.to_string(),
),
);
session.messages.push(tandem_types::Message::new(
MessageRole::Assistant,
vec![
MessagePart::ToolInvocation {
tool: "glob".to_string(),
args: json!({"pattern":"docs/**/*.md"}),
result: Some(json!({"output": ""})),
error: None,
},
MessagePart::ToolInvocation {
tool: "write".to_string(),
args: json!({"path":"marketing-brief.md","content":brief_text}),
result: Some(json!({"ok": true})),
error: None,
},
],
));
let requested_tools = vec![
"glob".to_string(),
"read".to_string(),
"websearch".to_string(),
"write".to_string(),
];
let tool_telemetry = summarize_automation_tool_activity(&node, &session, &requested_tools);
let (_accepted_output, artifact_validation, rejected) = validate_automation_artifact_output(
&node,
&session,
workspace_root.to_str().expect("workspace root string"),
"Done\n\n{\"status\":\"completed\"}",
&tool_telemetry,
None,
Some(("marketing-brief.md".to_string(), brief_text.clone())),
&std::collections::BTreeSet::new(),
);
assert_eq!(
rejected.as_deref(),
Some("research completed without concrete file reads or required source coverage")
);
assert_eq!(
artifact_validation
.get("semantic_block_reason")
.and_then(Value::as_str),
Some("research completed without concrete file reads or required source coverage")
);
assert_eq!(
artifact_validation
.get("validation_outcome")
.and_then(Value::as_str),
Some("needs_repair")
);
let (status, reason, approved): (String, Option<String>, Option<bool>) =
detect_automation_node_status(
&node,
"Done — `marketing-brief.md` was written.",
Some(&("marketing-brief.md".to_string(), brief_text)),
&tool_telemetry,
Some(&artifact_validation),
);
assert_eq!(status, "needs_repair");
assert_eq!(
reason.as_deref(),
Some("research completed without concrete file reads or required source coverage")
);
assert_eq!(approved, None);
let _ = std::fs::remove_dir_all(workspace_root);
}