ai-dispatch 10.51.0

Multi-AI CLI team orchestrator
// Serialization and backwards-compatibility coverage for worker job specs.
use super::BackgroundRunSpec;
use std::collections::HashMap;

fn sample_spec(read_only: bool) -> BackgroundRunSpec {
    BackgroundRunSpec {
        task_id: "t-1234".to_string(),
        worker_pid: Some(11),
        agent_name: "codex".to_string(),
        prompt: "fix the bug".to_string(),
        dir: Some("/tmp/project".to_string()),
        output: Some("json".to_string()),
        result_file: Some("result.md".to_string()),
        result_file_required: Some(true),
        model: Some("gpt-5.4".to_string()),
        budget: true,
        session_id: Some("session-123".to_string()),
        verify: Some("cargo check".to_string()),
        setup: Some("cargo fetch".to_string()),
        iterate: Some(3),
        eval: Some("cargo test".to_string()),
        eval_feedback_template: Some("Iteration {iteration}/{max_iterations}".to_string()),
        judge: Some("cursor".to_string()),
        judge_retry: true,
        max_duration_mins: Some(15),
        max_duration_secs: Some(900),
        max_task_cost: Some(2.5),
        idle_timeout_secs: Some(60),
        retry: 2,
        group: Some("core".to_string()),
        skills: vec!["ai-coding".to_string()],
        checklist: vec!["confirm retry path".to_string()],
        hooks: vec!["on_fail:echo fail".to_string()],
        template: Some("default".to_string()),
        worktree: Some("feature/retry".to_string()),
        base_branch: Some("main".to_string()),
        peer_review: Some("codex".to_string()),
        audit: true,
        audit_explicit: true,
        no_audit: false,
        scope: vec!["src".to_string()],
        interactive: true,
        on_done: Some("echo done".to_string()),
        cascade: vec!["notify".to_string()],
        parent_task_id: Some("t-parent".to_string()),
        env: Some(HashMap::from([("KEY".to_string(), "VALUE".to_string())])),
        env_forward: Some(vec!["HOME".to_string()]),
        agent_pid: Some(22),
        sandbox: true,
        read_only,
        audit_report_mode: true,
        container: Some("aid:test".to_string()),
        link_deps: true,
        pre_task_dirty_paths: Some(vec!["?? pre-existing.rs".to_string()]),
        foreground: true,
    }
}

#[test]
fn background_run_spec_round_trips_read_only() {
    let value = serde_json::to_value(sample_spec(true)).unwrap();
    assert_eq!(value.get("read_only").and_then(|v| v.as_bool()), Some(true));
    assert_eq!(value.get("result_file").and_then(|v| v.as_str()), Some("result.md"));
    assert_eq!(value.get("result_file_required").and_then(|v| v.as_bool()), Some(true));
    assert_eq!(value.get("iterate").and_then(|v| v.as_u64()), Some(3));
    assert!(value.get("pre_task_dirty_paths").is_some());
    assert_eq!(value.get("audit").and_then(|v| v.as_bool()), Some(true));

    let decoded: BackgroundRunSpec = serde_json::from_value(value).unwrap();
    assert!(decoded.read_only);
    assert!(decoded.audit_report_mode);
    assert_eq!(decoded.result_file.as_deref(), Some("result.md"));
    assert_eq!(decoded.result_file_required, Some(true));
    assert_eq!(decoded.eval.as_deref(), Some("cargo test"));
    assert_eq!(
        decoded.pre_task_dirty_paths.as_deref(),
        Some(&["?? pre-existing.rs".to_string()][..])
    );
    assert_eq!(decoded.hooks, ["on_fail:echo fail".to_string()]);
    assert_eq!(decoded.worktree.as_deref(), Some("feature/retry"));
    assert_eq!(decoded.base_branch.as_deref(), Some("main"));
    assert_eq!(decoded.peer_review.as_deref(), Some("codex"));
    assert_eq!(decoded.scope, ["src".to_string()]);
    assert!(decoded.budget);
    assert_eq!(decoded.session_id.as_deref(), Some("session-123"));
    assert_eq!(decoded.max_task_cost, Some(2.5));
    assert!(decoded.judge_retry);
}

#[test]
fn background_run_spec_defaults_read_only_to_false_when_missing() {
    let mut value = serde_json::to_value(sample_spec(false)).unwrap();
    value
        .as_object_mut()
        .unwrap()
        .remove("read_only");
    value
        .as_object_mut()
        .unwrap()
        .remove("audit_report_mode");
    value
        .as_object_mut()
        .unwrap()
        .remove("hooks");
    value
        .as_object_mut()
        .unwrap()
        .remove("audit");

    let decoded: BackgroundRunSpec = serde_json::from_value(value).unwrap();
    assert!(!decoded.read_only);
    assert!(!decoded.audit_report_mode);
    assert!(decoded.hooks.is_empty());
    assert!(!decoded.audit);
}

#[test]
fn legacy_detached_field_is_ignored() {
    let mut value = serde_json::to_value(sample_spec(false)).unwrap();
    value
        .as_object_mut()
        .unwrap()
        .insert("detached".to_string(), serde_json::Value::Bool(true));

    let decoded = serde_json::from_value::<BackgroundRunSpec>(value).unwrap();
    assert_eq!(decoded.task_id, "t-1234");
}