use agent_client_protocol_schema::v1::{ContentBlock, ContentChunk, TextContent};
use aion_core::{ActivityId, RunId, WorkflowId};
use aion_integration_acp::messages::MessageAccumulator;
use super::*;
fn event(kind: ActivityEventKind) -> ActivityEvent {
ActivityEvent {
workflow_id: WorkflowId::new(uuid::Uuid::nil()),
run_id: RunId::new(uuid::Uuid::from_u128(1)),
activity_id: ActivityId::from_sequence_position(1),
attempt: 1,
agent_id: uuid::Uuid::from_u128(2),
agent_role: "assistant".to_owned(),
emitted_at: chrono::Utc::now(),
worker_seq: 0,
store_seq: None,
ephemeral: false,
kind,
}
}
#[test]
fn a_token_delta_becomes_a_delta_frame() {
let mut frames = TurnFrames::new("t-1");
let produced = frames.translate(event(ActivityEventKind::Delta {
message_id: "m-1".to_owned(),
text_fragment: "hel".to_owned(),
}));
assert_eq!(
produced,
vec![AssistantSessionEvent::Delta {
turn_id: "t-1".to_owned(),
text: "hel".to_owned(),
}]
);
}
#[test]
fn a_completed_assistant_message_is_not_streamed_twice() {
let mut frames = TurnFrames::new("t-1");
let produced = frames.translate(event(ActivityEventKind::Message {
role: MessageRole::Assistant,
text: "hello".to_owned(),
}));
assert!(
produced.is_empty(),
"the deltas already carried this text; a second frame would double it"
);
}
#[test]
fn a_thinking_fragment_becomes_a_thought() {
let mut frames = TurnFrames::new("t-1");
let produced = frames.translate(event(ActivityEventKind::Progress {
detail: ProgressDetail::Thinking {
message_id: "m-1".to_owned(),
text: "considering".to_owned(),
},
}));
assert_eq!(
produced,
vec![AssistantSessionEvent::Thought {
turn_id: "t-1".to_owned(),
text: "considering".to_owned(),
}]
);
}
#[test]
fn what_the_acp_adapter_streams_as_reasoning_reaches_the_panel_as_a_thought() -> Result<(), String>
{
let mut accumulator = MessageAccumulator::new();
let chunk = ContentChunk::new(ContentBlock::Text(TextContent::new("considering")));
let streamed = accumulator.push_thought_chunk(&chunk);
let [(kind, ephemeral)] = streamed.as_slice() else {
return Err(format!(
"one live event per thought chunk, got {streamed:?}"
));
};
assert!(*ephemeral, "the live reasoning stream is ephemeral");
let mut frames = TurnFrames::new("t-1");
assert_eq!(
frames.translate(event(kind.clone())),
vec![AssistantSessionEvent::Thought {
turn_id: "t-1".to_owned(),
text: "considering".to_owned(),
}]
);
Ok(())
}
#[test]
fn a_progress_note_is_progress_not_a_thought() {
let mut frames = TurnFrames::new("t-1");
let produced = frames.translate(event(ActivityEventKind::Progress {
detail: ProgressDetail::Note {
text: "tool_call_delta".to_owned(),
},
}));
assert_eq!(
produced,
vec![AssistantSessionEvent::Raw {
turn_id: Some("t-1".to_owned()),
source: "progress".to_owned(),
value: serde_json::json!({ "detail": "Note", "text": "tool_call_delta" }),
}]
);
}
#[test]
fn a_tool_result_is_named_by_the_call_that_produced_it() {
let mut frames = TurnFrames::new("t-1");
let started = frames.translate(event(ActivityEventKind::ToolCall {
tool: "check_document".to_owned(),
call_id: "c-1".to_owned(),
input: serde_json::json!({ "path": "a.awl" }),
}));
assert_eq!(
started,
vec![AssistantSessionEvent::ToolCall {
turn_id: "t-1".to_owned(),
call_id: "c-1".to_owned(),
name: "check_document".to_owned(),
status: AssistantToolCallStatus::Started,
input: Some(serde_json::json!({ "path": "a.awl" })),
output: None,
}]
);
let finished = frames.translate(event(ActivityEventKind::ToolResult {
call_id: "c-1".to_owned(),
output: serde_json::json!({ "diagnostics": [] }),
is_error: false,
}));
assert_eq!(
finished,
vec![AssistantSessionEvent::ToolCall {
turn_id: "t-1".to_owned(),
call_id: "c-1".to_owned(),
name: "check_document".to_owned(),
status: AssistantToolCallStatus::Completed,
input: None,
output: Some(serde_json::json!({ "diagnostics": [] })),
}]
);
}
#[test]
fn a_result_with_no_remembered_call_says_so_rather_than_inventing_a_tool() {
let mut frames = TurnFrames::new("t-1");
let produced = frames.translate(event(ActivityEventKind::ToolResult {
call_id: "orphan".to_owned(),
output: serde_json::Value::Null,
is_error: true,
}));
match produced.as_slice() {
[AssistantSessionEvent::ToolCall { name, status, .. }] => {
assert_eq!(name, UNMATCHED_TOOL);
assert_eq!(*status, AssistantToolCallStatus::Failed);
}
other => panic_free_failure(other),
}
}
#[test]
fn a_permission_request_reaches_the_transcript_joined_to_its_decision() {
let mut frames = TurnFrames::new("t-1");
let held = frames.translate(event(ActivityEventKind::Raw {
source: "session/request_permission".to_owned(),
value: serde_json::json!({ "toolCall": { "toolCallId": "c-1" } }),
}));
assert!(
held.is_empty(),
"the ask alone says nothing about what was answered"
);
let decided = frames.translate(event(ActivityEventKind::Raw {
source: "session/request_permission/decision".to_owned(),
value: serde_json::json!({
"policy": "deny",
"toolCallId": "c-1",
"outcome": { "outcome": "selected", "optionId": "reject-1" }
}),
}));
assert_eq!(
decided,
vec![AssistantSessionEvent::PermissionAsk {
turn_id: "t-1".to_owned(),
request: serde_json::json!({ "toolCall": { "toolCallId": "c-1" } }),
decided: AssistantPermissionDecision::Deny,
}]
);
}
#[test]
fn an_allow_once_policy_is_recorded_as_allow_once() {
let mut frames = TurnFrames::new("t-1");
let _held = frames.translate(event(ActivityEventKind::Raw {
source: "session/request_permission".to_owned(),
value: serde_json::Value::Null,
}));
let decided = frames.translate(event(ActivityEventKind::Raw {
source: "session/request_permission/decision".to_owned(),
value: serde_json::json!({
"policy": "allow_once",
"outcome": { "outcome": "selected", "optionId": "allow-1" }
}),
}));
match decided.as_slice() {
[AssistantSessionEvent::PermissionAsk { decided, .. }] => {
assert_eq!(*decided, AssistantPermissionDecision::AllowOnce);
}
other => panic_free_failure(other),
}
}
#[test]
fn a_cancelled_outcome_is_recorded_verbatim_rather_than_reported_as_a_denial() {
let mut frames = TurnFrames::new("t-1");
let decided = frames.translate(event(ActivityEventKind::Raw {
source: "session/request_permission/decision".to_owned(),
value: serde_json::json!({
"policy": "allow_once",
"outcome": { "outcome": "cancelled" }
}),
}));
match decided.as_slice() {
[AssistantSessionEvent::Raw { source, value, .. }] => {
assert_eq!(source, "session/request_permission/decision");
assert_eq!(value["decision"]["outcome"]["outcome"], "cancelled");
}
other => panic_free_failure(other),
}
}
#[test]
fn an_unanswered_permission_request_still_reaches_the_record() {
let mut frames = TurnFrames::new("t-1");
let _held = frames.translate(event(ActivityEventKind::Raw {
source: "session/request_permission".to_owned(),
value: serde_json::json!({ "toolCall": { "toolCallId": "c-9" } }),
}));
let flushed = frames.flush();
match flushed.as_slice() {
[AssistantSessionEvent::Raw { source, value, .. }] => {
assert_eq!(source, "session/request_permission/unanswered");
assert_eq!(value["toolCall"]["toolCallId"], "c-9");
}
other => panic_free_failure(other),
}
assert!(
frames.flush().is_empty(),
"a flushed request is not flushed twice"
);
}
#[test]
fn an_unmapped_frame_passes_through_verbatim_rather_than_vanishing() {
let mut frames = TurnFrames::new("t-1");
let produced = frames.translate(event(ActivityEventKind::Raw {
source: "session/update/undecodable".to_owned(),
value: serde_json::json!({ "error": "bad frame" }),
}));
assert_eq!(
produced,
vec![AssistantSessionEvent::Raw {
turn_id: Some("t-1".to_owned()),
source: "session/update/undecodable".to_owned(),
value: serde_json::json!({ "error": "bad frame" }),
}]
);
}
#[test]
fn a_usage_estimate_is_kept_as_a_raw_frame_rather_than_dropped() {
let mut frames = TurnFrames::new("t-1");
let produced = frames.translate(event(ActivityEventKind::Progress {
detail: ProgressDetail::UsageEstimate {
input_tokens: Some(10),
output_tokens: Some(3),
},
}));
match produced.as_slice() {
[AssistantSessionEvent::Raw { source, value, .. }] => {
assert_eq!(source, "progress");
assert_eq!(value["input_tokens"], serde_json::json!(10));
}
other => panic_free_failure(other),
}
}
#[test]
fn the_terminal_stop_is_left_to_the_turns_own_result() {
let mut frames = TurnFrames::new("t-1");
let produced = frames.translate(event(ActivityEventKind::Stop {
reason: aion_core::StopKind::EndTurn,
}));
assert!(
produced.is_empty(),
"the turn's result carries the stop reason AND the final message"
);
}
fn panic_free_failure(produced: &[AssistantSessionEvent]) {
assert_eq!(
format!("{produced:?}"),
"<the expected frame shape>",
"the translator produced an unexpected shape"
);
}
#[test]
fn an_available_commands_update_becomes_a_classified_frame() {
let mut frames = TurnFrames::new("t-1");
let produced = frames.translate(event(ActivityEventKind::Raw {
source: AVAILABLE_COMMANDS_SOURCE.to_owned(),
value: serde_json::json!({
"availableCommands": [
{ "name": "compact", "description": "compact the conversation" },
{
"name": "plan",
"description": "draft a plan",
"input": { "hint": "what to plan" },
},
]
}),
}));
assert_eq!(
produced,
vec![AssistantSessionEvent::AvailableCommands {
commands: vec![
AssistantCommand {
name: "compact".to_owned(),
description: "compact the conversation".to_owned(),
input_hint: None,
},
AssistantCommand {
name: "plan".to_owned(),
description: "draft a plan".to_owned(),
input_hint: Some("what to plan".to_owned()),
},
],
}]
);
}
#[test]
fn an_empty_advertisement_is_a_withdrawal_rather_than_a_raw_frame() {
let mut frames = TurnFrames::new("t-1");
let produced = frames.translate(event(ActivityEventKind::Raw {
source: AVAILABLE_COMMANDS_SOURCE.to_owned(),
value: serde_json::json!({ "availableCommands": [] }),
}));
assert_eq!(
produced,
vec![AssistantSessionEvent::AvailableCommands {
commands: Vec::new(),
}]
);
}
#[test]
fn an_unreadable_command_payload_is_kept_verbatim_and_never_read_as_a_withdrawal() {
let mut frames = TurnFrames::new("t-1");
let value = serde_json::json!({ "commands": ["compact"] });
let produced = frames.translate(event(ActivityEventKind::Raw {
source: AVAILABLE_COMMANDS_SOURCE.to_owned(),
value: value.clone(),
}));
assert_eq!(
produced,
vec![AssistantSessionEvent::Raw {
turn_id: Some("t-1".to_owned()),
source: AVAILABLE_COMMANDS_SOURCE.to_owned(),
value,
}]
);
}
#[test]
fn a_malformed_entry_is_dropped_and_the_rest_of_the_advertisement_stands() {
let mut frames = TurnFrames::new("t-1");
let produced = frames.translate(event(ActivityEventKind::Raw {
source: AVAILABLE_COMMANDS_SOURCE.to_owned(),
value: serde_json::json!({
"availableCommands": [
{ "name": "compact" },
{ "name": "plan", "description": "draft a plan" },
]
}),
}));
assert_eq!(
produced,
vec![AssistantSessionEvent::AvailableCommands {
commands: vec![AssistantCommand {
name: "plan".to_owned(),
description: "draft a plan".to_owned(),
input_hint: None,
}],
}]
);
}
#[test]
fn the_classified_source_is_the_label_the_adapter_emits() {
assert_eq!(
AVAILABLE_COMMANDS_SOURCE,
format!(
"{}/available_commands_update",
aion_integration_acp::translate::UPDATE_SOURCE_PREFIX
)
);
}
#[test]
fn a_config_option_update_becomes_a_classified_frame() {
let mut frames = TurnFrames::new("t-1");
let produced = frames.translate(event(ActivityEventKind::Raw {
source: "session/update/config_option_update".to_owned(),
value: serde_json::json!({
"sessionUpdate": "config_option_update",
"configOptions": [{
"id": "model",
"name": "Model",
"category": "model",
"type": "select",
"currentValue": "opus",
"options": [
{ "value": "opus", "name": "Opus", "description": "the main one" },
{ "value": "fable", "name": "Fable" }
]
}]
}),
}));
assert_eq!(
produced,
vec![AssistantSessionEvent::ConfigOptions {
options: vec![aion_core::AssistantConfigOption {
id: "model".to_owned(),
name: "Model".to_owned(),
description: None,
category: Some("model".to_owned()),
value: aion_core::AssistantConfigValue::Select {
choices: vec![
aion_core::AssistantConfigChoice {
id: "opus".to_owned(),
name: "Opus".to_owned(),
description: Some("the main one".to_owned()),
group: None,
},
aion_core::AssistantConfigChoice {
id: "fable".to_owned(),
name: "Fable".to_owned(),
description: None,
group: None,
},
],
current: "opus".to_owned(),
},
}],
}]
);
}
#[test]
fn grouped_select_choices_flatten_with_their_group_label() -> Result<(), String> {
let mut frames = TurnFrames::new("t-1");
let produced = frames.translate(event(ActivityEventKind::Raw {
source: "session/update/config_option_update".to_owned(),
value: serde_json::json!({
"configOptions": [{
"id": "model",
"name": "Model",
"type": "select",
"currentValue": "opus",
"options": [{
"group": "anthropic",
"name": "Anthropic",
"options": [{ "value": "opus", "name": "Opus" }]
}]
}]
}),
}));
let [AssistantSessionEvent::ConfigOptions { options }] = produced.as_slice() else {
return Err(format!("expected one classified frame, got {produced:?}"));
};
let aion_core::AssistantConfigValue::Select { choices, .. } = &options[0].value else {
return Err(format!("expected a select, got {:?}", options[0].value));
};
assert_eq!(choices[0].group.as_deref(), Some("Anthropic"));
assert_eq!(choices[0].id, "opus");
Ok(())
}
#[test]
fn a_boolean_option_is_a_toggle_and_an_unknown_kind_is_skipped() {
let mut frames = TurnFrames::new("t-1");
let produced = frames.translate(event(ActivityEventKind::Raw {
source: "session/update/config_option_update".to_owned(),
value: serde_json::json!({
"configOptions": [
{ "id": "burst", "name": "Burst", "type": "boolean", "currentValue": true },
{ "id": "prose", "name": "Prose", "type": "freeform_text", "currentValue": "x" }
]
}),
}));
assert_eq!(
produced,
vec![AssistantSessionEvent::ConfigOptions {
options: vec![aion_core::AssistantConfigOption {
id: "burst".to_owned(),
name: "Burst".to_owned(),
description: None,
category: None,
value: aion_core::AssistantConfigValue::Toggle { current: true },
}],
}]
);
}
#[test]
fn an_unreadable_config_payload_passes_through_verbatim() {
let mut frames = TurnFrames::new("t-1");
let value = serde_json::json!({ "sessionUpdate": "config_option_update", "somethingElse": 1 });
let produced = frames.translate(event(ActivityEventKind::Raw {
source: "session/update/config_option_update".to_owned(),
value: value.clone(),
}));
assert_eq!(
produced,
vec![AssistantSessionEvent::Raw {
turn_id: Some("t-1".to_owned()),
source: "session/update/config_option_update".to_owned(),
value,
}]
);
}