use async_trait::async_trait;
use mlua_swarm::core::agent_context::RUN_ID_KEY;
use mlua_swarm::core::state::{DispatchOutcome, TaskSpec};
use mlua_swarm::store::replay::{InMemoryReplayStore, ReplayCursor, ReplayStore};
use mlua_swarm::store::run::{InMemoryRunStore, RunContext, RunRecord, RunStatus, RunStore};
use mlua_swarm::types::{RunId, StepId, TaskId};
use mlua_swarm::worker::adapter::{
InProcSpawner, SpawnError, SpawnerAdapter, WorkerFn, WorkerResult,
};
use mlua_swarm::worker::Worker;
use mlua_swarm::{CapToken, Ctx, Engine, EngineCfg, Role};
use serde_json::json;
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::{Arc, Mutex as StdMutex};
use std::time::Duration;
struct CountingSpawner {
inner: Arc<dyn SpawnerAdapter>,
counter: Arc<AtomicU32>,
last_ctx: Arc<StdMutex<Option<Ctx>>>,
}
#[async_trait]
impl SpawnerAdapter for CountingSpawner {
async fn spawn(
&self,
engine: &Engine,
ctx: &Ctx,
task_id: StepId,
attempt: u32,
token: CapToken,
) -> Result<Box<dyn Worker>, SpawnError> {
self.counter.fetch_add(1, Ordering::SeqCst);
*self.last_ctx.lock().unwrap() = Some(ctx.clone());
self.inner.spawn(engine, ctx, task_id, attempt, token).await
}
}
type SpawnerRig = (
Arc<dyn SpawnerAdapter>,
Arc<AtomicU32>,
Arc<StdMutex<Option<Ctx>>>,
);
fn build_spawner() -> SpawnerRig {
let mut sp: InProcSpawner = InProcSpawner::new();
sp.register("step-a", |inv| async move {
Ok(WorkerResult {
value: json!({ "by": "step-a", "prompt": inv.prompt }),
ok: true,
stats: None,
})
});
sp.register("step-b", |inv| async move {
Ok(WorkerResult {
value: json!({ "by": "step-b", "prompt": inv.prompt }),
ok: true,
stats: None,
})
});
let inner: Arc<dyn SpawnerAdapter> = Arc::new(sp);
let counter = Arc::new(AtomicU32::new(0));
let last_ctx: Arc<StdMutex<Option<Ctx>>> = Arc::new(StdMutex::new(None));
let wrapped: Arc<dyn SpawnerAdapter> = Arc::new(CountingSpawner {
inner,
counter: counter.clone(),
last_ctx: last_ctx.clone(),
});
let _: Option<WorkerFn> = None;
(wrapped, counter, last_ctx)
}
async fn dispatch_step(
engine: &Engine,
token: &CapToken,
spawner: &Arc<dyn SpawnerAdapter>,
agent: &str,
directive: serde_json::Value,
run_ctx: Option<&RunContext>,
) -> DispatchOutcome {
let tid = engine
.start_task(
token,
TaskSpec {
agent: agent.to_string(),
initial_directive: directive,
step_ctx: None,
check_policy: None,
},
)
.await
.expect("start_task");
engine
.dispatch_attempt_with_run_ctx(token, &tid, spawner, run_ctx)
.await
.expect("dispatch_attempt_with_run_ctx")
}
async fn attach_operator(engine: &Engine) -> CapToken {
engine
.attach("test-op", Role::Operator, Duration::from_secs(60))
.await
.expect("attach")
}
async fn seed_run(run_store: &Arc<dyn RunStore>, task_id: TaskId) -> RunId {
let run_id = RunId::new();
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
run_store
.create(RunRecord {
id: run_id.clone(),
task_id,
status: RunStatus::Running,
step_entries: Vec::new(),
degradations: Vec::new(),
operator_sid: None,
result_ref: None,
input_json: None,
created_at: now,
updated_at: now,
})
.await
.expect("seed RunRecord");
run_id
}
#[tokio::test]
async fn ctx_snapshot_replay_reconstructs_final_ctx_after_mid_run_restart() {
let engine_pristine = Engine::new(EngineCfg::default());
let op_pristine = attach_operator(&engine_pristine).await;
let (spawner_pristine, counter_pristine, last_ctx_pristine) = build_spawner();
let run_store_pristine: Arc<dyn RunStore> = Arc::new(InMemoryRunStore::new());
let run_id_pristine = seed_run(&run_store_pristine, TaskId::new()).await;
let rc_pristine = RunContext::new(run_id_pristine.clone(), run_store_pristine.clone());
let out_a_pristine = dispatch_step(
&engine_pristine,
&op_pristine,
&spawner_pristine,
"step-a",
json!("hello"),
Some(&rc_pristine),
)
.await;
let out_b_pristine = dispatch_step(
&engine_pristine,
&op_pristine,
&spawner_pristine,
"step-b",
json!("world"),
Some(&rc_pristine),
)
.await;
let pristine_step_b_ctx = last_ctx_pristine
.lock()
.unwrap()
.clone()
.expect("step-b Ctx captured on the pristine run");
assert_eq!(
counter_pristine.load(Ordering::SeqCst),
2,
"uninterrupted run must spawn both step-a and step-b"
);
assert!(matches!(&out_a_pristine, DispatchOutcome::Pass(v) if v["by"] == "step-a"));
assert!(matches!(&out_b_pristine, DispatchOutcome::Pass(v) if v["by"] == "step-b"));
let engine_partial = Engine::new(EngineCfg::default());
let op_partial = attach_operator(&engine_partial).await;
let (spawner_partial, counter_partial, _) = build_spawner();
let run_store_partial: Arc<dyn RunStore> = Arc::new(InMemoryRunStore::new());
let replay_store: Arc<dyn ReplayStore> = Arc::new(InMemoryReplayStore::new());
let run_id = seed_run(&run_store_partial, TaskId::new()).await;
let rc_partial = RunContext::new(run_id.clone(), run_store_partial.clone())
.with_replay_store(replay_store.clone());
let out_a_partial = dispatch_step(
&engine_partial,
&op_partial,
&spawner_partial,
"step-a",
json!("hello"),
Some(&rc_partial),
)
.await;
assert_eq!(
counter_partial.load(Ordering::SeqCst),
1,
"step-a must dispatch on the first run"
);
assert!(matches!(&out_a_partial, DispatchOutcome::Pass(v) if v["by"] == "step-a"));
let logged = replay_store
.list_by_run(&run_id)
.await
.expect("list replay rows");
assert_eq!(
logged.len(),
1,
"partial run must have logged exactly one row (step-a)"
);
assert_eq!(logged[0].step_ref, "step-a");
assert_eq!(logged[0].occurrence, 0);
let engine_replay = Engine::new(EngineCfg::default());
let op_replay = attach_operator(&engine_replay).await;
let (spawner_replay, counter_replay, last_ctx_replay) = build_spawner();
let cursor = ReplayCursor::from_entries(logged);
assert_eq!(cursor.len(), 1, "cursor must carry the one logged row");
let cursor_arc = Arc::new(StdMutex::new(cursor));
let rc_replay = RunContext::new(run_id.clone(), run_store_partial.clone())
.with_replay_store(replay_store.clone())
.with_replay_cursor(cursor_arc.clone());
let out_a_replay = dispatch_step(
&engine_replay,
&op_replay,
&spawner_replay,
"step-a",
json!("hello"),
Some(&rc_replay),
)
.await;
assert_eq!(
counter_replay.load(Ordering::SeqCst),
0,
"step-a replay HIT must skip the Adapter dispatch entirely (counter must stay 0)"
);
match &out_a_replay {
DispatchOutcome::Pass(v) => {
assert_eq!(
v["by"], "step-a",
"replay hit must return the stored step-a output verbatim"
);
}
other => panic!("expected Pass on replay hit, got {other:?}"),
}
let out_b_replay = dispatch_step(
&engine_replay,
&op_replay,
&spawner_replay,
"step-b",
json!("world"),
Some(&rc_replay),
)
.await;
assert_eq!(
counter_replay.load(Ordering::SeqCst),
1,
"step-b MISS must dispatch through the Adapter exactly once"
);
assert!(matches!(&out_b_replay, DispatchOutcome::Pass(v) if v["by"] == "step-b"));
let replayed_step_b_ctx = last_ctx_replay
.lock()
.unwrap()
.clone()
.expect("step-b Ctx captured on the replay run");
assert_eq!(
replayed_step_b_ctx.agent, pristine_step_b_ctx.agent,
"step-b agent must be identical across pristine and replay runs"
);
assert_eq!(
replayed_step_b_ctx.attempt, pristine_step_b_ctx.attempt,
"step-b attempt must be 1 in both runs (fresh Engine, first attempt)"
);
assert_eq!(
replayed_step_b_ctx
.meta
.runtime
.get(RUN_ID_KEY)
.and_then(|v| v.as_str()),
Some(run_id.as_str()),
"step-b Ctx must carry the propagated RunId in meta.runtime[RUN_ID_KEY]"
);
let final_logged = replay_store
.list_by_run(&run_id)
.await
.expect("list final replay rows");
assert_eq!(final_logged.len(), 2, "replay run must append step-b's row");
assert_eq!(final_logged[1].step_ref, "step-b");
}