a3s 0.10.8

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

use super::*;

async fn write_control_request(directory: &Path, request: &AgentControlProtocolRequest) -> PathBuf {
    let queue = directory.join(CONTROL_REQUEST_DIRECTORY);
    tokio::fs::create_dir_all(&queue).await.unwrap();
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        tokio::fs::set_permissions(&queue, std::fs::Permissions::from_mode(0o700))
            .await
            .unwrap();
    }
    let path = queue.join(format!("control-{}.json", request.request_id));
    tokio::fs::write(&path, serde_json::to_vec(request).unwrap())
        .await
        .unwrap();
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        tokio::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600))
            .await
            .unwrap();
    }
    path
}

#[test]
fn control_grants_are_stable_scoped_and_share_one_decision_token() {
    let temp = tempfile::tempdir().unwrap();
    let publisher = AgentPresencePublisher::for_directory(temp.path().to_path_buf());
    let activity_id = publisher.instance_id().to_string();
    let actions = [
        AgentControlActionKind::ApproveOnce,
        AgentControlActionKind::ApproveAlways,
        AgentControlActionKind::Deny,
    ];
    let first = publisher.reconcile_control_grants(
        [AgentControlGrantSpec::new(
            activity_id.clone(),
            "tool-a",
            actions,
        )],
        1_000,
    );
    let first = &first[&activity_id];
    assert_eq!(first.len(), 3);
    assert!(first.iter().all(|action| action.token == first[0].token));
    assert!(first
        .iter()
        .all(|action| action.target_instance_id == activity_id));

    let second = publisher.reconcile_control_grants(
        [AgentControlGrantSpec::new(
            activity_id.clone(),
            "tool-a",
            actions,
        )],
        2_000,
    );
    assert_eq!(second[&activity_id][0].token, first[0].token);
    assert!(second[&activity_id][0].expires_at_ms > first[0].expires_at_ms);

    let replacement = publisher.reconcile_control_grants(
        [AgentControlGrantSpec::new(
            activity_id.clone(),
            "tool-b",
            actions,
        )],
        2_100,
    );
    assert_ne!(replacement[&activity_id][0].token, first[0].token);
}

#[tokio::test]
async fn earliest_valid_control_consumes_all_alternatives_and_replay_fails() {
    let temp = tempfile::tempdir().unwrap();
    let publisher = AgentPresencePublisher::for_directory(temp.path().to_path_buf());
    let now = epoch_ms();
    let activity_id = publisher.instance_id().to_string();
    let actions = publisher.reconcile_control_grants(
        [AgentControlGrantSpec::new(
            activity_id.clone(),
            "tool-a",
            [
                AgentControlActionKind::ApproveOnce,
                AgentControlActionKind::Deny,
            ],
        )],
        now,
    );
    let descriptor = &actions[&activity_id][0];
    let request = |request_id: &str,
                   action: AgentControlActionKind,
                   created_at_ms: u64|
     -> AgentControlProtocolRequest {
        AgentControlProtocolRequest {
            schema: CONTROL_REQUEST_SCHEMA.to_string(),
            request_id: request_id.to_string(),
            target_instance_id: activity_id.clone(),
            activity_id: activity_id.clone(),
            action,
            message: None,
            token: descriptor.token.clone(),
            created_at_ms,
            expires_at_ms: descriptor.expires_at_ms,
        }
    };
    write_control_request(
        temp.path(),
        &request(
            "decision-later",
            AgentControlActionKind::Deny,
            now.saturating_add(1),
        ),
    )
    .await;
    write_control_request(
        temp.path(),
        &request("decision-first", AgentControlActionKind::ApproveOnce, now),
    )
    .await;

    let accepted = publisher
        .consume_control_requests(now.saturating_add(2))
        .await
        .unwrap();
    assert_eq!(
        accepted,
        vec![AgentControlRequest {
            activity_id: activity_id.clone(),
            context: "tool-a".to_string(),
            action: AgentControlActionKind::ApproveOnce,
            message: None,
        }]
    );
    assert!(
        std::fs::read_dir(temp.path().join(CONTROL_REQUEST_DIRECTORY))
            .unwrap()
            .next()
            .is_none()
    );

    write_control_request(
        temp.path(),
        &request(
            "decision-replay",
            AgentControlActionKind::ApproveOnce,
            now.saturating_add(3),
        ),
    )
    .await;
    assert!(publisher
        .consume_control_requests(now.saturating_add(3))
        .await
        .unwrap()
        .is_empty());
}

#[tokio::test]
async fn reply_control_carries_one_bounded_message_to_the_owning_tui() {
    let temp = tempfile::tempdir().unwrap();
    let publisher = AgentPresencePublisher::for_directory(temp.path().to_path_buf());
    let now = epoch_ms();
    let activity_id = publisher.instance_id().to_string();
    let actions = publisher.reconcile_control_grants(
        [AgentControlGrantSpec::new(
            activity_id.clone(),
            "active-turn",
            [AgentControlActionKind::Reply],
        )],
        now,
    );
    let descriptor = &actions[&activity_id][0];
    let request = AgentControlProtocolRequest {
        schema: CONTROL_REQUEST_SCHEMA.to_string(),
        request_id: "reply-first".to_string(),
        target_instance_id: activity_id.clone(),
        activity_id: activity_id.clone(),
        action: AgentControlActionKind::Reply,
        message: Some("Use the safer migration path.".to_string()),
        token: descriptor.token.clone(),
        created_at_ms: now,
        expires_at_ms: descriptor.expires_at_ms,
    };
    write_control_request(temp.path(), &request).await;

    assert_eq!(
        publisher.consume_control_requests(now).await.unwrap(),
        vec![AgentControlRequest {
            activity_id,
            context: "active-turn".to_string(),
            action: AgentControlActionKind::Reply,
            message: Some("Use the safer migration path.".to_string()),
        }]
    );
}