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:
1 parent
ff0fb56c67
commit
a02b555e66
1458 files changed
+10729
-21750
No files matched your search
@@ -10,18 +10,14 @@ pub(super) fn route_bg_task_stdout(
|
||||
) -> bool {
|
||||
let tc_id = tcu.tool_call_id.0.to_string();
|
||||
|
||||
// Check if this tool_call_id maps to a bg task
|
||||
let task_id = match session.bg_tool_call_to_task.get(&tc_id) {
|
||||
Some(tid) => tid.clone(),
|
||||
None => return false,
|
||||
};
|
||||
|
||||
// Extract stdout from the raw_output BashOutput
|
||||
if let Some(ref raw_output) = tcu.fields.raw_output {
|
||||
// The shell sends full cumulative output buffer — just overwrite.
|
||||
// Check for BashOutput type
|
||||
if raw_output.get("type").and_then(|v| v.as_str()) == Some("Bash") {
|
||||
// Try output_for_prompt first (pre-stripped string)
|
||||
let stdout =
|
||||
if let Some(s) = raw_output.get("output_for_prompt").and_then(|v| v.as_str()) {
|
||||
s.to_string()
|
||||
@@ -33,7 +29,8 @@ pub(super) fn route_bg_task_stdout(
|
||||
.collect();
|
||||
String::from_utf8_lossy(&bytes).into_owned()
|
||||
} else {
|
||||
return true; // Consumed but no extractable output
|
||||
// Consumed but no extractable output
|
||||
return true;
|
||||
};
|
||||
|
||||
// Capture the shell-side `truncated` flag — once true, it stays
|
||||
@@ -59,7 +56,8 @@ pub(super) fn route_bg_task_stdout(
|
||||
}
|
||||
}
|
||||
|
||||
true // Consumed — don't pass to tracker
|
||||
// Consumed — don't pass to tracker
|
||||
true
|
||||
}
|
||||
|
||||
/// Handle `kigi/task_backgrounded` — a bash command transitioned to background.
|
||||
@@ -72,13 +70,11 @@ pub(super) fn route_bg_task_stdout(
|
||||
/// entry's running state is cleared. Otherwise a fresh `BgTask` block
|
||||
/// is pushed.
|
||||
pub(super) fn handle_task_backgrounded(notif: &acp::ExtNotification, app: &mut AppView) -> bool {
|
||||
// Parse the SessionNotification envelope
|
||||
let Ok(session_notif) = serde_json::from_str::<SessionNotification>(notif.params.get()) else {
|
||||
tracing::warn!("Failed to parse kigi/task_backgrounded");
|
||||
return false;
|
||||
};
|
||||
|
||||
// Extract TaskBackgrounded fields
|
||||
let (tool_call_id, task_id, command, cwd, output_file, monitor_description, notif_description) =
|
||||
match session_notif.update {
|
||||
XaiSessionUpdate::TaskBackgrounded {
|
||||
@@ -153,8 +149,8 @@ pub(super) fn handle_task_backgrounded(notif: &acp::ExtNotification, app: &mut A
|
||||
.or_else(|| non_blank(notif_description))
|
||||
.or_else(|| non_blank(deferred_description));
|
||||
|
||||
// Create central bg task state (description may still be filled from the
|
||||
// Execute block on demotion before we insert into the map).
|
||||
// description stays None here; it may still be filled from the Execute
|
||||
// block on demotion before we insert into the map.
|
||||
let mut bg_task = BgTaskState {
|
||||
task_id: task_id.clone(),
|
||||
tool_call_id: tool_call_id.clone(),
|
||||
@@ -503,7 +499,7 @@ pub(super) fn handle_git_head_changed(notif: &acp::ExtNotification, app: &mut Ap
|
||||
return false;
|
||||
};
|
||||
|
||||
// Find the agent by ACP session id (not local AgentId) and update its git display cache
|
||||
// Match by ACP session id, not local AgentId.
|
||||
if let Some(agent) = app.agents.values_mut().find(|a| {
|
||||
a.session
|
||||
.session_id
|
||||
@@ -549,7 +545,6 @@ pub(super) fn handle_git_head_changed(notif: &acp::ExtNotification, app: &mut Ap
|
||||
}
|
||||
|
||||
pub(super) fn handle_task_completed(notif: &acp::ExtNotification, app: &mut AppView) -> bool {
|
||||
// The payload is a SessionNotification wrapping TaskCompleted { task_snapshot }
|
||||
let Ok(session_notif) = serde_json::from_str::<SessionNotification>(notif.params.get()) else {
|
||||
tracing::warn!("Failed to parse kigi/task_completed");
|
||||
return false;
|
||||
@@ -579,7 +574,6 @@ pub(super) fn handle_task_completed(notif: &acp::ExtNotification, app: &mut AppV
|
||||
"Background task completed"
|
||||
);
|
||||
|
||||
// Determine success once, reused for both bg_task status and scrollback block.
|
||||
let success = exit_code == Some(0) || (exit_code.is_none() && signal.is_none());
|
||||
|
||||
// Synthetic completion emitted by the agent's cold-load reconciliation
|
||||
@@ -594,7 +588,6 @@ pub(super) fn handle_task_completed(notif: &acp::ExtNotification, app: &mut AppV
|
||||
return false;
|
||||
};
|
||||
|
||||
// Compute elapsed duration from the bg task state (if we have it).
|
||||
// Prefer the human description for "Task completed/failed: …" labels
|
||||
// (same as "Task started"), falling back to the raw command only when
|
||||
// no description was supplied.
|
||||
|
||||
@@ -18,7 +18,6 @@ pub(crate) fn handle_ask_user_question(
|
||||
AskUserQuestionExtRequest, AskUserQuestionExtResponse,
|
||||
};
|
||||
|
||||
// Parse the typed request from the ext-method params.
|
||||
let ext_req: AskUserQuestionExtRequest = match serde_json::from_str(ext.request.params.get()) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
@@ -54,7 +53,6 @@ pub(crate) fn handle_ask_user_question(
|
||||
return false;
|
||||
};
|
||||
|
||||
// If a question is already active, cancel it before replacing.
|
||||
if let Some(mut old_qv) = agent.question_view.take() {
|
||||
agent.turn_paused_duration += old_qv.opened_at.elapsed();
|
||||
tracing::warn!(
|
||||
@@ -89,7 +87,6 @@ pub(crate) fn handle_ask_user_question(
|
||||
}
|
||||
}
|
||||
|
||||
// Stash the current prompt and create the question view.
|
||||
agent.question_view = Some(QuestionViewState::with_response_tx(
|
||||
ext_req.tool_call_id,
|
||||
ext_req.questions,
|
||||
@@ -98,7 +95,6 @@ pub(crate) fn handle_ask_user_question(
|
||||
ext_req.mode,
|
||||
));
|
||||
|
||||
// Clear prompt for question interaction.
|
||||
agent.prompt.set_text("");
|
||||
|
||||
// Stamp the "last activity" anchor so the
|
||||
@@ -131,7 +127,6 @@ pub(super) fn handle_exit_plan_mode(
|
||||
) -> bool {
|
||||
use crate::views::plan_approval_view::{ExitPlanModeExtRequest, PlanApprovalViewState};
|
||||
|
||||
// 1. Parse typed request from raw JSON params.
|
||||
let params: ExitPlanModeExtRequest = match serde_json::from_str(ext.request.params.get()) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
@@ -146,7 +141,7 @@ pub(super) fn handle_exit_plan_mode(
|
||||
}
|
||||
};
|
||||
|
||||
// 2. Route by the request's session id (like `session/update`), so a
|
||||
// Route by the request's session id (like `session/update`), so a
|
||||
// plan-approval raised by a BACKGROUND session lands on its own view even
|
||||
// when the user isn't currently focused on it — rather than failing.
|
||||
let Some(id) = interaction_target_agent(app, ¶ms.session_id) else {
|
||||
|
||||
@@ -78,12 +78,6 @@ pub(super) fn handle_mcp_init_progress(notif: &acp::ExtNotification, app: &mut A
|
||||
pub(super) fn handle_mcp_tools_changed(notif: &acp::ExtNotification, app: &mut AppView) -> bool {
|
||||
let method = notif.method.as_ref();
|
||||
|
||||
// Both `kigi/mcp_initialized` and (newer shell)
|
||||
// `kigi/mcp/tools_changed` carry `sessionId`. Route by it so a
|
||||
// background agent's notification updates *its* state — not
|
||||
// whichever agent is foregrounded. Unknown and subagent (child)
|
||||
// sessions are dropped; a missing sessionId falls back to the
|
||||
// active agent (legacy shells).
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct Payload {
|
||||
@@ -127,7 +121,7 @@ pub(super) fn handle_mcp_tools_changed(notif: &acp::ExtNotification, app: &mut A
|
||||
}
|
||||
|
||||
// Modal refresh: schedule a debounced refetch for the OWNING agent
|
||||
// (routed by sessionId — was active_view), per-agent coalesced.
|
||||
// (routed by sessionId), per-agent coalesced.
|
||||
let modal_open = app
|
||||
.agents
|
||||
.get(&id)
|
||||
@@ -149,11 +143,9 @@ pub(super) fn handle_mcp_tools_changed(notif: &acp::ExtNotification, app: &mut A
|
||||
redraw
|
||||
}
|
||||
|
||||
/// Per-agent coalescing test for [`Effect::FetchMcpsList`].
|
||||
/// An earlier approach used `matches!(e, FetchMcpsList { .. })`
|
||||
/// which collapsed across agents — a pending fetch on agent A would
|
||||
/// drop the push for agent B. Now we key on `agent_id` so each
|
||||
/// agent's refetch is independently debounced.
|
||||
/// Per-agent coalescing test for [`Effect::FetchMcpsList`]. Keyed on
|
||||
/// `agent_id` so a pending fetch on agent A does not drop the push for
|
||||
/// agent B; each agent's refetch is independently debounced.
|
||||
pub(super) fn agent_has_pending_mcps_fetch(app: &AppView, agent_id: AgentId) -> bool {
|
||||
app.pending_effects.iter().any(|e| {
|
||||
matches!(
|
||||
@@ -243,9 +235,7 @@ pub(super) fn handle_mcp_server_status(notif: &acp::ExtNotification, app: &mut A
|
||||
// `Option<serde_json::Value>` (always `null` today;
|
||||
// reserved). If the value is present but not an array of
|
||||
// `McpToolEntry`-isomorphic objects we drop ONLY the tools
|
||||
// update and still apply the status — the previous strict
|
||||
// typing would have dropped the whole push on any shape
|
||||
// mismatch.
|
||||
// update and still apply the status.
|
||||
let new_tools = payload.tools.and_then(|raw| {
|
||||
match serde_json::from_value::<Vec<McpToolEntry>>(raw) {
|
||||
Ok(entries) => Some(
|
||||
@@ -279,12 +269,11 @@ pub(super) fn handle_mcp_server_status(notif: &acp::ExtNotification, app: &mut A
|
||||
/// on config reload (`crates/codegen/kigi-shell/src/agent/mvp_agent.rs`
|
||||
/// → `notify_servers_updated`). The shell's
|
||||
/// `McpServersUpdated` wire shape (`{ mcpServers: [...] }`) is
|
||||
/// intentionally session-agnostic by design
|
||||
/// An attempt to route by
|
||||
/// `sessionId` therefore always fell back to `app.active_view` and
|
||||
/// re-created the multi-agent bug.
|
||||
/// intentionally session-agnostic by design. Routing by `sessionId`
|
||||
/// would therefore always fall back to `app.active_view` and
|
||||
/// reintroduce the multi-agent bug.
|
||||
///
|
||||
/// Routing now correctly broadcasts: every agent with an open
|
||||
/// Routing broadcasts: every agent with an open
|
||||
/// extensions modal gets a per-agent debounced [`Effect::FetchMcpsList`].
|
||||
/// Per-agent coalescing keeps a second push from displacing an
|
||||
/// in-flight fetch on the same agent. Agents without an open modal
|
||||
|
||||
@@ -394,7 +394,6 @@ pub(crate) fn handle(msg: AcpClientMessage, app: &mut AppView) -> bool {
|
||||
// the turn is current.
|
||||
agent.flush_pending_follow_ups(notif_pid);
|
||||
}
|
||||
// Detect plan mode transitions from tool call completions.
|
||||
plan_mode_modal_refresh_needed |=
|
||||
detect_plan_mode_change(¬if.request.update, agent);
|
||||
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
use super::*;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Permission request handling
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Route a permission request to the agent that owns its `session_id`, queue
|
||||
/// it on that agent's view, and return whether the active view needs a redraw.
|
||||
///
|
||||
@@ -21,7 +17,6 @@ pub(super) fn handle_permission_request(
|
||||
perm: kigi_acp_lib::AcpArgs<acp::RequestPermissionRequest>,
|
||||
app: &mut AppView,
|
||||
) -> bool {
|
||||
// 1. Look up the owning agent by session_id (root or subagent view).
|
||||
let matched = match find_session_match(app, &perm.request.session_id) {
|
||||
Some(m) => m,
|
||||
None => {
|
||||
@@ -40,12 +35,12 @@ pub(super) fn handle_permission_request(
|
||||
return false;
|
||||
};
|
||||
|
||||
// 2. YOLO mode: auto-approve immediately on the owning agent so background
|
||||
// turns aren't blocked waiting for the user to switch back.
|
||||
// YOLO mode: auto-approve immediately on the owning agent so background
|
||||
// turns aren't blocked waiting for the user to switch back.
|
||||
//
|
||||
// If no `AllowOnce` option exists, falls through to
|
||||
// `enqueue_permission` even in YOLO mode (won't pick
|
||||
// `AllowAlways` by default).
|
||||
// If no `AllowOnce` option exists, falls through to
|
||||
// `enqueue_permission` even in YOLO mode (won't pick
|
||||
// `AllowAlways` by default).
|
||||
if agent.session.is_yolo()
|
||||
&& let Some(allow) = perm
|
||||
.request
|
||||
@@ -61,13 +56,13 @@ pub(super) fn handle_permission_request(
|
||||
)),
|
||||
)))
|
||||
.ok();
|
||||
return false; // no redraw needed
|
||||
return false;
|
||||
}
|
||||
|
||||
// 3. Fire notification so the user notices the pending approval.
|
||||
// Rate-limit: only fire the bell/popup on the empty→non-empty
|
||||
// transition to avoid stacking notifications during concurrent
|
||||
// permission requests.
|
||||
// Fire notification so the user notices the pending approval.
|
||||
// Rate-limit: only fire the bell/popup on the empty→non-empty
|
||||
// transition to avoid stacking notifications during concurrent
|
||||
// permission requests.
|
||||
if !app
|
||||
.notification_service
|
||||
.should_suppress_permission_notification()
|
||||
@@ -81,9 +76,9 @@ pub(super) fn handle_permission_request(
|
||||
app.notification_service.mark_permission_notified();
|
||||
}
|
||||
|
||||
// 4. Queue on the owning agent's view. Subagent provenance for display
|
||||
// is still resolved via subagent_sessions in enqueue_permission().
|
||||
// Redraw is only needed when the owning agent is currently visible.
|
||||
// Queue on the owning agent's view. Subagent provenance for display
|
||||
// is still resolved via subagent_sessions in enqueue_permission().
|
||||
// Redraw is only needed when the owning agent is currently visible.
|
||||
let needs_redraw = enqueue_permission(perm, agent);
|
||||
needs_redraw && is_active
|
||||
}
|
||||
@@ -96,7 +91,6 @@ fn enqueue_permission(
|
||||
perm: kigi_acp_lib::AcpArgs<acp::RequestPermissionRequest>,
|
||||
agent: &mut AgentView,
|
||||
) -> bool {
|
||||
// 1. Parse bash highlights from request meta (imported from kigi-shell).
|
||||
let bash_highlights: Option<BashCommandHighlights> = perm
|
||||
.request
|
||||
.meta
|
||||
@@ -107,9 +101,8 @@ fn enqueue_permission(
|
||||
.map(|h| kigi_workspace::permission::default_always_allow_scope(&h.highlighted_words))
|
||||
.unwrap_or(0);
|
||||
|
||||
// 1b. Parse MCP scope state from the `allow-always-mcp` option's meta.
|
||||
// Mutually exclusive with the bash flow at the per-request level —
|
||||
// the same prompt cannot carry both.
|
||||
// The `allow-always-mcp` option's meta is mutually exclusive with the bash
|
||||
// flow at the per-request level — the same prompt cannot carry both.
|
||||
let mcp_scope = perm
|
||||
.request
|
||||
.options
|
||||
@@ -128,35 +121,30 @@ fn enqueue_permission(
|
||||
selected: McpScope::Tool,
|
||||
});
|
||||
|
||||
// 2. Build subagent provenance label.
|
||||
// If session_id differs from the root session, look up subagent info.
|
||||
let subagent_label = resolve_subagent_label(agent, &perm.request.session_id);
|
||||
|
||||
// 3. Build title and description from the tool call.
|
||||
let (title, description, bash_command_raw) =
|
||||
build_permission_display(&perm.request, bash_highlights.as_ref());
|
||||
|
||||
// 4. Assign a monotonic ID.
|
||||
let perm_id = agent.next_perm_req_id;
|
||||
agent.next_perm_req_id += 1;
|
||||
|
||||
// 5. Stash prompt on queue transition: empty -> non-empty.
|
||||
// Do NOT stash again if the queue is already non-empty (that would
|
||||
// capture followup text from the current permission as the "original"
|
||||
// prompt, losing the user's real input).
|
||||
// Stash prompt on queue transition: empty -> non-empty.
|
||||
// Do NOT stash again if the queue is already non-empty (that would
|
||||
// capture followup text from the current permission as the "original"
|
||||
// prompt, losing the user's real input).
|
||||
if agent.permission_queue.is_empty() && agent.permission_stashed_prompt.is_none() {
|
||||
agent.permission_stashed_prompt = Some(agent.prompt.stash());
|
||||
agent.prompt.set_text("");
|
||||
}
|
||||
|
||||
// 6. Clone options before moving perm into the struct.
|
||||
let options = perm.request.options.clone();
|
||||
|
||||
// 7. Cursor preselection (sticky last-used → configured default → the
|
||||
// enable-always-approve row → index 0). See `permission_cursor`.
|
||||
// Cursor preselection (sticky last-used → configured default → the
|
||||
// enable-always-approve row → index 0). See `permission_cursor`.
|
||||
let active_idx = crate::appearance::permission_cursor::resolve_initial_cursor(&options);
|
||||
|
||||
// 8. Queue the request FIFO (do NOT replace/cancel existing requests).
|
||||
// Queue the request FIFO (do NOT replace/cancel existing requests).
|
||||
agent.permission_queue.push_back(PermissionViewState {
|
||||
request: perm,
|
||||
id: perm_id,
|
||||
@@ -182,7 +170,7 @@ fn enqueue_permission(
|
||||
// turn ended". The same field powers the dashboard relative-time label.
|
||||
agent.last_active_at = Some(std::time::Instant::now());
|
||||
|
||||
true // needs redraw
|
||||
true
|
||||
}
|
||||
|
||||
/// Build a subagent provenance label for display.
|
||||
@@ -200,20 +188,17 @@ fn enqueue_permission(
|
||||
/// Returns `None` for root session (no provenance needed).
|
||||
fn resolve_subagent_label(agent: &AgentView, session_id: &acp::SessionId) -> Option<String> {
|
||||
let sid = session_id.0.as_ref();
|
||||
// Check if this is the root session (no provenance needed).
|
||||
if let Some(ref root_sid) = agent.session.session_id
|
||||
&& root_sid.0.as_ref() == sid
|
||||
{
|
||||
return None;
|
||||
}
|
||||
// Tier 1: tracked subagent with full metadata.
|
||||
if let Some(info) = agent.subagent_sessions.get(sid) {
|
||||
return Some(format!(
|
||||
"Subagent \"{}\" ({}):",
|
||||
info.description, info.subagent_type
|
||||
));
|
||||
}
|
||||
// Tier 2: non-root session with no tracked info.
|
||||
Some("Child session (untracked):".to_string())
|
||||
}
|
||||
|
||||
@@ -364,7 +349,6 @@ fn is_edit_permission(req: &acp::RequestPermissionRequest) -> bool {
|
||||
})
|
||||
}
|
||||
|
||||
/// Cancel a permission request by sending `Cancelled` on the response channel.
|
||||
fn cancel_permission(perm: kigi_acp_lib::AcpArgs<acp::RequestPermissionRequest>) {
|
||||
perm.response_tx
|
||||
.send(Ok(acp::RequestPermissionResponse::new(
|
||||
|
||||
@@ -16,13 +16,8 @@ pub(super) enum SessionMatch {
|
||||
}
|
||||
|
||||
impl SessionMatch {
|
||||
/// The owning agent's id, regardless of variant.
|
||||
///
|
||||
/// For `Root`, this is the agent whose root session matched. For `Child`,
|
||||
/// this is the parent agent that owns the matching `subagent_views` entry.
|
||||
/// Callers that only need to look up the owning agent (without
|
||||
/// distinguishing root vs child) should use this instead of duplicating
|
||||
/// the `match { Root(id) | Child(id) => id }` pattern.
|
||||
/// The owning agent's id, regardless of variant — for `Child`, the parent
|
||||
/// agent that owns the matching `subagent_views` entry.
|
||||
pub(super) fn agent_id(self) -> AgentId {
|
||||
match self {
|
||||
SessionMatch::Root(id) | SessionMatch::Child(id) => id,
|
||||
@@ -32,9 +27,6 @@ impl SessionMatch {
|
||||
|
||||
/// Resolve the agent that owns a notification's `session_id` and whether the
|
||||
/// active view is affected.
|
||||
///
|
||||
/// Convenience wrapper around `find_session_match` + `is_matched_agent_active`
|
||||
/// + `agents.get_mut()`, used by the bg-task notification handlers.
|
||||
pub(super) fn resolve_notif_agent<'a>(
|
||||
app: &'a mut AppView,
|
||||
session_id: &acp::SessionId,
|
||||
@@ -126,16 +118,15 @@ pub(super) fn find_session_match(
|
||||
app: &AppView,
|
||||
session_id: &acp::SessionId,
|
||||
) -> Option<SessionMatch> {
|
||||
// Single pass over `app.agents`: prefer an exact root match (returned
|
||||
// immediately, since root takes precedence) but track the first child
|
||||
// match seen as a fallback used after the full scan completes.
|
||||
// Single pass over `app.agents`: an exact root match is returned
|
||||
// immediately (root takes precedence), while the first child match seen is
|
||||
// tracked as a fallback applied after the full scan — so root still wins
|
||||
// when both could match.
|
||||
//
|
||||
// Comparing `Option<&SessionId>` to `Some(&session_id)` borrows both
|
||||
// sides -- no SessionId clone. The HashMap lookup uses the inner `&str`
|
||||
// directly via the `Borrow<str>` impl on `String`, so no allocation
|
||||
// either. This preserves the previous two-pass semantics (root wins
|
||||
// when both could match) while halving the iteration cost on the hot
|
||||
// notification path.
|
||||
// either.
|
||||
let child_key: &str = session_id.0.as_ref();
|
||||
let mut child_match: Option<AgentId> = None;
|
||||
for (id, agent) in &app.agents {
|
||||
|
||||
@@ -140,7 +140,7 @@ pub(super) fn handle_settings_update(notif: &acp::ExtNotification, app: &mut App
|
||||
// shell always publishes this field from its live remote tier, so None
|
||||
// means remote settings cleared it (or an older shell that cannot deliver the
|
||||
// remote tier at all) — either way resolving without a remote value is
|
||||
// correct, and it reverts a previously cached remote enable back to the
|
||||
// correct, and it reverts a cached remote enable back to the
|
||||
// local/default (off) resolution instead of leaving Some(true) stuck
|
||||
// until restart.
|
||||
let remote = kigi_shell::util::config::RemoteSettings {
|
||||
|
||||
@@ -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