mirror of
https://github.com/nearai/ironclaw.git
synced 2026-09-03 08:06:01 +08:00
* feat: port ratatui tui onto staging * Add TUI model picker for /model * Fix TUI CI lint failures * Format /tools output as vertical list * Restore TUI approval modal on thread switch * Re-emit pending approval events on follow-up messages * Improve TUI thread handling and activity UI * Sort TUI resume conversations by activity * fix(tui): address PR review feedback * Add TUI thread detail modal for activity sidebar * feat(tui): improve conversation scrolling UX - Mouse wheel: 1-line increments (was 3-line jumps) - PageUp/PageDown: full-page scroll based on viewport height (was 5 lines) - Add scrollbar widget on conversation right edge (track │, thumb ┃) - Add "↓ N more ↓ End to return" indicator when scrolled up - Add auto-follow (pinned_to_bottom) that disengages on scroll-up and re-engages when reaching bottom or pressing End - Clamp scroll offset to valid range (can't scroll past content) - Add End key binding to jump to bottom Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(tui): use engine context pressure data for status bar The context bar was using cumulative session tokens (total_input + total_output) which grow unboundedly across turns, making the bar always show 100% after a few exchanges. Now uses the actual context window usage from ContextPressure events when available, falling back to cumulative tokens only before the first engine update arrives. Also syncs context_window from the engine's max_tokens so the limit reflects the real model capability instead of name-based heuristics. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(tui): render markdown in thread detail modal The thread detail modal was displaying raw markdown text (plain line splitting). Now uses render_markdown() for proper formatting of headers, lists, bold, code blocks, etc. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(tui): hydrate sidebar with engine threads and routines at startup The TUI sidebar was empty until the first user message because EngineThreadList and RoutineUpdate events were only sent after processing a message. Now sends initial data right before the message loop so the activity panel shows existing threads and routines immediately on startup. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(tui): use owner_id for engine thread hydration at startup list_engine_threads filters by user_id, so passing "" matched no threads. Now uses self.owner_id() which matches the TUI channel's user_id, so threads are visible in the sidebar immediately. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(tui): fix CI — type errors and formatting in TUI tests Wrap `started_at` and `updated_at` in `Some(...)` to match `Option<DateTime<Utc>>` after upstream struct change, and run `cargo fmt` on files with formatting drift. [skip-regression-check] Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(ci): resolve clippy warnings — collapsible ifs and needless borrow Collapse three nested `if` blocks into `if && let` chains and remove a needless `&` on the `process_list_threads` call, all in agent_loop.rs. [skip-regression-check] Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(ci): add live_harness.rs with updated StatusUpdate patterns The live_harness.rs file was added to staging after this branch diverged. When CI merges the PR into staging, the file uses old StatusUpdate patterns that don't account for the new `detail` and `call_id` fields added by this branch. Add the file with `..` rest patterns to fix the merge-time compile errors. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
156 lines
5.1 KiB
Rust
156 lines
5.1 KiB
Rust
//! E2E trace tests: status event verification.
|
|
//!
|
|
//! Validates that StatusUpdate events are emitted in the correct order
|
|
//! during tool execution: ToolStarted must precede ToolCompleted for
|
|
//! each tool invocation.
|
|
|
|
#[cfg(feature = "libsql")]
|
|
mod support;
|
|
|
|
#[cfg(feature = "libsql")]
|
|
mod tests {
|
|
use std::time::Duration;
|
|
|
|
use ironclaw::channels::StatusUpdate;
|
|
|
|
use crate::support::test_rig::TestRigBuilder;
|
|
use crate::support::trace_llm::LlmTrace;
|
|
|
|
/// For a 3-tool chain (echo -> echo -> echo), verify that:
|
|
/// 1. ToolStarted fires before ToolCompleted for each tool.
|
|
/// 2. The total number of ToolStarted equals ToolCompleted.
|
|
/// 3. No ToolCompleted appears without a preceding ToolStarted for that name.
|
|
#[tokio::test]
|
|
async fn test_status_event_ordering() {
|
|
let trace = LlmTrace::from_file(concat!(
|
|
env!("CARGO_MANIFEST_DIR"),
|
|
"/tests/fixtures/llm_traces/coverage/status_events_tool_chain.json"
|
|
))
|
|
.expect("failed to load status_events_tool_chain.json");
|
|
|
|
let rig = TestRigBuilder::new()
|
|
.with_trace(trace.clone())
|
|
.build()
|
|
.await;
|
|
|
|
rig.send_message("Run the tool chain").await;
|
|
let responses = rig.wait_for_responses(1, Duration::from_secs(15)).await;
|
|
|
|
// Declarative expects from fixture (tools_used, all_tools_succeeded, min_responses).
|
|
rig.verify_trace_expects(&trace, &responses);
|
|
|
|
// Extra: event ordering checks (not expressible as expects).
|
|
let events = rig.captured_status_events();
|
|
let tool_events: Vec<&StatusUpdate> = events
|
|
.iter()
|
|
.filter(|e| {
|
|
matches!(
|
|
e,
|
|
StatusUpdate::ToolStarted { .. } | StatusUpdate::ToolCompleted { .. }
|
|
)
|
|
})
|
|
.collect();
|
|
|
|
let starts: Vec<&str> = tool_events
|
|
.iter()
|
|
.filter_map(|e| match e {
|
|
StatusUpdate::ToolStarted { name, .. } => Some(name.as_str()),
|
|
_ => None,
|
|
})
|
|
.collect();
|
|
let completions: Vec<&str> = tool_events
|
|
.iter()
|
|
.filter_map(|e| match e {
|
|
StatusUpdate::ToolCompleted { name, .. } => Some(name.as_str()),
|
|
_ => None,
|
|
})
|
|
.collect();
|
|
|
|
assert!(
|
|
starts.len() >= 3,
|
|
"Expected >= 3 ToolStarted events, got {}: {:?}",
|
|
starts.len(),
|
|
starts
|
|
);
|
|
assert_eq!(
|
|
starts.len(),
|
|
completions.len(),
|
|
"ToolStarted count ({}) != ToolCompleted count ({})",
|
|
starts.len(),
|
|
completions.len()
|
|
);
|
|
|
|
// Verify ordering: for each ToolCompleted, a ToolStarted for the same
|
|
// tool name must appear earlier in the event list.
|
|
let mut pending_starts: Vec<String> = Vec::new();
|
|
for event in &tool_events {
|
|
match event {
|
|
StatusUpdate::ToolStarted { name, .. } => {
|
|
pending_starts.push(name.clone());
|
|
}
|
|
StatusUpdate::ToolCompleted { name, .. } => {
|
|
let pos = pending_starts.iter().rposition(|n| n == name);
|
|
assert!(
|
|
pos.is_some(),
|
|
"ToolCompleted for '{name}' without preceding ToolStarted. \
|
|
Pending starts: {pending_starts:?}"
|
|
);
|
|
pending_starts.remove(pos.unwrap());
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
assert!(
|
|
pending_starts.is_empty(),
|
|
"ToolStarted without matching ToolCompleted: {pending_starts:?}"
|
|
);
|
|
|
|
// Extra: metrics checks.
|
|
let metrics = rig.collect_metrics().await;
|
|
assert!(
|
|
metrics.llm_calls >= 4,
|
|
"Expected >= 4 LLM calls, got {}",
|
|
metrics.llm_calls
|
|
);
|
|
assert!(
|
|
metrics.total_tool_calls() >= 3,
|
|
"Expected >= 3 tool invocations in metrics"
|
|
);
|
|
|
|
rig.shutdown();
|
|
}
|
|
|
|
/// Verify that Thinking events are emitted during agent processing.
|
|
#[tokio::test]
|
|
async fn test_thinking_events_captured() {
|
|
let trace = LlmTrace::from_file(concat!(
|
|
env!("CARGO_MANIFEST_DIR"),
|
|
"/tests/fixtures/llm_traces/simple_text.json"
|
|
))
|
|
.expect("failed to load simple_text.json");
|
|
|
|
let rig = TestRigBuilder::new().with_trace(trace).build().await;
|
|
|
|
rig.send_message("hello").await;
|
|
let _responses = rig.wait_for_responses(1, Duration::from_secs(10)).await;
|
|
|
|
let events = rig.captured_status_events();
|
|
|
|
let has_processing_event = events
|
|
.iter()
|
|
.any(|e| matches!(e, StatusUpdate::Thinking(_) | StatusUpdate::Status(_)));
|
|
|
|
if !has_processing_event {
|
|
eprintln!(
|
|
"[INFO] No Thinking/Status events captured. \
|
|
Agent may not emit these for simple text responses. \
|
|
Captured events: {:?}",
|
|
events
|
|
);
|
|
}
|
|
|
|
rig.shutdown();
|
|
}
|
|
}
|