a3s 0.9.7

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(&["--local".into(), "rust".into(), "async".into()])
        .expect("local deepresearch args");
    assert_eq!(opts.query, "rust async");
    assert_eq!(opts.runtime_mode, DeepResearchRuntimeMode::Local);

    let opts =
        parse_deepresearch_args(&["--os".into(), "market".into()]).expect("os deepresearch args");
    assert_eq!(opts.query, "market");
    assert_eq!(opts.runtime_mode, DeepResearchRuntimeMode::Os);

    let opts = parse_deepresearch_args(&["compare".into(), "runtimes".into()])
        .expect("auto deepresearch args");
    assert_eq!(opts.query, "compare runtimes");
    assert_eq!(opts.runtime_mode, DeepResearchRuntimeMode::Auto);
}

#[tokio::test]
async fn deepresearch_cli_os_mode_is_temporarily_disabled() {
    let err = execute_deepresearch_in(
        &["--os".into(), "market".into()],
        Path::new("."),
        CodeConfig::default(),
        PathBuf::from(".a3s/memory"),
    )
    .await
    .expect_err("--os should be disabled before touching OS Runtime");
    let message = err.to_string();
    assert!(message.contains("temporarily disabled"), "{message}");
    assert!(message.contains("Function-as-a-Service"), "{message}");
}

#[test]
fn deepresearch_cli_policy_only_allows_report_artifact_writes() {
    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::Allow
    );
    assert_eq!(
        policy.check(
            "Write",
            &serde_json::json!({
                "file_path": ".a3s/research/local-test/index.html",
                "content": "<!doctype html><html><body></body></html>"
            })
        ),
        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
    );
    assert_eq!(
        policy.check(
            "write",
            &serde_json::json!({
                "file_path": ".a3s/research/local-test/../../README.md",
                "content": "path traversal"
            })
        ),
        PermissionDecision::Deny
    );
}

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

    assert_eq!(
        deep_research_report_phase_tool_permission(
            "generate_object",
            &serde_json::json!({"schema_name": "deep_research_report"}),
        ),
        PermissionDecision::Allow
    );
    assert_eq!(
        deep_research_report_phase_tool_permission(
            "write",
            &serde_json::json!({"file_path": ".a3s/research/topic/report.md"}),
        ),
        PermissionDecision::Allow
    );
    assert_eq!(
        deep_research_report_phase_tool_permission(
            "write",
            &serde_json::json!({"file_path": "README.md"}),
        ),
        PermissionDecision::Deny
    );
    assert_eq!(
        deep_research_report_phase_tool_permission(
            "web_search",
            &serde_json::json!({"query": "restart research"}),
        ),
        PermissionDecision::Deny
    );
}

#[tokio::test]
async fn deepresearch_cli_unreportable_evidence_finishes_as_a_degraded_host_report() {
    let workspace = std::env::temp_dir().join(format!(
        "a3s-deepresearch-cli-degraded-{}-{}",
        std::process::id(),
        std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap()
            .as_nanos()
    ));
    std::fs::create_dir_all(&workspace).unwrap();
    let cfg = workspace.join("config.acl");
    test_config(&cfg);
    let agent = Agent::new(cfg.to_string_lossy().to_string()).await.unwrap();
    let llm = Arc::new(ScriptedLlmClient::new(vec![tool_call_response(
        "toolu_write_readme",
        "write",
        serde_json::json!({
            "file_path": "README.md",
            "content": "this model response must never be consumed"
        }),
    )]));
    let opts = SessionOptions::new()
        .with_llm_client(llm)
        .with_planning_mode(a3s_code_core::PlanningMode::Disabled);
    let session = agent
        .session_async(workspace.to_string_lossy().to_string(), Some(opts))
        .await
        .unwrap();
    let report_tool_gate = DeepResearchReportToolGate::default();

    let synthesis = synthesize_deepresearch_report(
        &session,
        &workspace,
        "no accepted evidence",
        false,
        r#"{"mode":"direct_web_degraded","research":{"status":"failed"}}"#,
        1,
        None,
        &report_tool_gate,
    )
    .await
    .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!(!workspace.join("README.md").exists());
    assert!(!crate::tui::deep_research_output_has_internal_leak(
        &std::fs::read_to_string(&synthesis.artifacts.markdown).unwrap()
    ));
    assert!(!report_tool_gate.report_only());

    let _ = std::fs::remove_dir_all(&workspace);
}