use super::*;
#[test]
fn gateway_rejects_nested_session_history() {
assert!(
validate_gateway_event(&EventMsg::SessionHistory(
mobius::protocol::SessionHistoryEvent { events: Vec::new() }
))
.is_err()
);
assert!(validate_gateway_event(&EventMsg::ContextCompacted).is_ok());
}
#[test]
fn projected_preview_drops_the_raw_nested_event_duplicate() {
let mut event = EventMsg::Frontend(FrontendEvent::Preview {
id: "/root/reviewer".into(),
title: "reviewer".into(),
subtitle: "Full context".into(),
page_id: "/root/reviewer:latest".into(),
update: mobius::protocol::FrontendPreviewUpdate::Replace,
events: vec![FrontendPreviewEvent {
recorded_at_ms: 1,
event: EventMsg::ContextCompacted,
}],
next: None,
});
clear_projected_preview_events(&mut event);
assert!(matches!(
event,
EventMsg::Frontend(FrontendEvent::Preview {
id,
events,
..
}) if id == "/root/reviewer" && events.is_empty()
));
}
#[test]
fn router_reuse_ignores_local_recipe_changes_but_not_provider_changes() {
let root = tempfile::tempdir().expect("root");
let workspace = root.path().join("workspace");
let state_dir = root.path().join("state");
std::fs::create_dir(&workspace).expect("workspace");
std::fs::create_dir(&state_dir).expect("state directory");
let base = ChatSpec::new(
&workspace,
VersionedAgentConfig {
revision: 1,
config: AgentComposition::default(),
},
&state_dir,
None,
)
.expect("chat spec");
let changes: [fn(&mut AgentComposition); 2] = [
|config: &mut AgentComposition| config.system_prompt.push_str(" updated"),
|config: &mut AgentComposition| config.max_model_steps += 1,
];
for change in changes {
let mut next = base.clone();
change(&mut next.agent.config);
assert!(provider_config_unchanged(&base, &next));
}
let mut provider_changed = base.clone();
provider_changed
.agent
.config
.provider
.model
.push_str("-changed");
assert!(!provider_config_unchanged(&base, &provider_changed));
}
#[test]
fn journal_delivery_accepts_loaded_records_and_rejects_gaps() {
assert_eq!(
classify_journal_sequence(5, 3, JournalDelivery::LoadedStartup).expect("loaded record"),
JournalSequence::AlreadyLoaded
);
assert_eq!(
classify_journal_sequence(5, 5, JournalDelivery::LoadedStartup).expect("loaded high-water"),
JournalSequence::AlreadyLoaded
);
assert_eq!(
classify_journal_sequence(5, 6, JournalDelivery::Live).expect("next record"),
JournalSequence::Next
);
assert!(
classify_journal_sequence(5, 7, JournalDelivery::Live)
.expect_err("sequence gap")
.to_string()
.contains("expected 6")
);
assert!(
classify_journal_sequence(5, 5, JournalDelivery::Live)
.expect_err("stale live record")
.to_string()
.contains("expected 6")
);
}
#[test]
fn widget_snapshot_is_namespaced_updated_and_removed() {
let widget = |id: &str, text: &str| mobius::protocol::FrontendWidget {
id: id.into(),
slot: mobius::protocol::FrontendSlot::Header,
text: text.into(),
tone: mobius::protocol::FrontendTone::Neutral,
symbol: None,
icon_only: false,
progress: None,
content: None,
action: None,
};
let mut widgets = SessionWidgets::new();
update_widgets(
&mut widgets,
&EventMsg::Frontend(FrontendEvent::Widget {
capability: "tasks".into(),
item: widget("z-older", "one"),
}),
);
update_widgets(
&mut widgets,
&EventMsg::Frontend(FrontendEvent::Widget {
capability: "tasks".into(),
item: widget("a-newer", "two"),
}),
);
update_widgets(
&mut widgets,
&EventMsg::Frontend(FrontendEvent::Widget {
capability: "subagents".into(),
item: widget("status", "other"),
}),
);
update_widgets(
&mut widgets,
&EventMsg::Frontend(FrontendEvent::Widget {
capability: "tasks".into(),
item: widget("z-older", "updated"),
}),
);
update_widgets(
&mut widgets,
&EventMsg::Frontend(FrontendEvent::RemoveWidget {
capability: "subagents".into(),
id: "status".into(),
}),
);
assert_eq!(
widgets
.into_iter()
.map(|((capability, id), item)| (capability, id, item.text))
.collect::<Vec<_>>(),
vec![
("tasks".into(), "z-older".into(), "updated".into()),
("tasks".into(), "a-newer".into(), "two".into())
]
);
}
#[test]
fn completed_execution_stats_project_without_an_active_run() {
let stats = ExecutionStats {
run_count: 3,
failed_run_count: 1,
aborted_run_count: 1,
model_calls: 5,
tool_calls: 8,
failed_tool_calls: 2,
elapsed_ms: 900,
usage: TokenUsage {
total_tokens: 42,
..TokenUsage::default()
},
};
let projected = completed_run_stats(&stats);
assert_eq!(
(
projected.run_count,
projected.failed_run_count,
projected.aborted_run_count,
projected.model_calls,
projected.tool_calls,
projected.failed_tool_calls,
projected.elapsed_ms,
projected.usage.total_tokens,
projected.active,
),
(3, 1, 1, 5, 8, 2, 900, 42, None)
);
}
#[test]
fn recent_runs_group_under_the_nearest_visible_session_in_source_order() {
let sessions = vec![
session_summary("root", None, true, Some("Root preview")),
session_summary("nested-agent", Some("agent"), false, None),
session_summary("agent", Some("root"), false, None),
session_summary("fork", Some("root"), true, None),
session_summary("fork-agent", Some("fork"), false, None),
];
let records = vec![
execution_record("nested-agent", "nested", 5),
execution_record("fork-agent", "fork-agent", 4),
execution_record("root", "root", 3),
execution_record("fork", "fork", 2),
];
let mut metadata = SessionCatalogMetadata::new();
metadata.insert(
"root".into(),
catalog::SessionMetadata {
title: Some("Renamed root".into()),
..catalog::SessionMetadata::default()
},
);
let groups = recent_run_groups(records, &sessions, &metadata);
let projection = groups
.iter()
.map(|group| {
(
group.session_id.as_str(),
group.title.as_str(),
group
.runs
.iter()
.map(|run| (run.session_id.as_str(), run.turn_id.as_str()))
.collect::<Vec<_>>(),
)
})
.collect::<Vec<_>>();
assert_eq!(
projection,
vec![
(
"root",
"Renamed root",
vec![("nested-agent", "nested"), ("root", "root")]
),
(
"fork",
"Untitled",
vec![("fork-agent", "fork-agent"), ("fork", "fork")]
)
]
);
}
#[test]
fn recent_runs_omit_metadata_hidden_roots() {
let sessions = vec![
session_summary("hidden", None, true, None),
session_summary("hidden-agent", Some("hidden"), false, None),
session_summary("shown", None, true, Some(" Shown thread ")),
];
let records = vec![
execution_record("hidden-agent", "hidden", 2),
execution_record("shown", "shown", 1),
];
let mut metadata = SessionCatalogMetadata::new();
metadata.insert(
"hidden".into(),
catalog::SessionMetadata {
hidden: true,
..catalog::SessionMetadata::default()
},
);
let groups = recent_run_groups(records, &sessions, &metadata);
assert!(matches!(
groups.as_slice(),
[SessionRunGroup { session_id, title, runs }]
if session_id == "shown" && title == "Shown thread" && runs[0].turn_id == "shown"
));
}
fn session_summary(
session_id: &str,
parent_session_id: Option<&str>,
catalog_visible: bool,
first_user_message: Option<&str>,
) -> SessionSummary {
SessionSummary {
session_id: session_id.into(),
session_context: SessionContext::default(),
parent_session_id: parent_session_id.map(str::to_owned),
parent_sequence: parent_session_id.map(|_| 0),
sequence: 0,
catalog_visible,
first_user_message: first_user_message.map(str::to_owned),
execution_stats: ExecutionStats::default(),
created_at: 0,
updated_at: 0,
}
}
fn execution_record(session_id: &str, turn_id: &str, started_at_ms: i64) -> ExecutionRecord {
ExecutionRecord {
session_id: session_id.into(),
submission_id: format!("submission-{turn_id}"),
turn_id: turn_id.into(),
started_at_ms,
finished_at_ms: started_at_ms,
elapsed_ms: 0,
outcome: ExecutionOutcome::Completed,
model_calls: 0,
tool_calls: 0,
failed_tool_calls: 0,
usage: TokenUsage::default(),
}
}