use super::*;
use crate::harness::Harness;
use crate::interrupt;
use std::os::unix::fs::PermissionsExt;
use std::process::{Output, Stdio};
const FIXTURE: &str = "THIRDSHIFT_SESSION_TEST_DIR";
const WARNING: &str = "warning: the session never loaded thirdshift-implement with its skill tool";
const USAGE: &str = "42 input tokens (0 cache read, 0 cache write), 7 output tokens (0 reasoning)";
struct Fixture(tempfile::TempDir);
impl Fixture {
fn new(script: &str) -> Self {
let fixture = Self(tempfile::tempdir().unwrap());
fs::create_dir(fixture.path("bin")).unwrap();
fs::create_dir(fixture.path("logs")).unwrap();
let cli = fixture.path("bin/opencode");
fs::write(
&cli,
format!(
r#"#!/bin/bash
if test "$1" = session; then
echo export >> "$THIRDSHIFT_SESSION_TEST_DIR/calls"
cat "$THIRDSHIFT_SESSION_TEST_DIR/export.json"
exit 0
fi
echo session >> "$THIRDSHIFT_SESSION_TEST_DIR/calls"
echo $$ > "$THIRDSHIFT_SESSION_TEST_DIR/pid"
echo '{{"type":"step_start","sessionID":"s1","part":{{}}}}'
{script}
"#
),
)
.unwrap();
fs::set_permissions(cli, fs::Permissions::from_mode(0o755)).unwrap();
fs::write(
fixture.path("export.json"),
r#"{"info":{"outcome":"failed","error":{"message":"later retained failure"}},"messages":[{"type":"assistant","tokens":{"input":42,"output":7},"content":[{"type":"text","text":"retained reply"}]}]}"#,
)
.unwrap();
fixture
}
fn path(&self, path: &str) -> PathBuf {
self.0.path().join(path)
}
fn command(&self, test: &str) -> Command {
let mut command = Command::new(std::env::current_exe().unwrap());
let path = std::env::join_paths(
std::iter::once(self.path("bin"))
.chain(std::env::split_paths(&std::env::var_os("PATH").unwrap())),
)
.unwrap();
command
.args(["--exact", test, "--nocapture"])
.env(FIXTURE, self.0.path())
.env("PATH", path);
command
}
fn assert_reporting(&self, output: Output) {
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(output.status.success(), "{output:?}");
assert_eq!(stderr.matches(WARNING).count(), 1, "{stderr}");
assert_eq!(stderr.matches(USAGE).count(), 1, "{stderr}");
assert_eq!(stderr.matches("session ended after").count(), 1, "{stderr}");
assert!(!stderr.contains("resuming"), "{stderr}");
assert_eq!(
fs::read_to_string(self.path("calls")).unwrap(),
"session\nexport\n"
);
}
}
impl Drop for Fixture {
fn drop(&mut self) {
if let Ok(pid) = fs::read_to_string(self.path("pid"))
&& let Ok(pid) = pid.trim().parse::<libc::pid_t>()
{
unsafe {
libc::kill(-pid, libc::SIGKILL);
libc::kill(pid, libc::SIGKILL);
}
}
}
}
fn execute_session(root: &Path) -> anyhow::Error {
interrupt::install().unwrap();
logs::begin(logs::Begin::ChildRun("fixture"));
let worktree = root.join("worktree");
fs::create_dir(&worktree).unwrap();
assert!(
Command::new("git")
.args(["init", "-q"])
.arg(&worktree)
.status()
.unwrap()
.success()
);
let logs = Logs {
name: "7".into(),
dir: root.join("logs"),
};
let choice = Choice {
harness: Harness::OpenCode,
..Choice::default()
};
let prompt = format!("/thirdshift-implement {}", "p".repeat(1024 * 1024));
let (result, log) = Sessions::within(&logs, &worktree, &choice, |sessions| {
sessions.run_to_final_message("implement", &prompt)
});
assert!(log.unwrap().exists());
result.unwrap_err()
}
fn assert_owned_child_stopped(root: &Path) {
let pid: libc::pid_t = fs::read_to_string(root.join("pid"))
.unwrap()
.trim()
.parse()
.unwrap();
assert_ne!(unsafe { libc::kill(pid, 0) }, 0, "owned child survived");
}
#[test]
fn completed_stdout_is_reported_after_a_broken_prompt() {
if let Some(root) = std::env::var_os(FIXTURE) {
let root = Path::new(&root);
let error = execute_session(root);
assert!(
error
.to_string()
.contains("could not write the prompt to opencode"),
"{error:#}"
);
assert_eq!(
error.downcast_ref::<std::io::Error>().unwrap().kind(),
std::io::ErrorKind::BrokenPipe
);
assert_owned_child_stopped(root);
return;
}
let fixture = Fixture::new(
r#"echo '{"type":"error","sessionID":"s1","error":{"message":"later turn failure"}}'
exec 1>&-
for attempt in {1..500}; do
grep -q 'later turn failure' "$THIRDSHIFT_SESSION_TEST_DIR"/logs/*.jsonl && break
sleep 0.01
done
exec 0<&-
exec sleep 5"#,
);
fixture.assert_reporting(
fixture
.command("session::execution_tests::completed_stdout_is_reported_after_a_broken_prompt")
.output()
.unwrap(),
);
}
#[test]
fn partial_stdout_is_reported_after_log_failure_with_a_blocked_prompt() {
if let Some(root) = std::env::var_os(FIXTURE) {
let root = Path::new(&root);
let error = execute_session(root);
assert!(
error.to_string().contains("could not write")
&& error.to_string().contains("implement.jsonl"),
"{error:#}"
);
assert_eq!(
error.downcast_ref::<std::io::Error>().unwrap().kind(),
std::io::ErrorKind::BrokenPipe
);
assert_owned_child_stopped(root);
return;
}
let fixture = Fixture::new(
r#"echo ready-to-fail
for attempt in {1..500}; do
test -f "$THIRDSHIFT_SESSION_TEST_DIR/log-closed" && break
sleep 0.01
done
echo another-line
exec sleep 5"#,
);
let log = fixture.path("logs/7-fixture-implement.jsonl");
assert!(Command::new("mkfifo").arg(&log).status().unwrap().success());
let child = fixture
.command("session::execution_tests::partial_stdout_is_reported_after_log_failure_with_a_blocked_prompt")
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
let mut reader = BufReader::new(File::open(log).unwrap());
let mut line = String::new();
loop {
line.clear();
if reader.read_line(&mut line).unwrap() == 0 || line == "ready-to-fail\n" {
break;
}
}
drop(reader);
fs::write(fixture.path("log-closed"), "").unwrap();
fixture.assert_reporting(child.wait_with_output().unwrap());
}
fn assert_no_completion_reporting(output: Output) {
assert!(output.status.success(), "{output:?}");
let stderr = String::from_utf8_lossy(&output.stderr);
for unexpected in [WARNING, USAGE, "session ended after", "resuming"] {
assert!(!stderr.contains(unexpected), "{stderr}");
}
}
#[test]
fn interruption_suppresses_recovered_interpretation_and_retained_reads() {
if let Some(root) = std::env::var_os(FIXTURE) {
let root = Path::new(&root);
let error = execute_session(root);
assert_eq!(format!("{error:#}"), "interrupted");
assert_owned_child_stopped(root);
return;
}
let fixture = Fixture::new(
r#"exec 1>&-
for attempt in {1..500}; do
grep -q 'sessionID' "$THIRDSHIFT_SESSION_TEST_DIR"/logs/*.jsonl && break
sleep 0.01
done
kill -INT "$PPID"
exec sleep 5"#,
);
assert_no_completion_reporting(
fixture.command("session::execution_tests::interruption_suppresses_recovered_interpretation_and_retained_reads").output().unwrap(),
);
assert_eq!(
fs::read_to_string(fixture.path("calls")).unwrap(),
"session\n"
);
}
#[test]
fn startup_failure_preserves_its_cause_without_completion_reporting() {
if let Some(root) = std::env::var_os(FIXTURE) {
let error = execute_session(Path::new(&root));
assert_eq!(error.to_string(), "could not run opencode");
assert_eq!(
error.downcast_ref::<std::io::Error>().unwrap().kind(),
std::io::ErrorKind::PermissionDenied
);
return;
}
let fixture = Fixture::new("exit 0");
fs::set_permissions(
fixture.path("bin/opencode"),
fs::Permissions::from_mode(0o644),
)
.unwrap();
assert_no_completion_reporting(
fixture.command("session::execution_tests::startup_failure_preserves_its_cause_without_completion_reporting").output().unwrap(),
);
assert!(!fixture.path("calls").exists());
}