use super::support::*;
#[test]
fn dispatch_returns_structured_json() {
let temp = tempfile::TempDir::new().unwrap();
let provider = Arc::new(CountingProvider::new("never"));
let result = dispatch_subagents(
json!({"tasks":[{"intent":"one","identity":"frontend-dev"}],"concurrency":1}),
{
let mut cfg = config(provider, temp.path());
cfg.profiles.insert(
"frontend-dev".to_string(),
profile("frontend-dev", "Frontend prompt"),
);
cfg
},
);
assert!(result.success, "{}", result.content);
assert!(result.content.contains("\"id\": \"g1\""));
assert!(result.content.contains("\"identity\": \"frontend-dev\""));
}
#[test]
fn dispatch_truncates_large_result_fields_without_changing_json_schema() {
let mut output = SubagentsOutput {
summary: SubagentsSummary {
total: 2,
completed: 1,
failed: 1,
total_tokens: None,
},
results: vec![
SubagentTaskResult {
id: "g1".to_string(),
status: SubagentStatus::Completed,
intent: "large output".to_string(),
agent: None,
identity: None,
cwd: PathBuf::from("/tmp"),
session_id: Some("child-session-1".to_string()),
session_path: Some(PathBuf::from("/tmp/subagents/child-session-1.jsonl")),
total_tokens: None,
usage: None,
changed_files: vec![PathBuf::from("src/lib.rs")],
output: "x".repeat(SUBAGENT_RESULT_OUTPUT_CHAR_LIMIT + 100),
structured_output: None,
output_truncated: false,
error: None,
},
SubagentTaskResult {
id: "g2".to_string(),
status: SubagentStatus::Failed,
intent: "large error".to_string(),
agent: None,
identity: None,
cwd: PathBuf::from("/tmp"),
session_id: Some("child-session-2".to_string()),
session_path: Some(PathBuf::from("/tmp/subagents/child-session-2.jsonl")),
total_tokens: None,
usage: None,
changed_files: vec![PathBuf::from("src/main.rs")],
output: String::new(),
structured_output: None,
output_truncated: false,
error: Some("e".repeat(SUBAGENT_RESULT_ERROR_CHAR_LIMIT + 100)),
},
],
};
truncate_subagents_output(&mut output);
let json = serde_json::to_string_pretty(&output).unwrap();
let value: Value = serde_json::from_str(&json).unwrap();
assert_eq!(value["summary"]["total"], 2);
assert_eq!(value["results"].as_array().unwrap().len(), 2);
assert_eq!(value["results"][0]["id"], "g1");
assert_eq!(value["results"][0]["status"], "completed");
assert_eq!(
value["results"][0]["session_path"],
"/tmp/subagents/child-session-1.jsonl"
);
assert_eq!(value["results"][0]["changed_files"][0], "src/lib.rs");
assert_eq!(value["results"][0]["output_truncated"], true);
assert!(value["results"][0]["error"].is_null());
assert!(
value["results"][0]["output"]
.as_str()
.unwrap()
.contains(SUBAGENT_TRUNCATION_MARKER.trim_start())
);
assert_eq!(value["results"][1]["id"], "g2");
assert_eq!(value["results"][1]["status"], "failed");
assert_eq!(
value["results"][1]["session_path"],
"/tmp/subagents/child-session-2.jsonl"
);
assert_eq!(value["results"][1]["changed_files"][0], "src/main.rs");
assert_eq!(value["results"][1]["output_truncated"], true);
assert!(
value["results"][1]["error"]
.as_str()
.unwrap()
.contains(SUBAGENT_TRUNCATION_MARKER.trim_start())
);
}
#[test]
fn subagents_structured_output_serializes() {
let output = SubagentsOutput {
summary: SubagentsSummary {
total: 1,
completed: 1,
failed: 0,
total_tokens: None,
},
results: vec![SubagentTaskResult {
id: "g1".to_string(),
status: SubagentStatus::Completed,
intent: "structured".to_string(),
agent: None,
identity: Some("tars-code-writing-execution".to_string()),
cwd: PathBuf::from("."),
session_id: None,
session_path: None,
total_tokens: None,
usage: None,
changed_files: Vec::new(),
output: "{}".to_string(),
structured_output: Some(json!({"phase": "IMPLEMENT"})),
output_truncated: false,
error: None,
}],
};
let json = serde_json::to_string_pretty(&SerializableSubagentsOutput::from(&output)).unwrap();
let value: Value = serde_json::from_str(&json).unwrap();
assert_eq!(
value["results"][0]["structured_output"]["phase"],
"IMPLEMENT"
);
}
#[cfg(unix)]
#[test]
fn subagents_output_serializes_non_utf8_paths_lossily() {
use std::ffi::OsString;
use std::os::unix::ffi::OsStringExt;
let non_utf8 = PathBuf::from(OsString::from_vec(b"bad-\xFF-path".to_vec()));
let output = SubagentsOutput {
summary: SubagentsSummary {
total: 1,
completed: 1,
failed: 0,
total_tokens: None,
},
results: vec![SubagentTaskResult {
id: "g1".to_string(),
status: SubagentStatus::Completed,
intent: "non utf8 paths".to_string(),
agent: None,
identity: None,
cwd: non_utf8.clone(),
session_id: Some("session".to_string()),
session_path: Some(non_utf8.clone()),
total_tokens: None,
usage: None,
changed_files: vec![non_utf8],
output: "done".to_string(),
structured_output: None,
output_truncated: false,
error: None,
}],
};
let json = serde_json::to_string_pretty(&SerializableSubagentsOutput::from(&output)).unwrap();
let value: Value = serde_json::from_str(&json).unwrap();
assert_ne!(json, "{}");
assert_eq!(value["summary"]["total"], 1);
assert!(value["results"][0]["cwd"].as_str().unwrap().contains('�'));
assert!(
value["results"][0]["session_path"]
.as_str()
.unwrap()
.contains('�')
);
assert!(
value["results"][0]["changed_files"][0]
.as_str()
.unwrap()
.contains('�')
);
}
#[test]
fn subagent_usage_deserializes_legacy_snapshot_shape() {
let snapshot = crate::output::NormalizedUsageSnapshot {
effective_input: 7,
output: 2,
cache_read: 1,
cache_known: true,
};
let result: SubagentTaskResult = serde_json::from_value(json!({
"id": "g1",
"status": "completed",
"intent": "legacy usage",
"agent": null,
"identity": null,
"cwd": ".",
"session_id": null,
"session_path": null,
"total_tokens": null,
"usage": {
"effective_input": 7,
"output": 2,
"cache_read": 1,
"cache_known": true
},
"changed_files": [],
"output": "done",
"structured_output": null,
"output_truncated": false,
"error": null
}))
.unwrap();
assert_eq!(
result.usage,
Some(crate::output::NormalizedUsageAggregate {
whole_run: snapshot,
latest: Some(snapshot),
latest_request_sequence: None,
latest_final: false,
})
);
}
#[test]
fn subagent_usage_metadata_round_trips_through_result_serde() {
let snapshot = crate::output::NormalizedUsageSnapshot {
effective_input: 18_400,
output: 1_300,
cache_read: 7_912,
cache_known: true,
};
let usage = crate::output::NormalizedUsageAggregate {
whole_run: snapshot,
latest: None,
latest_request_sequence: Some(9),
latest_final: false,
};
let result = SubagentTaskResult {
id: "g1".to_string(),
status: SubagentStatus::Completed,
intent: "metadata".to_string(),
agent: None,
identity: None,
cwd: PathBuf::from("."),
session_id: None,
session_path: None,
total_tokens: None,
usage: Some(usage),
changed_files: Vec::new(),
output: "done".to_string(),
structured_output: None,
output_truncated: false,
error: None,
};
let encoded = serde_json::to_value(&result).unwrap();
assert_eq!(encoded["usage"]["latest_request_sequence"], 9);
assert_eq!(encoded["usage"]["latest_final"], false);
let decoded: SubagentTaskResult = serde_json::from_value(encoded).unwrap();
assert_eq!(decoded.usage, Some(usage));
}