use hotl_engine::{EngineEvent, Outcome};
use hotl_types::TokenUsage;
use serde_json::json;
#[path = "../src/wire.rs"]
#[allow(dead_code)]
mod wire;
#[test]
fn every_frame_is_tagged_and_versioned() {
let events = [
EngineEvent::TextDelta("hi".into()),
EngineEvent::ThinkingDelta("mm".into()),
EngineEvent::ToolStart {
name: "read".into(),
summary: "read ./x".into(),
},
EngineEvent::ToolDone {
name: "read".into(),
ok: true,
},
EngineEvent::ToolDenied {
name: "write".into(),
},
EngineEvent::ToolAutoAllowed {
name: "bash".into(),
rule: "ls*".into(),
},
EngineEvent::Retrying {
attempt: 1,
reason: "429".into(),
},
EngineEvent::FallbackModel { model: "m2".into() },
EngineEvent::PromptQueued,
EngineEvent::Compacted { degraded: false },
EngineEvent::TodosChanged { items: Vec::new() },
EngineEvent::TurnDone {
outcome: Outcome::Done { text: "ok".into() },
usage: TokenUsage::default(),
},
];
for e in events {
let f = wire::json_frame(&e, "test-model");
assert!(f["type"].is_string(), "untagged frame: {f}");
assert_eq!(f["schema_version"], json!(wire::JSON_STREAM_SCHEMA_VERSION));
let text = f.to_string();
assert!(!text.contains(" { "), "Debug-formatted payload in {f}");
}
}
#[test]
fn turn_done_carries_a_structured_outcome() {
let f = wire::json_frame(
&EngineEvent::TurnDone {
outcome: Outcome::DoomLoop {
pattern: "read ./x".into(),
},
usage: TokenUsage::default(),
},
"test-model",
);
assert_eq!(f["type"], "turn_done");
assert_eq!(f["outcome"]["kind"], "doom_loop");
assert_eq!(f["outcome"]["pattern"], "read ./x");
assert!(f["usage"]["input_tokens"].is_u64());
}
#[test]
fn every_outcome_variant_is_tagged_and_carries_its_payload() {
let cases = [
(
Outcome::Done { text: "t".into() },
"done",
Some(("text", "t")),
),
(Outcome::Cancelled, "cancelled", None),
(Outcome::TurnLimit, "turn_limit", None),
(Outcome::Refused, "refused", None),
(
Outcome::DoomLoop {
pattern: "p".into(),
},
"doom_loop",
Some(("pattern", "p")),
),
(
Outcome::ToolFailureBudget {
tool: "bash".into(),
},
"tool_failure_budget",
Some(("tool", "bash")),
),
(
Outcome::Error {
message: "m".into(),
},
"error",
Some(("message", "m")),
),
];
for (outcome, kind, payload) in cases {
let f = wire::outcome_frame(&outcome);
assert_eq!(f["kind"], kind, "outcome tag for {kind}");
if let Some((key, value)) = payload {
assert_eq!(f[key], value, "{kind} must carry `{key}`");
}
}
}
#[test]
fn thinking_deltas_carry_their_text() {
let f = wire::json_frame(
&EngineEvent::ThinkingDelta("reasoning".into()),
"test-model",
);
assert_eq!(f["type"], "thinking_delta");
assert_eq!(f["text"], "reasoning");
}
#[test]
fn turn_done_usage_carries_hit_ratio_when_cache_activity_is_present() {
let usage = TokenUsage {
input_tokens: 25,
output_tokens: 5,
cache_read_input_tokens: 50,
cache_creation_input_tokens: 25,
..Default::default()
};
let f = wire::json_frame(
&EngineEvent::TurnDone {
outcome: Outcome::Done { text: "ok".into() },
usage,
},
"test-model",
);
assert_eq!(f["usage"]["hit_ratio"], json!(0.5));
}
#[test]
fn turn_done_usage_omits_hit_ratio_without_cache_activity() {
let f = wire::json_frame(
&EngineEvent::TurnDone {
outcome: Outcome::Done { text: "ok".into() },
usage: TokenUsage::default(),
},
"test-model",
);
assert!(
f["usage"].get("hit_ratio").is_none(),
"no cache activity must mean no hit_ratio key: {f}"
);
}
#[test]
fn turn_done_usage_carries_cost_usd_for_a_catalogued_model() {
let usage = TokenUsage {
input_tokens: 1_000_000,
output_tokens: 1_000_000,
..Default::default()
};
let f = wire::json_frame(
&EngineEvent::TurnDone {
outcome: Outcome::Done { text: "ok".into() },
usage,
},
"claude-opus-4-8",
);
let cost = f["usage"]["cost_usd"]
.as_f64()
.expect("a catalogued model must carry cost_usd");
assert!((cost - 30.0).abs() < 1e-9, "was {cost}");
}
#[test]
fn turn_done_usage_omits_cost_usd_for_an_uncatalogued_model() {
let usage = TokenUsage {
input_tokens: 10,
..Default::default()
};
let f = wire::json_frame(
&EngineEvent::TurnDone {
outcome: Outcome::Done { text: "ok".into() },
usage,
},
"totally-unheard-of-model",
);
assert!(
f["usage"].get("cost_usd").is_none(),
"an uncatalogued model must omit cost_usd, not guess: {f}"
);
}
#[test]
fn the_schema_version_reflects_the_breaking_outcome_change() {
let stamped = wire::json_frame(&EngineEvent::PromptQueued, "test-model")["schema_version"]
.as_u64()
.expect("every frame carries a numeric schema_version");
assert!(
stamped >= 2,
"the outcome shape changed from a Debug string to a tagged object"
);
}