a3s 0.10.5

a3s — A3S coding agent CLI; `a3s code` launches the interactive TUI
Documentation
use super::*;

#[test]
fn parses_deepresearch_cli_options() {
    let opts =
        parse_deepresearch_args(&["compare".into(), "runtimes".into()]).expect("deepresearch args");
    assert_eq!(opts.query, "compare runtimes");
    assert_eq!(opts.evidence_scope, None);

    let local_only = parse_deepresearch_args(&[
        "--local-only".into(),
        "compare".into(),
        "public".into(),
        "sources".into(),
    ])
    .expect("explicit local-only evidence scope");
    assert_eq!(
        local_only.evidence_scope,
        Some(crate::tui::DeepResearchEvidenceScope::LocalOnly)
    );

    let web = parse_deepresearch_args(&[
        "--web".into(),
        "do".into(),
        "not".into(),
        "use".into(),
        "web".into(),
    ])
    .expect("explicit web evidence scope");
    assert_eq!(
        web.evidence_scope,
        Some(crate::tui::DeepResearchEvidenceScope::WebAndWorkspace)
    );

    let conflict =
        parse_deepresearch_args(&["--local-only".into(), "--web".into(), "conflict".into()])
            .expect_err("conflicting evidence scopes");
    assert!(conflict.to_string().contains("conflicts"), "{conflict}");
}

#[tokio::test]
async fn deepresearch_cli_rejects_removed_runtime_selection() {
    let err = execute_deepresearch_in(
        &["--os".into(), "market".into()],
        Path::new("."),
        CodeConfig::default(),
        PathBuf::from(".a3s/memory"),
    )
    .await
    .expect_err("--os should be rejected before building a DeepResearch session");
    let message = err.to_string();
    assert!(
        message.contains("runtime selection has been removed"),
        "{message}"
    );
    assert!(message.contains("--web or --local-only"), "{message}");

    let local = parse_deepresearch_args(&["--local".into(), "market".into()])
        .expect_err("--local must not create a second runtime route");
    assert!(
        local
            .to_string()
            .contains("runtime selection has been removed"),
        "{local}"
    );
}

#[tokio::test]
async fn deepresearch_cli_resolves_account_model_before_building_the_session() {
    let workspace = tempfile::tempdir().expect("DeepResearch workspace");
    let config = CodeConfig::from_acl(
        r#"
            default_model = "codex/account-model"
            memory {
              llmExtraction = false
            }
        "#,
    )
    .expect("account-model config");
    let scripted: Arc<dyn LlmClient> = Arc::new(ScriptedLlmClient::new(Vec::new()));
    let resolved_route = Arc::new(Mutex::new(None));
    let captured_route = Arc::clone(&resolved_route);

    let session = build_deepresearch_session_with_resolver(
        workspace.path().to_string_lossy().as_ref(),
        config,
        workspace.path().join("memory"),
        move |config, options, session_id| {
            *captured_route.lock().unwrap() = Some((
                config.default_model.clone(),
                options.session_id.clone(),
                session_id.to_string(),
                options.continuation_enabled,
                options.max_continuation_turns,
                options.max_tool_rounds,
                options.max_parallel_tasks,
                options.auto_delegation.as_ref().map(|delegation| {
                    (
                        delegation.enabled,
                        delegation.auto_parallel,
                        delegation.allow_manual_delegation,
                    )
                }),
                options.manual_delegation_enabled,
                options.auto_parallel_delegation,
            ));
            Ok(scripted)
        },
    )
    .await
    .expect("account-backed DeepResearch session");

    let (
        model,
        option_session_id,
        resolver_session_id,
        continuation_enabled,
        max_continuation_turns,
        max_tool_rounds,
        max_parallel_tasks,
        auto_delegation,
        manual_delegation_enabled,
        auto_parallel_delegation,
    ) = resolved_route
        .lock()
        .unwrap()
        .clone()
        .expect("DeepResearch model resolver call");
    assert_eq!(model.as_deref(), Some("codex/account-model"));
    assert_eq!(
        option_session_id.as_deref(),
        Some(resolver_session_id.as_str())
    );
    assert_eq!(session.id(), resolver_session_id);
    assert_eq!(continuation_enabled, Some(false));
    assert_eq!(max_continuation_turns, None);
    assert_eq!(max_tool_rounds, None);
    assert_eq!(max_parallel_tasks, Some(1));
    assert_eq!(auto_delegation, Some((false, false, true)));
    assert_eq!(manual_delegation_enabled, Some(true));
    assert_eq!(auto_parallel_delegation, Some(false));
}

#[test]
fn deepresearch_cli_policy_denies_model_writes_including_report_artifacts() {
    use a3s_code_core::permissions::PermissionDecision;

    let policy = deepresearch_cli_permission_policy();
    assert_eq!(
        policy.check(
            "write",
            &serde_json::json!({
                "file_path": ".a3s/research/local-test/report.md",
                "content": "# Report"
            })
        ),
        PermissionDecision::Deny
    );
    assert_eq!(
        policy.check(
            "Write",
            &serde_json::json!({
                "file_path": ".a3s/research/local-test/index.html",
                "content": "<!doctype html><html><body></body></html>"
            })
        ),
        PermissionDecision::Deny
    );
    assert_eq!(
        policy.check("read", &serde_json::json!({"file_path": "src/lib.rs"})),
        PermissionDecision::Allow
    );
    assert_eq!(
        policy.check("web_search", &serde_json::json!({"query": "a3s"})),
        PermissionDecision::Allow
    );
    assert_eq!(
        policy.check("bash", &serde_json::json!({"command": "ls -la"})),
        PermissionDecision::Deny
    );
    assert_eq!(
        policy.check(
            "write",
            &serde_json::json!({"file_path": "README.md", "content": "oops"})
        ),
        PermissionDecision::Deny
    );
}

#[test]
fn deepresearch_cli_missing_publication_materializes_only_a_degraded_host_report() {
    let workspace = tempfile::tempdir().expect("degraded CLI report workspace");
    let synthesis = materialize_deepresearch_cli_recovery(
        workspace.path(),
        "no accepted evidence",
        "the standalone engine returned without a publication",
        r#"{"mode":"engine_failure","research":{"status":"failed"}}"#,
        None,
    )
    .expect("the Host should materialize a bounded recovery report");

    assert_eq!(synthesis.status, DeepResearchReportStatus::Degraded);
    assert!(synthesis.artifacts.markdown.is_file());
    assert!(synthesis.artifacts.html.is_file());
    assert!(std::fs::read_to_string(&synthesis.artifacts.markdown)
        .unwrap()
        .contains("A3S_DEEP_RESEARCH_ARTIFACT:recovery:v1"));
}