a3s 0.10.8

a3s — A3S coding agent CLI; `a3s code` launches the interactive TUI
Documentation
use super::*;
use a3s_tui::style::{strip_ansi, visible_len};

fn grant(tool: &str, args: serde_json::Value) -> ExactPermissionGrant {
    ExactPermissionGrant::from_invocation(tool, &args)
}

fn panel_with_both_scopes() -> PermissionPanel {
    PermissionPanel::new(PermissionGrantSnapshot {
        session: vec![
            grant("bash", serde_json::json!({"command": "cargo test"})),
            grant(
                "write",
                serde_json::json!({"file_path": "README.md", "content": "updated"}),
            ),
        ],
        project: vec![grant(
            "edit",
            serde_json::json!({"file_path": "src/lib.rs", "new_string": "updated"}),
        )],
    })
}

#[test]
fn menu_separates_session_and_project_grants() {
    let panel = panel_with_both_scopes();
    let plain = strip_ansi(&permission_menu_lines(&panel, 96, 12).join("\n"));

    assert!(plain.contains("S"), "{plain}");
    assert!(
        plain.contains("session · expires when this TUI exits"),
        "{plain}"
    );
    assert!(plain.contains("P"), "{plain}");
    assert!(plain.contains("project · .a3s/permissions.acl"), "{plain}");
    assert!(plain.contains("2 session · 1 project"), "{plain}");
}

#[test]
fn filter_searches_scope_tool_and_exact_arguments() {
    let mut panel = panel_with_both_scopes();
    panel.query = "project src/lib.rs".to_string();
    panel.reset_filter_selection();

    let selected = panel.selected_grant().expect("project grant should match");
    assert_eq!(selected.scope, PermissionGrantScope::Project);
    assert_eq!(selected.grant.tool_name(), "edit");

    panel.query = "session cargo test".to_string();
    panel.reset_filter_selection();
    assert_eq!(
        panel
            .selected_grant()
            .expect("session grant should match")
            .grant
            .tool_name(),
        "bash"
    );
}

#[test]
fn revocation_requires_two_matching_keypresses() {
    let mut panel = panel_with_both_scopes();
    let delete = KeyEvent {
        code: KeyCode::Delete,
        modifiers: KeyModifiers::NONE,
    };

    assert!(matches!(
        panel.handle_key(&delete),
        PermissionPanelAction::None
    ));
    assert!(panel.revoke_armed.is_some());
    assert!(matches!(
        panel.handle_key(&delete),
        PermissionPanelAction::Revoke(PermissionGrantRow {
            scope: PermissionGrantScope::Session,
            ..
        })
    ));
    assert!(panel.revoke_armed.is_none());
}

#[test]
fn moving_selection_disarms_a_pending_revocation() {
    let mut panel = panel_with_both_scopes();
    panel.arm_or_revoke_selected();
    assert!(panel.revoke_armed.is_some());

    panel.move_selection(1);
    assert!(panel.revoke_armed.is_none());
    assert_eq!(panel.selected_grant().unwrap().grant.tool_name(), "write");
}

#[test]
fn snapshot_refresh_preserves_stable_selection_when_possible() {
    let mut panel = panel_with_both_scopes();
    panel.move_selection(2);
    let selected = panel.selected_grant().unwrap().identity();

    panel.sync_snapshot(PermissionGrantSnapshot {
        session: vec![grant(
            "read",
            serde_json::json!({"file_path": "Cargo.toml"}),
        )],
        project: vec![grant(
            "edit",
            serde_json::json!({"file_path": "src/lib.rs", "new_string": "different"}),
        )],
    });

    assert_eq!(panel.selected_grant().unwrap().identity(), selected);
}

#[test]
fn exact_details_explain_scope_arguments_and_revocation_boundary() {
    let panel = panel_with_both_scopes();
    let details = permission_details_document(panel.selected_grant().unwrap());

    assert!(details.contains("Scope: session"), "{details}");
    assert!(details.contains("Tool: bash"), "{details}");
    assert!(details.contains("\"command\": \"cargo test\""), "{details}");
    assert!(
        details.contains("future permission checks only"),
        "{details}"
    );
    assert!(details.contains("does not cancel a tool"), "{details}");
}

#[test]
fn empty_and_narrow_menus_remain_renderable() {
    let panel = PermissionPanel::new(PermissionGrantSnapshot::default());
    for width in [1_usize, 20, 48] {
        let lines = permission_menu_lines(&panel, width, 12);
        assert!(!lines.is_empty());
        assert!(
            lines.iter().all(|line| visible_len(line) <= width),
            "width={width}: {lines:?}"
        );
    }
    let plain = strip_ansi(&permission_menu_lines(&panel, 64, 12).join("\n"));
    assert!(plain.contains("no remembered permission grants"), "{plain}");
}