Files
ironclaw/tests/e2e_status_events.rs
firat.sertgoz 482ee57c5f feat(tui): port full-featured Ratatui terminal UI onto staging (#1973)
* 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>
2026-04-08 23:23:39 +09:00

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();
}
}