#![allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::indexing_slicing,
reason = "test code"
)]
use super::super::*;
use super::helpers::*;
use crate::conversation::Opening;
use kernel::ToolOutcome;
#[test]
fn a_full_turn_appends_the_canonical_event_sequence() {
let mut ledger = TestLedger::new();
let mut model = OneShotModel {
calls: vec![probe_call()],
};
let mut conversation = Conversation::new();
conversation.push_task_lines("probe the city", "one probe", Opening::FromJob, "the User");
let mut lines = lines();
let turn = opened_on::<1>(&mut lines);
let turn = advance(
turn.assemble(
Interrupt::None,
&mut ledger,
RunPrompt::new(&prefix(), &mut PromptRecord::default()),
&conversation,
&[],
&shape(),
)
.unwrap(),
);
let turn = advance(
turn.call(
Interrupt::None,
&mut ledger,
&mut model,
&BuildingPolicy::default(),
Generating::Unwatched,
)
.unwrap(),
);
let mut invoked = 0u32;
let turn = advance(
turn.execute_concurrent(
Interrupt::None,
&mut ledger,
&mut |_call: &ToolCall, _: TimeMs| {
invoked += 1;
Ok(ToolOutcome {
result: Payload::empty(),
attachments: Vec::new(),
})
},
&mut |_| Interrupt::None,
)
.unwrap(),
);
let PhaseOutcome::Advanced(report) = turn.record(Interrupt::None, &mut ledger).unwrap() else {
panic!("the boundary was not interrupted");
};
assert_eq!(invoked, 1);
assert_eq!(report.calls_made(), 1);
lines.barrier(&mut ledger).unwrap();
assert_eq!(
ledger.kinds(),
[
"prompt_assembled",
"model_called",
"model_returned",
"tool_called",
"tool_result"
]
);
assert_eq!(lines.refs().len(), 5);
assert_eq!(lines.durable(report.model_returned()), Ok(lines.refs()[2]));
assert_eq!(report.assistant().len(), 1);
assert_eq!(report.wave_results().len(), 1);
match &report.wave_results()[0] {
ContentBlock::ToolResult {
tool_use_id,
is_error,
..
} => {
assert_eq!(tool_use_id, "call-1");
assert!(!is_error);
}
other => panic!("expected a tool_result block, got {other:?}"),
}
let called: serde_json::Value = serde_json::from_slice(&ledger.lines[1]).unwrap();
assert_eq!(called["data"]["model"], "script");
let tool_called: serde_json::Value = serde_json::from_slice(&ledger.lines[3]).unwrap();
assert_eq!(tool_called["data"]["id"], "call-1");
let tool_result: serde_json::Value = serde_json::from_slice(&ledger.lines[4]).unwrap();
assert_eq!(tool_result["data"]["tool_use_id"], "call-1");
}
#[test]
fn cancel_at_the_call_boundary_stops_before_any_model_bytes() {
let mut ledger = TestLedger::new();
let mut model = OneShotModel { calls: vec![] };
let turn = opened::<1>();
let turn = advance(
turn.assemble(
Interrupt::None,
&mut ledger,
RunPrompt::new(&prefix(), &mut PromptRecord::default()),
blank_conversation(),
&[],
&shape(),
)
.unwrap(),
);
let outcome = turn
.call(
Interrupt::Cancel,
&mut ledger,
&mut model,
&BuildingPolicy::default(),
Generating::Unwatched,
)
.unwrap();
match outcome {
PhaseOutcome::Cancelled(cancelled) => {
assert_eq!(
cancelled.refs().last().unwrap().kind(),
kernel::EventKind::CancelReceived
);
}
PhaseOutcome::Advanced(_) => panic!("cancel must end the turn at the boundary"),
}
assert_eq!(ledger.kinds(), ["prompt_assembled", "cancel_received"]);
}
#[test]
fn steer_at_a_boundary_records_and_advances() {
let mut ledger = TestLedger::new();
let mut model = OneShotModel { calls: vec![] };
let mut lines = lines();
let turn = opened_on::<4>(&mut lines);
let turn = advance(
turn.assemble(
Interrupt::Steer {
speaker: crate::conversation::Speaker::Person,
text: "prefer the short route".to_owned(),
},
&mut ledger,
RunPrompt::new(&prefix(), &mut PromptRecord::default()),
blank_conversation(),
&[],
&shape(),
)
.unwrap(),
);
let turn = advance(
turn.call(
Interrupt::None,
&mut ledger,
&mut model,
&BuildingPolicy::default(),
Generating::Unwatched,
)
.unwrap(),
);
closed(turn, &mut ledger);
lines.barrier(&mut ledger).unwrap();
assert_eq!(
ledger.kinds(),
[
"steer_received",
"prompt_assembled",
"model_called",
"model_returned"
]
);
let steer: serde_json::Value = serde_json::from_slice(&ledger.lines[0]).unwrap();
assert_eq!(steer["data"]["source"], "user");
assert_eq!(steer["data"]["text"], "prefer the short route");
}
#[test]
fn a_tool_error_lands_in_tool_result_not_in_the_turn() {
let mut ledger = TestLedger::new();
let mut model = OneShotModel {
calls: vec![ToolCall {
id: "call-9".to_owned(),
name: kernel::ToolName::parse("broken").unwrap(),
args: Payload::empty(),
}],
};
let mut lines = lines();
let turn = opened_on::<2>(&mut lines);
let turn = advance(
turn.assemble(
Interrupt::None,
&mut ledger,
RunPrompt::new(&prefix(), &mut PromptRecord::default()),
blank_conversation(),
&[],
&shape(),
)
.unwrap(),
);
let turn = advance(
turn.call(
Interrupt::None,
&mut ledger,
&mut model,
&BuildingPolicy::default(),
Generating::Unwatched,
)
.unwrap(),
);
let turn = advance(
turn.execute_concurrent(
Interrupt::None,
&mut ledger,
&mut |call: &ToolCall, _: TimeMs| {
Err(AxError::failure(
AxCode::ToolUnavailable,
"invoke tool",
call.name.to_string(),
)
.with_recovery("this bench registers no tools"))
},
&mut |_| Interrupt::None,
)
.unwrap(),
);
let PhaseOutcome::Advanced(report) = turn.record(Interrupt::None, &mut ledger).unwrap() else {
panic!("the boundary was not interrupted");
};
lines.barrier(&mut ledger).unwrap();
assert_eq!(report.calls_made(), 1);
let last = ledger.lines.last().unwrap();
let value: serde_json::Value = serde_json::from_slice(last).unwrap();
assert_eq!(value["kind"], "tool_result");
assert_eq!(value["data"]["error"]["code"], "E_TOOL_UNAVAILABLE");
match &report.wave_results()[0] {
ContentBlock::ToolResult { is_error, .. } => assert!(is_error),
other => panic!("expected a tool_result block, got {other:?}"),
}
}
#[test]
fn the_ledger_chain_stays_verifiable_after_a_turn() {
let mut ledger = TestLedger::new();
let mut model = OneShotModel { calls: vec![] };
let turn = opened::<3>();
let turn = advance(
turn.assemble(
Interrupt::None,
&mut ledger,
RunPrompt::new(&prefix(), &mut PromptRecord::default()),
blank_conversation(),
&[],
&shape(),
)
.unwrap(),
);
let turn = advance(
turn.call(
Interrupt::None,
&mut ledger,
&mut model,
&BuildingPolicy::default(),
Generating::Unwatched,
)
.unwrap(),
);
let turn = advance(
turn.execute_concurrent(
Interrupt::None,
&mut ledger,
&mut |_call: &ToolCall, _: TimeMs| panic!("empty wave must not invoke"),
&mut |_| Interrupt::None,
)
.unwrap(),
);
let _report = turn.record(Interrupt::None, &mut ledger).unwrap();
crate::replay::verify_lines(ledger.lines.clone()).unwrap();
}
#[test]
fn a_wave_halted_between_two_calls_does_not_make_the_second() {
let mut ledger = TestLedger::new();
let second = ToolCall {
id: "call-2".to_owned(),
name: kernel::ToolName::parse("probe").unwrap(),
args: Payload::empty(),
};
let mut model = OneShotModel {
calls: vec![probe_call(), second],
};
let turn = opened::<9>();
let turn = advance(
turn.assemble(
Interrupt::None,
&mut ledger,
RunPrompt::new(&prefix(), &mut PromptRecord::default()),
blank_conversation(),
&[],
&shape(),
)
.unwrap(),
);
let turn = advance(
turn.call(
Interrupt::None,
&mut ledger,
&mut model,
&BuildingPolicy::default(),
Generating::Unwatched,
)
.unwrap(),
);
let mut invoked = 0u32;
let outcome = turn
.execute_concurrent(
Interrupt::None,
&mut ledger,
&mut |_call: &ToolCall, _: TimeMs| {
invoked += 1;
Ok(ToolOutcome {
result: Payload::empty(),
attachments: Vec::new(),
})
},
&mut |call| match call {
0 => Interrupt::None,
_ => Interrupt::Cancel,
},
)
.unwrap();
let PhaseOutcome::Cancelled(cancelled) = outcome else {
panic!("a halted wave ends the turn");
};
assert_eq!(invoked, 1, "the second call was never made");
assert!(
!cancelled.refs().is_empty(),
"the cancellation is on the ledger"
);
let kinds = ledger.kinds();
assert_eq!(
kinds.iter().filter(|kind| *kind == "tool_called").count(),
1,
"nothing was written down for work nothing did: {kinds:?}"
);
assert_eq!(
kinds.last().map(String::as_str),
Some("cancel_received"),
"{kinds:?}"
);
}
#[test]
fn assembling_borrows_the_conversation_and_the_tools() {
let mut ledger = TestLedger::new();
let mut conversation = Conversation::new();
conversation.push_task_lines("probe the city", "one probe", Opening::FromJob, "the User");
let tools = [kernel::ToolDef {
name: kernel::ToolName::parse("exec").unwrap(),
description: "run a command".to_owned(),
input_schema: Payload::empty(),
}];
let turn = opened::<1>();
let calling = advance(
turn.assemble(
Interrupt::None,
&mut ledger,
RunPrompt::new(&prefix(), &mut PromptRecord::default()),
&conversation,
&tools,
&shape(),
)
.unwrap(),
);
assert_eq!(
(
calling.state.chat.messages.as_ptr(),
calling.state.chat.tools.as_ptr()
),
(conversation.messages().as_ptr(), tools.as_ptr())
);
}