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
@@ -2,8 +2,6 @@
|
||||
|
||||
use super::*;
|
||||
|
||||
// ── agent-bound kinds (bash) ─────────
|
||||
|
||||
/// A bash command typed while a turn is RUNNING takes the
|
||||
/// server-authoritative immediate path (Effect + optimistic echo, no local
|
||||
/// queue entry).
|
||||
@@ -23,9 +21,7 @@ fn bash_while_running_is_server_authoritative() {
|
||||
}
|
||||
other => panic!("expected immediate SendBashCommand, got {other:?}"),
|
||||
};
|
||||
// Not in the local queue.
|
||||
assert_eq!(app.agents[&id].session.queue_len(), 0);
|
||||
// Optimistic echo present with kind="bash".
|
||||
let q = app
|
||||
.shared_prompt_queue("test-session")
|
||||
.expect("echo present");
|
||||
@@ -54,8 +50,8 @@ fn auth_complete_triggers_bundle_status_fetch() {
|
||||
);
|
||||
|
||||
assert!(matches!(app.auth_state, AuthState::Done));
|
||||
// Pager only refreshes the on-disk catalog snapshot; the actual
|
||||
// bundle download now runs inside the shell post-auth.
|
||||
// Pager only refreshes the on-disk catalog snapshot; the bundle
|
||||
// download runs inside the shell post-auth.
|
||||
assert!(
|
||||
effects
|
||||
.iter()
|
||||
|
||||
@@ -13,8 +13,6 @@ fn last_system_text(app: &AppView) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
// ── /usage dispatch tests ───────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn show_usage_returns_fetch_usage_effect() {
|
||||
let mut app = test_app_with_agent();
|
||||
|
||||
@@ -1398,9 +1398,9 @@ fn dashboard_plan_description_transforms_snapshot_and_chip_ranges() {
|
||||
);
|
||||
}
|
||||
|
||||
/// The sessions picker modal was removed; `/sessions` survives as an alias
|
||||
/// of `/dashboard`. It must resolve to the dashboard command and inherit
|
||||
/// the dashboard feature-flag gate (hidden by canonical name, fail-closed).
|
||||
/// `/sessions` is an alias of `/dashboard`; it must resolve to the
|
||||
/// dashboard command and inherit its feature-flag gate (hidden by
|
||||
/// canonical name, fail-closed).
|
||||
#[serial_test::serial(KIGI_AGENT_DASHBOARD)]
|
||||
#[test]
|
||||
fn dashboard_slash_sessions_aliases_dashboard() {
|
||||
@@ -1871,7 +1871,7 @@ fn dashboard_deferred_plan_mode_applied_on_session_created() {
|
||||
}
|
||||
|
||||
/// Any non-empty prompt — even a single character — dispatches a
|
||||
/// new session (the old 4-char floor was relaxed to 1 char).
|
||||
/// new session.
|
||||
#[serial_test::serial(KIGI_AGENT_DASHBOARD)]
|
||||
#[test]
|
||||
fn dashboard_dispatch_single_char_creates_session() {
|
||||
@@ -1971,10 +1971,7 @@ fn dashboard_dispatch_with_top_level_selection_creates_new_session() {
|
||||
);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Enter / Ctrl+S behaviour matrix.
|
||||
//
|
||||
// Pins the contract the user spelled out (Ctrl+S is "send + open";
|
||||
// Enter / Ctrl+S behaviour matrix (Ctrl+S is "send + open";
|
||||
// Shift/Alt+Enter insert a newline):
|
||||
//
|
||||
// button + empty prompt + Enter → Create + open detail
|
||||
@@ -1987,11 +1984,6 @@ fn dashboard_dispatch_with_top_level_selection_creates_new_session() {
|
||||
// The dispatch input always spawns a NEW session — a selected row is
|
||||
// the navigation cursor (Enter on an empty prompt opens it), never a
|
||||
// reply target.
|
||||
//
|
||||
// Tests drive the whole stack — the state handler emits an
|
||||
// action, the dispatcher runs, and we assert the resulting
|
||||
// view + selection + attached_agent.
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
/// 2 — Button focused + non-empty + Enter → new session,
|
||||
/// STAY on the dashboard, no attached_agent.
|
||||
@@ -2174,10 +2166,8 @@ fn dashboard_attach_top_level_switches_to_agent_view() {
|
||||
);
|
||||
}
|
||||
|
||||
/// Attach routes through `focus_row`, so a previously selected
|
||||
/// section header is cleared — the row and section cursors stay
|
||||
/// mutually exclusive (a bare `selected` assignment used to leave
|
||||
/// both active).
|
||||
/// Attach routes through `focus_row`, so any selected section header
|
||||
/// is cleared — the row and section cursors stay mutually exclusive.
|
||||
#[serial_test::serial(KIGI_AGENT_DASHBOARD)]
|
||||
#[test]
|
||||
fn dashboard_attach_clears_selected_section() {
|
||||
@@ -2321,11 +2311,10 @@ fn dashboard_attach_subagent_lazily_replays_deferred_transcript() {
|
||||
/// No auto-attached popup. The user reaches an agent's view by
|
||||
/// pressing Enter on its row.
|
||||
///
|
||||
/// Opening from an agent now lands in NEW-SESSION mode (the
|
||||
/// Opening from an agent lands in NEW-SESSION mode (the
|
||||
/// `[+ New Agent]` button focused, no row selected) so typing +
|
||||
/// Enter dispatches a brand new agent. Previously the dashboard
|
||||
/// pre-seeded `selected` to the came-from agent, which armed reply
|
||||
/// mode and trapped the user replying to that one agent.
|
||||
/// Enter dispatches a brand new agent, rather than pre-seeding
|
||||
/// `selected` to the came-from agent and arming reply mode.
|
||||
#[serial_test::serial(KIGI_AGENT_DASHBOARD)]
|
||||
#[test]
|
||||
fn dashboard_open_does_not_auto_attach_to_focused_agent() {
|
||||
@@ -2439,12 +2428,9 @@ fn dashboard_dispatch_after_open_from_agent_spawns_new_sessions() {
|
||||
);
|
||||
}
|
||||
|
||||
/// Opening from Welcome leaves the `[+ New Agent]`
|
||||
/// button as the default focus. Previously the dashboard
|
||||
/// seeded selection to the first agent so Enter would attach
|
||||
/// without navigating; with the button taking that role,
|
||||
/// selection stays empty and the button signals what Enter
|
||||
/// (on an empty prompt) will do.
|
||||
/// Opening from Welcome leaves the `[+ New Agent]` button as the
|
||||
/// default focus: selection stays empty, and the button signals
|
||||
/// what Enter (on an empty prompt) will do.
|
||||
#[serial_test::serial(KIGI_AGENT_DASHBOARD)]
|
||||
#[test]
|
||||
fn dashboard_open_from_welcome_focuses_new_agent_button() {
|
||||
@@ -2468,9 +2454,6 @@ fn dashboard_open_from_welcome_focuses_new_agent_button() {
|
||||
);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Dashboard mouse-wheel scrolling
|
||||
//
|
||||
// Mouse wheel is intentionally decoupled from the selected row:
|
||||
// scrolling only moves the viewport, leaving `selected` alone.
|
||||
// `DashboardState::handle_scroll` flags
|
||||
@@ -2478,7 +2461,6 @@ fn dashboard_open_from_welcome_focuses_new_agent_button() {
|
||||
// `clamp_viewport` skips its snap-to-selection pull-back so the
|
||||
// viewport can travel past the cursor. Selection-driven nav
|
||||
// (arrows, click) clears the flag and re-engages the snap.
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
#[serial_test::serial(KIGI_AGENT_DASHBOARD)]
|
||||
#[test]
|
||||
@@ -3364,7 +3346,8 @@ fn dashboard_toggle_auto_approve_flips_yolo_on_selected_agent() {
|
||||
#[serial_test::serial(KIGI_AGENT_DASHBOARD)]
|
||||
#[test]
|
||||
fn dashboard_toggle_auto_approve_with_no_selection_toasts() {
|
||||
let mut app = test_app(); // no agent
|
||||
// no agent
|
||||
let mut app = test_app();
|
||||
open_dashboard(&mut app);
|
||||
let effects = dispatch_dashboard_toggle_auto_approve(&mut app);
|
||||
assert!(effects.is_empty());
|
||||
@@ -3439,13 +3422,9 @@ fn dashboard_rename_end_to_end_top_level_row() {
|
||||
);
|
||||
}
|
||||
|
||||
/// `DashboardCancelRename` emits no effects and
|
||||
/// leaves `display_name` untouched. Previously named
|
||||
/// `dashboard_rename_cancel_via_esc_does_not_emit_effect`
|
||||
/// but that name implied Esc keystroke routing — the test
|
||||
/// actually dispatches `Action::DashboardCancelRename` directly.
|
||||
/// The Esc-keystroke routing is now pinned by the sibling test
|
||||
/// `dashboard_rename_esc_keystroke_routes_to_cancel`.
|
||||
/// `DashboardCancelRename` emits no effects and leaves `display_name`
|
||||
/// untouched. Esc-keystroke routing to this action is pinned separately
|
||||
/// by `dashboard_rename_esc_keystroke_routes_to_cancel`.
|
||||
#[serial_test::serial(KIGI_AGENT_DASHBOARD)]
|
||||
#[test]
|
||||
fn dashboard_rename_cancel_action_emits_no_effect() {
|
||||
@@ -3809,19 +3788,14 @@ fn dashboard_close_shortcuts_help_clears_modal() {
|
||||
assert!(app.dashboard.as_ref().unwrap().shortcuts_modal.is_none());
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// `[+ New Agent]` button
|
||||
//
|
||||
// The header button is the default cursor target when no row
|
||||
// is selected. Up-arrow from the first row, Esc deselect,
|
||||
// dashboard-open-from-welcome, and `reanchor_selection`-drops
|
||||
// all land here. Enter-with-empty-prompt and click both
|
||||
// dispatch `DashboardCreateNewAgentWithDetail`, which
|
||||
// creates a session AND switches into detail view. Enter
|
||||
// with a NON-empty prompt falls through to
|
||||
// `DashboardDispatch`, which creates the session but stays
|
||||
// `[+ New Agent]` button: the header button is the default cursor
|
||||
// target when no row is selected. Up-arrow from the first row, Esc
|
||||
// deselect, dashboard-open-from-welcome, and `reanchor_selection`-drops
|
||||
// all land here. Enter-with-empty-prompt and click both dispatch
|
||||
// `DashboardCreateNewAgentWithDetail`, which creates a session AND
|
||||
// switches into detail view. Enter with a NON-empty prompt falls
|
||||
// through to `DashboardDispatch`, which creates the session but stays
|
||||
// on the dashboard.
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
#[serial_test::serial(KIGI_AGENT_DASHBOARD)]
|
||||
#[test]
|
||||
@@ -3986,9 +3960,7 @@ fn dashboard_filter_state_known_token_via_dispatch() {
|
||||
));
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Real extract_recent_lines coverage.
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
/// count=0 returns empty even when scrollback has entries.
|
||||
#[test]
|
||||
@@ -4136,9 +4108,7 @@ fn extract_recent_lines_strips_ansi() {
|
||||
assert!(out[0].contains("evil"));
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Stop-confirm full lifecycle.
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
/// A press > 2s after the first re-arms (does NOT close).
|
||||
#[serial_test::serial(KIGI_AGENT_DASHBOARD)]
|
||||
@@ -4254,7 +4224,8 @@ fn dashboard_permission_select_drops_stale_request() {
|
||||
last_response_truncated: false,
|
||||
question: Some("q?".into()),
|
||||
options: vec![("allow".into(), "Allow".into())],
|
||||
request_id: Some(123), // mismatched id
|
||||
// mismatched id
|
||||
request_id: Some(123),
|
||||
reject_option: None,
|
||||
},
|
||||
));
|
||||
@@ -4687,7 +4658,8 @@ fn dashboard_question_answer_sends_and_clears() {
|
||||
let effects = dispatch_dashboard_question_answer(
|
||||
&mut app,
|
||||
crate::views::dashboard::DashboardRowId::TopLevel(AgentId(0)),
|
||||
Some(1), // pick "Postgres"
|
||||
// pick "Postgres"
|
||||
Some(1),
|
||||
String::new(),
|
||||
);
|
||||
assert!(effects.is_empty());
|
||||
@@ -4745,7 +4717,8 @@ fn dashboard_question_answer_walks_multiple_questions() {
|
||||
let fields = compute_peek_fields(&row, &app.agents).expect("ask surfaced");
|
||||
assert!(fields.question.as_deref().unwrap().starts_with("(1/2)"));
|
||||
assert!(fields.request_id.is_none());
|
||||
assert_eq!(fields.reject_option, Some(2)); // 2 options + "Other"
|
||||
// 2 options + "Other"
|
||||
assert_eq!(fields.reject_option, Some(2));
|
||||
// Plant a peek with a stale draft to verify the advance reset.
|
||||
if let Some(d) = app.dashboard.as_mut() {
|
||||
let mut p = PeekPanelState::new(row.clone(), fields);
|
||||
@@ -4787,7 +4760,8 @@ fn dashboard_peek_auto_opens_for_selected_row() {
|
||||
let mut app = test_app_with_agent();
|
||||
mark_agent_nonempty(&mut app, AgentId(0));
|
||||
open_dashboard(&mut app);
|
||||
let area = Rect::new(0, 0, 80, 24); // tall enough for the peek
|
||||
// tall enough for the peek
|
||||
let area = Rect::new(0, 0, 80, 24);
|
||||
let reg = crate::actions::ActionRegistry::defaults();
|
||||
|
||||
// Select a row, then render → the peek opens by default.
|
||||
@@ -4972,7 +4946,7 @@ fn build_rows_keeps_pinned_empty_local_session() {
|
||||
assert_eq!(rows.len(), 1, "a pinned empty session is kept");
|
||||
}
|
||||
|
||||
// -- Conversation-origin roster rows (chat-mode dashboard fallback) ----
|
||||
// Conversation-origin roster rows (chat-mode dashboard fallback).
|
||||
|
||||
#[test]
|
||||
fn dashboard_attach_roster_focuses_existing_local_agent() {
|
||||
|
||||
@@ -373,7 +373,6 @@ fn authenticating_seq(app: &AppView) -> u64 {
|
||||
ref other => panic!("expected Authenticating, got {other:?}"),
|
||||
}
|
||||
}
|
||||
/// Extract text from the last system message in an agent's scrollback.
|
||||
fn last_system_text(app: &AppView, id: AgentId) -> String {
|
||||
system_text_from_end(app, id, 0)
|
||||
}
|
||||
@@ -648,8 +647,6 @@ fn open_session_picker_with(
|
||||
pending_delete: None,
|
||||
});
|
||||
}
|
||||
/// Toast strings match the expected format and contain on/off
|
||||
/// status.
|
||||
fn read_toast(app: &AppView) -> String {
|
||||
let agent = app.agents.get(&AgentId(0)).expect("agent must exist");
|
||||
agent
|
||||
@@ -748,7 +745,6 @@ fn agent_scrollback_len(app: &AppView) -> usize {
|
||||
app.agents.get(&AgentId(0)).unwrap().scrollback.len()
|
||||
}
|
||||
use crate::scrollback::blocks::UserPromptBlock;
|
||||
/// Helper: open the dashboard against an existing `app`.
|
||||
fn open_dashboard(app: &mut AppView) {
|
||||
let _ = dispatch_open_dashboard(app);
|
||||
}
|
||||
@@ -792,12 +788,10 @@ fn dashboard_row_order(app: &AppView) -> Vec<crate::views::dashboard::DashboardR
|
||||
/// Build a synthetic `PermissionViewState` with the given id and
|
||||
/// options. Pushes it to the agent's permission_queue.
|
||||
///
|
||||
/// Returns the response receiver so tests can verify
|
||||
/// the response was actually `send`'d through the oneshot. The
|
||||
/// previous version dropped the receiver (`_rx`), which let
|
||||
/// "happy-path" tests assert the queue was popped but masked
|
||||
/// regressions where the pop happened without the corresponding
|
||||
/// send.
|
||||
/// Returns the response receiver so tests can verify the response
|
||||
/// was actually `send`'d through the oneshot — a dropped receiver
|
||||
/// would let "happy-path" tests assert the queue was popped while
|
||||
/// masking regressions where the pop happens without a send.
|
||||
fn push_synthetic_permission(
|
||||
agent: &mut crate::app::agent_view::AgentView,
|
||||
id: usize,
|
||||
|
||||
@@ -147,8 +147,6 @@ fn accept_word_select_tip_no_op_when_tip_not_showing() {
|
||||
);
|
||||
}
|
||||
|
||||
// ── /plan slash command tests ─────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn slash_plan_no_args_not_in_plan_enters_plan_mode() {
|
||||
let mut app = test_app_with_agent();
|
||||
@@ -296,7 +294,6 @@ fn set_plan_mode_mutates_only_active_agent_not_others() {
|
||||
);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// `set_yolo_mode` dispatcher unit tests (security-relevant)
|
||||
//
|
||||
// SHELL-owned, but with rollback semantics: a disk-write failure
|
||||
@@ -337,7 +334,6 @@ fn set_plan_mode_mutates_only_active_agent_not_others() {
|
||||
// - Failure toast: "✗ Could not save permission_mode: {error}"
|
||||
// — exact format pinned via `assert_eq!` in
|
||||
// `rollback_permission_mode_reverts_state_no_effect`.
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
/// Slash gate sync: both toggles stay offered while modes change; only the
|
||||
/// auto feature gate suppresses `/auto`.
|
||||
@@ -453,10 +449,8 @@ fn set_yolo_mode_off_to_on_emits_persist_with_rollback() {
|
||||
crate::app::actions::PermissionModePersist::WithRollback("ask"),
|
||||
"rollback must revert to the prior canonical (was 'ask')"
|
||||
);
|
||||
// Explicit session_id assertion
|
||||
// (previously hidden behind `..` — a regression that
|
||||
// dropped session_id silently broke the ACP
|
||||
// notification gate at effects.rs).
|
||||
// If dropped, session_id silently breaks the ACP
|
||||
// notification gate at effects.rs.
|
||||
assert!(
|
||||
session_id.is_some(),
|
||||
"session_id must be threaded through for ACP notification gating"
|
||||
@@ -892,7 +886,7 @@ fn set_yolo_mode_on_drains_multi_item_queue() {
|
||||
Ok(Ok(acp::RequestPermissionResponse {
|
||||
outcome: acp::RequestPermissionOutcome::Selected(_),
|
||||
..
|
||||
})) => {} // OK
|
||||
})) => {}
|
||||
other => panic!(
|
||||
"item {i} did not receive AllowOnce Selected response: {other:?} — \
|
||||
drain skipped items beyond the first?",
|
||||
@@ -970,7 +964,7 @@ fn set_yolo_mode_on_duplicate_dispatch_still_drains_queue() {
|
||||
Ok(Ok(acp::RequestPermissionResponse {
|
||||
outcome: acp::RequestPermissionOutcome::Selected(_),
|
||||
..
|
||||
})) => {} // OK
|
||||
})) => {}
|
||||
other => panic!(
|
||||
"duplicate dispatch must auto-approve the newly queued permission, got {other:?}",
|
||||
),
|
||||
@@ -1377,7 +1371,8 @@ fn cycle_mode_pre_session_normal_to_plan_does_not_persist_permission_mode() {
|
||||
/// mutation are all gated by the same `app.active_view` guard).
|
||||
#[test]
|
||||
fn set_yolo_mode_no_op_when_no_active_agent() {
|
||||
let mut app = test_app(); // no agent, active_view = Welcome
|
||||
// No agent, active_view = Welcome.
|
||||
let mut app = test_app();
|
||||
let default_yolo_before = app.default_yolo;
|
||||
let perm_mode_before = app.current_ui.permission_mode.clone();
|
||||
|
||||
@@ -1441,7 +1436,6 @@ fn set_yolo_mode_refreshes_open_modal_snapshots() {
|
||||
);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Dispatch-layer integration tests for
|
||||
// `Action::SetPermissionMode(kind)`.
|
||||
//
|
||||
@@ -1472,7 +1466,6 @@ fn set_yolo_mode_refreshes_open_modal_snapshots() {
|
||||
// - `apply_setting_rollback("permission_mode", Enum("default"))`
|
||||
// restores `current_ui.permission_mode = Some("default")`
|
||||
// (preserves canonical; doesn't re-emit any Effect).
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn set_permission_mode_default_overrides_canonical_to_default() {
|
||||
@@ -2130,14 +2123,12 @@ fn set_theme_auto_enables_auto_mode_and_persists_auto() {
|
||||
});
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────
|
||||
// set_plan_mode dispatch-level coverage.
|
||||
//
|
||||
// Mirrors the `coding_data_sharing` and `yolo` test
|
||||
// patterns. These exercise the dispatch path directly (not the
|
||||
// modal Enter path or the slash-command parser path), so they cover
|
||||
// the same plumbing every entry point ultimately funnels through.
|
||||
// ────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Idempotent ON: dispatcher sees `prev == new`,
|
||||
/// toasts but emits NO Effect (saves a wasted ACP round-trip).
|
||||
|
||||
@@ -2,19 +2,15 @@
|
||||
|
||||
use super::*;
|
||||
|
||||
/// `ConfirmResetSetting
|
||||
/// { Reset }` on `permission_mode` (the security-critical SHELL Enum)
|
||||
/// dispatches `Action::SetPermissionMode(PermissionModeKind::Ask)`
|
||||
/// (the typed Action, per the modal-commit ↔ typed-setter
|
||||
/// rule) via recursive dispatch. Emits
|
||||
/// `Effect::PersistPermissionMode` — verifies the recursive
|
||||
/// dispatch reaches the YOLO pipeline through
|
||||
/// `set_permission_mode` rather than the legacy `set_yolo_mode`.
|
||||
/// Resetting `permission_mode` must dispatch the typed
|
||||
/// `Action::SetPermissionMode(PermissionModeKind::Ask)` via recursive
|
||||
/// dispatch, reaching the YOLO pipeline through `set_permission_mode`
|
||||
/// rather than the legacy `set_yolo_mode`.
|
||||
#[test]
|
||||
fn dispatch_confirm_reset_setting_reset_dispatches_set_permission_mode_for_permission_mode() {
|
||||
use crate::views::modal::ResetSettingsResult;
|
||||
let mut app = test_app_with_agent();
|
||||
// Flip yolo on first (default is OFF = "ask").
|
||||
// Default is OFF ("ask"); flip yolo on first so the reset is a real transition.
|
||||
let _ = dispatch(Action::SetYoloMode(true), &mut app);
|
||||
assert!(app.agents[&AgentId(0)].session.is_yolo());
|
||||
|
||||
@@ -36,7 +32,6 @@ fn dispatch_confirm_reset_setting_reset_dispatches_set_permission_mode_for_permi
|
||||
has_persist,
|
||||
"Reset of permission_mode must emit PersistPermissionMode, got {effects:?}",
|
||||
);
|
||||
// Agent's yolo flag is reset to default (off).
|
||||
assert!(
|
||||
!app.agents[&AgentId(0)].session.is_yolo(),
|
||||
"agent.session.yolo_mode must be reset to default (off)",
|
||||
@@ -105,7 +100,6 @@ fn set_yolo_mode_on_drains_permission_queue_with_allow_once() {
|
||||
|
||||
let _ = dispatch(Action::SetYoloMode(true), &mut app);
|
||||
|
||||
// Queue is drained.
|
||||
assert!(
|
||||
app.agents[&AgentId(0)].permission_queue.is_empty(),
|
||||
"YOLO ON must drain the permission_queue",
|
||||
@@ -231,7 +225,6 @@ fn set_permission_mode_always_approve_blocked_by_policy_pin() {
|
||||
assert_eq!(app.current_ui.permission_mode.as_deref(), Some("ask"));
|
||||
}
|
||||
|
||||
/// SetPermissionMode(Auto) persists auto and does not enable yolo.
|
||||
#[test]
|
||||
fn set_permission_mode_auto_persists_without_yolo() {
|
||||
use crate::app::actions::PermissionModeKind;
|
||||
@@ -283,27 +276,16 @@ fn set_permission_mode_auto_degrades_to_ask_when_gated_off() {
|
||||
);
|
||||
}
|
||||
|
||||
/// Rollback with an unknown canonical: defensively defaults to
|
||||
/// "ask" (the safe fallback — fewer prompts on a corrupt
|
||||
/// rollback value is worse, more prompts is safer).
|
||||
///
|
||||
/// The previous docstring claimed "logs a
|
||||
/// warning and defaults to 'ask'" — the warning log is fired via
|
||||
/// `tracing::warn!` in `apply_setting_rollback`'s arm, but the
|
||||
/// test doesn't capture/assert it. The fix is documentary: the
|
||||
/// test pins the OBSERVABLE behaviour (state defaults to "ask")
|
||||
/// and acknowledges that the warn-log is best-effort visibility
|
||||
/// for developers, not a contract surface the test enforces.
|
||||
/// `tracing_test::traced_test` capture would be more rigorous
|
||||
/// but is not currently used in this crate.
|
||||
/// Rollback with an unknown canonical defensively defaults to "ask" (the
|
||||
/// safe fallback — fewer prompts on a corrupt rollback value is worse than
|
||||
/// more prompts). `apply_setting_rollback` also fires `tracing::warn!` for
|
||||
/// this case, but the test only pins the observable state, not the log.
|
||||
#[test]
|
||||
fn rollback_permission_mode_unknown_canonical_defaults_to_ask() {
|
||||
use crate::settings::SettingValue;
|
||||
let mut app = test_app_with_agent();
|
||||
// Pre-set to true.
|
||||
let _ = dispatch(Action::SetYoloMode(true), &mut app);
|
||||
|
||||
// Garbage canonical rolls back to "ask" (the safe default).
|
||||
let _ = dispatch(
|
||||
Action::TaskComplete(TaskResult::SettingPersistFailed {
|
||||
key: "permission_mode",
|
||||
@@ -318,31 +300,23 @@ fn rollback_permission_mode_unknown_canonical_defaults_to_ask() {
|
||||
"unknown canonical → safe default (ask = no auto-approve)",
|
||||
);
|
||||
assert_eq!(app.current_ui.permission_mode.as_deref(), Some("ask"));
|
||||
// The failure toast is the standard
|
||||
// `✗ Could not save permission_mode: …` format. A future
|
||||
// enhancement could differentiate "schema corruption" from
|
||||
// "real disk failure" in the toast text, but currently the
|
||||
// user sees the same wording; pinned here so a future
|
||||
// divergence is intentional.
|
||||
// The toast text does not currently distinguish schema corruption from
|
||||
// a real disk failure; both produce the standard
|
||||
// `✗ Could not save permission_mode: …` wording.
|
||||
}
|
||||
|
||||
/// Rollback path refreshes open modal
|
||||
/// snapshots in the same way the success path does. Mirror of
|
||||
/// `set_yolo_mode_refreshes_open_modal_snapshots` for the
|
||||
/// `apply_setting_rollback` entry into `set_yolo_mode_inner`.
|
||||
/// Without this, a modal that's open when a disk write fails
|
||||
/// shows a stale "always-approve" indicator after the state
|
||||
/// has rolled back to "ask".
|
||||
/// The rollback path (`apply_setting_rollback` into `set_yolo_mode_inner`)
|
||||
/// must refresh open modal snapshots the same way the success path does.
|
||||
/// Without this, a modal open when a disk write fails would keep showing a
|
||||
/// stale "always-approve" indicator after the state rolls back to "ask".
|
||||
#[test]
|
||||
fn rollback_permission_mode_refreshes_open_modal_snapshots() {
|
||||
use crate::settings::SettingValue;
|
||||
use crate::views::modal::ActiveModal;
|
||||
|
||||
let mut app = test_app_with_agent();
|
||||
// Pre-set yolo=true via the typed setter so the rollback
|
||||
// captures real prior state.
|
||||
let _ = dispatch(Action::SetYoloMode(true), &mut app);
|
||||
// Open the modal AFTER the optimistic toggle so the open-time
|
||||
// Open the modal after the optimistic toggle so the open-time
|
||||
// snapshot reflects yolo=true.
|
||||
let _ = dispatch(Action::OpenSettings, &mut app);
|
||||
let agent = app.agents.get(&AgentId(0)).unwrap();
|
||||
@@ -354,7 +328,6 @@ fn rollback_permission_mode_refreshes_open_modal_snapshots() {
|
||||
"pre-rollback snapshot reflects optimistic state (yolo=true)",
|
||||
);
|
||||
|
||||
// Simulate disk-write failure → rollback to "ask".
|
||||
let _ = dispatch(
|
||||
Action::TaskComplete(TaskResult::SettingPersistFailed {
|
||||
key: "permission_mode",
|
||||
@@ -364,7 +337,6 @@ fn rollback_permission_mode_refreshes_open_modal_snapshots() {
|
||||
&mut app,
|
||||
);
|
||||
|
||||
// The modal's snapshot MUST refresh to the rolled-back value.
|
||||
let agent = app.agents.get(&AgentId(0)).unwrap();
|
||||
let Some(ActiveModal::Settings { state }) = &agent.active_modal else {
|
||||
panic!("modal must stay open after rollback");
|
||||
@@ -395,9 +367,9 @@ fn set_permission_mode_ask_emits_brand_consistent_toast() {
|
||||
assert!(!app.agents[&AgentId(0)].session.is_yolo());
|
||||
assert_eq!(app.current_ui.permission_mode.as_deref(), Some("ask"));
|
||||
|
||||
// Toast brands as "Permission mode" not
|
||||
// "Always-approve". Previously the Ask arm reused `yolo_toast(false)`
|
||||
// which produced "✓ Always-approve: off" — a brand mismatch.
|
||||
// Toast must brand as "Permission mode", not "Always-approve" — the Ask
|
||||
// arm must not fall back to `yolo_toast(false)`, which produces the
|
||||
// wrong brand.
|
||||
let toast = app.agents[&AgentId(0)]
|
||||
.toast
|
||||
.as_ref()
|
||||
@@ -426,14 +398,12 @@ fn set_permission_mode_ask_emits_brand_consistent_toast() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Regression test. A `--yolo`
|
||||
/// startup sets `agent.session.yolo_mode = true` but leaves
|
||||
/// `app.current_ui.permission_mode` at `None`. Without the
|
||||
/// LIVE-precedence capture, dispatching `SetPermissionMode(Default)`
|
||||
/// would produce `WithRollback("ask")` — diverging the pager from
|
||||
/// the shell on disk failure (the ACP suppress-on-failure gate
|
||||
/// keeps the shell at YOLO, but the pager would roll back to
|
||||
/// non-YOLO). This test pins the LIVE-precedence fix.
|
||||
/// A `--yolo` startup sets `agent.session.yolo_mode = true` but leaves
|
||||
/// `app.current_ui.permission_mode` at `None`. Without LIVE-precedence
|
||||
/// capture, dispatching `SetPermissionMode(Default)` would produce
|
||||
/// `WithRollback("ask")` — diverging the pager from the shell on disk
|
||||
/// failure (the ACP suppress-on-failure gate keeps the shell at YOLO, but
|
||||
/// the pager would roll back to non-YOLO).
|
||||
#[test]
|
||||
fn set_permission_mode_with_live_yolo_and_no_ui_mirror_rolls_back_to_always_approve() {
|
||||
use crate::app::actions::PermissionModeKind;
|
||||
@@ -450,8 +420,6 @@ fn set_permission_mode_with_live_yolo_and_no_ui_mirror_rolls_back_to_always_appr
|
||||
&mut app,
|
||||
);
|
||||
|
||||
// The dispatch flipped yolo off (Default projects onto
|
||||
// bool=false) and set the canonical to "default".
|
||||
assert!(!app.agents[&AgentId(0)].session.is_yolo());
|
||||
assert_eq!(app.current_ui.permission_mode.as_deref(), Some("default"));
|
||||
|
||||
@@ -473,17 +441,13 @@ fn set_permission_mode_with_live_yolo_and_no_ui_mirror_rolls_back_to_always_appr
|
||||
}
|
||||
}
|
||||
|
||||
/// `apply_setting_rollback("permission_mode",
|
||||
/// Enum("default"))` — the rollback arm that preserves the
|
||||
/// "default" canonical through a failed-persist. The headline
|
||||
/// architectural contract: rolling back to "default" must NOT
|
||||
/// collapse onto "ask" via the inner's bool projection.
|
||||
/// `apply_setting_rollback("permission_mode", Enum("default"))` must
|
||||
/// preserve the "default" canonical through a failed-persist, not collapse
|
||||
/// onto "ask" via the inner's bool projection.
|
||||
#[test]
|
||||
fn rollback_permission_mode_default_canonical_preserves_default() {
|
||||
use crate::settings::SettingValue;
|
||||
let mut app = test_app_with_agent();
|
||||
// Pre-flip to YOLO so the rollback has somewhere to roll
|
||||
// back FROM.
|
||||
let _ = dispatch(Action::SetYoloMode(true), &mut app);
|
||||
assert!(app.agents[&AgentId(0)].session.is_yolo());
|
||||
assert_eq!(
|
||||
@@ -491,8 +455,6 @@ fn rollback_permission_mode_default_canonical_preserves_default() {
|
||||
Some("always-approve"),
|
||||
);
|
||||
|
||||
// Simulate disk-write failure with `rollback_value =
|
||||
// Enum("default")`.
|
||||
let effects = dispatch(
|
||||
Action::TaskComplete(TaskResult::SettingPersistFailed {
|
||||
key: "permission_mode",
|
||||
@@ -509,7 +471,6 @@ fn rollback_permission_mode_default_canonical_preserves_default() {
|
||||
"rollback path must not re-emit Effects, got {effects:?}",
|
||||
);
|
||||
|
||||
// Yolo flipped to false (Default projects onto bool=false).
|
||||
assert!(
|
||||
!app.agents[&AgentId(0)].session.is_yolo(),
|
||||
"Default projects onto yolo=false; agent.session.yolo_mode must flip back",
|
||||
@@ -526,7 +487,6 @@ fn rollback_permission_mode_default_canonical_preserves_default() {
|
||||
);
|
||||
}
|
||||
|
||||
/// Non-empty permission_queue → NeedsInput.
|
||||
#[test]
|
||||
fn classify_top_level_permission_queue_non_empty_is_needs_input() {
|
||||
use crate::views::dashboard::{RowState, classify_top_level};
|
||||
|
||||
@@ -110,8 +110,6 @@ fn show_undo_tip_no_op_when_flag_off() {
|
||||
assert!(!app.agents[&id].ephemeral_tip.is_active());
|
||||
}
|
||||
|
||||
// ── Small-screen tip (`show_small_screen_tip` + its one-shot trigger) ──
|
||||
|
||||
/// `show_small_screen_tip` on a drawable agent shows the tip and increments
|
||||
/// the per-session seen count in memory (nothing persisted — the fn returns
|
||||
/// nothing, so it cannot raise effects).
|
||||
@@ -557,9 +555,9 @@ fn chip_submit_while_enqueued_clears_follow_up_chips() {
|
||||
// A chip click submitted while a turn is RUNNING *and* the local queue
|
||||
// is non-empty takes the ENQUEUE path, not immediate-server-send:
|
||||
// `immediate_server_send_eligible` is false whenever `pending_prompts`
|
||||
// is non-empty. Before the fix, only the immediate-send branch cleared
|
||||
// the chips, so this path left them on screen after the user had already
|
||||
// acted on one. The clear now runs for every `SubmitFollowUp` path.
|
||||
// is non-empty. The chip clear must run on this path too, not just the
|
||||
// immediate-send branch, or the chips would linger on screen after the
|
||||
// user had already acted on one.
|
||||
let mut app = test_app_with_agent();
|
||||
let id = AgentId(0);
|
||||
{
|
||||
@@ -586,7 +584,7 @@ fn chip_submit_while_enqueued_clears_follow_up_chips() {
|
||||
.any(|e| matches!(e, Effect::SendPrompt { text, .. } if text == "Summarize")),
|
||||
"chip must be enqueued, not immediate-sent, got {effects:?}"
|
||||
);
|
||||
// The chips are cleared on the enqueue path too (the bug fix).
|
||||
// The chips are cleared on the enqueue path too.
|
||||
assert!(
|
||||
app.agents[&id].follow_ups.is_none(),
|
||||
"enqueue chip path must clear chips"
|
||||
@@ -597,7 +595,6 @@ fn chip_submit_while_enqueued_clears_follow_up_chips() {
|
||||
fn send_prompt_while_running_queues_without_drain() {
|
||||
let mut app = test_app_with_agent();
|
||||
let id = AgentId(0);
|
||||
// Simulate a running turn.
|
||||
app.agents.get_mut(&id).unwrap().session.state = AgentState::TurnRunning;
|
||||
|
||||
let effects = dispatch(Action::SendPrompt("queued".into()), &mut app);
|
||||
@@ -1689,8 +1686,6 @@ fn bash_while_idle_stays_on_local_path() {
|
||||
assert!(app.agents[&id].bash_turn);
|
||||
}
|
||||
|
||||
// ── Reconnect-pending dispatch guards ─────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn send_prompt_blocked_during_reconnect() {
|
||||
let mut app = test_app_with_agent();
|
||||
@@ -2049,7 +2044,8 @@ fn prompt_history_loaded_refreshes_open_history_search_with_current_query() {
|
||||
fn agent_send_before_paste_probe_keeps_image() {
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||
let mut app = test_app_with_agent();
|
||||
app.project_picker_shown = true; // don't intercept the send with the picker
|
||||
// don't intercept the send with the picker
|
||||
app.project_picker_shown = true;
|
||||
let id = AgentId(0);
|
||||
{
|
||||
let agent = app.agents.get_mut(&id).unwrap();
|
||||
@@ -2131,7 +2127,8 @@ fn interject_before_paste_probe_keeps_image() {
|
||||
let id = AgentId(0);
|
||||
{
|
||||
let agent = app.agents.get_mut(&id).unwrap();
|
||||
agent.session.state = AgentState::TurnRunning; // interject needs a live turn
|
||||
// interject needs a live turn
|
||||
agent.session.state = AgentState::TurnRunning;
|
||||
agent.set_active_pane(ActivePane::Prompt, true);
|
||||
agent.prompt.set_text("look at this");
|
||||
}
|
||||
@@ -2215,7 +2212,8 @@ fn interject_before_paste_probe_keeps_image() {
|
||||
#[test]
|
||||
fn agent_paste_completion_after_switch_does_not_send_to_other_agent() {
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||
let mut app = test_app_with_agent(); // agent A = AgentId(0), active view
|
||||
// agent A = AgentId(0), active view
|
||||
let mut app = test_app_with_agent();
|
||||
app.project_picker_shown = true;
|
||||
let a = AgentId(0);
|
||||
let b = AgentId(1);
|
||||
@@ -2351,8 +2349,6 @@ fn slash_and_exit_input_does_not_trigger_project_picker() {
|
||||
assert!(!input_can_trigger_project_picker(" "));
|
||||
}
|
||||
|
||||
// ── Minimal-mode slash gate tests ───────────────────────────────────
|
||||
|
||||
/// Returns true if any system block in agent 0's scrollback contains
|
||||
/// `needle`. Avoids `last_system_text`'s "last block must be System" panic
|
||||
/// for the allowed-command control (which may leave no system block).
|
||||
@@ -2409,8 +2405,6 @@ fn minimal_mode_allows_mode_agnostic_slash_command() {
|
||||
);
|
||||
}
|
||||
|
||||
// ── /queue (ShowQueue) dispatch tests ───────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn show_queue_empty_commits_empty_message() {
|
||||
let mut app = test_app_with_agent();
|
||||
@@ -2447,8 +2441,6 @@ fn show_queue_no_active_agent_is_noop() {
|
||||
assert!(effects.is_empty(), "ShowQueue without an agent is a no-op");
|
||||
}
|
||||
|
||||
// ── Send-now cancel marker suppression (PromptResponse rail) ────────
|
||||
|
||||
/// Count of "Turn cancelled by user …" marker blocks in the agent's scrollback.
|
||||
fn count_cancelled_markers(app: &AppView, id: AgentId) -> usize {
|
||||
let agent = &app.agents[&id];
|
||||
|
||||
@@ -437,7 +437,7 @@ fn slash_model_invalid_arg_produces_scrollback_error() {
|
||||
/// `/model` + Enter (required args missing) must NOT error into scrollback —
|
||||
/// it re-opens the prompt in the args phase so the existing dropdown lists
|
||||
/// the catalog (= the connected providers' models). This is the documented
|
||||
/// "Blocks" row of `is_command_complete`, which previously had no consumer.
|
||||
/// "Blocks" row of `is_command_complete`.
|
||||
#[test]
|
||||
fn slash_model_no_args_reopens_the_model_picker() {
|
||||
let mut app = test_app_with_agent();
|
||||
@@ -1183,9 +1183,7 @@ fn find_agent_by_session_id_finds_inactive_agent() {
|
||||
/// Cross-setting smoke test.
|
||||
/// Verifies that the dispatcher routes each Action to the
|
||||
/// correct setter (catches a copy-paste registration bug
|
||||
/// where two setters were swapped). The original 5-setting
|
||||
/// matrix shrank to 2 after the user-feedback drop of
|
||||
/// `session_picker_grouped` / `load_envrc` / `use_leader`.
|
||||
/// where two setters were swapped).
|
||||
#[test]
|
||||
fn pr13_each_setter_writes_to_its_own_mirror() {
|
||||
let mut app = test_app_with_agent();
|
||||
@@ -1201,9 +1199,8 @@ fn pr13_each_setter_writes_to_its_own_mirror() {
|
||||
/// Three-way alignment pin: the PAGER registry default must agree
|
||||
/// with `PagerLocalSnapshot::default()` (covered by
|
||||
/// `defaults_match_pager_state` in `registry::tests`) AND with
|
||||
/// `AgentView::new`'s runtime initializer. This is the third leg
|
||||
/// of the triangle that was previously missing — the
|
||||
/// registry test alone can't see `AgentView::new`'s constant.
|
||||
/// `AgentView::new`'s runtime initializer — the registry test alone
|
||||
/// can't see `AgentView::new`'s constant.
|
||||
#[test]
|
||||
fn pager_registry_default_matches_agent_view_new_initializer() {
|
||||
use crate::settings::{SettingKind, SettingOwner, SettingsRegistry};
|
||||
@@ -1267,9 +1264,9 @@ fn pager_registry_default_matches_agent_view_new_initializer() {
|
||||
}
|
||||
}
|
||||
/// If the user picks the regular "Yes, proceed" option (NOT
|
||||
/// enable-always-approve), the dispatcher must behave exactly as
|
||||
/// before — no PersistPermissionMode effect, no YOLO flip. Pins
|
||||
/// that the new code path is gated strictly on the id check.
|
||||
/// enable-always-approve), the dispatcher must emit no
|
||||
/// PersistPermissionMode effect and no YOLO flip: the
|
||||
/// always-approve path is gated strictly on the id check.
|
||||
#[test]
|
||||
fn regular_allow_once_does_not_trigger_always_approve_persist() {
|
||||
use std::sync::Arc;
|
||||
@@ -1404,7 +1401,6 @@ fn show_tasks_no_active_agent_is_noop() {
|
||||
let effects = dispatch(Action::ShowTasks, &mut app);
|
||||
assert!(effects.is_empty(), "ShowTasks without an agent is a no-op");
|
||||
}
|
||||
/// classify_top_level decision matrix.
|
||||
#[test]
|
||||
fn classify_top_level_branches() {
|
||||
use crate::views::dashboard::{RowState, classify_top_level};
|
||||
@@ -1563,7 +1559,6 @@ fn peek_label_reflects_last_response_type() {
|
||||
.push_block(RenderBlock::user_prompt("do it"));
|
||||
assert_eq!(extract_last_response_type(agent), "Idle");
|
||||
}
|
||||
/// agent.question_view.is_some() → NeedsInput.
|
||||
#[test]
|
||||
fn classify_top_level_question_view_some_is_needs_input() {
|
||||
use crate::views::dashboard::{RowState, classify_top_level};
|
||||
@@ -1602,7 +1597,6 @@ fn top_level_label_strips_control_characters() {
|
||||
);
|
||||
assert!(top.label.contains("evil"));
|
||||
}
|
||||
/// Build a synthetic MouseEvent for tests.
|
||||
fn mouse_event(
|
||||
kind: crossterm::event::MouseEventKind,
|
||||
col: u16,
|
||||
@@ -1615,11 +1609,6 @@ fn mouse_event(
|
||||
modifiers: crossterm::event::KeyModifiers::NONE,
|
||||
}
|
||||
}
|
||||
/// Left-click on a row selects it (single click).
|
||||
/// Single left-click on a row attaches the
|
||||
/// conversation immediately (was: selects only, required
|
||||
/// double-click to attach). The user explicitly reported the
|
||||
/// previous click-to-select behaviour as unresponsive.
|
||||
#[serial_test::serial(KIGI_AGENT_DASHBOARD)]
|
||||
#[test]
|
||||
fn mouse_left_click_attaches_immediately() {
|
||||
@@ -1645,11 +1634,8 @@ fn mouse_left_click_attaches_immediately() {
|
||||
}
|
||||
assert_eq!(d.selected, Some(id));
|
||||
}
|
||||
/// Every left-click attaches, including
|
||||
/// rapid repeated clicks. The previous design used a 500ms window
|
||||
/// to distinguish single (select) from double (attach) click;
|
||||
/// the new design makes every click attach so the user's mental
|
||||
/// model "click = open" always holds.
|
||||
/// There is no double-click window — every click attaches, so the
|
||||
/// user's mental model "click = open" always holds.
|
||||
#[serial_test::serial(KIGI_AGENT_DASHBOARD)]
|
||||
#[test]
|
||||
fn mouse_repeated_click_keeps_attaching() {
|
||||
@@ -1682,9 +1668,8 @@ fn mouse_repeated_click_keeps_attaching() {
|
||||
other => panic!("expected DashboardAttach on second click, got {other:?}"),
|
||||
}
|
||||
}
|
||||
/// Clicks after the previous 500ms-double-click
|
||||
/// window also attach (the previous test asserted single-click
|
||||
/// behaviour for >500ms-apart clicks; now every click attaches).
|
||||
/// Clicks more than 500ms apart also attach — there is no
|
||||
/// double-click window.
|
||||
#[serial_test::serial(KIGI_AGENT_DASHBOARD)]
|
||||
#[test]
|
||||
fn mouse_click_after_long_pause_still_attaches() {
|
||||
@@ -1715,7 +1700,6 @@ fn mouse_click_after_long_pause_still_attaches() {
|
||||
other => panic!("expected DashboardAttach, got {other:?}"),
|
||||
}
|
||||
}
|
||||
/// Click on the peek close-button rect closes the peek.
|
||||
#[serial_test::serial(KIGI_AGENT_DASHBOARD)]
|
||||
#[test]
|
||||
fn mouse_click_on_peek_close_rect_clears_peek() {
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
use super::*;
|
||||
|
||||
// ── Worktree session tests ───────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn open_new_worktree_dialog_sets_dialog_state() {
|
||||
let mut app = test_app();
|
||||
@@ -26,7 +24,6 @@ fn worktree_forked_sets_session_id_eagerly_and_emits_load() {
|
||||
);
|
||||
let id = AgentId(0);
|
||||
|
||||
// Before WorktreeForked: session_id is None, loading_replay is false.
|
||||
assert!(app.agents[&id].session.session_id.is_none());
|
||||
assert!(!app.agents[&id].session.loading_replay);
|
||||
|
||||
@@ -45,21 +42,17 @@ fn worktree_forked_sets_session_id_eagerly_and_emits_load() {
|
||||
&mut app,
|
||||
);
|
||||
|
||||
// session_id set eagerly.
|
||||
assert_eq!(
|
||||
app.agents[&id].session.session_id,
|
||||
Some(acp::SessionId::new("forked-sess-1"))
|
||||
);
|
||||
// loading_replay enabled so UI suppresses redraws during replay.
|
||||
assert!(app.agents[&id].session.loading_replay);
|
||||
// CWD updated to worktree.
|
||||
assert_eq!(app.agents[&id].session.cwd, session_cwd);
|
||||
assert!(app.agents[&id].session.is_worktree);
|
||||
// Emits LoadSession effect.
|
||||
assert_eq!(effects.len(), 1);
|
||||
assert!(matches!(&effects[0], Effect::LoadSession { session_id, .. }
|
||||
if session_id == "forked-sess-1"));
|
||||
// Scrollback has the "Worktree ready" message.
|
||||
assert!(!app.agents[&id].scrollback.is_empty());
|
||||
}
|
||||
|
||||
@@ -93,13 +86,11 @@ fn worktree_forked_with_restore_shows_summary_in_scrollback() {
|
||||
&mut app,
|
||||
);
|
||||
|
||||
// Should emit LoadSession.
|
||||
assert_eq!(effects.len(), 1);
|
||||
assert!(matches!(
|
||||
&effects[0],
|
||||
Effect::LoadSession { session_id, .. } if session_id == "forked-sess-2"
|
||||
));
|
||||
// Scrollback should contain the restore summary.
|
||||
let has_restore_msg = app.agents[&id]
|
||||
.scrollback
|
||||
.entries_in_range(0..app.agents[&id].scrollback.len())
|
||||
@@ -161,7 +152,6 @@ fn worktree_forked_with_restore_failure_shows_warning_banner() {
|
||||
);
|
||||
assert!(text.contains("restore aborted"));
|
||||
assert!(text.contains("MERGE_HEAD present"));
|
||||
// The success banner must NOT also appear.
|
||||
let success_present = entries
|
||||
.iter()
|
||||
.any(|e| matches!(&e.block, RenderBlock::System(s) if s.text.contains("Code restored")));
|
||||
@@ -412,7 +402,6 @@ fn dispatch_fork_no_flag_non_git_skips_modal_and_forks_without_worktree() {
|
||||
Action::Fork(fork_args(None, Some("explore offline"))),
|
||||
&mut app,
|
||||
);
|
||||
// Must skip the modal and emit ForkSession immediately.
|
||||
assert!(
|
||||
matches!(effects.as_slice(), [Effect::ForkSession { .. }]),
|
||||
"non-git cwd must skip modal and emit ForkSession, got {effects:?}"
|
||||
@@ -450,7 +439,6 @@ fn dispatch_fork_no_flag_always_opens_question_modal() {
|
||||
}
|
||||
other => panic!("expected Fork, got {other:?}"),
|
||||
}
|
||||
// The modal must offer four options: Yes / No / Always / Never.
|
||||
assert_eq!(
|
||||
qv.questions[0].options.len(),
|
||||
4,
|
||||
|
||||
@@ -1846,7 +1846,6 @@ fn dashboard_stop_double_press_via_handle_key_closes_top_level() {
|
||||
"second Ctrl+X via handle_input must close the target agent (Issue 300 regression)",
|
||||
);
|
||||
}
|
||||
/// Top-level resolver round-trip via real AgentView.
|
||||
#[test]
|
||||
fn session_id_resolver_round_trip_top_level() {
|
||||
use crate::views::dashboard::{DashboardRowId, PersistedRowId, SessionIdResolver};
|
||||
@@ -1864,7 +1863,6 @@ fn session_id_resolver_round_trip_top_level() {
|
||||
};
|
||||
assert!(resolver.resolve(&absent).is_none());
|
||||
}
|
||||
/// Subagent resolver round-trip.
|
||||
#[test]
|
||||
fn session_id_resolver_round_trip_subagent() {
|
||||
use crate::views::dashboard::{DashboardRowId, PersistedRowId, SessionIdResolver};
|
||||
|
||||
@@ -1606,7 +1606,7 @@ fn build_mode_query_arms_debounce_despite_title_hits_and_force_skips_it() {
|
||||
);
|
||||
}
|
||||
/// Build mode: a sub-2-char query clears the content results AND
|
||||
/// invalidates the previously armed debounce, so its late expiry can't
|
||||
/// invalidates the debounce armed earlier, so its late expiry can't
|
||||
/// resurrect the search.
|
||||
#[test]
|
||||
fn build_mode_short_query_clears_results_and_invalidates_armed_debounce() {
|
||||
|
||||
@@ -73,8 +73,6 @@ fn session_created_with_flag_but_modal_closed_clears_flag_no_fetches() {
|
||||
assert!(!app.agents[&id].pending_extensions_fetch);
|
||||
}
|
||||
|
||||
// ── /new dispatcher tests ─────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn dispatch_new_session_opens_question_modal_in_git_repo() {
|
||||
let mut app = new_session_test_app();
|
||||
@@ -125,8 +123,6 @@ fn dispatch_new_session_skips_modal_in_non_git_repo() {
|
||||
);
|
||||
}
|
||||
|
||||
// ── Session close (shared with dashboard) ─────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn close_inactive_agent_drops_it() {
|
||||
let mut app = three_agent_app();
|
||||
|
||||
@@ -184,9 +184,9 @@ fn stashed_model_keeps_model_when_unsupported() {
|
||||
|
||||
#[test]
|
||||
fn effort_max_rejected_when_model_offers_no_max() {
|
||||
// Since the Xhigh/Max split, "max" is its own canonical level — a model
|
||||
// whose menu has no max-valued option rejects it with the offered list
|
||||
// (previously the parse alias silently rode it onto the xhigh option).
|
||||
// "max" is its own canonical level, distinct from "xhigh": a model
|
||||
// whose menu has no max-valued option rejects it with the offered
|
||||
// list rather than falling back to the xhigh option.
|
||||
let models = models_with_current(true);
|
||||
let out = take_deferred_model_switch(None, &models, Some("max"));
|
||||
assert_eq!(
|
||||
|
||||
@@ -126,10 +126,10 @@ fn toggle_vim_mode_propagates_to_open_subagent_views() {
|
||||
);
|
||||
}
|
||||
/// `/vim-mode` must toggle vim from the DASHBOARD too (not just an
|
||||
/// agent view) — previously it early-returned unless an agent was
|
||||
/// active, so it was a silent no-op and the overview's j/k never
|
||||
/// turned on. Turning vim ON also focuses the overview so j/k
|
||||
/// navigate immediately; turning it OFF returns focus to the input.
|
||||
/// agent view) — an early return without an active agent would make
|
||||
/// this a silent no-op and the overview's j/k would never turn on.
|
||||
/// Turning vim ON also focuses the overview so j/k navigate
|
||||
/// immediately; turning it OFF returns focus to the input.
|
||||
#[serial_test::serial(KIGI_AGENT_DASHBOARD)]
|
||||
#[test]
|
||||
fn toggle_vim_mode_works_on_dashboard_and_focuses_overview() {
|
||||
@@ -776,11 +776,9 @@ fn refresh_open_settings_modals_updates_reset_confirm_settings_state() {
|
||||
_ => panic!("expected ResetSettingsConfirm still active"),
|
||||
}
|
||||
}
|
||||
/// The
|
||||
/// dispatch-arm bail-out path is verified directly via
|
||||
/// `#[should_panic]` in debug mode rather than the previous
|
||||
/// `if cfg!(debug_assertions) { return; }` placebo which gave
|
||||
/// debug-mode CI no coverage of the routing-bug bail-out.
|
||||
/// The dispatch-arm bail-out path is verified directly via
|
||||
/// `#[should_panic]` in debug mode, giving debug-mode CI real
|
||||
/// coverage of the routing-bug bail-out.
|
||||
#[test]
|
||||
#[cfg(debug_assertions)]
|
||||
#[should_panic(expected = "OpenResetConfirm dispatched without an open Settings modal")]
|
||||
|
||||
@@ -42,7 +42,6 @@ fn send_while_idle_with_nonempty_shared_queue_routes_to_server() {
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or_else(|| panic!("expected immediate SendPrompt for 'c', got {effects:?}"));
|
||||
// Did NOT start a local turn or adopt "c" as the running prompt.
|
||||
assert!(
|
||||
!app.agents[&id].session.state.is_turn_running(),
|
||||
"must not promote 'c' to a local running turn"
|
||||
@@ -220,8 +219,6 @@ fn show_usage_on_welcome_screen_is_noop() {
|
||||
);
|
||||
}
|
||||
|
||||
// ── Minimal update-notice tests ──────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn minimal_update_notice_commits_a_system_block() {
|
||||
let mut app = test_app_with_agent();
|
||||
|
||||
@@ -541,16 +541,12 @@ fn switch_model_complete_success_updates_model_and_pushes_message() {
|
||||
&mut app,
|
||||
);
|
||||
|
||||
// Pending flag cleared.
|
||||
assert!(!app.agents[&id].session.model_switch_pending);
|
||||
// Current model updated.
|
||||
assert_eq!(
|
||||
app.agents[&id].session.models.current,
|
||||
Some(model_id.clone())
|
||||
);
|
||||
// Success message pushed to scrollback.
|
||||
assert_eq!(app.agents[&id].scrollback.len(), initial_scrollback + 1);
|
||||
// PersistPreferredModel effect emitted.
|
||||
assert_eq!(effects.len(), 1);
|
||||
assert!(matches!(
|
||||
&effects[0],
|
||||
@@ -632,7 +628,8 @@ fn switch_model_complete_persists_resolved_effort_from_catalog_meta() {
|
||||
Action::TaskComplete(TaskResult::SwitchModelComplete {
|
||||
agent_id: id,
|
||||
model_id: model_id.clone(),
|
||||
effort: None, // user typed `/model Blackbox 4.7` with no effort
|
||||
// user typed `/model Blackbox 4.7` with no effort
|
||||
effort: None,
|
||||
result: Ok(()),
|
||||
prev_model_id: None,
|
||||
}),
|
||||
@@ -752,9 +749,7 @@ fn switch_model_complete_failure_pushes_error_and_clears_pending() {
|
||||
assert!(effects.is_empty());
|
||||
// Pending flag cleared.
|
||||
assert!(!app.agents[&id].session.model_switch_pending);
|
||||
// Current model unchanged.
|
||||
assert_eq!(app.agents[&id].session.models.current, old_current);
|
||||
// Error message pushed to scrollback.
|
||||
assert_eq!(app.agents[&id].scrollback.len(), initial_scrollback + 1);
|
||||
}
|
||||
|
||||
@@ -794,16 +789,13 @@ fn switch_model_incompatible_agent_shows_question_modal() {
|
||||
|
||||
// No effects emitted (modal is synchronous state).
|
||||
assert!(effects.is_empty());
|
||||
// Pending flag cleared.
|
||||
assert!(!app.agents[&id].session.model_switch_pending);
|
||||
// Question modal is open.
|
||||
assert!(app.agents[&id].question_view.is_some());
|
||||
let qv = app.agents[&id].question_view.as_ref().unwrap();
|
||||
assert!(matches!(
|
||||
qv.local_kind,
|
||||
Some(crate::views::question_view::LocalQuestionKind::AgentTypeMismatch { .. })
|
||||
));
|
||||
// No error message pushed to scrollback.
|
||||
assert_eq!(app.agents[&id].scrollback.len(), initial_scrollback);
|
||||
}
|
||||
|
||||
@@ -855,7 +847,6 @@ fn incompatible_agent_rollback_restores_previous_model() {
|
||||
&mut app,
|
||||
);
|
||||
|
||||
// models.current must be rolled back to the previous model.
|
||||
assert_eq!(
|
||||
app.agents[&id].session.models.current,
|
||||
Some(prev_model),
|
||||
@@ -901,12 +892,10 @@ fn incompatible_agent_closes_active_modal() {
|
||||
&mut app,
|
||||
);
|
||||
|
||||
// Active modal must be closed.
|
||||
assert!(
|
||||
app.agents[&id].active_modal.is_none(),
|
||||
"active modal must be closed when IncompatibleAgent fires",
|
||||
);
|
||||
// Question modal must be open.
|
||||
assert!(
|
||||
app.agents[&id].question_view.is_some(),
|
||||
"question modal must be open",
|
||||
@@ -950,7 +939,6 @@ fn same_agent_type_switch_no_modal() {
|
||||
// Model should be switched, no modal.
|
||||
assert_eq!(app.agents[&id].session.models.current, Some(model_b));
|
||||
assert!(app.agents[&id].question_view.is_none());
|
||||
// Should emit PersistPreferredModel.
|
||||
assert!(
|
||||
effects
|
||||
.iter()
|
||||
@@ -965,10 +953,8 @@ fn switch_model_pending_lifecycle() {
|
||||
let id = AgentId(0);
|
||||
let model_id = acp::ModelId::new(std::sync::Arc::from("kigi-4.5"));
|
||||
|
||||
// Initially false.
|
||||
assert!(!app.agents[&id].session.model_switch_pending);
|
||||
|
||||
// Action sets pending.
|
||||
dispatch(
|
||||
Action::SwitchModel {
|
||||
model_id: model_id.clone(),
|
||||
@@ -978,7 +964,6 @@ fn switch_model_pending_lifecycle() {
|
||||
);
|
||||
assert!(app.agents[&id].session.model_switch_pending);
|
||||
|
||||
// TaskResult clears pending.
|
||||
dispatch(
|
||||
Action::TaskComplete(TaskResult::SwitchModelComplete {
|
||||
agent_id: id,
|
||||
@@ -1012,7 +997,6 @@ fn no_deferred_switch_means_no_extra_effect() {
|
||||
&mut app,
|
||||
);
|
||||
|
||||
// No SwitchModel effect.
|
||||
assert!(
|
||||
!effects
|
||||
.iter()
|
||||
@@ -1178,8 +1162,6 @@ fn available_commands_refreshed_empty_is_noop() {
|
||||
);
|
||||
}
|
||||
|
||||
// -- Session deletion from the /resume picker -----------------------
|
||||
|
||||
#[test]
|
||||
fn delete_session_complete_removes_only_matching_source_and_id() {
|
||||
use crate::views::modal::ActiveModal;
|
||||
@@ -1470,13 +1452,11 @@ fn rename_session_failed_keeps_local_display_name_and_pushes_system_block() {
|
||||
&mut app,
|
||||
);
|
||||
|
||||
// No rollback.
|
||||
assert_eq!(
|
||||
app.agents[&AgentId(0)].display_name.as_deref(),
|
||||
Some("optimistic title"),
|
||||
"display_name must NOT roll back on RenameSessionFailed"
|
||||
);
|
||||
// System block appended with the error.
|
||||
let scrollback = &app.agents[&AgentId(0)].scrollback;
|
||||
assert_eq!(
|
||||
scrollback.len(),
|
||||
@@ -1500,10 +1480,9 @@ fn rename_session_failed_keeps_local_display_name_and_pushes_system_block() {
|
||||
fn rollback_known_key_reverts_cache_and_no_effect() {
|
||||
use crate::settings::SettingValue;
|
||||
let mut app = test_app_with_agent();
|
||||
// Toggle to true first.
|
||||
let _ = dispatch(Action::SetCompactMode(true), &mut app);
|
||||
assert!(app.current_ui.compact_mode);
|
||||
// Now simulate persist failure that rolls back to false.
|
||||
// Simulate persist failure that rolls back to false.
|
||||
let effects = dispatch(
|
||||
Action::TaskComplete(TaskResult::SettingPersistFailed {
|
||||
key: "compact_mode",
|
||||
@@ -1516,7 +1495,6 @@ fn rollback_known_key_reverts_cache_and_no_effect() {
|
||||
effects.is_empty(),
|
||||
"rollback path must NOT emit any new Effects (would loop)",
|
||||
);
|
||||
// Cache is reverted.
|
||||
assert!(!app.current_ui.compact_mode);
|
||||
}
|
||||
|
||||
@@ -1689,8 +1667,6 @@ fn rollback_to_always_approve_blocked_by_policy_pin() {
|
||||
assert!(!app.default_yolo);
|
||||
}
|
||||
|
||||
// -- SessionListLoaded ------------------------------------------------
|
||||
|
||||
/// Canary: an empty list surfaces the generic "no sessions" toast.
|
||||
#[test]
|
||||
fn session_list_empty_shows_generic_toast() {
|
||||
|
||||
@@ -195,8 +195,8 @@ fn open_block_viewer_uses_markdown_viewer_for_agent_message_with_image_ref() {
|
||||
|
||||
assert!(effects.is_empty());
|
||||
let agent = app.agents.get(&id).unwrap();
|
||||
// Agent messages with image refs now open the normal markdown viewer
|
||||
// (inline media rendering moved to the tool call block).
|
||||
// Agent messages with image refs open the normal markdown viewer;
|
||||
// inline media rendering happens in the tool call block.
|
||||
assert!(agent.block_viewer.is_some());
|
||||
}
|
||||
|
||||
@@ -230,15 +230,15 @@ fn open_block_viewer_opens_image_only_blocks_natively() {
|
||||
let _guard = set_protocol_for_test(GraphicsProtocol::Kitty);
|
||||
let effects = dispatch(Action::OpenBlockViewer, &mut app);
|
||||
|
||||
// Generated media now opens in the OS-native viewer (fire-and-forget),
|
||||
// so neither the in-app block viewer nor image viewer is shown.
|
||||
// Generated media opens in the OS-native viewer (fire-and-forget);
|
||||
// neither the in-app block viewer nor image viewer is shown.
|
||||
assert!(effects.is_empty());
|
||||
let agent = app.agents.get(&id).unwrap();
|
||||
assert!(agent.block_viewer.is_none());
|
||||
assert!(agent.image_viewer.is_none());
|
||||
}
|
||||
|
||||
// -- Plugins tab: group-collapse seeding on PluginsListLoaded --------------
|
||||
// Plugins tab: group-collapse seeding on PluginsListLoaded
|
||||
|
||||
fn plugins_list_response() -> kigi_hooks_plugins_types::PluginsListResponse {
|
||||
use crate::views::extensions_modal::test_plugin_info;
|
||||
|
||||
@@ -8,7 +8,7 @@ use super::*;
|
||||
/// `Internal error: "session failed to respond"`. An `acp::Error` carries
|
||||
/// no `promptId`, so before the Err-arm gate this error was misattributed
|
||||
/// to the running turn and rendered as a spurious "Turn failed", detonating
|
||||
/// an unrelated in-flight turn. The handler now gates the Err arm on the
|
||||
/// an unrelated in-flight turn. The handler gates the Err arm on the
|
||||
/// `prompt_id` the pager minted for that RPC: an error whose id is NOT the
|
||||
/// running turn is discarded; the running turn is left untouched.
|
||||
#[test]
|
||||
@@ -1022,13 +1022,11 @@ fn bg_task_kill_failed_clears_pending_kill_on_inactive_agent() {
|
||||
assert!(task.kill_requested_at.is_none());
|
||||
}
|
||||
|
||||
/// build_rows handles many subagents (placeholder).
|
||||
#[test]
|
||||
fn build_rows_collapses_many_subagents() {
|
||||
use crate::views::dashboard::build_rows;
|
||||
let mut app = test_app_with_agent();
|
||||
let agent = app.agents.get_mut(&AgentId(0)).unwrap();
|
||||
// Insert 9 subagents.
|
||||
for i in 0..9 {
|
||||
let info = make_test_subagent(&format!("c{i}"), &format!("sa{i}"));
|
||||
agent
|
||||
@@ -1078,7 +1076,6 @@ fn build_rows_seven_subagents_no_placeholder() {
|
||||
assert!(!rows.last().unwrap().is_more_placeholder);
|
||||
}
|
||||
|
||||
/// At the threshold (exactly 8), no placeholder.
|
||||
#[test]
|
||||
fn build_rows_eight_subagents_no_placeholder() {
|
||||
use crate::views::dashboard::build_rows;
|
||||
@@ -1208,7 +1205,6 @@ fn subagent_label_strips_control_characters() {
|
||||
"subagent label must not retain \\x1b: {:?}",
|
||||
sub.label
|
||||
);
|
||||
// Visible characters survive.
|
||||
assert!(
|
||||
sub.label.contains("evil"),
|
||||
"sanitised label should preserve printable characters, got {:?}",
|
||||
|
||||
Reference in new issue
Block a user