scv-server 0.4.4

Authoritative session and tool server for SCV
Documentation
//! Unit tests for `src/workbench.rs`.

use std::os::unix::fs::PermissionsExt as _;

use scv_client::Layout;
use scv_protocol::{AgentState, JobOrigin, RunEventKind, SendMode};

use super::*;

/// An instance whose Codex is a script that answers at once.
fn instance_with_fake_codex() -> (tempfile::TempDir, Instance) {
    let home = tempfile::tempdir().unwrap();
    let path = std::fs::canonicalize(home.path()).unwrap();
    let agent = path.join("fake-codex");
    std::fs::write(
        &agent,
        concat!(
            "#!/bin/sh\n",
            "echo '{\"type\":\"thread.started\",\"thread_id\":\"0199a213-81c0-7800-8aa1-bbab2a035a53\"}'\n",
            "echo '{\"type\":\"item.started\",\"item\":{\"type\":\"command_execution\",\"command\":\"cargo test\"}}'\n",
            "echo '{\"type\":\"item.completed\",\"item\":{\"id\":\"i1\",\"type\":\"agent_message\",\"text\":\"landed 0.9.9\"}}'\n",
            "echo '{\"type\":\"turn.completed\",\"usage\":{\"input_tokens\":1,\"output_tokens\":1}}'\n",
        ),
    )
    .unwrap();
    std::fs::set_permissions(&agent, std::fs::Permissions::from_mode(0o755)).unwrap();
    std::fs::write(
        path.join("config.toml"),
        format!(
            "[agents.codex]\ncommand = {:?}\ntransport = \"resume\"\n",
            agent.display().to_string()
        ),
    )
    .unwrap();
    std::fs::set_permissions(
        path.join("config.toml"),
        std::fs::Permissions::from_mode(0o600),
    )
    .unwrap();
    let instance = Instance {
        layout: Layout::new(&path),
        overrides: ConfigOverrides::default(),
    };
    (home, instance)
}

fn hosts(instance: &Instance) -> Arc<Hosts> {
    let registry = Arc::new(DelegationRegistry::new(&instance.layout));
    Hosts::new(instance.clone(), registry)
}

async fn snapshot(hosts: &Arc<Hosts>) -> scv_protocol::WorkbenchSnapshot {
    control(hosts, DaemonCommand::Workbench { watch: false })
        .await
        .unwrap()
        .workbench
        .unwrap()
}

#[tokio::test]
async fn the_owners_job_runs_in_the_workbench_session_for_its_workspace() {
    let (_home, instance) = instance_with_fake_codex();
    let hosts = hosts(&instance);
    let workspace = tempfile::tempdir().unwrap();
    let workspace_path = std::fs::canonicalize(workspace.path()).unwrap();
    let status = control(
        &hosts,
        DaemonCommand::WorkbenchStart {
            workspace: workspace_path.display().to_string(),
            agent: "codex".into(),
            prompt: "Land the fix".into(),
            cwd: None,
            model: None,
            effort: None,
            worktree: false,
        },
    )
    .await
    .unwrap();
    let host = scv_tools::workbench::host_session(&workspace_path);
    assert_eq!(
        status.workbench_job.as_deref(),
        Some(format!("{host}/job-1").as_str())
    );
    assert!(status.workbench_note.unwrap().starts_with("Started"));
    let job = tokio::time::timeout(Duration::from_secs(20), async {
        loop {
            let snapshot = snapshot(&hosts).await;
            if let Some(job) = snapshot
                .jobs
                .into_iter()
                .find(|job| job.state == AgentState::Completed)
            {
                break job;
            }
            tokio::time::sleep(Duration::from_millis(50)).await;
        }
    })
    .await
    .unwrap();
    assert_eq!(job.session, host);
    assert_eq!(job.started_by, JobOrigin::Owner);
    assert_eq!(job.task, "Land the fix");
    let run = job.run.clone().unwrap();
    let page = control(
        &hosts,
        DaemonCommand::RunLog {
            handle: run.clone(),
            after: 0,
            limit: None,
        },
    )
    .await
    .unwrap()
    .run_log
    .unwrap();
    let texts: Vec<(RunEventKind, String)> = page
        .events
        .into_iter()
        .map(|event| (event.kind, event.text))
        .collect();
    assert!(
        texts.contains(&(RunEventKind::Tool, "$ cargo test".into())),
        "{texts:?}"
    );
    assert!(
        texts.contains(&(RunEventKind::Text, "landed 0.9.9".into())),
        "{texts:?}"
    );
    // The same workspace reuses its workbench session.
    assert_eq!(
        hosts
            .session_for(&workspace_path.display().to_string())
            .await
            .unwrap(),
        host
    );
    let session = snapshot(&hosts)
        .await
        .sessions
        .into_iter()
        .find(|session| session.id == host)
        .unwrap();
    assert_eq!((session.label.as_str(), session.live), (LABEL, true));
    hosts.shutdown().await;
    assert!(
        !snapshot(&hosts)
            .await
            .sessions
            .iter()
            .any(|session| session.id == host && session.live)
    );
}

#[tokio::test]
async fn workbench_actions_refuse_what_they_cannot_do_with_safe_messages() {
    let (_home, instance) = instance_with_fake_codex();
    let hosts = hosts(&instance);
    let relative = control(
        &hosts,
        DaemonCommand::WorkbenchStart {
            workspace: "relative/dir".into(),
            agent: "codex".into(),
            prompt: "go".into(),
            cwd: None,
            model: None,
            effort: None,
            worktree: false,
        },
    )
    .await
    .unwrap_err();
    assert!(relative.contains("absolute"), "{relative}");
    let unknown = control(
        &hosts,
        DaemonCommand::WorkbenchSend {
            session: "nobody".into(),
            conversation: "codex-1".into(),
            prompt: "hi".into(),
            mode: SendMode::Auto,
        },
    )
    .await
    .unwrap_err();
    assert!(unknown.contains("does not run in this daemon"), "{unknown}");
    let log = control(
        &hosts,
        DaemonCommand::RunLog {
            handle: "codex-abcdef".into(),
            after: 0,
            limit: None,
        },
    )
    .await
    .unwrap_err();
    assert!(log.contains("no activity log"), "{log}");
    let approval = control(
        &hosts,
        DaemonCommand::WorkbenchApprove {
            id: "ap-x".into(),
            approved: true,
        },
    )
    .await
    .unwrap_err();
    assert!(approval.contains("no approval request"), "{approval}");
    let resume = control(
        &hosts,
        DaemonCommand::WorkbenchResume {
            session: "nobody".into(),
            job: "job-1".into(),
        },
    )
    .await
    .unwrap_err();
    assert!(resume.contains("no record"), "{resume}");
    let status = control(&hosts, DaemonCommand::Worktrees).await.unwrap();
    assert!(status.worktrees.is_empty() && status.workbench.is_none());
    assert!(control(&hosts, DaemonCommand::Status).await.is_err());
    assert!(is_workbench_command(&DaemonCommand::Worktrees));
    assert!(!is_workbench_command(&DaemonCommand::Reload));
}

#[tokio::test]
async fn the_snapshot_lists_queues_kept_from_sessions_that_stopped() {
    let (_home, instance) = instance_with_fake_codex();
    let hosts = hosts(&instance);
    let mut child = std::process::Command::new("true").spawn().unwrap();
    let gone = scv_tools::delegation::ProcessIdentity::of(child.id()).unwrap();
    child.wait().unwrap();
    persist::save(
        &instance.layout,
        &persist::SavedQueue {
            v: 1,
            session: "old-session".into(),
            label: "TUI".into(),
            workspace: "/w".into(),
            owner: gone,
            saved_unix: persist::unix_now(),
            paused: false,
            entries: vec![scv_protocol::QueueEntry {
                queue_id: "q".into(),
                revision: 1,
                prompt: "next".into(),
                submitter: "r".into(),
                attachments: Vec::new(),
            }],
            active: None,
        },
    );
    let snapshot = snapshot(&hosts).await;
    assert_eq!(snapshot.restorable.len(), 1);
    assert_eq!(snapshot.restorable[0].id, "old-session");
}