docs(comments): rewrite comments across all crates to the guidelines
Sweep every first-party crate source (1956 .rs files) to the project comment guidelines: delete redundant restatements, decorative banners, change narration, and end-of-line comments; keep and tighten the crucial ones (invariants, bug rationale, SAFETY blocks, ported-source attribution). No functional code changed. Every edit is proven comment-only against the prior tree by a comment-stripping lexer (string/char/raw-string aware) plus a separate doctest-fence check. Where removing a comment made rustfmt or clippy want to re-lay-out adjacent code, the minimal triggering comment is restored so code tokens stay byte-identical. Gates green: cargo fmt --all --check (0 diffs), cargo check and cargo clippy --workspace --all-targets (0 warnings). Adds scripts/check_codegen_comment_guidelines.py — the enforcement gate for these guidelines (flags banners, end-of-line comments, change narration, and commented-out code).
This commit is contained in:
@@ -6,8 +6,7 @@
|
||||
/// background `monitor`/bash task (`TaskBackgrounded`) must restore into
|
||||
/// `bg_tasks` on a resumed / second terminal — not be dropped by the
|
||||
/// default match arm — so the idle "watching" status line and the Tasks pane
|
||||
/// match the originating terminal. (Before this routing only subagents
|
||||
/// survived resume.)
|
||||
/// match the originating terminal.
|
||||
#[test]
|
||||
fn ext_session_update_replay_restores_bg_task() {
|
||||
let mut app = make_app_with_agent("sess-1");
|
||||
@@ -129,7 +128,6 @@
|
||||
setup_pending_execute_tool(&mut app, tc_id);
|
||||
send_late_bg_detection(&mut app, tc_id);
|
||||
|
||||
// Tool should be in BOTH pending_tools and bg_deferred_tools
|
||||
let agent = app.agents.get(&AgentId(0)).unwrap();
|
||||
assert!(agent.session.tracker.pending_tool_entry_id(tc_id).is_some());
|
||||
assert!(agent.session.tracker.bg_deferred_tools.contains_key(tc_id));
|
||||
@@ -260,14 +258,12 @@
|
||||
assert!(changed);
|
||||
|
||||
let agent = app.agents.get(&AgentId(0)).unwrap();
|
||||
// Parent scrollback must NOT have the bg task block.
|
||||
assert_eq!(agent.scrollback.len(), 0, "parent scrollback must be empty");
|
||||
assert!(
|
||||
agent.session.bg_tasks.is_empty(),
|
||||
"parent session must not have the bg task"
|
||||
);
|
||||
|
||||
// Child view must have the bg task.
|
||||
let child = agent.subagent_views.get("child-sess").unwrap();
|
||||
assert_eq!(child.scrollback.len(), 1);
|
||||
assert!(child.session.bg_tasks.contains_key("task-child-1"));
|
||||
@@ -403,25 +399,21 @@
|
||||
fn task_completed_routes_to_child_session() {
|
||||
let mut app = make_app_with_parent_and_child("parent-sess", "child-sess");
|
||||
|
||||
// First, background a task on the child.
|
||||
let bg_notif =
|
||||
make_task_backgrounded_notif("child-sess", "tc-child-2", "task-child-2", "echo hi");
|
||||
handle_task_backgrounded(&bg_notif, &mut app);
|
||||
|
||||
// Now complete it.
|
||||
let notif = make_task_completed_notif("child-sess", "task-child-2", "echo hi", Some(0));
|
||||
let changed = handle_task_completed(¬if, &mut app);
|
||||
assert!(changed);
|
||||
|
||||
let agent = app.agents.get(&AgentId(0)).unwrap();
|
||||
// Parent must NOT have a completion block.
|
||||
assert_eq!(
|
||||
agent.scrollback.len(),
|
||||
0,
|
||||
"parent scrollback must not have completion block"
|
||||
);
|
||||
|
||||
// Child must have both the started and completed blocks.
|
||||
let child = agent.subagent_views.get("child-sess").unwrap();
|
||||
assert_eq!(child.scrollback.len(), 2, "child: started + completed");
|
||||
let bg = child.session.bg_tasks.get("task-child-2").unwrap();
|
||||
@@ -432,7 +424,6 @@
|
||||
fn task_completed_root_still_routes_to_parent() {
|
||||
let mut app = make_app_with_parent_and_child("parent-sess", "child-sess");
|
||||
|
||||
// Background and complete a task on the parent.
|
||||
let bg_notif =
|
||||
make_task_backgrounded_notif("parent-sess", "tc-root-2", "task-root-2", "echo root");
|
||||
handle_task_backgrounded(&bg_notif, &mut app);
|
||||
@@ -510,7 +501,6 @@
|
||||
fn monitor_event_root_still_routes_to_parent() {
|
||||
let mut app = make_app_with_parent_and_child("parent-sess", "child-sess");
|
||||
|
||||
// Background a task on the parent.
|
||||
let bg_notif =
|
||||
make_task_backgrounded_notif("parent-sess", "tc-root-3", "task-root-3", "tail -f");
|
||||
handle_task_backgrounded(&bg_notif, &mut app);
|
||||
@@ -560,7 +550,6 @@
|
||||
fn task_completed_child_inactive_returns_false_but_mutates_state() {
|
||||
let mut app = make_app_with_parent_and_child("parent-sess", "child-sess");
|
||||
|
||||
// Background a task on the child first.
|
||||
let bg_notif = make_task_backgrounded_notif(
|
||||
"child-sess",
|
||||
"tc-compl-inact",
|
||||
@@ -569,7 +558,6 @@
|
||||
);
|
||||
handle_task_backgrounded(&bg_notif, &mut app);
|
||||
|
||||
// Now switch away.
|
||||
let other = make_agent(Some("other-sess"));
|
||||
app.agents.insert(AgentId(1), other);
|
||||
crate::app::dispatch::switch_to_agent(
|
||||
|
||||
@@ -30,7 +30,6 @@
|
||||
.session
|
||||
.current_prompt_id = Some("p1".into());
|
||||
|
||||
// Active turn (p1) chips applied via the wire.
|
||||
assert!(handle_ext_notification(
|
||||
&follow_ups_ext_with_prompt("resp-1", "p1", &["a"]),
|
||||
&mut app
|
||||
@@ -39,7 +38,6 @@
|
||||
app.agents.get_mut(&AgentId(0)).unwrap().clear_follow_ups();
|
||||
assert!(app.agents[&AgentId(0)].follow_ups.is_none());
|
||||
|
||||
// (a) Re-delivery of the active turn re-renders.
|
||||
assert!(
|
||||
handle_ext_notification(
|
||||
&follow_ups_ext_with_prompt("resp-1", "p1", &["a"]),
|
||||
@@ -56,7 +54,6 @@
|
||||
"resp-1"
|
||||
);
|
||||
|
||||
// Adopt a new turn p2; clear.
|
||||
app.agents
|
||||
.get_mut(&AgentId(0))
|
||||
.unwrap()
|
||||
@@ -64,7 +61,6 @@
|
||||
.current_prompt_id = Some("p2".into());
|
||||
app.agents.get_mut(&AgentId(0)).unwrap().clear_follow_ups();
|
||||
|
||||
// (b) Prior turn (p1) replay must NOT revive.
|
||||
assert!(
|
||||
!handle_ext_notification(
|
||||
&follow_ups_ext_with_prompt("resp-1", "p1", &["a"]),
|
||||
@@ -199,7 +195,6 @@
|
||||
let affected = handle_ext_notification(&follow_ups_ext(&big, &["x"]), &mut app);
|
||||
assert!(!affected, "an oversized response_id must be rejected");
|
||||
assert!(app.agents[&AgentId(0)].follow_ups.is_none());
|
||||
// A sane-length id still works.
|
||||
let ok = "r".repeat(super::MAX_RESPONSE_ID_LEN);
|
||||
assert!(handle_ext_notification(
|
||||
&follow_ups_ext(&ok, &["x"]),
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
#![cfg_attr(rustfmt, rustfmt::skip)]
|
||||
use super::*;
|
||||
|
||||
// ── derive_child_cwd ─────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn derive_child_cwd_uses_child_cwd_from_info() {
|
||||
let parent_cwd = PathBuf::from("/parent/cwd");
|
||||
@@ -89,7 +87,6 @@
|
||||
);
|
||||
assert!(child_view.is_worktree);
|
||||
assert_eq!(child_view.main_repo.as_deref(), Some("main-repo"));
|
||||
// Parent must not be affected.
|
||||
assert!(parent.current_branch.is_none());
|
||||
assert!(!parent.is_worktree);
|
||||
}
|
||||
|
||||
@@ -45,11 +45,6 @@
|
||||
"last_event": "verify_started",
|
||||
"last_event_detail": "round 2 of 3",
|
||||
"last_event_timestamp": "2026-05-24T00:00:00Z",
|
||||
// Field absent on today's `SessionUpdate::GoalUpdated` — simulates
|
||||
// a future shell adding a new wire field. With trailing `..` in
|
||||
// the destructure and no `deny_unknown_fields` on the variant,
|
||||
// this must parse and the pager must still produce a
|
||||
// GoalDisplayState mapped from the known subset.
|
||||
"future_field_for_pr5": "ignored-by-todays-pager"
|
||||
}
|
||||
});
|
||||
@@ -107,7 +102,6 @@
|
||||
Some("2026-05-24T00:00:00Z")
|
||||
);
|
||||
assert_eq!(goal.pause_message, None);
|
||||
// Classifier fields default to `None` / `false` when absent.
|
||||
assert_eq!(goal.classifier_runs_attempted, None);
|
||||
assert_eq!(goal.classifier_max_runs, None);
|
||||
assert_eq!(goal.last_classifier_verdict, None);
|
||||
@@ -177,8 +171,6 @@
|
||||
"transition to Complete pushes one e2e marker with the goal's total time",
|
||||
);
|
||||
|
||||
// A repeat Complete update (e.g. a late notification) must not
|
||||
// duplicate the marker.
|
||||
send(&mut app, "complete", 620_000);
|
||||
assert_eq!(
|
||||
goal_markers(&app).len(),
|
||||
@@ -238,7 +230,6 @@
|
||||
"chip cleared on cleared status"
|
||||
);
|
||||
|
||||
// A stale late update for the cleared goal must not resurrect it.
|
||||
let affected = send_goal_update(&mut app, "g1", "complete", 5_000);
|
||||
assert!(
|
||||
app.agents.get(&AgentId(0)).unwrap().goal_state.is_none(),
|
||||
@@ -268,7 +259,6 @@
|
||||
// the prior goal's carried elapsed floor.
|
||||
let mut app = make_app_with_agent("sess-A");
|
||||
send_goal_update(&mut app, "g1", "active", 10_000);
|
||||
// Switch directly to a different goal with a small elapsed base.
|
||||
send_goal_update(&mut app, "g2", "active", 500);
|
||||
let elapsed = app
|
||||
.agents
|
||||
@@ -290,7 +280,6 @@
|
||||
// on receipt into the cached bool (no per-frame stat).
|
||||
let mut app = make_app_with_agent("sess-A");
|
||||
|
||||
// A real on-disk path → cached exists = true.
|
||||
let f = tempfile::NamedTempFile::new().unwrap();
|
||||
let real_path = f.path().to_string_lossy().into_owned();
|
||||
let mut update = goal_update_value("g1", "active", 0);
|
||||
@@ -312,7 +301,6 @@
|
||||
Some(real_path.as_str())
|
||||
);
|
||||
|
||||
// A missing path → cached exists = false (modal renders "(unavailable)").
|
||||
let mut update = goal_update_value("g1", "active", 0);
|
||||
update["last_classifier_details_path"] = serde_json::json!("/no/such/details-xyz.md");
|
||||
dispatch_goal_update(&mut app, update);
|
||||
@@ -336,9 +324,7 @@
|
||||
// be omitted from the wire payload and must surface as `None` in
|
||||
// the destructured arm — i.e. the pager keeps mapping the known
|
||||
// subset cleanly when the shell-side struct grows or when an
|
||||
// older shell omits newer optional fields. Drop a handful of
|
||||
// optional keys from the payload and assert they materialise as
|
||||
// `None` on the resulting `GoalDisplayState`.
|
||||
// older shell omits newer optional fields.
|
||||
let mut app = make_app_with_agent("sess-A");
|
||||
|
||||
let raw_payload = serde_json::json!({
|
||||
@@ -349,26 +335,14 @@
|
||||
"objective": "minimal payload",
|
||||
"status": "active",
|
||||
"phase": "idle",
|
||||
// token_budget omitted — Option<i64> must default to None.
|
||||
"tokens_used": 0,
|
||||
"elapsed_ms": 0,
|
||||
"total_deliverables": 0,
|
||||
"completed_deliverables": 0,
|
||||
// current_deliverable_idx omitted — Option<u32> -> None.
|
||||
// current_deliverable_title omitted — Option<String> -> None.
|
||||
// current_subagent_role omitted — Option<String> -> None.
|
||||
"total_worker_rounds": 0,
|
||||
"total_verify_rounds": 0,
|
||||
"token_baseline": 0,
|
||||
"finished_subagent_tokens": 0,
|
||||
// live_subagent_tokens omitted — Option<u64> -> None.
|
||||
// live_context_pct omitted — Option<u8> -> None.
|
||||
// live_turn_count omitted — Option<u32> -> None.
|
||||
// live_tool_call_count omitted — Option<u32> -> None.
|
||||
// last_event omitted — Option<String> -> None.
|
||||
// last_event_detail omitted — Option<String> -> None.
|
||||
// last_event_timestamp omitted — Option<String> -> None.
|
||||
// pause_message omitted — Option<String> -> None.
|
||||
}
|
||||
});
|
||||
let raw = serde_json::value::to_raw_value(&raw_payload).unwrap();
|
||||
@@ -391,7 +365,6 @@
|
||||
.as_ref()
|
||||
.expect("GoalUpdated must populate goal_state even with all Option fields omitted");
|
||||
|
||||
// Required fields landed as sent.
|
||||
assert_eq!(goal.goal_id, "g-min");
|
||||
assert_eq!(goal.objective, "minimal payload");
|
||||
assert_eq!(goal.status, GoalDisplayStatus::Active);
|
||||
@@ -405,9 +378,6 @@
|
||||
assert_eq!(goal.token_baseline, 0);
|
||||
assert_eq!(goal.finished_subagent_tokens, 0);
|
||||
|
||||
// Every omitted Option<T> wire field must surface as None — this
|
||||
// is the property that keeps the destructure stable as the shell
|
||||
// grows additive optional fields.
|
||||
assert_eq!(goal.token_budget, None, "token_budget");
|
||||
assert_eq!(goal.current_deliverable_id, None, "current_deliverable_id");
|
||||
assert_eq!(
|
||||
|
||||
@@ -75,9 +75,8 @@
|
||||
|
||||
#[test]
|
||||
fn permission_for_inactive_agent_queues_on_owning_agent() {
|
||||
// The headline behavior change in handle_permission_request:
|
||||
// permissions for an inactive owning agent now QUEUE (not cancel)
|
||||
// so the user sees them on switching back.
|
||||
// A permission for an inactive owning agent queues on that agent (rather
|
||||
// than being cancelled), so the user sees it on switching back.
|
||||
let mut app = make_app_with_agent("sess-A");
|
||||
insert_agent(&mut app, AgentId(1), Some("sess-B"));
|
||||
switch_active_to(&mut app, AgentId(1));
|
||||
@@ -101,8 +100,6 @@
|
||||
!affected,
|
||||
"permission queued on a non-active agent must not request a redraw"
|
||||
);
|
||||
// Permission is still pending; the response_tx must still be alive
|
||||
// (no auto-cancel was sent).
|
||||
assert!(
|
||||
rx.try_recv().is_err(),
|
||||
"permission must NOT have been answered yet (queued, not cancelled)"
|
||||
@@ -255,8 +252,6 @@
|
||||
);
|
||||
}
|
||||
|
||||
// ── Plan approval persistence tests ─────────────────────────
|
||||
|
||||
#[test]
|
||||
fn close_viewer_preserves_plan_approval_state() {
|
||||
let mut app = make_app_with_agent("sess-A");
|
||||
@@ -279,11 +274,9 @@
|
||||
let agent = app.agents.get(&AgentId(0)).unwrap();
|
||||
assert!(agent.plan_approval_view.is_some(), "approval should be set");
|
||||
|
||||
// Close the viewer (simulates Esc / close button).
|
||||
let agent = app.agents.get_mut(&AgentId(0)).unwrap();
|
||||
agent.cancel_line_viewer();
|
||||
|
||||
// Approval state must survive the close.
|
||||
let agent = app.agents.get(&AgentId(0)).unwrap();
|
||||
assert!(
|
||||
agent.plan_approval_view.is_some(),
|
||||
@@ -291,7 +284,6 @@
|
||||
);
|
||||
assert!(agent.line_viewer.is_none(), "viewer should be closed");
|
||||
|
||||
// Response must NOT have been sent (still waiting for user).
|
||||
assert!(
|
||||
rx.try_recv().is_err(),
|
||||
"response must not be sent on viewer close"
|
||||
@@ -323,12 +315,10 @@
|
||||
&mut app,
|
||||
);
|
||||
|
||||
// Close viewer.
|
||||
let agent = app.agents.get_mut(&AgentId(0)).unwrap();
|
||||
agent.cancel_line_viewer();
|
||||
assert!(agent.line_viewer.is_none());
|
||||
|
||||
// Reopen plan preview — inline content is in plan_approval_view.plan_content.
|
||||
agent.show_plan_preview();
|
||||
|
||||
assert!(agent.line_viewer.is_some(), "viewer should reopen");
|
||||
@@ -361,11 +351,9 @@
|
||||
&mut app,
|
||||
);
|
||||
|
||||
// Close viewer.
|
||||
let agent = app.agents.get_mut(&AgentId(0)).unwrap();
|
||||
agent.cancel_line_viewer();
|
||||
|
||||
// User types new text in the prompt while viewer is closed.
|
||||
agent.prompt.set_text("my new prompt text");
|
||||
|
||||
agent.reopen_plan_approval();
|
||||
@@ -378,7 +366,6 @@
|
||||
"stashed prompt should be restored after reopen + approve"
|
||||
);
|
||||
|
||||
// Response should be approved.
|
||||
let response = rx.blocking_recv().expect("should have sent response");
|
||||
let raw = response.expect("should be Ok");
|
||||
let parsed: serde_json::Value = serde_json::from_str(raw.0.get()).unwrap();
|
||||
|
||||
@@ -64,7 +64,6 @@
|
||||
assert_eq!(count_parked(agent), 0, "no marker on screen");
|
||||
}
|
||||
|
||||
// A task completing in the still-parked window must stay silent.
|
||||
handle_ext_notification(
|
||||
&make_task_completed_notif("sess-park", "t10", "sleep 10", Some(0)),
|
||||
&mut app,
|
||||
@@ -101,7 +100,6 @@
|
||||
assert!(agent.renders_parked());
|
||||
}
|
||||
|
||||
// sleep 10 exits → full marker with "2 commands still running."
|
||||
handle_ext_notification(
|
||||
&make_task_completed_notif("sess-park", "t10", "sleep 10", Some(0)),
|
||||
&mut app,
|
||||
@@ -111,12 +109,10 @@
|
||||
&make_task_completed_notif("sess-park", "t10", "sleep 10", Some(0)),
|
||||
&mut app,
|
||||
);
|
||||
// sleep 15 exits → full marker with "1 command still running."
|
||||
handle_ext_notification(
|
||||
&make_task_completed_notif("sess-park", "t15", "sleep 15", Some(0)),
|
||||
&mut app,
|
||||
);
|
||||
// sleep 20 exits → nothing left; no "0 commands" line.
|
||||
handle_ext_notification(
|
||||
&make_task_completed_notif("sess-park", "t20", "sleep 20", Some(0)),
|
||||
&mut app,
|
||||
@@ -438,7 +434,6 @@
|
||||
),
|
||||
&mut app,
|
||||
);
|
||||
// Only the initial parked marker — no countdown re-push.
|
||||
let agent = app.agents.get_mut(&AgentId(0)).unwrap();
|
||||
assert_eq!(parked_marker_messages(agent).len(), 1);
|
||||
}
|
||||
@@ -465,8 +460,6 @@
|
||||
assert!(parked_marker_messages(agent).is_empty());
|
||||
}
|
||||
|
||||
// -- imminent waits do not park (awaited work already finished) ----------
|
||||
|
||||
/// Waiting on a task that already completed: no marker, slot stays free.
|
||||
#[test]
|
||||
fn wait_on_already_completed_task_pushes_no_parked_marker() {
|
||||
@@ -715,9 +708,9 @@
|
||||
|
||||
#[test]
|
||||
fn interjection_notification_pushes_block_to_matching_session() {
|
||||
// Multi-client fix: an interjection typed in one pane is broadcast by
|
||||
// the shell as kigi/session/interjection; EVERY attached pane (incl.
|
||||
// the originator, which no longer pushes a local block) renders it.
|
||||
// An interjection typed in one pane is broadcast by the shell as
|
||||
// kigi/session/interjection; every attached pane — including the
|
||||
// originator — renders it from the broadcast rather than a local push.
|
||||
let mut app = make_app_with_agent("sess-view");
|
||||
let affected =
|
||||
handle_ext_notification(&interjection_ext("sess-view", "also add tests"), &mut app);
|
||||
@@ -746,8 +739,6 @@
|
||||
|
||||
#[test]
|
||||
fn interjection_notification_renders_for_a_viewer() {
|
||||
// A viewer (attached_as_viewer) watching another client's session must
|
||||
// also render interjections broadcast for that session.
|
||||
let mut app = make_app_with_agent("sess-view");
|
||||
app.agents.get_mut(&AgentId(0)).unwrap().attached_as_viewer = true;
|
||||
let affected =
|
||||
|
||||
@@ -3,9 +3,6 @@
|
||||
|
||||
#[test]
|
||||
fn mcp_init_progress_updates_seeded_progress_in_place() {
|
||||
// When a session is seeded with mcp_init_progress{0,0}, a
|
||||
// subsequent init_progress notification must update total and
|
||||
// connected IN PLACE — preserving started_at for timer accuracy.
|
||||
let mut app = make_app_with_agent("sess-1");
|
||||
let agent = app.agents.get_mut(&AgentId(0)).unwrap();
|
||||
agent.mcp_init_progress = Some(crate::app::agent_view::McpInitProgress {
|
||||
@@ -66,7 +63,6 @@
|
||||
"owner is background — mutation must not request a redraw"
|
||||
);
|
||||
|
||||
// Owner mutated.
|
||||
let owner_modal = app
|
||||
.agents
|
||||
.get(&AgentId(0))
|
||||
@@ -81,7 +77,6 @@
|
||||
assert_eq!(owner_servers[0].tool_count, 2);
|
||||
assert_eq!(owner_servers[0].tools.len(), 2);
|
||||
|
||||
// Active agent's modal must be untouched.
|
||||
let active_modal = app
|
||||
.agents
|
||||
.get(&AgentId(1))
|
||||
@@ -101,8 +96,6 @@
|
||||
|
||||
#[test]
|
||||
fn mcp_init_progress_creates_when_none() {
|
||||
// When mcp_init_progress is None (no seed), init_progress
|
||||
// creates a fresh McpInitProgress.
|
||||
let mut app = make_app_with_agent("sess-1");
|
||||
assert!(app.agents[&AgentId(0)].mcp_init_progress.is_none());
|
||||
|
||||
@@ -117,7 +110,6 @@
|
||||
|
||||
#[test]
|
||||
fn mcp_initialized_clears_progress() {
|
||||
// kigi/mcp_initialized must set mcp_init_progress to None.
|
||||
let mut app = make_app_with_agent("sess-1");
|
||||
let agent = app.agents.get_mut(&AgentId(0)).unwrap();
|
||||
agent.mcp_init_progress = Some(crate::app::agent_view::McpInitProgress {
|
||||
@@ -137,9 +129,6 @@
|
||||
|
||||
#[test]
|
||||
fn mcp_full_lifecycle_seed_to_clear() {
|
||||
// Full N-server lifecycle:
|
||||
// seed(0/0) → init_progress(0/3) → init_progress(2/3)
|
||||
// → init_progress(3/3) → mcp_initialized → None
|
||||
let mut app = make_app_with_agent("sess-1");
|
||||
let agent = app.agents.get_mut(&AgentId(0)).unwrap();
|
||||
agent.mcp_init_progress = Some(crate::app::agent_view::McpInitProgress {
|
||||
@@ -148,22 +137,18 @@
|
||||
started_at: Instant::now(),
|
||||
});
|
||||
|
||||
// Shell reports real count.
|
||||
handle_ext_notification(&make_mcp_init_progress_notif(3, 0), &mut app);
|
||||
let p = app.agents[&AgentId(0)].mcp_init_progress.as_ref().unwrap();
|
||||
assert_eq!((p.total, p.connected), (3, 0));
|
||||
|
||||
// Incremental progress.
|
||||
handle_ext_notification(&make_mcp_init_progress_notif(3, 2), &mut app);
|
||||
let p = app.agents[&AgentId(0)].mcp_init_progress.as_ref().unwrap();
|
||||
assert_eq!((p.total, p.connected), (3, 2));
|
||||
|
||||
// All connected.
|
||||
handle_ext_notification(&make_mcp_init_progress_notif(3, 3), &mut app);
|
||||
let p = app.agents[&AgentId(0)].mcp_init_progress.as_ref().unwrap();
|
||||
assert_eq!((p.total, p.connected), (3, 3));
|
||||
|
||||
// mcp_initialized clears everything.
|
||||
handle_ext_notification(&make_mcp_initialized_notif("sess-1"), &mut app);
|
||||
assert!(
|
||||
app.agents[&AgentId(0)].mcp_init_progress.is_none(),
|
||||
@@ -173,10 +158,10 @@
|
||||
|
||||
#[test]
|
||||
fn mcp_zero_server_lifecycle() {
|
||||
// 0-server lifecycle (the bug scenario):
|
||||
// 0-server bug scenario: for 0 servers the shell must still emit
|
||||
// mcp_initialized; without that terminal event the progress
|
||||
// indicator sticks forever.
|
||||
// seed(0/0) → init_progress(0/0) → mcp_initialized → None
|
||||
// Previously mcp_initialized was never sent for 0 servers,
|
||||
// leaving a stuck progress indicator.
|
||||
let mut app = make_app_with_agent("sess-1");
|
||||
let agent = app.agents.get_mut(&AgentId(0)).unwrap();
|
||||
agent.mcp_init_progress = Some(crate::app::agent_view::McpInitProgress {
|
||||
@@ -185,12 +170,10 @@
|
||||
started_at: Instant::now(),
|
||||
});
|
||||
|
||||
// Shell sends 0/0 for the 0-server case.
|
||||
handle_ext_notification(&make_mcp_init_progress_notif(0, 0), &mut app);
|
||||
let p = app.agents[&AgentId(0)].mcp_init_progress.as_ref().unwrap();
|
||||
assert_eq!((p.total, p.connected), (0, 0));
|
||||
|
||||
// Shell now sends mcp_initialized (root-cause fix).
|
||||
handle_ext_notification(&make_mcp_initialized_notif("sess-1"), &mut app);
|
||||
assert!(
|
||||
app.agents[&AgentId(0)].mcp_init_progress.is_none(),
|
||||
@@ -200,9 +183,6 @@
|
||||
|
||||
#[test]
|
||||
fn mcp_init_progress_routes_to_background_session() {
|
||||
// init_progress carrying a background session's sessionId must update
|
||||
// *that* agent's indicator, not the foregrounded one, and must not
|
||||
// force a redraw (the background spinner isn't visible).
|
||||
let mut app = make_app_with_agent("sess-A");
|
||||
app.agents.insert(AgentId(1), make_agent(Some("sess-B")));
|
||||
|
||||
@@ -223,10 +203,9 @@
|
||||
|
||||
#[test]
|
||||
fn mcp_initialized_routes_to_background_session() {
|
||||
// mcp_initialized for a background session must clear *that* agent's
|
||||
// indicator while leaving the foreground agent's intact. Previously
|
||||
// the clear was applied to whichever agent was active, so a
|
||||
// background agent's spinner could stick forever.
|
||||
// mcp_initialized must clear the indicator of the agent named by
|
||||
// sessionId, not whichever agent is active — else a background
|
||||
// agent's spinner sticks forever.
|
||||
let mut app = make_app_with_agent("sess-A");
|
||||
app.agents.insert(AgentId(1), make_agent(Some("sess-B")));
|
||||
for id in [AgentId(0), AgentId(1)] {
|
||||
@@ -256,8 +235,6 @@
|
||||
|
||||
#[test]
|
||||
fn mcp_initialized_unknown_session_is_dropped() {
|
||||
// An mcp_initialized for a session that matches no agent must not
|
||||
// clear anyone's indicator (no misrouting to the active agent).
|
||||
let mut app = make_app_with_agent("sess-A");
|
||||
app.agents.get_mut(&AgentId(0)).unwrap().mcp_init_progress =
|
||||
Some(crate::app::agent_view::McpInitProgress {
|
||||
@@ -289,7 +266,6 @@
|
||||
connected: 1,
|
||||
started_at: Instant::now(),
|
||||
});
|
||||
// Register a subagent child view keyed by the child session id.
|
||||
app.agents
|
||||
.get_mut(&AgentId(0))
|
||||
.unwrap()
|
||||
@@ -299,7 +275,6 @@
|
||||
Box::new(make_agent(Some("child-sess"))),
|
||||
);
|
||||
|
||||
// init_progress for the child session must leave the parent untouched.
|
||||
let changed = handle_ext_notification(
|
||||
&make_mcp_init_progress_notif_for(5, 0, "child-sess"),
|
||||
&mut app,
|
||||
@@ -315,7 +290,6 @@
|
||||
"parent spinner must be untouched by a subagent's init",
|
||||
);
|
||||
|
||||
// mcp_initialized for the child session must not clear the parent.
|
||||
let changed =
|
||||
handle_ext_notification(&make_mcp_initialized_notif_for("child-sess"), &mut app);
|
||||
assert!(!changed);
|
||||
@@ -329,8 +303,6 @@
|
||||
fn server_status_handler_noop_when_modal_closed_background() {
|
||||
use kigi_shell::extensions::mcp::McpServerStatus;
|
||||
let mut app = make_app_two_agents();
|
||||
// Owner is background and has NO modal open. server_status
|
||||
// must be a silent no-op (no Effect scheduling, no redraw).
|
||||
let notif = make_server_status_notif("sess-owner", "alpha", McpServerStatus::Ready, None);
|
||||
let redraw = handle_mcp_server_status(¬if, &mut app);
|
||||
assert!(!redraw, "closed-modal cheap path must not request a redraw");
|
||||
@@ -355,8 +327,6 @@
|
||||
fn server_status_handler_noop_when_modal_closed_foreground() {
|
||||
use kigi_shell::extensions::mcp::McpServerStatus;
|
||||
let mut app = make_app_two_agents();
|
||||
// Foreground = agent 1 (sess-active). Send a push targeting
|
||||
// the foregrounded agent, no modal open.
|
||||
let notif = make_server_status_notif("sess-active", "alpha", McpServerStatus::Ready, None);
|
||||
let redraw = handle_mcp_server_status(¬if, &mut app);
|
||||
assert!(
|
||||
@@ -504,7 +474,6 @@
|
||||
#[test]
|
||||
fn servers_updated_broadcasts_to_every_agent_with_open_modal() {
|
||||
let mut app = make_app_two_agents();
|
||||
// Open modals on BOTH agents — broadcast must hit both.
|
||||
seed_owner_agent_with_open_modal(&mut app);
|
||||
{
|
||||
let active = app.agents.get_mut(&AgentId(1)).unwrap();
|
||||
@@ -552,13 +521,9 @@
|
||||
assert_eq!(targets, vec![0, 1]);
|
||||
}
|
||||
|
||||
/// Agents without an open modal must NOT receive a refetch (cheap
|
||||
/// path) even though they are eligible to receive the broadcast in
|
||||
/// principle.
|
||||
#[test]
|
||||
fn servers_updated_skips_agents_with_closed_modal() {
|
||||
let mut app = make_app_two_agents();
|
||||
// Only agent 1 (foregrounded) has a modal.
|
||||
{
|
||||
let active = app.agents.get_mut(&AgentId(1)).unwrap();
|
||||
active.extensions_modal = Some(make_mcps_modal_with_servers(Vec::new()));
|
||||
@@ -623,9 +588,6 @@
|
||||
fn mcp_initialized_clears_init_progress_on_owner() {
|
||||
use crate::app::agent_view::McpInitProgress;
|
||||
let mut app = make_app_two_agents();
|
||||
// Seed init progress on the OWNER (agent 0) and on the
|
||||
// active view (agent 1). The push must clear only the
|
||||
// owner's overlay.
|
||||
{
|
||||
let owner = app.agents.get_mut(&AgentId(0)).unwrap();
|
||||
owner.mcp_init_progress = Some(McpInitProgress {
|
||||
@@ -698,7 +660,6 @@
|
||||
#[test]
|
||||
fn tools_changed_pre_h2_falls_back_to_active_view() {
|
||||
let mut app = make_app_two_agents();
|
||||
// Active agent (agent 1) gets a modal; owner (agent 0) does not.
|
||||
{
|
||||
let active = app.agents.get_mut(&AgentId(1)).unwrap();
|
||||
active.extensions_modal = Some(make_mcps_modal_with_servers(Vec::new()));
|
||||
|
||||
@@ -122,7 +122,6 @@ pub(super) fn compressed_entry(
|
||||
compressed_height: 1018,
|
||||
}
|
||||
}
|
||||
/// Most recent `SessionEvent` pushed to the scrollback, if any.
|
||||
pub(super) fn last_session_event(sb: &ScrollbackState) -> Option<SessionEvent> {
|
||||
(0..sb.len())
|
||||
.rev()
|
||||
@@ -160,7 +159,6 @@ pub(super) fn interjection_broadcast(
|
||||
),
|
||||
)
|
||||
}
|
||||
/// A Running background task registered on the agent's root session.
|
||||
pub(super) fn insert_running_task(agent: &mut AgentView, task_id: &str, command: &str) {
|
||||
agent
|
||||
.session
|
||||
@@ -363,7 +361,6 @@ pub(super) fn queue_changed_ext(session_id: &str, ids: &[&str]) -> acp::ExtNotif
|
||||
std::sync::Arc::from(serde_json::value::to_raw_value(¶ms).unwrap()),
|
||||
)
|
||||
}
|
||||
/// Build a `kigi/queue/changed` notification carrying `runningPromptId`.
|
||||
pub(super) fn queue_changed_running(
|
||||
session_id: &str,
|
||||
ids: &[&str],
|
||||
@@ -407,7 +404,6 @@ pub(super) fn app_with_running_p1_and_stashed_b1() -> AppView {
|
||||
assert!(app.pending_running_adoptions.contains_key(& AgentId(0)));
|
||||
app
|
||||
}
|
||||
/// Drive a live Execute tool_call `session/update` through the full handler.
|
||||
pub(super) fn send_tool_call_update(
|
||||
app: &mut AppView,
|
||||
prompt_id: &str,
|
||||
@@ -438,7 +434,6 @@ pub(super) fn send_tool_call_update(
|
||||
app,
|
||||
);
|
||||
}
|
||||
/// Dispatch an `Ok(EndTurn)` PromptResponse for `prompt_id`.
|
||||
pub(super) fn prompt_response(app: &mut AppView, prompt_id: &str) {
|
||||
use crate::app::actions::{Action, TaskResult};
|
||||
crate::app::dispatch::dispatch(
|
||||
@@ -567,7 +562,6 @@ pub(super) fn make_token_notification_message(
|
||||
})
|
||||
}
|
||||
use crate::scrollback::block::RenderBlock;
|
||||
/// Build an `AgentMessageChunk` notification carrying `text` for `session_id`.
|
||||
pub(super) fn make_agent_chunk_message(
|
||||
session_id: &str,
|
||||
text: &str,
|
||||
@@ -584,7 +578,6 @@ pub(super) fn make_agent_chunk_message(
|
||||
response_tx: tx,
|
||||
})
|
||||
}
|
||||
/// `AgentMessageChunk` with `promptId`/`isReplay` + optional `eventId`.
|
||||
pub(super) fn make_agent_chunk_meta(
|
||||
session_id: &str,
|
||||
text: &str,
|
||||
@@ -613,7 +606,6 @@ pub(super) fn make_agent_chunk_meta(
|
||||
response_tx: tx,
|
||||
})
|
||||
}
|
||||
/// `promptId`-tagged chunk (no `eventId`) — drives the viewer live-delta path.
|
||||
pub(super) fn make_agent_chunk_message_with_prompt(
|
||||
session_id: &str,
|
||||
text: &str,
|
||||
@@ -622,7 +614,6 @@ pub(super) fn make_agent_chunk_message_with_prompt(
|
||||
) -> AcpClientMessage {
|
||||
make_agent_chunk_meta(session_id, text, prompt_id, None, is_replay)
|
||||
}
|
||||
/// Live (`isReplay=false`) chunk with an optional `eventId`, for dedup tests.
|
||||
pub(super) fn make_agent_chunk_with_event(
|
||||
session_id: &str,
|
||||
text: &str,
|
||||
@@ -631,7 +622,6 @@ pub(super) fn make_agent_chunk_with_event(
|
||||
) -> AcpClientMessage {
|
||||
make_agent_chunk_meta(session_id, text, prompt_id, event_id, false)
|
||||
}
|
||||
/// Replay-marked chunk with an eventId, as `session/load` emits.
|
||||
pub(super) fn replay_chunk(
|
||||
session_id: &str,
|
||||
text: &str,
|
||||
@@ -650,7 +640,6 @@ pub(super) fn scrollback_has_system_text(agent: &mut AgentView, needle: &str) ->
|
||||
)
|
||||
})
|
||||
}
|
||||
/// `Plan` update message with the given entry contents.
|
||||
pub(super) fn plan_update_msg(
|
||||
session_id: &str,
|
||||
entries: &[&str],
|
||||
@@ -715,8 +704,6 @@ pub(super) fn xai_unhandled_notif(
|
||||
std::sync::Arc::from(serde_json::value::to_raw_value(&payload).unwrap()),
|
||||
)
|
||||
}
|
||||
/// Build an `agent_message_chunk` notification carrying both `totalTokens`
|
||||
/// and an explicit `eventId`, for context/dedup interaction tests.
|
||||
pub(super) fn make_token_notification_with_event(
|
||||
session_id: &str,
|
||||
total_tokens: u64,
|
||||
@@ -741,7 +728,6 @@ pub(super) fn make_token_notification_with_event(
|
||||
response_tx: tx,
|
||||
})
|
||||
}
|
||||
/// Build an `kigi/session/prompt_complete` ext-notification for `session_id`.
|
||||
pub(super) fn prompt_complete_ext(session_id: &str) -> acp::ExtNotification {
|
||||
let raw = serde_json::value::to_raw_value(
|
||||
&serde_json::json!({ "sessionId" : session_id, "stopReason" : "end_turn", }),
|
||||
@@ -749,12 +735,9 @@ pub(super) fn prompt_complete_ext(session_id: &str) -> acp::ExtNotification {
|
||||
.unwrap();
|
||||
acp::ExtNotification::new("kigi/session/prompt_complete", std::sync::Arc::from(raw))
|
||||
}
|
||||
/// Insert a fresh agent at `id` with an optional pre-assigned session id.
|
||||
pub(super) fn insert_agent(app: &mut AppView, id: AgentId, session_id: Option<&str>) {
|
||||
app.agents.insert(id, make_agent(session_id));
|
||||
}
|
||||
/// Build an `kigi/session/prompt_complete` ext-notification with an explicit
|
||||
/// `stopReason` and optional `agentResult`.
|
||||
pub(super) fn prompt_complete_ext_with_reason(
|
||||
session_id: &str,
|
||||
stop_reason: &str,
|
||||
@@ -872,7 +855,6 @@ pub(super) fn xai_wake_turn_completed_notif(
|
||||
std::sync::Arc::from(serde_json::value::to_raw_value(&payload).unwrap()),
|
||||
)
|
||||
}
|
||||
/// The newest turn-marker block on the agent's scrollback.
|
||||
pub(super) fn last_marker_block(
|
||||
sb: &ScrollbackState,
|
||||
) -> &crate::scrollback::blocks::SessionEventBlock {
|
||||
@@ -884,8 +866,6 @@ pub(super) fn last_marker_block(
|
||||
})
|
||||
.expect("a turn-end marker must exist")
|
||||
}
|
||||
/// Build a `HookExecution` update (one successful run) on the
|
||||
/// `kigi/session/update` rail, optionally stamped `isReplay`.
|
||||
/// `prompt_id == None` models pre-attribution shells.
|
||||
pub(super) fn xai_hook_execution_notif_for_prompt(
|
||||
session_id: &str,
|
||||
@@ -932,7 +912,6 @@ pub(super) fn count_lifecycle_blocks(
|
||||
})
|
||||
.count()
|
||||
}
|
||||
/// Stop-hook groups on the last turn-terminal session-event marker, if any.
|
||||
pub(super) fn last_marker_stop_hook_groups(
|
||||
sb: &crate::scrollback::state::ScrollbackState,
|
||||
) -> Option<usize> {
|
||||
@@ -945,7 +924,6 @@ pub(super) fn last_marker_stop_hook_groups(
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
/// Work-only status lines ("N … still running") pushed as system rows.
|
||||
pub(super) fn work_status_lines(sb: &ScrollbackState) -> Vec<String> {
|
||||
(0..sb.len())
|
||||
.filter_map(|i| match sb.get(i).map(|e| &e.block) {
|
||||
@@ -970,12 +948,10 @@ pub(super) fn seed_two_bg_tasks_and_announce(app: &mut AppView, session_id: &str
|
||||
);
|
||||
app.agents.get_mut(&AgentId(0)).unwrap().end_work_announced = true;
|
||||
}
|
||||
/// Build an `kigi/session/interjection` ext-notification (no id).
|
||||
pub(super) fn interjection_ext(session_id: &str, text: &str) -> acp::ExtNotification {
|
||||
interjection_ext_with_id(session_id, text, None)
|
||||
}
|
||||
/// Build an `kigi/session/interjection` ext-notification with an optional
|
||||
/// `interjectionId` (the originator-dedup key).
|
||||
/// `interjectionId` is the originator-dedup key.
|
||||
pub(super) fn interjection_ext_with_id(
|
||||
session_id: &str,
|
||||
text: &str,
|
||||
@@ -988,7 +964,6 @@ pub(super) fn interjection_ext_with_id(
|
||||
let raw = serde_json::value::to_raw_value(&payload).unwrap();
|
||||
acp::ExtNotification::new("kigi/session/interjection", std::sync::Arc::from(raw))
|
||||
}
|
||||
/// Text of the most recent user prompt block in scrollback, if any.
|
||||
/// Interjections render as standard user prompt blocks.
|
||||
pub(super) fn last_interjection_text(sb: &ScrollbackState) -> Option<String> {
|
||||
(0..sb.len())
|
||||
@@ -1008,7 +983,6 @@ pub(super) fn switch_active_to(app: &mut AppView, id: AgentId) {
|
||||
crate::app::dispatch::SwitchCause::Picker,
|
||||
);
|
||||
}
|
||||
/// Concatenate the text of every `AgentMessage` block in this view's scrollback.
|
||||
pub(super) fn agent_message_text(view: &AgentView) -> String {
|
||||
let mut out = String::new();
|
||||
for i in 0..view.scrollback.len() {
|
||||
@@ -1020,7 +994,6 @@ pub(super) fn agent_message_text(view: &AgentView) -> String {
|
||||
}
|
||||
out
|
||||
}
|
||||
/// Build a `Plan` notification with one entry per `entries` string.
|
||||
pub(super) fn make_plan_message(session_id: &str, entries: &[&str]) -> AcpClientMessage {
|
||||
let (tx, _rx) = tokio::sync::oneshot::channel();
|
||||
let plan_entries = entries
|
||||
@@ -1040,7 +1013,6 @@ pub(super) fn make_plan_message(session_id: &str, entries: &[&str]) -> AcpClient
|
||||
response_tx: tx,
|
||||
})
|
||||
}
|
||||
/// Build an `AvailableCommandsUpdate` notification with the given command names.
|
||||
pub(super) fn make_commands_update_message(
|
||||
session_id: &str,
|
||||
names: &[&str],
|
||||
@@ -1061,8 +1033,6 @@ pub(super) fn make_commands_update_message(
|
||||
response_tx: tx,
|
||||
})
|
||||
}
|
||||
/// Build a `ToolCallUpdate` notification carrying a Bash `raw_output`
|
||||
/// chunk for `tool_call_id`. Used to drive the bg-task stdout route.
|
||||
pub(super) fn make_bash_stdout_message(
|
||||
session_id: &str,
|
||||
tool_call_id: &str,
|
||||
@@ -1090,7 +1060,6 @@ pub(super) fn make_bash_stdout_message(
|
||||
response_tx: tx,
|
||||
})
|
||||
}
|
||||
/// Build an `ExtNotification` envelope for `kigi/session_notification`.
|
||||
pub(super) fn make_ext_session_notification(
|
||||
session_id: &str,
|
||||
update: XaiSessionUpdate,
|
||||
@@ -1101,7 +1070,6 @@ pub(super) fn make_ext_session_notification(
|
||||
update,
|
||||
)
|
||||
}
|
||||
/// Build an `ExtNotification` envelope with an explicit xAI session method.
|
||||
pub(super) fn make_ext_session_notification_with_method(
|
||||
session_id: &str,
|
||||
method: &str,
|
||||
@@ -1179,7 +1147,6 @@ pub(super) fn test_subagent_progress(
|
||||
error_count: 0,
|
||||
}
|
||||
}
|
||||
/// Snapshot of subagent state after SubagentSpawned for method-parity tests.
|
||||
pub(super) struct SubagentSpawnSnapshot {
|
||||
description: String,
|
||||
subagent_type: String,
|
||||
@@ -1210,7 +1177,6 @@ pub(super) fn snapshot_after_subagent_spawn(
|
||||
scrollback_entry_id: info.scrollback_entry_id,
|
||||
}
|
||||
}
|
||||
/// Snapshot after SubagentFinished for method-parity tests.
|
||||
pub(super) struct SubagentFinishSnapshot {
|
||||
finished: bool,
|
||||
status: Option<String>,
|
||||
@@ -1427,8 +1393,6 @@ pub(super) fn dispatch_goal_update(
|
||||
app,
|
||||
)
|
||||
}
|
||||
/// Build + dispatch a `GoalUpdated` for `sess-A` with the given id /
|
||||
/// status / elapsed; returns whether the notification requested a redraw.
|
||||
pub(super) fn send_goal_update(
|
||||
app: &mut AppView,
|
||||
goal_id: &str,
|
||||
@@ -1437,8 +1401,6 @@ pub(super) fn send_goal_update(
|
||||
) -> bool {
|
||||
dispatch_goal_update(app, goal_update_value(goal_id, status, elapsed_ms))
|
||||
}
|
||||
/// Build a minimal `RequestPermission` message that carries `session_id`
|
||||
/// and one `AllowOnce` option.
|
||||
pub(super) fn make_permission_message(
|
||||
session_id: &str,
|
||||
) -> (
|
||||
@@ -1543,8 +1505,6 @@ pub(super) fn make_replayed_task_backgrounded_notif(
|
||||
let raw = serde_json::value::to_raw_value(¬if).unwrap();
|
||||
acp::ExtNotification::new("kigi/session/update", std::sync::Arc::from(raw))
|
||||
}
|
||||
/// Register a pending Execute tool call in the tracker and send an InProgress
|
||||
/// update to create the scrollback entry. Returns the agent for further use.
|
||||
pub(super) fn setup_pending_execute_tool(app: &mut AppView, tc_id: &str) {
|
||||
let agent = app.agents.get_mut(&AgentId(0)).unwrap();
|
||||
let meta = crate::acp::meta::NotificationMeta::default();
|
||||
@@ -1568,7 +1528,6 @@ pub(super) fn setup_pending_execute_tool(app: &mut AppView, tc_id: &str) {
|
||||
);
|
||||
agent.session.tracker.handle_update(update, &meta, &mut agent.scrollback);
|
||||
}
|
||||
/// Send a late InProgress update with is_background=true to trigger late bg detection.
|
||||
pub(super) fn send_late_bg_detection(app: &mut AppView, tc_id: &str) {
|
||||
use serde_json::json;
|
||||
use kigi_tools::types::output::{BashOutput, ToolOutput};
|
||||
@@ -1728,9 +1687,7 @@ pub(super) fn make_reasoning_models_update_notif(
|
||||
acp::ExtNotification::new("kigi/models/update", std::sync::Arc::from(raw))
|
||||
}
|
||||
/// Seed a session's model catalog with the given ids and mark
|
||||
/// `current_model_id` as the active one (must be in the list). Used by
|
||||
/// the `ModelChanged` broadcast tests to set up a starting state that
|
||||
/// the simulated remote/local switch then transitions away from.
|
||||
/// `current_model_id` as the active one (must be in the list).
|
||||
pub(super) fn seed_models(agent: &mut AgentView, current: &str, available: &[&str]) {
|
||||
for id in available {
|
||||
let model_id = acp::ModelId::new(std::sync::Arc::from(*id));
|
||||
@@ -1796,7 +1753,6 @@ pub(super) fn make_current_mode_update(mode_id: &str) -> acp::SessionUpdate {
|
||||
acp::CurrentModeUpdate::new(acp::SessionModeId::new(mode_id)),
|
||||
)
|
||||
}
|
||||
/// Helper: build an `kigi/mcp/init_progress` notification.
|
||||
pub(super) fn make_mcp_init_progress_notif(
|
||||
total: u32,
|
||||
connected: u32,
|
||||
@@ -1865,8 +1821,6 @@ pub(super) fn make_servers_updated_notif() -> acp::ExtNotification {
|
||||
let raw = serde_json::value::to_raw_value(&payload).unwrap();
|
||||
acp::ExtNotification::new("kigi/mcp/servers_updated", std::sync::Arc::from(raw))
|
||||
}
|
||||
/// Real post-handshake / auth-recovery wire shape:
|
||||
/// `McpToolsChanged { sessionId, serverName, tools }`.
|
||||
pub(super) fn make_tools_changed_notif_post_h2(
|
||||
session_id: &str,
|
||||
) -> acp::ExtNotification {
|
||||
@@ -1886,8 +1840,6 @@ pub(super) fn make_tools_changed_notif_pre_h2() -> acp::ExtNotification {
|
||||
let raw = serde_json::value::to_raw_value(&payload).unwrap();
|
||||
acp::ExtNotification::new("kigi/mcp/tools_changed", std::sync::Arc::from(raw))
|
||||
}
|
||||
/// Real `mcp_initialized` wire shape:
|
||||
/// `{ sessionId, mcpToolCount, elapsedMs }`.
|
||||
pub(super) fn make_mcp_initialized_notif(session_id: &str) -> acp::ExtNotification {
|
||||
let payload = serde_json::json!(
|
||||
{ "sessionId" : session_id, "mcpToolCount" : 12_u64, "elapsedMs" : 250_u64, }
|
||||
@@ -1895,7 +1847,6 @@ pub(super) fn make_mcp_initialized_notif(session_id: &str) -> acp::ExtNotificati
|
||||
let raw = serde_json::value::to_raw_value(&payload).unwrap();
|
||||
acp::ExtNotification::new("kigi/mcp_initialized", std::sync::Arc::from(raw))
|
||||
}
|
||||
/// Helper: `init_progress` notification carrying an explicit sessionId.
|
||||
pub(super) fn make_mcp_init_progress_notif_for(
|
||||
total: u32,
|
||||
connected: u32,
|
||||
@@ -1909,7 +1860,6 @@ pub(super) fn make_mcp_init_progress_notif_for(
|
||||
.unwrap();
|
||||
acp::ExtNotification::new("kigi/mcp/init_progress", std::sync::Arc::from(raw))
|
||||
}
|
||||
/// Helper: `mcp_initialized` notification for a specific sessionId.
|
||||
pub(super) fn make_mcp_initialized_notif_for(session_id: &str) -> acp::ExtNotification {
|
||||
let raw = serde_json::value::to_raw_value(
|
||||
&serde_json::json!(
|
||||
|
||||
@@ -186,7 +186,7 @@
|
||||
agent_b.session.models.current = Some(id_5);
|
||||
}
|
||||
|
||||
// kigi-5 removed from catalog.
|
||||
// kigi-4.5 removed from catalog.
|
||||
let notif = make_models_update_notif("kigi-4", &["kigi-3", "kigi-4"]);
|
||||
handle_models_update(¬if, &mut app);
|
||||
|
||||
@@ -206,7 +206,7 @@
|
||||
"agent A's model must be preserved"
|
||||
);
|
||||
|
||||
// B's kigi-5 was removed — must fall back to shell's kigi-4, not A's kigi-3.
|
||||
// B's kigi-4.5 was removed — must fall back to shell's kigi-4, not A's kigi-3.
|
||||
let agent_b = app.agents.get(&AgentId(1)).unwrap();
|
||||
assert_eq!(
|
||||
agent_b
|
||||
@@ -230,7 +230,6 @@
|
||||
let agent = app.agents.get_mut(&AgentId(0)).unwrap();
|
||||
seed_models(agent, "kigi-3", &["kigi-3", "kigi-4"]);
|
||||
let scrollback_before = agent.scrollback.len();
|
||||
// Follower: no local switch in flight.
|
||||
assert!(!agent.session.model_switch_pending);
|
||||
|
||||
let notif = model_changed_ext("sess-1", "kigi-4", None);
|
||||
@@ -326,8 +325,6 @@
|
||||
let mut app = make_app_with_agent("sess-1");
|
||||
let agent = app.agents.get_mut(&AgentId(0)).unwrap();
|
||||
seed_models(agent, "kigi-3", &["kigi-3", "kigi-4"]);
|
||||
// Invoker: a local switch is in flight (set by Action::SwitchModel /
|
||||
// set_default_model before the SetSessionModelRequest is sent).
|
||||
agent.session.model_switch_pending = true;
|
||||
let scrollback_before = agent.scrollback.len();
|
||||
|
||||
|
||||
@@ -45,8 +45,6 @@
|
||||
|
||||
let mut app = make_app_with_agent("sess-1");
|
||||
|
||||
// Establish the shared queue with p1 present, then put the agent in
|
||||
// EditingQueued{server_id: Some("p1")}.
|
||||
assert!(handle_ext_notification(
|
||||
&queue_changed_ext("sess-1", &["p1"]),
|
||||
&mut app
|
||||
@@ -1018,8 +1016,9 @@
|
||||
"adoption-on-load must not grow the scrollback"
|
||||
);
|
||||
|
||||
// A live (non-replay) chunk stamped with the adopted prompt id now
|
||||
// passes the gate and renders (previously dropped → the viewer froze).
|
||||
// A live (non-replay) chunk stamped with the adopted prompt id
|
||||
// passes the gate and renders; without the adoption the gate would
|
||||
// drop it and the viewer would freeze.
|
||||
let (tx, _rx) = tokio::sync::oneshot::channel();
|
||||
let request = acp::SessionNotification::new(
|
||||
acp::SessionId::new("sess-1"),
|
||||
@@ -1750,8 +1749,7 @@
|
||||
);
|
||||
}
|
||||
|
||||
/// The credit-limit early return discards the popped adoption's buffer.
|
||||
/// A stash whose pid replayed a durable terminal is discarded, never adopted.
|
||||
/// A stash whose pid replayed a durable terminal is discarded, never adopted.
|
||||
#[test]
|
||||
fn terminal_in_replay_stash_is_discarded_not_adopted() {
|
||||
let mut app = app_with_running_p1_and_stashed_b1();
|
||||
|
||||
@@ -51,7 +51,6 @@
|
||||
let mut app = make_app_with_agent("sess-dedup");
|
||||
let id = AgentId(0);
|
||||
|
||||
// First event applies (active agent → affected==true) and sets highwater.
|
||||
let a1 = handle(
|
||||
make_agent_chunk_with_event("sess-dedup", "hello", "p1", Some("sess-dedup-5")),
|
||||
&mut app,
|
||||
@@ -59,7 +58,6 @@
|
||||
assert!(a1, "first event must apply");
|
||||
assert_eq!(app.agents[&id].last_applied_event_seq, Some(5));
|
||||
|
||||
// Exact duplicate eventId → dropped (not affected), highwater unchanged.
|
||||
let a2 = handle(
|
||||
make_agent_chunk_with_event("sess-dedup", "hello", "p1", Some("sess-dedup-5")),
|
||||
&mut app,
|
||||
@@ -67,7 +65,6 @@
|
||||
assert!(!a2, "a duplicate eventId must be dropped");
|
||||
assert_eq!(app.agents[&id].last_applied_event_seq, Some(5));
|
||||
|
||||
// Stale lower eventId → dropped.
|
||||
let a3 = handle(
|
||||
make_agent_chunk_with_event("sess-dedup", "hello", "p1", Some("sess-dedup-3")),
|
||||
&mut app,
|
||||
@@ -75,7 +72,6 @@
|
||||
assert!(!a3, "a lower (already-passed) eventId must be dropped");
|
||||
assert_eq!(app.agents[&id].last_applied_event_seq, Some(5));
|
||||
|
||||
// New higher eventId → applies, highwater advances.
|
||||
let a4 = handle(
|
||||
make_agent_chunk_with_event("sess-dedup", "world", "p1", Some("sess-dedup-9")),
|
||||
&mut app,
|
||||
@@ -83,7 +79,6 @@
|
||||
assert!(a4, "a new (higher) eventId must apply");
|
||||
assert_eq!(app.agents[&id].last_applied_event_seq, Some(9));
|
||||
|
||||
// No eventId (older shell) → always applies; highwater untouched.
|
||||
let a5 = handle(
|
||||
make_agent_chunk_with_event("sess-dedup", "again", "p1", None),
|
||||
&mut app,
|
||||
@@ -101,7 +96,6 @@
|
||||
fn replayed_history_with_event_id_resets_does_not_break_resume() {
|
||||
let mut app = make_app_with_agent("sess-resume");
|
||||
let id = AgentId(0);
|
||||
// Replay arrives inside a `session/load` window.
|
||||
app.agents.get_mut(&id).unwrap().session.loading_replay = true;
|
||||
|
||||
// eventIds climb (5, 9) then reset below the peak (2, 4): resumed twice.
|
||||
@@ -125,7 +119,6 @@
|
||||
Some("sess-resume-4"),
|
||||
"the reconnect cursor follows the last APPLIED event id, replay included"
|
||||
);
|
||||
// SessionLoaded completes the window.
|
||||
app.agents.get_mut(&id).unwrap().session.loading_replay = false;
|
||||
|
||||
assert!(
|
||||
@@ -228,13 +221,10 @@
|
||||
agent.begin_session_reload(1);
|
||||
}
|
||||
|
||||
// Partial replay lands before the load fails...
|
||||
assert!(!handle(
|
||||
replay_chunk("sess-rc", "h1", "sess-rc-1"),
|
||||
&mut app
|
||||
));
|
||||
// ...along with live post-cursor traffic on BOTH streams, advancing
|
||||
// both highwaters inside the doomed staging.
|
||||
let _ = handle(
|
||||
make_agent_chunk_with_event("sess-rc", "live tail", "p9", Some("sess-rc-40")),
|
||||
&mut app,
|
||||
@@ -303,7 +293,6 @@
|
||||
agent.begin_session_reload(1);
|
||||
}
|
||||
|
||||
// Post-cursor tail arrives as LIVE updates (no isReplay).
|
||||
assert!(!handle(
|
||||
make_agent_chunk_with_event("sess-rc", "tail", "p2", Some("sess-rc-4")),
|
||||
&mut app,
|
||||
@@ -375,12 +364,10 @@
|
||||
1,
|
||||
"gen-1 partial replay is discarded; gen-2 staging holds only the placeholder"
|
||||
);
|
||||
// Finalizing the dead gen-1 window is rejected.
|
||||
assert!(!agent.finish_session_reload(1, true));
|
||||
assert!(agent.session.loading_replay);
|
||||
}
|
||||
|
||||
// Gen-2 load fails → the ORIGINAL transcript comes back.
|
||||
let agent = app.agents.get_mut(&id).unwrap();
|
||||
assert!(agent.finish_session_reload(2, false));
|
||||
assert!(scrollback_has_system_text(agent, "pre-outage content"));
|
||||
@@ -401,7 +388,6 @@
|
||||
agent
|
||||
.scrollback
|
||||
.push_block(RenderBlock::system("pre-outage content"));
|
||||
// In-flight fresh-view load: open batch + placeholder + replay flag.
|
||||
agent.scrollback.begin_batch();
|
||||
let pid = agent
|
||||
.scrollback
|
||||
@@ -468,7 +454,6 @@
|
||||
);
|
||||
app.agents.get_mut(&id).unwrap().begin_session_reload(1);
|
||||
|
||||
// Replayed Plan overwrites the (fresh) live pane during the window.
|
||||
let _ = handle(
|
||||
plan_update_msg("sess-todo", &["replayed-task"], Some("sess-todo-2"), true),
|
||||
&mut app,
|
||||
@@ -494,7 +479,6 @@
|
||||
);
|
||||
app.agents.get_mut(&id).unwrap().begin_session_reload(1);
|
||||
|
||||
// A LIVE tail Plan applied in-window is newer than the stash.
|
||||
let _ = handle(
|
||||
plan_update_msg("sess-todo", &["tail-task"], Some("sess-todo-2"), false),
|
||||
&mut app,
|
||||
@@ -544,7 +528,6 @@
|
||||
assert_eq!(app.agents[&id].scrollback.len(), 1);
|
||||
assert_eq!(app.agents[&id].last_applied_xai_event_seq, Some(10));
|
||||
|
||||
// Exact re-delivery: dropped, nothing re-applied, cursor unchanged.
|
||||
assert!(!handle_ext_notification(
|
||||
&xai_model_switch_notif("sess-xdup", "sess-xdup-10"),
|
||||
&mut app
|
||||
@@ -559,7 +542,6 @@
|
||||
Some("sess-xdup-10")
|
||||
);
|
||||
|
||||
// A newer event still applies.
|
||||
assert!(handle_ext_notification(
|
||||
&xai_model_switch_notif("sess-xdup", "sess-xdup-11"),
|
||||
&mut app
|
||||
@@ -603,7 +585,6 @@
|
||||
"an unhandled xAI update must not advance the dedup highwater"
|
||||
);
|
||||
|
||||
// An applied kind (ModelAutoSwitched) advances both.
|
||||
assert!(handle_ext_notification(
|
||||
&xai_model_switch_notif("sess-ig", "sess-ig-8"),
|
||||
&mut app
|
||||
@@ -759,7 +740,6 @@
|
||||
Some("sess-cur-5")
|
||||
);
|
||||
|
||||
// Duplicate (deduped) — cursor unchanged.
|
||||
assert!(!handle(
|
||||
make_agent_chunk_with_event("sess-cur", "a", "p1", Some("sess-cur-5")),
|
||||
&mut app,
|
||||
@@ -810,7 +790,6 @@
|
||||
let mut app = make_app_with_agent("sess-xai");
|
||||
let id = AgentId(0);
|
||||
|
||||
// Replay-stamped with no load in flight → dropped, nothing pushed.
|
||||
let replay_meta = serde_json::json!({ "isReplay": true, "eventId": "sess-xai-7" });
|
||||
assert!(!handle_ext_notification(
|
||||
&model_switch_notif(Some(replay_meta.clone())),
|
||||
@@ -822,8 +801,6 @@
|
||||
assert!(agent.last_seen_event_id.is_none());
|
||||
}
|
||||
|
||||
// Same update inside a reload window → applied and marks the window
|
||||
// as full-replay (finishing keeps the staged state, drops the stash).
|
||||
{
|
||||
let agent = app.agents.get_mut(&id).unwrap();
|
||||
agent
|
||||
@@ -875,7 +852,6 @@
|
||||
agent.begin_session_reload(1);
|
||||
}
|
||||
|
||||
// Replayed spawn, no finish (mirrors a mid-subagent reconnect replay).
|
||||
let payload = SessionNotification {
|
||||
session_id: acp::SessionId::new("sess-sub"),
|
||||
update: test_subagent_spawned("sess-sub", "child-sub"),
|
||||
@@ -905,8 +881,6 @@
|
||||
"the child view exists and is tracked"
|
||||
);
|
||||
|
||||
// A live child delta after the swap still renders into the child view:
|
||||
// pager-side routing is intact when the leader delivers it.
|
||||
let child_len_before = app.agents[&id].subagent_views["child-sub"].scrollback.len();
|
||||
let _ = handle(
|
||||
make_agent_chunk_with_event("child-sub", "child live text", "p-child", None),
|
||||
@@ -929,7 +903,6 @@
|
||||
let mut app = make_app_with_agent("sess-ctx");
|
||||
let id = AgentId(0);
|
||||
|
||||
// Fresh live delta: high eventId, high token count.
|
||||
let _ = handle(
|
||||
make_token_notification_with_event("sess-ctx", 500_000, "sess-ctx-20"),
|
||||
&mut app,
|
||||
@@ -940,7 +913,6 @@
|
||||
);
|
||||
assert_eq!(app.agents[&id].last_applied_event_seq, Some(20));
|
||||
|
||||
// Stale historical replay delta: lower eventId (deduped), lower tokens.
|
||||
let _ = handle(
|
||||
make_token_notification_with_event("sess-ctx", 120_000, "sess-ctx-7"),
|
||||
&mut app,
|
||||
@@ -950,7 +922,6 @@
|
||||
Some(500_000),
|
||||
"a deduped stale delta must not regress context_used to its lower value"
|
||||
);
|
||||
// Highwater unchanged by the deduped event.
|
||||
assert_eq!(app.agents[&id].last_applied_event_seq, Some(20));
|
||||
}
|
||||
|
||||
@@ -961,10 +932,8 @@
|
||||
fn reconnect_finalize_reload_skips_adoption_when_terminal_in_replay() {
|
||||
let mut app = make_app_with_agent("sess-1");
|
||||
let id = AgentId(0);
|
||||
// Open a reconnect reload window (enters the replay window, clean set).
|
||||
app.agents.get_mut(&id).unwrap().begin_session_reload(1);
|
||||
|
||||
// The running turn's terminal arrives in the reconnect replay → recorded.
|
||||
let _ = handle_ext_notification(
|
||||
&xai_turn_completed_notif("sess-1", "p-run", "end_turn", true),
|
||||
&mut app,
|
||||
@@ -999,7 +968,6 @@
|
||||
seed_models(agent, "kigi-3", &["kigi-3", "kigi-4"]);
|
||||
}
|
||||
|
||||
// Unknown model → ignored → both markers untouched.
|
||||
assert!(!handle_ext_notification(
|
||||
&model_changed_ext_with_event("sess-1", "kigi-99-unknown", "sess-1-7"),
|
||||
&mut app
|
||||
@@ -1013,7 +981,6 @@
|
||||
"an ignored ModelChanged must not advance the dedup highwater"
|
||||
);
|
||||
|
||||
// Known model → applied → both markers advance.
|
||||
assert!(handle_ext_notification(
|
||||
&model_changed_ext_with_event("sess-1", "kigi-4", "sess-1-8"),
|
||||
&mut app
|
||||
|
||||
@@ -15,16 +15,12 @@
|
||||
let result = handle_scheduled_task_inject_prompt(¬if, &mut app);
|
||||
assert!(result);
|
||||
|
||||
// Agent should now be in TurnRunning (drain happened, prompt was sent).
|
||||
let agent = app.agents.get(&AgentId(0)).unwrap();
|
||||
assert!(agent.session.state.is_turn_running());
|
||||
assert!(agent.session.pending_prompts.is_empty());
|
||||
|
||||
// Scrollback should have a cron prompt block.
|
||||
assert!(!agent.scrollback.is_empty());
|
||||
|
||||
// pending_effects should contain a SendPromptBlocks with system-reminder framing,
|
||||
// displayText/displayAsCron meta, and a scheduler-fired- prompt_id prefix.
|
||||
match &app.pending_effects[0] {
|
||||
Effect::SendPromptBlocks {
|
||||
blocks, prompt_id, ..
|
||||
@@ -54,10 +50,9 @@
|
||||
// The leader routes `kigi/scheduled_task_inject_prompt` to the SINGLE
|
||||
// session driver, so any client that receives it IS the driver and must
|
||||
// enqueue + run it — even one that attached via `session/load`
|
||||
// (`attached_as_viewer == true`). Previously this handler latched on
|
||||
// `attached_as_viewer` and skipped, which stranded the cron loop with no
|
||||
// output whenever the designated driver was an attacher (the sticky-flag
|
||||
// bug). Pin the corrected behavior: the inject drives the turn.
|
||||
// (`attached_as_viewer == true`). Gating the inject on that flag would
|
||||
// strand the cron loop with no output when the designated driver is an
|
||||
// attacher (the sticky-flag bug): the inject must drive the turn.
|
||||
let mut app = make_app_with_agent("sess-1");
|
||||
app.agents.get_mut(&AgentId(0)).unwrap().attached_as_viewer = true;
|
||||
|
||||
@@ -110,7 +105,6 @@
|
||||
|
||||
let result = handle_scheduled_task_inject_prompt(¬if, &mut app);
|
||||
assert!(!result);
|
||||
// Nothing should be enqueued.
|
||||
let agent = app.agents.get(&AgentId(0)).unwrap();
|
||||
assert!(agent.session.pending_prompts.is_empty());
|
||||
}
|
||||
@@ -126,7 +120,6 @@
|
||||
|
||||
let result = handle_scheduled_task_inject_prompt(¬if, &mut app);
|
||||
assert!(!result);
|
||||
// Agent should still be idle, nothing enqueued.
|
||||
let agent = app.agents.get(&AgentId(0)).unwrap();
|
||||
assert!(agent.session.state.is_idle());
|
||||
}
|
||||
@@ -134,7 +127,6 @@
|
||||
#[test]
|
||||
fn inject_prompt_busy_agent_enqueues_without_draining() {
|
||||
let mut app = make_app_with_agent("sess-1");
|
||||
// Make the agent busy.
|
||||
let agent = app.agents.get_mut(&AgentId(0)).unwrap();
|
||||
agent.session.state = AgentState::TurnRunning;
|
||||
|
||||
@@ -147,14 +139,12 @@
|
||||
let result = handle_scheduled_task_inject_prompt(¬if, &mut app);
|
||||
assert!(result);
|
||||
|
||||
// Prompt should be queued but not drained (agent was busy).
|
||||
let agent = app.agents.get(&AgentId(0)).unwrap();
|
||||
assert_eq!(agent.session.pending_prompts.len(), 1);
|
||||
assert_eq!(
|
||||
agent.session.pending_prompts[0].kind,
|
||||
crate::app::agent::QueueEntryKind::Cron
|
||||
);
|
||||
// No effects produced (drain was a no-op since agent was busy).
|
||||
assert!(app.pending_effects.is_empty());
|
||||
}
|
||||
|
||||
@@ -185,7 +175,6 @@
|
||||
1
|
||||
);
|
||||
|
||||
// A re-fire of the same task while it is still queued must not pile up.
|
||||
assert!(handle_scheduled_task_inject_prompt(
|
||||
&make_inject_notif(&payload),
|
||||
&mut app
|
||||
@@ -212,7 +201,6 @@
|
||||
"humanSchedule": "every 1m",
|
||||
});
|
||||
|
||||
// First fire on an idle agent drains into a running cron turn.
|
||||
assert!(handle_scheduled_task_inject_prompt(
|
||||
&make_inject_notif(&payload),
|
||||
&mut app
|
||||
@@ -221,7 +209,6 @@
|
||||
assert!(agent.session.state.is_turn_running());
|
||||
assert!(agent.session.pending_prompts.is_empty());
|
||||
|
||||
// A re-fire while that same loop turn is running must be skipped, not queued.
|
||||
assert!(handle_scheduled_task_inject_prompt(
|
||||
&make_inject_notif(&payload),
|
||||
&mut app
|
||||
@@ -379,7 +366,6 @@
|
||||
#[test]
|
||||
fn fired_updates_correct_agent_when_active_view_differs() {
|
||||
let mut app = make_app_two_agents();
|
||||
// Seed a known task on agent 0.
|
||||
{
|
||||
let agent0 = app.agents.get_mut(&AgentId(0)).unwrap();
|
||||
agent0.session.scheduled_tasks.insert(
|
||||
@@ -411,7 +397,6 @@
|
||||
"non-active agent mutation should not trigger redraw"
|
||||
);
|
||||
|
||||
// Agent 0's next_fire_at must be updated.
|
||||
let agent0 = app.agents.get(&AgentId(0)).unwrap();
|
||||
let info = agent0.session.scheduled_tasks.get("task-owner").unwrap();
|
||||
assert_eq!(
|
||||
@@ -420,7 +405,6 @@
|
||||
"next_fire_at must update on the owning agent, not the active one"
|
||||
);
|
||||
|
||||
// Agent 1 must be completely untouched.
|
||||
let agent1 = app.agents.get(&AgentId(1)).unwrap();
|
||||
assert!(
|
||||
agent1.session.scheduled_tasks.is_empty(),
|
||||
@@ -460,7 +444,6 @@
|
||||
#[test]
|
||||
fn deleted_removes_from_correct_agent_when_active_view_differs() {
|
||||
let mut app = make_app_two_agents();
|
||||
// Seed task on agent 0.
|
||||
{
|
||||
let agent0 = app.agents.get_mut(&AgentId(0)).unwrap();
|
||||
agent0.session.scheduled_tasks.insert(
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
#![cfg_attr(rustfmt, rustfmt::skip)]
|
||||
use super::*;
|
||||
|
||||
// ── apply_session_event ────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn apply_compaction_started_sets_activity() {
|
||||
let mut session = make_session(Some("s1"));
|
||||
@@ -232,7 +230,6 @@
|
||||
Some("legacy_auth"),
|
||||
"Unauthorized (401) ... deprecated authentication method"
|
||||
));
|
||||
// Unrelated failures must not be treated as re-authable.
|
||||
assert!(!is_reauthable_failure(
|
||||
Some("server_error"),
|
||||
"internal server error"
|
||||
@@ -339,7 +336,6 @@
|
||||
));
|
||||
}
|
||||
|
||||
/// Non-auth terminal failures still render the standard RetryFailed.
|
||||
#[test]
|
||||
fn apply_retry_state_generic_failure_still_shows_retry_failed() {
|
||||
let mut session = make_session(Some("s1"));
|
||||
@@ -544,8 +540,6 @@
|
||||
assert!(!apply_session_event(&update, &mut session, &mut scrollback, false));
|
||||
}
|
||||
|
||||
// ── handle_child_session_notification ──────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn child_compact_completed_updates_subagent_info() {
|
||||
let mut agent = make_agent(Some("root-sess"));
|
||||
@@ -616,7 +610,6 @@
|
||||
#[test]
|
||||
fn child_notification_without_view_returns_false() {
|
||||
let mut agent = make_agent(Some("root-sess"));
|
||||
// No child view registered.
|
||||
let update = XaiSessionUpdate::AutoCompactStarted {
|
||||
tokens_used: 90000,
|
||||
context_window: 131072,
|
||||
@@ -659,8 +652,6 @@
|
||||
assert!(!changed);
|
||||
}
|
||||
|
||||
// ── apply_retry_state ─────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn retry_failed_encrypted_content_sets_model_incompatible() {
|
||||
use kigi_shell::extensions::notification::RetryState;
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
#[test]
|
||||
fn acp_chunk_for_inactive_agent_lands_in_its_scrollback() {
|
||||
// Regression: switching away from a streaming agent must not
|
||||
// discard chunks bound for that agent. Before this fix, only
|
||||
// `TaskResult::PromptResponse` survived, so the user saw a bare
|
||||
// "Worked for X.Xs" with no body text.
|
||||
// discard chunks bound for that agent. If only
|
||||
// `TaskResult::PromptResponse` were routed, the user would see a
|
||||
// bare "Worked for X.Xs" with no body text.
|
||||
let mut app = make_app_with_agent("sess-A");
|
||||
insert_agent(&mut app, AgentId(1), Some("sess-B"));
|
||||
switch_active_to(&mut app, AgentId(1));
|
||||
@@ -32,7 +32,6 @@
|
||||
|
||||
#[test]
|
||||
fn acp_chunk_for_active_agent_returns_affected_true() {
|
||||
// Baseline: chunk for the visible agent triggers a redraw.
|
||||
let mut app = make_app_with_agent("sess-A");
|
||||
insert_agent(&mut app, AgentId(1), Some("sess-B"));
|
||||
switch_active_to(&mut app, AgentId(1));
|
||||
@@ -46,9 +45,6 @@
|
||||
|
||||
#[test]
|
||||
fn acp_chunk_for_subagent_routes_through_parent() {
|
||||
// Subagent (child) chunk must land in the parent's
|
||||
// `subagent_views[child_sid]` even when a different agent is
|
||||
// currently active.
|
||||
let mut app = make_app_with_agent("sess-A");
|
||||
insert_agent(&mut app, AgentId(1), Some("sess-B"));
|
||||
switch_active_to(&mut app, AgentId(1));
|
||||
@@ -169,7 +165,6 @@
|
||||
let mut app = make_app_with_agent("sess-A");
|
||||
insert_agent(&mut app, AgentId(1), Some("sess-B"));
|
||||
switch_active_to(&mut app, AgentId(1));
|
||||
// Sanity: A's todo starts empty.
|
||||
assert_eq!(
|
||||
app.agents.get(&AgentId(0)).unwrap().todo.counts().total(),
|
||||
0,
|
||||
@@ -277,9 +272,6 @@
|
||||
|
||||
#[test]
|
||||
fn acp_chunks_for_two_agents_dont_cross_contaminate() {
|
||||
// Send chunks to both A and B in sequence; each landing in its own
|
||||
// scrollback proves the demux works in both directions regardless
|
||||
// of which agent is currently active.
|
||||
let mut app = make_app_with_agent("sess-A");
|
||||
insert_agent(&mut app, AgentId(1), Some("sess-B"));
|
||||
switch_active_to(&mut app, AgentId(1));
|
||||
|
||||
@@ -154,8 +154,8 @@
|
||||
}
|
||||
|
||||
/// The live-refresh flip mirrors `set_group_tool_verbs_inner`'s stale
|
||||
/// group-expansion cleanup: a previously expanded verb slot must not
|
||||
/// survive a remote flip as an expanded header.
|
||||
/// group-expansion cleanup: a verb slot expanded before the flip must
|
||||
/// not survive it as an expanded header.
|
||||
#[test]
|
||||
fn settings_update_flip_resets_stale_group_expansion() {
|
||||
crate::appearance::cache::set_group_tool_verbs(true);
|
||||
@@ -204,16 +204,16 @@
|
||||
// Two agents both in auto; the active tab's global mirror reads "ask"
|
||||
// (a tab switch / Shift+Tab re-anchored it away from auto). A
|
||||
// mid-session gate kill-switch (`auto_permission_mode_enabled=false`)
|
||||
// must clear the per-session auto flag on BOTH agents. The old code
|
||||
// gated this fan-out on `current_ui.permission_mode == "auto"`, so it
|
||||
// skipped background agents and left stale `auto_mode` that
|
||||
// must clear the per-session auto flag on BOTH agents. Gating the
|
||||
// fan-out on `current_ui.permission_mode == "auto"` would skip
|
||||
// background agents and leave stale `auto_mode` that
|
||||
// `switch_to_agent` could re-anchor back to "auto" on return.
|
||||
let mut app = make_app_two_agents();
|
||||
app.auto_mode_gate = true;
|
||||
for agent in app.agents.values_mut() {
|
||||
agent.session.auto_mode = true;
|
||||
}
|
||||
// Active tab's mirror is NOT "auto" — the old bug's skip condition.
|
||||
// Active tab's mirror is NOT "auto" — reproduces the skip condition.
|
||||
app.current_ui.permission_mode = Some("ask".into());
|
||||
|
||||
let killswitch = acp::ExtNotification::new(
|
||||
@@ -263,7 +263,6 @@
|
||||
let _ = handle_ext_notification(&killswitch, &mut app);
|
||||
|
||||
assert!(!app.auto_mode_gate, "gate must be off after kill-switch");
|
||||
// Sibling always-approve is untouched — the kill-switch clears only auto.
|
||||
assert!(
|
||||
app.agents[&AgentId(2)].session.is_yolo(),
|
||||
"sibling always-approve must stay yolo after the auto kill-switch"
|
||||
|
||||
@@ -538,7 +538,6 @@
|
||||
});
|
||||
}
|
||||
|
||||
/// Live spawn: meta prompt without updates.jsonl still injects the task once.
|
||||
#[test]
|
||||
fn subagent_spawn_live_injects_meta_prompt_once_without_updates() {
|
||||
with_replay_disk_home(|home| {
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
let agent = app.agents.get_mut(&AgentId(0)).unwrap();
|
||||
agent.session.start_turn(&mut agent.scrollback);
|
||||
agent.session.current_prompt_id = Some("pid-stuck".into());
|
||||
agent.session.cancel_turn(&mut agent.scrollback); // CancelTurn → TurnCancelling
|
||||
agent.session.cancel_turn(&mut agent.scrollback);
|
||||
assert!(!agent.attached_as_viewer);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user