pub(crate) fn write_defensive_completion_marker(
workspace: &dyn crate::workspace::Workspace,
logger: &crate::logger::Logger,
final_phase: crate::reducer::event::PipelinePhase,
) -> bool {
if let Err(err) = workspace.create_dir_all(std::path::Path::new(".agent/tmp")) {
logger.error(&format!(
"Failed to create completion marker directory: {err}"
));
return false;
}
let marker_path = std::path::Path::new(".agent/tmp/completion_marker");
let content = format!(
"failure\nEvent loop exited without normal completion (final_phase={final_phase:?})"
);
if let Err(err) = workspace.write(marker_path, &content) {
logger.error(&format!(
"Failed to write defensive completion marker: {err}"
));
return false;
}
logger.info("Defensive completion marker written: failure");
true
}
#[cfg(test)]
mod completion_tests {
use super::*;
use crate::workspace::{DirEntry, MemoryWorkspace, Workspace};
use std::io;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
#[derive(Debug)]
struct TrackingWorkspace {
inner: MemoryWorkspace,
tmp_created: Mutex<bool>,
}
impl TrackingWorkspace {
fn new() -> Self {
Self {
inner: MemoryWorkspace::new(PathBuf::from("/test/repo")),
tmp_created: Mutex::new(false),
}
}
fn tmp_created(&self) -> bool {
*self.tmp_created.lock().expect("mutex poisoned")
}
}
impl crate::workspace::Workspace for TrackingWorkspace {
fn root(&self) -> &Path {
self.inner.root()
}
fn read(&self, relative: &Path) -> io::Result<String> {
self.inner.read(relative)
}
fn read_bytes(&self, relative: &Path) -> io::Result<Vec<u8>> {
self.inner.read_bytes(relative)
}
fn write(&self, relative: &Path, content: &str) -> io::Result<()> {
self.inner.write(relative, content)
}
fn write_bytes(&self, relative: &Path, content: &[u8]) -> io::Result<()> {
self.inner.write_bytes(relative, content)
}
fn append_bytes(&self, relative: &Path, content: &[u8]) -> io::Result<()> {
self.inner.append_bytes(relative, content)
}
fn exists(&self, relative: &Path) -> bool {
self.inner.exists(relative)
}
fn is_file(&self, relative: &Path) -> bool {
self.inner.is_file(relative)
}
fn is_dir(&self, relative: &Path) -> bool {
self.inner.is_dir(relative)
}
fn remove(&self, relative: &Path) -> io::Result<()> {
self.inner.remove(relative)
}
fn remove_if_exists(&self, relative: &Path) -> io::Result<()> {
self.inner.remove_if_exists(relative)
}
fn remove_dir_all(&self, relative: &Path) -> io::Result<()> {
self.inner.remove_dir_all(relative)
}
fn remove_dir_all_if_exists(&self, relative: &Path) -> io::Result<()> {
self.inner.remove_dir_all_if_exists(relative)
}
fn create_dir_all(&self, relative: &Path) -> io::Result<()> {
if relative == Path::new(".agent/tmp") {
*self.tmp_created.lock().expect("mutex poisoned") = true;
}
self.inner.create_dir_all(relative)
}
fn read_dir(&self, relative: &Path) -> io::Result<Vec<DirEntry>> {
self.inner.read_dir(relative)
}
fn rename(&self, from: &Path, to: &Path) -> io::Result<()> {
self.inner.rename(from, to)
}
fn write_atomic(&self, relative: &Path, content: &str) -> io::Result<()> {
self.inner.write_atomic(relative, content)
}
fn set_readonly(&self, relative: &Path) -> io::Result<()> {
self.inner.set_readonly(relative)
}
fn set_writable(&self, relative: &Path) -> io::Result<()> {
self.inner.set_writable(relative)
}
}
#[test]
fn test_defensive_completion_marker_creates_tmp_dir() {
let workspace = TrackingWorkspace::new();
let logger = crate::logger::Logger::new(crate::logger::Colors { enabled: false });
let wrote = write_defensive_completion_marker(
&workspace,
&logger,
crate::reducer::event::PipelinePhase::AwaitingDevFix,
);
assert!(wrote, "marker write should succeed");
assert!(
workspace.tmp_created(),
"should create .agent/tmp before writing marker"
);
assert!(
workspace.exists(Path::new(".agent/tmp/completion_marker")),
"completion marker should exist after defensive write"
);
}
#[test]
fn test_defensive_completion_marker_content_format() {
let workspace = MemoryWorkspace::new(PathBuf::from("/test/repo"));
let logger = crate::logger::Logger::new(crate::logger::Colors { enabled: false });
let wrote = write_defensive_completion_marker(
&workspace,
&logger,
crate::reducer::event::PipelinePhase::Development,
);
assert!(wrote, "marker write should succeed");
let content = workspace
.read(Path::new(".agent/tmp/completion_marker"))
.expect("should read marker");
assert!(
content.starts_with("failure\n"),
"marker should start with failure status"
);
assert!(
content.contains("Development"),
"marker should include final phase"
);
}
}