use std::os::unix::fs::PermissionsExt as _;
use scv_client::Layout;
use scv_protocol::{AgentState, JobOrigin, RunEventKind, SendMode};
use super::*;
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:?}"
);
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");
}