a3s 0.10.5

a3s — A3S coding agent CLI; `a3s code` launches the interactive TUI
Documentation
use std::path::{Path, PathBuf};

use super::super::catalog::ComponentKind;
use super::super::state::{Trust, UpdateState};
use super::*;

fn fixture(message: &str) -> OperationPlan {
    OperationPlan {
        schema_version: PLAN_SCHEMA_VERSION,
        component: ComponentId::parse("box").unwrap(),
        action: "install",
        source: "release".to_string(),
        requested_source: Some("release".to_string()),
        channel: Some("stable".to_string()),
        scope: Some("user".to_string()),
        migration: Some(false),
        target: "linux-x86_64".to_string(),
        ownership: "a3s".to_string(),
        mutates: true,
        requested_version: Some("1.2.3".to_string()),
        local_package: None,
        resolved_sources: BTreeMap::from([("box".to_string(), "github-release".to_string())]),
        resolved_releases: BTreeMap::new(),
        resolved_release_bundles: BTreeMap::new(),
        resolved_registry_packages: BTreeMap::new(),
        prerequisites: BTreeMap::new(),
        force: Some(false),
        allow_unsigned: Some(false),
        cascade: None,
        purge: None,
        current: None,
        message: message.to_string(),
    }
}

#[test]
fn digest_is_stable_and_excludes_presentation_text() {
    let first = OperationPlanSet::new("component.install", vec![fixture("first")]).unwrap();
    let second = OperationPlanSet::new("component.install", vec![fixture("second")]).unwrap();
    assert_eq!(first.plan_digest, second.plan_digest);
    assert_eq!(first.plan_digest.len(), 64);
    assert!(first
        .plan_digest
        .bytes()
        .all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase()));
}

#[test]
fn digest_changes_with_semantics_or_operation_order() {
    let first = fixture("same");
    let mut forced = first.clone();
    forced.force = Some(true);
    let mut different_version = first.clone();
    different_version.requested_version = Some("2.0.0".to_string());
    let mut different_source = first.clone();
    different_source.source = "homebrew:a3s-lab/tap/a3s-box".to_string();
    different_source.requested_source = Some("homebrew".to_string());
    let mut purged = first.clone();
    purged.purge = Some(true);
    assert_ne!(
        plan_digest("component.install", std::slice::from_ref(&first)).unwrap(),
        plan_digest("component.install", &[forced.clone()]).unwrap()
    );
    for changed in [different_version, different_source, purged] {
        assert_ne!(
            plan_digest("component.install", std::slice::from_ref(&first)).unwrap(),
            plan_digest("component.install", &[changed]).unwrap()
        );
    }
    assert_ne!(
        plan_digest("component.install", &[first.clone(), forced.clone()]).unwrap(),
        plan_digest("component.install", &[forced, first]).unwrap()
    );
}

#[test]
fn current_state_is_part_of_the_digest() {
    let mut first = fixture("same");
    first.current = Some(PlannedCurrentState::from(&ComponentState {
        id: ComponentId::parse("box").unwrap(),
        kind: ComponentKind::Product,
        description: String::new(),
        presence: Presence::Managed,
        health: Health::Ready,
        update: UpdateState::Unknown,
        trust: Trust::FirstParty,
        provenance: Some(InstallProvenance::GithubRelease),
        version: Some("1.0.0".to_string()),
        path: Some(PathBuf::from("/components/box")),
        message: None,
    }));
    let mut second = first.clone();
    second.current.as_mut().unwrap().version = Some("2.0.0".to_string());
    assert_ne!(
        plan_digest("component.install", &[first]).unwrap(),
        plan_digest("component.install", &[second]).unwrap()
    );
}

#[test]
fn receipt_ownership_and_checksums_are_part_of_the_digest() {
    let mut first = fixture("same");
    first.current = Some(PlannedCurrentState {
        presence: Presence::Managed,
        health: Health::Ready,
        provenance: Some(InstallProvenance::GithubRelease),
        version: Some("1.0.0".to_string()),
        path: Some(PlannedPath::new(Path::new("/components/box/a3s-box"))),
        receipt: Some(PlannedReceipt {
            schema_version: 1,
            component_id: "box".to_string(),
            version: "1.0.0".to_string(),
            provenance: InstallProvenance::GithubRelease,
            install_root: PlannedPath::new(Path::new("/components/box")),
            executable_path: Some(PlannedPath::new(Path::new("/components/box/a3s-box"))),
            owned_paths: vec![PlannedPath::new(Path::new("/components/box"))],
            source: Some("https://example.invalid/releases/v1.0.0".to_string()),
            artifact_checksums: BTreeMap::from([("box.tar.gz".to_string(), "a".repeat(64))]),
        }),
    });
    let mut second = first.clone();
    second
        .current
        .as_mut()
        .unwrap()
        .receipt
        .as_mut()
        .unwrap()
        .artifact_checksums
        .insert("box.tar.gz".to_string(), "b".repeat(64));
    assert_ne!(
        plan_digest("component.uninstall", &[first]).unwrap(),
        plan_digest("component.uninstall", &[second]).unwrap()
    );
}

#[test]
fn expected_digest_rejects_a_changed_plan() {
    let plan = OperationPlanSet::new("component.install", vec![fixture("same")]).unwrap();
    plan.verify_expected(Some(plan.digest())).unwrap();
    let error = plan.verify_expected(Some(&"0".repeat(64))).unwrap_err();
    let mismatch = error.downcast_ref::<ComponentPlanMismatch>().unwrap();
    assert_eq!(mismatch.expected, "0".repeat(64));
    assert_eq!(mismatch.actual, plan.digest());
}

#[test]
fn resolved_release_is_part_of_the_digest() {
    let mut first = fixture("same");
    first.resolved_releases.insert(
        "box".to_string(),
        ResolvedRelease {
            version: "1.2.3".to_string(),
            tag: "v1.2.3".to_string(),
            target: "linux-x86_64".to_string(),
            archive_name: "a3s-box-v1.2.3-linux-x86_64.tar.gz".to_string(),
            asset_url: "https://example.invalid/box.tar.gz".to_string(),
            sha256: "a".repeat(64),
        },
    );
    let mut second = first.clone();
    second.resolved_releases.get_mut("box").unwrap().sha256 = "b".repeat(64);
    assert_ne!(
        plan_digest("component.install", &[first]).unwrap(),
        plan_digest("component.install", &[second]).unwrap()
    );
}

#[tokio::test]
async fn local_package_fingerprint_is_stable_and_tracks_content() {
    let temp = tempfile::tempdir().unwrap();
    let package = temp.path().join("package");
    std::fs::create_dir_all(package.join("nested")).unwrap();
    std::fs::write(package.join("nested/tool"), b"first").unwrap();

    let first = fingerprint_local_package(&package).await.unwrap();
    let repeated = fingerprint_local_package(&package).await.unwrap();
    assert_eq!(first, repeated);
    assert_eq!(first.file_count, 1);
    assert_eq!(first.byte_count, 5);

    std::fs::write(package.join("nested/tool"), b"other").unwrap();
    let changed = fingerprint_local_package(&package).await.unwrap();
    assert_ne!(first.sha256, changed.sha256);

    let mut first_plan = fixture("same");
    first_plan.local_package = Some(first);
    let mut changed_plan = first_plan.clone();
    changed_plan.local_package = Some(changed);
    assert_ne!(
        plan_digest("component.install", &[first_plan]).unwrap(),
        plan_digest("component.install", &[changed_plan]).unwrap()
    );
}