Sampler / inference (PRD F3):
- kimi_compat.rs: single adaptation point for the Kimi chat/completions
dialect (thinking-field mapping, model_id stripping, empty-content
tool-call message fix, stream_options.include_usage), with kimi-cli
source citations
- Rate-limit handling reworked for Kimi/Moonshot semantics; UA kigi/{version}
- /models replaces the xAI models-v2 endpoint everywhere; idle model
refresh carries X-Msh-* device headers only (X-XAI-Token-Auth and
x-grok-client-mode/CLIENT_MODE_HEADER machinery deleted)
Cloud-surface excision (PRD §5, zero-egress):
- remote/ conversations lane, cli-chat-proxy-types crate, prod/ dir,
share command, credit bar: deleted (single local session lane;
paginate() replaces merge_and_paginate)
- Subscription/tier gate stack deleted end-to-end: AppView
gate/tier/team/ZDR fields, app/subscription.rs watch loop,
dispatch/billing.rs paywall + SuperGrok upsell, free-usage-exhausted
chain, tier-restricted commands, GateInfo, RemoteSettings gate fields,
SettingsUpdateNotification gate fields
- /privacy + coding-data-sharing setting deleted (backed by a dead xAI
RPC; Kigi is zero-egress — nothing to share or retain remotely)
Auth UX correctness (user-reported):
- Device-flow fixtures now mirror the live Kimi payload shape
(https://www.kimi.com/code/authorize_device?user_code=..., verified
against auth.kimi.com); the fabricated auth.kimi.com/device?code=...
URLs are gone
- open_browser_detached is a no-op under cfg(test): unit tests drove
wiremock fixture URLs into the real browser (root cause of the
"garbage mock link" ABCD-1234 tabs)
- Welcome/pager-minimal rebrand: Grok Build -> Kigi, grok.com ->
kimi.com, "Sign in to Grok" -> "Sign in to Kimi"
242 lines
9.3 KiB
Rust
242 lines
9.3 KiB
Rust
//! Tests for session status and sharing dispatchers.
|
|
|
|
use super::*;
|
|
|
|
/// Regression (leader-mode turn-end race): when this client is briefly Idle
|
|
/// (`is_turn_running() == false`, `current_prompt_id` cleared) but the server
|
|
/// still has queued prompts — visible as a non-empty `shared_queue` mirror —
|
|
/// a newly-sent prompt must route to the SERVER (immediate-send), NOT be
|
|
/// locally drained as a phantom running turn. The failure mode: a
|
|
/// `send_route_plain immediate=false is_turn_running=false shared_queue_len=5`
|
|
/// path taking `local_drain`, leaving the prompt shown running on the sender
|
|
/// while it was actually queued behind the existing entries on the leader and
|
|
/// every other client.
|
|
#[test]
|
|
fn send_while_idle_with_nonempty_shared_queue_routes_to_server() {
|
|
let mut app = test_app_with_agent();
|
|
let id = AgentId(0);
|
|
// Two prompts already queued on the server (as a broadcast would leave
|
|
// things): populate the authoritative map AND mirror it into the agent.
|
|
app.push_optimistic_prompt_echo("test-session", "q1", "a", "prompt");
|
|
app.push_optimistic_prompt_echo("test-session", "q2", "b", "prompt");
|
|
{
|
|
let snapshot = app.shared_prompt_queue("test-session").cloned().unwrap();
|
|
let agent = app.agents.get_mut(&id).unwrap();
|
|
// Turn-end window: locally Idle with no current prompt, but the
|
|
// server's queue (mirrored from the last broadcast) still has work.
|
|
agent.session.state = AgentState::Idle;
|
|
agent.session.current_prompt_id = None;
|
|
agent.shared_queue = snapshot;
|
|
assert!(agent.session.pending_prompts.is_empty());
|
|
}
|
|
|
|
let effects = dispatch(Action::SendPrompt("c".into()), &mut app);
|
|
|
|
// Routed to the server (immediate-send), keyed by a fresh prompt_id.
|
|
let pid = effects
|
|
.iter()
|
|
.find_map(|e| match e {
|
|
Effect::SendPrompt {
|
|
text, prompt_id, ..
|
|
} if text == "c" => Some(prompt_id.clone()),
|
|
_ => 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"
|
|
);
|
|
assert!(
|
|
app.agents[&id].session.current_prompt_id.is_none(),
|
|
"must not set current_prompt_id locally for a server-queued prompt"
|
|
);
|
|
// Echoed into the shared queue BEHIND the existing entries (position 3).
|
|
let q = app
|
|
.shared_prompt_queue("test-session")
|
|
.expect("optimistic echo present");
|
|
assert_eq!(q.len(), 3, "c queued behind q1, q2");
|
|
assert_eq!(q.last().map(|e| e.id.as_str()), Some(pid.as_str()));
|
|
assert_eq!(q.last().map(|e| e.text.as_str()), Some("c"));
|
|
}
|
|
|
|
/// Direct unit test of the `scrub_error_for_toast` helper —
|
|
/// pins the threshold and the fallback string against drift.
|
|
#[test]
|
|
fn scrub_error_for_toast_unit() {
|
|
// Empty + short messages pass through.
|
|
assert_eq!(scrub_error_for_toast(""), "");
|
|
assert_eq!(scrub_error_for_toast("ok"), "ok");
|
|
assert_eq!(scrub_error_for_toast("network timeout"), "network timeout");
|
|
// At-threshold (120 chars) still passes through.
|
|
let len_120 = "x".repeat(120);
|
|
assert_eq!(scrub_error_for_toast(&len_120), len_120);
|
|
// Over-threshold (121 chars) triggers scrub.
|
|
let len_121 = "x".repeat(121);
|
|
assert_eq!(
|
|
scrub_error_for_toast(&len_121),
|
|
"server error (see logs for details)"
|
|
);
|
|
// Control chars trigger scrub even at short lengths.
|
|
assert_eq!(
|
|
scrub_error_for_toast("hi\nthere"),
|
|
"server error (see logs for details)"
|
|
);
|
|
assert_eq!(
|
|
scrub_error_for_toast("hi\rthere"),
|
|
"server error (see logs for details)"
|
|
);
|
|
// Format-category (Cf) chars also trigger scrub — bidi
|
|
// overrides, zero-width joiner / space, BOM. Prevents
|
|
// Trojan-Source-style visual spoofing
|
|
// where a toast READS as one thing but bytes encode
|
|
// another via embedded RIGHT-TO-LEFT-OVERRIDE.
|
|
assert_eq!(
|
|
scrub_error_for_toast("opt\u{202E}-out"),
|
|
"server error (see logs for details)",
|
|
"RIGHT-TO-LEFT OVERRIDE (U+202E) must be scrubbed",
|
|
);
|
|
assert_eq!(
|
|
scrub_error_for_toast("opt\u{200B}out"),
|
|
"server error (see logs for details)",
|
|
"ZERO WIDTH SPACE (U+200B) must be scrubbed",
|
|
);
|
|
assert_eq!(
|
|
scrub_error_for_toast("\u{FEFF}leading BOM"),
|
|
"server error (see logs for details)",
|
|
"BOM (U+FEFF) must be scrubbed",
|
|
);
|
|
assert_eq!(
|
|
scrub_error_for_toast("zwj\u{200D}joiner"),
|
|
"server error (see logs for details)",
|
|
"ZERO WIDTH JOINER (U+200D) must be scrubbed",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn dispatch_rename_session_updates_display_name_locally() {
|
|
let mut app = test_app_with_agent();
|
|
let effects = dispatch_rename_session(&mut app, "renamed via slash".into());
|
|
assert_eq!(effects.len(), 1);
|
|
assert_eq!(
|
|
app.agents[&AgentId(0)].display_name.as_deref(),
|
|
Some("renamed via slash"),
|
|
"/rename must also update local display_name cache"
|
|
);
|
|
}
|
|
|
|
/// `ConfirmResetSetting { choice: Reset }` on a SHARED Bool
|
|
/// target restores the Settings modal AND fires the typed
|
|
/// `Action::SetCompactMode(default)` via recursive dispatch —
|
|
/// the `Effect::PersistSetting` is the externally-observable
|
|
/// signal. Also asserts the ui_snapshot was
|
|
/// refreshed to the new (post-reset) value (symmetric with the
|
|
/// Cancel test's snapshot assertion).
|
|
#[test]
|
|
fn dispatch_confirm_reset_setting_reset_dispatches_typed_setter_for_shared_bool() {
|
|
use crate::settings::SettingValue;
|
|
use crate::views::modal::{ActiveModal, ResetSettingsResult};
|
|
let mut app = test_app_with_agent();
|
|
// Flip compact_mode to true so we can observe the reset back
|
|
// to its default (false).
|
|
let _ = dispatch(Action::SetCompactMode(true), &mut app);
|
|
assert!(app.current_ui.compact_mode);
|
|
|
|
setup_reset_confirm_open(&mut app, "compact_mode");
|
|
|
|
let effects = dispatch(
|
|
Action::ConfirmResetSetting {
|
|
choice: ResetSettingsResult::Reset,
|
|
},
|
|
&mut app,
|
|
);
|
|
|
|
// Recursive dispatch into Action::SetCompactMode(false) emits
|
|
// the persist effect.
|
|
assert_eq!(effects.len(), 1);
|
|
match &effects[0] {
|
|
Effect::PersistSetting { key, value, .. } => {
|
|
assert_eq!(*key, "compact_mode");
|
|
assert_eq!(value, &SettingValue::Bool(false));
|
|
}
|
|
other => panic!("expected PersistSetting, got {other:?}"),
|
|
}
|
|
// In-memory state is reset to the default.
|
|
assert!(!app.current_ui.compact_mode);
|
|
// Modal is restored AND ui_snapshot reflects the new value
|
|
// (symmetric with the Cancel test).
|
|
let agent = app.agents.get(&AgentId(0)).expect("agent must exist");
|
|
match &agent.active_modal {
|
|
Some(ActiveModal::Settings { state }) => {
|
|
assert!(
|
|
!state.ui_snapshot.compact_mode,
|
|
"ui_snapshot must reflect the post-reset value"
|
|
);
|
|
}
|
|
_ => panic!("Reset branch must restore the Settings modal"),
|
|
}
|
|
}
|
|
|
|
/// `ConfirmResetSetting { choice: Reset }` on a SHARED Enum
|
|
/// target (`theme`) dispatches `Action::SetTheme(default)` via
|
|
/// recursive dispatch — verifies the action_for_reset Enum arm.
|
|
#[test]
|
|
fn dispatch_confirm_reset_setting_reset_dispatches_typed_setter_for_shared_enum() {
|
|
use crate::settings::SettingValue;
|
|
use crate::views::modal::ResetSettingsResult;
|
|
let mut app = test_app_with_agent();
|
|
// Flip theme to a non-default first.
|
|
let _ = dispatch(Action::SetTheme("tokyonight".to_string()), &mut app);
|
|
assert_eq!(app.current_ui.theme.as_deref(), Some("tokyonight"));
|
|
|
|
setup_reset_confirm_open(&mut app, "theme");
|
|
|
|
let effects = dispatch(
|
|
Action::ConfirmResetSetting {
|
|
choice: ResetSettingsResult::Reset,
|
|
},
|
|
&mut app,
|
|
);
|
|
|
|
// Reset → SetTheme("groknight") (the registered default).
|
|
assert_eq!(effects.len(), 1);
|
|
match &effects[0] {
|
|
Effect::PersistSetting { key, value, .. } => {
|
|
assert_eq!(*key, "theme");
|
|
assert_eq!(value, &SettingValue::Enum("groknight"));
|
|
}
|
|
other => panic!("expected PersistSetting, got {other:?}"),
|
|
}
|
|
assert_eq!(app.current_ui.theme.as_deref(), Some("groknight"));
|
|
}
|
|
|
|
#[test]
|
|
fn show_usage_on_welcome_screen_is_noop() {
|
|
let mut app = test_app();
|
|
let effects = dispatch(Action::ShowUsage, &mut app);
|
|
assert!(
|
|
effects.is_empty(),
|
|
"ShowUsage with no active agent should be a no-op"
|
|
);
|
|
}
|
|
|
|
// ── Minimal update-notice tests ──────────────────────────────────────
|
|
|
|
#[test]
|
|
fn minimal_update_notice_commits_a_system_block() {
|
|
let mut app = test_app_with_agent();
|
|
let before = agent_scrollback_len(&app);
|
|
commit_minimal_update_notice(&mut app, "9.9.9");
|
|
assert_eq!(agent_scrollback_len(&app), before + 1);
|
|
let text = last_system_text(&app, AgentId(0));
|
|
assert!(text.contains("Update available: v9.9.9"), "got: {text:?}");
|
|
assert!(text.contains("restart to apply"), "got: {text:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn minimal_update_notice_no_active_agent_is_noop() {
|
|
let mut app = test_app();
|
|
// Must not panic and must not require an agent.
|
|
commit_minimal_update_notice(&mut app, "9.9.9");
|
|
}
|