#![cfg(all(feature = "ipc", feature = "serde"))]
mod common;
use std::path::{Path, PathBuf};
use lazily::{
CausalReceipt, CommandApplyStatus, CommandMessage, CommandProjection, CommandProjectionImage,
ReceiptMessage, ReceiptOutcome,
};
use serde_json::Value;
const FIXTURE_DIR: &str = "../lazily-spec/conformance/message-passing";
fn fixtures_present() -> bool {
Path::new(FIXTURE_DIR).is_dir()
}
fn load(name: &str) -> Value {
let path = PathBuf::from(FIXTURE_DIR).join(name);
let raw = crate::common::spec_read_to_string(&path)
.unwrap_or_else(|e| panic!("read {}: {e}", path.display()));
serde_json::from_str(&raw).unwrap_or_else(|e| panic!("parse {}: {e}", path.display()))
}
fn fold_frame(projection: &mut CommandProjection, frame: &Value) -> CommandApplyStatus {
let schema = frame["schema"].as_str().expect("frame.schema");
let wire = &frame["wire"];
match schema {
"message-passing" => {
let message: CommandMessage =
serde_json::from_value(wire.clone()).expect("decode CommandMessage");
projection.apply_message(&message)
}
"receipts" => {
let message: ReceiptMessage =
serde_json::from_value(wire.clone()).expect("decode ReceiptMessage");
let ReceiptMessage::CausalReceipts(batch) = message;
let mut last = CommandApplyStatus::Unknown;
for receipt in &batch.receipts {
last = projection.observe_receipt(receipt);
}
last
}
other => panic!("unknown frame schema {other}"),
}
}
fn frames_of(obj: &Value) -> &Vec<Value> {
obj["frames"].as_array().expect("frames array")
}
fn assert_projection(projection: &CommandProjection, expect: &Value) {
let want: CommandProjectionImage =
serde_json::from_value(expect["projection"].clone()).expect("decode expect.projection");
assert_eq!(projection.to_image(), want, "projection image mismatch");
}
#[test]
fn editor_route_submit_is_nonterminal() {
if !fixtures_present() {
return;
}
let fx = load("editor_route_submit.json");
let mut p = CommandProjection::new();
for frame in frames_of(&fx) {
fold_frame(&mut p, frame);
}
assert_projection(&p, &fx["expect"]);
assert!(p.terminal_for("cmd-run-1").is_none());
}
#[test]
fn sync_tmux_layout_submit_shared_blob() {
if !fixtures_present() {
return;
}
let fx = load("sync_tmux_layout_submit.json");
let mut p = CommandProjection::new();
for frame in frames_of(&fx) {
fold_frame(&mut p, frame);
}
assert_projection(&p, &fx["expect"]);
}
#[test]
fn accepted_then_applied_receipt_is_terminal_only_at_receipt() {
if !fixtures_present() {
return;
}
let fx = load("accepted_then_applied_receipt.json");
let frames = frames_of(&fx);
let terminal_at = fx["expect"]["terminal_after_frame_index"]
.as_u64()
.expect("terminal_after_frame_index") as usize;
let mut p = CommandProjection::new();
for (i, frame) in frames.iter().enumerate() {
fold_frame(&mut p, frame);
let is_terminal = p.terminal_for("cmd-run-1").is_some();
if i < terminal_at {
assert!(!is_terminal, "frame {i}: must still be non-terminal");
} else {
assert!(is_terminal, "frame {i}: must be terminal");
}
}
assert_projection(&p, &fx["expect"]);
}
#[test]
fn stale_generation_events_and_receipts_are_ignored() {
if !fixtures_present() {
return;
}
let fx = load("stale_generation_ignored.json");
let frames = frames_of(&fx);
let ignored: Vec<usize> = fx["expect"]["ignored_frame_indices"]
.as_array()
.expect("ignored_frame_indices")
.iter()
.map(|v| v.as_u64().unwrap() as usize)
.collect();
let mut p = CommandProjection::new();
for (i, frame) in frames.iter().enumerate() {
let status = fold_frame(&mut p, frame);
if ignored.contains(&i) {
assert!(
matches!(status, CommandApplyStatus::StaleGeneration { .. }),
"frame {i}: expected StaleGeneration, got {status:?}"
);
}
}
assert_projection(&p, &fx["expect"]);
}
#[test]
fn terminal_conflict_fails_closed() {
if !fixtures_present() {
return;
}
let fx = load("terminal_conflict_fail_closed.json");
let frames = frames_of(&fx);
let conflict_at = fx["expect"]["conflict_after_frame_index"]
.as_u64()
.expect("conflict_after_frame_index") as usize;
let command_id = fx["expect"]["conflict_command_id"].as_str().unwrap();
let mut p = CommandProjection::new();
for (i, frame) in frames.iter().enumerate() {
let status = fold_frame(&mut p, frame);
if i == conflict_at {
assert!(
matches!(status, CommandApplyStatus::TerminalConflict { .. }),
"frame {i}: expected TerminalConflict, got {status:?}"
);
}
}
assert!(p.has_conflict(command_id), "conflict must be flagged");
let before: CommandProjectionImage =
serde_json::from_value(fx["expect"]["projection_before_conflict"].clone()).unwrap();
assert_eq!(p.to_image(), before);
}
#[test]
fn cancel_preempts_nonterminal_scenarios() {
if !fixtures_present() {
return;
}
let fx = load("cancel_preempts_nonterminal.json");
for scenario in fx["scenarios"].as_array().expect("scenarios") {
let name = scenario["name"].as_str().unwrap();
let mut p = CommandProjection::new();
for frame in scenario["frames"].as_array().unwrap() {
fold_frame(&mut p, frame);
}
assert_projection(&p, &scenario["expect"]);
assert_eq!(
p.terminal_for("cmd-run-1").map(|e| e.status),
p.entry("cmd-run-1").map(|e| e.status),
"scenario {name}: terminal command exposed via terminal_for"
);
}
}
#[test]
fn reconnect_command_projection_resyncs() {
if !fixtures_present() {
return;
}
let fx = load("reconnect_command_projection.json");
let mut p = CommandProjection::new();
for frame in frames_of(&fx) {
fold_frame(&mut p, frame);
}
assert_projection(&p, &fx["expect"]);
}
#[test]
fn rpc_call_waits_for_terminal() {
if !fixtures_present() {
return;
}
let fx = load("rpc_call_waits_for_terminal.json");
let frames = frames_of(&fx);
let rpc = &fx["expect"]["rpc"];
let command_id = rpc["command_id"].as_str().unwrap();
let resolves_at = rpc["resolves_after_frame_index"].as_u64().unwrap() as usize;
let unresolved: Vec<usize> = rpc["unresolved_after_frame_indices"]
.as_array()
.unwrap()
.iter()
.map(|v| v.as_u64().unwrap() as usize)
.collect();
let mut p = CommandProjection::new();
for (i, frame) in frames.iter().enumerate() {
fold_frame(&mut p, frame);
let resolved = p.terminal_for(command_id).is_some();
if unresolved.contains(&i) {
assert!(!resolved, "frame {i}: RPC call must NOT have resolved");
}
if i == resolves_at {
assert!(resolved, "frame {i}: RPC call must resolve here");
}
}
assert_projection(&p, &fx["expect"]);
}
#[test]
fn receipt_outcome_maps_are_covered() {
let mut p = CommandProjection::new();
let submit_json = serde_json::json!({
"CommandSubmit": {
"command_id": "cmd-x",
"causation_id": "cmd-x",
"source": "test",
"target": "controller",
"namespace": "agent-doc",
"name": "editor_route",
"authority_generation": 1,
"idempotency_key": "k",
"deadline_ms": 0,
"policy": { "dedupe": "none", "supersede": false, "cancel_on_preempt": false },
"payload_type": "agent-doc.editor_route.v1",
"payload_hash": "sha256:00",
"payload": { "Inline": [1] },
"required_features": []
}
});
let message: CommandMessage = serde_json::from_value(submit_json).unwrap();
p.apply_message(&message);
let r = CausalReceipt::rejected("r1", "cmd-x", "controller", 1).with_reason("timed_out");
assert_eq!(r.outcome, ReceiptOutcome::Rejected);
p.observe_receipt(&r);
assert_eq!(
p.terminal_for("cmd-x").unwrap().status,
lazily::CommandStatus::TimedOut
);
}