a3s 0.10.8

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

#[test]
fn manual_jobs_preempt_automatic_jobs_and_only_one_is_claimed() {
    let temporary = tempfile::tempdir().expect("temporary workspace");
    let workspace = temporary.path();
    let auto = create_fixture_base(workspace, "Auto Base", CompilationPolicy::SmartAuto);
    let manual = create_fixture_base(workspace, "Manual Base", CompilationPolicy::Manual);
    let now = Utc::now();
    let auto_path = PathBuf::from(auto.knowledge_base.path);
    let manual_path = PathBuf::from(manual.knowledge_base.path);
    let mut auto_state = read_state(&auto_path).expect("auto state");
    let mut manual_state = read_state(&manual_path).expect("manual state");
    let auto_digest = auto_state.source_digest.clone().expect("auto digest");
    let manual_digest = manual_state.source_digest.clone().expect("manual digest");
    enqueue_job(
        &auto_path,
        &auto.knowledge_base.id,
        CompilationTrigger::SmartAuto,
        &auto_digest,
        now,
        &mut auto_state,
    )
    .expect("queue automatic job");
    enqueue_job(
        &manual_path,
        &manual.knowledge_base.id,
        CompilationTrigger::Manual,
        &manual_digest,
        now + chrono::Duration::seconds(1),
        &mut manual_state,
    )
    .expect("queue manual job");

    let claim = claim_next(workspace, now + chrono::Duration::seconds(2)).expect("claim job");
    assert!(claim.claimed);
    assert_eq!(
        claim.job.expect("claimed job").trigger,
        CompilationTrigger::Manual
    );
    assert!(
        !claim_next(workspace, now + chrono::Duration::seconds(3))
            .expect("second claim")
            .claimed
    );
}

#[test]
fn one_running_job_blocks_claims_across_all_monitored_workspaces() {
    let temporary = tempfile::tempdir().expect("temporary root");
    let first_workspace = temporary.path().join("first");
    let second_workspace = temporary.path().join("second");
    let first = create_fixture_base(&first_workspace, "First", CompilationPolicy::Manual);
    let second = create_fixture_base(&second_workspace, "Second", CompilationPolicy::Manual);
    request_compilation(&first_workspace, &first.knowledge_base.id, Utc::now())
        .expect("queue first compilation");
    request_compilation(&second_workspace, &second.knowledge_base.id, Utc::now())
        .expect("queue second compilation");
    let workspaces = vec![first_workspace.clone(), second_workspace.clone()];

    let claimed = claim_next_in_workspaces(&workspaces, Utc::now()).expect("claim global job");
    assert!(claimed.claimed);
    assert!(claimed.workspace_root.is_some());
    assert!(
        !claim_next_in_workspaces(&workspaces, Utc::now())
            .expect("block second global claim")
            .claimed
    );
}

#[test]
fn changes_during_compilation_become_a_manual_follow_up_generation() {
    let temporary = tempfile::tempdir().expect("temporary workspace");
    let workspace = temporary.path();
    let mutation = create_fixture_base(workspace, "Follow Up", CompilationPolicy::Manual);
    let id = mutation.knowledge_base.id;
    let source = workspace.join("source/Follow-Up.md");
    let first = request_compilation(workspace, &id, Utc::now()).expect("request first compilation");
    let first_job = first.job.expect("first job");
    let claim = claim_next(workspace, Utc::now()).expect("claim first compilation");
    assert_eq!(
        claim.job.as_ref().map(|job| job.id.as_str()),
        Some(first_job.id.as_str())
    );
    std::fs::write(source, "changed").expect("change source during compilation");

    let pending = request_compilation(workspace, &id, Utc::now()).expect("request follow up");
    assert!(pending.knowledge_base.compilation.pending_changes);
    let output = PathBuf::from(first_job.output_path).join("wiki");
    std::fs::write(
        output.join("index.md"),
        "---\ntype: Note\n---\n# Compiled\n",
    )
    .expect("write compiler output");
    let completed = complete_job(
        workspace,
        &id,
        &first_job.id,
        CompilationOutcome::Succeeded,
        false,
        None,
        Some(COMPILER_CONTRACT_VERSION),
        Utc::now(),
    )
    .expect("complete first compilation");

    assert_eq!(
        completed.job.expect("follow-up job").trigger,
        CompilationTrigger::Manual
    );
    assert_eq!(
        completed.knowledge_base.compilation.phase,
        CompilationPhase::Queued
    );
}

#[test]
fn failed_compilation_preserves_last_good_wiki_and_schedules_only_transient_retries() {
    let temporary = tempfile::tempdir().expect("temporary workspace");
    let workspace = temporary.path();
    let mutation = create_fixture_base(workspace, "Failure", CompilationPolicy::Manual);
    let id = mutation.knowledge_base.id;
    let base = PathBuf::from(mutation.knowledge_base.path);
    std::fs::create_dir_all(base.join("wiki")).expect("create last good wiki directory");
    std::fs::write(base.join("wiki/index.md"), "last good").expect("write last good wiki");
    let requested = request_compilation(workspace, &id, Utc::now()).expect("request compilation");
    let job = requested.job.expect("queued job");
    claim_next(workspace, Utc::now()).expect("claim compilation");

    complete_job(
        workspace,
        &id,
        &job.id,
        CompilationOutcome::Failed,
        true,
        Some("compiler unavailable"),
        None,
        Utc::now(),
    )
    .expect("record transient failure");

    assert_eq!(
        std::fs::read_to_string(base.join("wiki/index.md")).expect("read last good wiki"),
        "last good"
    );
    let state = read_state(&base).expect("failed state");
    let retry_at = state
        .retry_at
        .as_deref()
        .and_then(parse_time)
        .expect("transient retry time");
    assert_eq!(
        poll_compilations(workspace, retry_at).expect("poll manual retry"),
        1
    );
    let retried = read_state(&base).expect("retried state");
    assert_eq!(retried.phase, CompilationPhase::Queued);
    let retry_job = read_job(
        &base,
        retried.active_job_id.as_deref().expect("retry job ID"),
    )
    .expect("retry job");
    assert_eq!(retry_job.trigger, CompilationTrigger::Retry);

    let permanent = create_fixture_base(workspace, "Permanent", CompilationPolicy::Manual);
    let permanent_id = permanent.knowledge_base.id;
    let permanent_base = PathBuf::from(permanent.knowledge_base.path);
    let permanent_job = request_compilation(workspace, &permanent_id, Utc::now())
        .expect("request permanent failure compilation")
        .job
        .expect("permanent failure job");
    cancel_job(workspace, &id, &retry_job.id, Utc::now()).expect("clear retry fixture");
    claim_next(workspace, Utc::now()).expect("claim permanent failure compilation");
    complete_job(
        workspace,
        &permanent_id,
        &permanent_job.id,
        CompilationOutcome::Failed,
        false,
        Some("invalid source"),
        None,
        Utc::now(),
    )
    .expect("record permanent failure");
    assert!(read_state(&permanent_base)
        .expect("permanent failure state")
        .retry_at
        .is_none());
}

#[test]
fn smart_auto_waits_for_quiet_window_and_minimum_interval() {
    let now = Utc::now();
    let digest = "sha256:pending".to_string();
    let mut state = CompilationState {
        policy: CompilationPolicy::SmartAuto,
        pending_source_digest: Some(digest),
        change_detected_at: Some(now.to_rfc3339()),
        ..CompilationState::default()
    };
    assert!(!auto_compile_due(
        &state,
        now + chrono::Duration::seconds(29)
    ));
    assert!(auto_compile_due(
        &state,
        now + chrono::Duration::seconds(30)
    ));
    state.last_auto_requested_at = Some((now + chrono::Duration::seconds(30)).to_rfc3339());
    assert!(!auto_compile_due(
        &state,
        now + chrono::Duration::seconds(629)
    ));
    assert!(auto_compile_due(
        &state,
        now + chrono::Duration::seconds(630)
    ));
}

#[test]
fn enabling_smart_auto_keeps_an_unchanged_successful_generation_current() {
    let temporary = tempfile::tempdir().expect("temporary workspace");
    let workspace = temporary.path();
    let mutation = create_fixture_base(workspace, "Current", CompilationPolicy::Manual);
    let id = mutation.knowledge_base.id;
    let base = PathBuf::from(mutation.knowledge_base.path);
    let requested = request_compilation(workspace, &id, Utc::now())
        .expect("request current generation compilation");
    let job = requested.job.expect("queued current generation");
    claim_next(workspace, Utc::now()).expect("claim current generation");
    std::fs::write(
        PathBuf::from(&job.output_path).join("wiki/index.md"),
        "---\ntype: Note\n---\n# Current\n",
    )
    .expect("write compiler output");
    let completed = complete_job(
        workspace,
        &id,
        &job.id,
        CompilationOutcome::Succeeded,
        false,
        None,
        Some("fixture-compiler/1.0.0"),
        Utc::now(),
    )
    .expect("complete current generation");
    assert!(!completed.knowledge_base.compilation.recompile_recommended);

    let updated = set_policy(workspace, &id, CompilationPolicy::SmartAuto, Utc::now())
        .expect("enable smart automatic compilation");

    assert_eq!(
        updated.knowledge_base.compilation.phase,
        CompilationPhase::Succeeded
    );
    assert!(!updated.knowledge_base.compilation.pending_changes);
    assert!(updated
        .knowledge_base
        .compilation
        .next_auto_compile_at
        .is_none());
    let state = read_state(&base).expect("read updated compilation state");
    assert_eq!(
        state.last_compiled_digest.as_deref(),
        state.source_digest.as_deref()
    );
}

fn create_fixture_base(
    workspace: &Path,
    name: &str,
    policy: CompilationPolicy,
) -> personal_bases::KnowledgeBaseMutation {
    let source_root = workspace.join("source");
    std::fs::create_dir_all(&source_root).expect("create source root");
    let source = source_root.join(format!("{}.md", name.replace(' ', "-")));
    std::fs::write(&source, "fixture").expect("write source fixture");
    let plan = source_packages::plan_source_selection(
        workspace,
        &[source],
        None,
        Duration::ZERO,
        SystemTime::now(),
    )
    .expect("source fixture plan");
    personal_bases::materialize_personal_base(
        workspace,
        name,
        Some("Compilation test fixture"),
        "selection",
        |staging| {
            source_packages::materialize_source_package(staging, &plan)?;
            initialize_for_source_package(
                staging,
                &plan.snapshot.content_digest,
                policy,
                Utc::now(),
            )
        },
    )
    .expect("create fixture base")
}