mod common;
use std::path::Path;
use common::salvor;
use predicates::prelude::*;
use salvor_core::{Effect, Event, EventEnvelope, RunId, SequenceNumber};
use salvor_store::{EventStore, SqliteStore};
use serde_json::json;
use tempfile::tempdir;
use uuid::Uuid;
async fn seed_dangling_write(path: &Path) -> RunId {
let store = SqliteStore::open(path).expect("store opens");
let run_id = RunId::from_uuid(Uuid::new_v4());
let events = vec![
Event::RunStarted {
agent_def_hash: "sha256:agent".into(),
input: json!("publish otters"),
labels: None,
},
Event::ToolCallRequested {
seq: SequenceNumber::new(1),
tool: "publish".into(),
input: json!({"doc": "otters"}),
effect: Effect::Write,
idempotency_key: Some("k-otters".into()),
},
];
for (i, event) in events.into_iter().enumerate() {
let envelope = EventEnvelope::new(
run_id,
SequenceNumber::new(i as u64),
time::OffsetDateTime::UNIX_EPOCH,
event,
);
store.append(&envelope).await.expect("seed append");
}
run_id
}
#[tokio::test]
async fn resume_reports_then_resolve_records_the_completion() {
let dir = tempdir().expect("tempdir");
let store_path = dir.path().join("salvor.db");
let run_id = seed_dangling_write(&store_path).await;
let uuid = run_id.as_uuid().to_string();
salvor(&store_path)
.args(["resume", &uuid, "--agent", "/does/not/exist.toml"])
.assert()
.code(1)
.stdout(
predicate::str::contains("needs reconciliation")
.and(predicate::str::contains("recorded at:"))
.and(predicate::str::contains("publish"))
.and(predicate::str::contains("k-otters"))
.and(predicate::str::contains(format!("salvor resolve {uuid}"))),
);
salvor(&store_path)
.args(["resolve", &uuid, "--output", r#"{"published": true}"#])
.assert()
.success()
.stdout(predicate::str::contains("resolved"));
let store = SqliteStore::open(&store_path).expect("store opens");
let log = store.read_log(run_id).await.expect("log reads");
assert_eq!(log.len(), 3, "resolve appends exactly one event");
match &log[2].event {
Event::ToolCallCompleted { seq, output } => {
assert_eq!(*seq, SequenceNumber::new(1), "correlates to the intent");
assert_eq!(*output, json!({"published": true}));
}
other => panic!("expected a tool completion, got {other:?}"),
}
salvor(&store_path)
.args(["resolve", &uuid, "--output", "1"])
.assert()
.code(1)
.stderr(predicate::str::contains("does not need reconciliation"));
}