F3: Kimi inference pipeline + full grok cloud-surface excision

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"
This commit is contained in:
2026-07-17 16:05:51 -04:00
parent fe1f885bb3
commit ea0ce9d15f
231 changed files with 4730 additions and 26358 deletions
@@ -1494,391 +1494,6 @@ fn rename_session_failed_keeps_local_display_name_and_pushes_system_block() {
);
}
// ── GateRefreshed subscription flow ─────────────────────────────
/// Regression: when the 30s gate poll detects the subscription gate has
/// been lifted, it must emit `CheckSubscription` so the shell refreshes
/// the JWT. Without this the auth token still lacks the subscription
/// claim and all API calls return 403.
#[test]
fn gate_refreshed_emits_check_subscription_on_gate_lift() {
let mut app = test_app();
// User starts gated (no subscription).
app.gate = Some(kigi_shell::auth::GateInfo {
message: "SuperGrok subscription required".into(),
url: Some("https://grok.com/supergrok".into()),
label: Some("Subscribe".into()),
});
assert!(!app.has_access());
// Server-side settings now show no gate (user purchased subscription).
let settings = kigi_shell::util::config::RemoteSettings::default();
let effects = dispatch_task_result(
TaskResult::GateRefreshed {
settings: Some(settings),
},
&mut app,
);
// Gate must be lifted.
assert!(app.has_access(), "gate should be lifted");
assert!(app.welcome_prompt_focused, "prompt should be focused");
// Must emit CheckSubscription to trigger shell-side JWT refresh.
assert!(
effects
.iter()
.any(|e| matches!(e, Effect::CheckSubscription { verify: None })),
"must emit CheckSubscription to refresh JWT; got: {effects:?}"
);
}
/// When the gate poll returns settings that still have a gate, no
/// effects should be emitted and the user stays blocked.
#[test]
fn gate_refreshed_no_effect_when_still_gated() {
let mut app = test_app();
app.gate = Some(kigi_shell::auth::GateInfo {
message: "Subscribe".into(),
url: None,
label: None,
});
let settings = kigi_shell::util::config::RemoteSettings {
gate_message: Some("Subscribe".into()),
..Default::default()
};
let effects = dispatch_task_result(
TaskResult::GateRefreshed {
settings: Some(settings),
},
&mut app,
);
assert!(!app.has_access(), "gate should remain");
assert!(effects.is_empty(), "no effects when still gated");
}
/// When the user was never gated, GateRefreshed is a no-op.
#[test]
fn gate_refreshed_no_effect_when_already_unblocked() {
let mut app = test_app();
assert!(app.has_access()); // no gate
let settings = kigi_shell::util::config::RemoteSettings::default();
let effects = dispatch_task_result(
TaskResult::GateRefreshed {
settings: Some(settings),
},
&mut app,
);
assert!(effects.is_empty(), "no effects when already unblocked");
}
/// A gate newly imposed by the 30s settings poll (possibly stale) must be
/// deferred for live verification instead of painting the paywall directly:
/// the gate is held out of `app.gate` and a `CheckSubscription` +
/// verify-timeout pair is emitted.
#[test]
fn gate_refreshed_newly_blocked_defers_gate_for_verification() {
let mut app = test_app();
assert!(app.has_access()); // ungated
let settings = kigi_shell::util::config::RemoteSettings {
gate_message: Some("Subscribe".into()),
..Default::default()
};
let effects = dispatch_task_result(
TaskResult::GateRefreshed {
settings: Some(settings),
},
&mut app,
);
assert!(
app.has_access(),
"deferred gate must not show as paywall before verification"
);
assert!(app.pending_gate_verification.is_some());
assert!(
effects
.iter()
.any(|e| matches!(e, Effect::CheckSubscription { verify: Some(_) })),
"must live-check before showing the paywall; got: {effects:?}"
);
assert!(
effects
.iter()
.any(|e| matches!(e, Effect::ScheduleGateVerifyTimeout { .. })),
"must arm the verification timeout; got: {effects:?}"
);
}
// ── Stale-gate verification resolution ──────────────────────────
fn test_gate() -> kigi_shell::auth::GateInfo {
kigi_shell::auth::GateInfo {
message: "Subscribe".into(),
url: None,
label: None,
}
}
/// The live check confirmed access (meta without a gate): the deferred
/// stale gate is dropped and the paywall never shows.
#[test]
fn verify_check_with_meta_resolves_pending_gate() {
let mut app = test_app();
let _effs = app.impose_gate(test_gate());
assert!(app.has_access());
let meta = serde_json::to_value(kigi_shell::auth::AuthMeta::default()).unwrap();
dispatch_task_result(
TaskResult::CheckSubscriptionComplete {
verify: Some(app.gate_verify_gen),
meta: Some(meta),
},
&mut app,
);
assert!(app.has_access(), "live check says subscribed — no paywall");
assert!(app.pending_gate_verification.is_none());
}
/// The verification's own check failed (meta None) while its stale gate
/// was deferred: err on blocking — the deferred gate is promoted.
#[test]
fn verify_check_failure_promotes_pending_gate() {
let mut app = test_app();
let _effs = app.impose_gate(test_gate());
let effects = dispatch_task_result(
TaskResult::CheckSubscriptionComplete {
verify: Some(app.gate_verify_gen),
meta: None,
},
&mut app,
);
assert!(!app.has_access(), "check failed — deferred gate must show");
assert!(app.pending_gate_verification.is_none());
assert!(
effects
.iter()
.any(|e| matches!(e, Effect::SchedulePaywallCheck)),
"freshly shown gate must arm the 5s auto-lift chain; got: {effects:?}"
);
}
/// A failed GENERIC check (watch / focus / paywall chain — no generation)
/// must never promote a deferred gate: only the deferral's own
/// generation-scoped check or timeout may (a superseded or unrelated check
/// failing is not evidence about the current verification).
#[test]
fn check_subscription_complete_failure_leaves_pending_gate_untouched() {
let mut app = test_app();
let _effs = app.impose_gate(test_gate());
let effects = dispatch_task_result(
TaskResult::CheckSubscriptionComplete {
verify: None,
meta: None,
},
&mut app,
);
assert!(effects.is_empty());
assert!(
app.has_access(),
"generic check failure must not promote the deferred gate"
);
assert!(
app.pending_gate_verification.is_some(),
"verification must stay in flight"
);
}
/// A failed verification check from a SUPERSEDED deferral (older
/// generation) must not promote the newer pending gate.
#[test]
fn verify_check_stale_generation_failure_is_ignored() {
let mut app = test_app();
let _effs = app.impose_gate(test_gate());
let stale_gen = app.gate_verify_gen;
// Second deferral supersedes the first (its check is in flight).
let _effs = app.impose_gate(test_gate());
let effects = dispatch_task_result(
TaskResult::CheckSubscriptionComplete {
verify: Some(stale_gen),
meta: None,
},
&mut app,
);
assert!(effects.is_empty());
assert!(
app.has_access(),
"superseded verification failure must not promote the newer gate"
);
assert!(app.pending_gate_verification.is_some());
}
/// A check failure with no deferred gate (the plain paywall-poller path)
/// must not invent a gate.
#[test]
fn check_subscription_complete_failure_without_pending_gate_is_noop() {
let mut app = test_app();
dispatch_task_result(
TaskResult::CheckSubscriptionComplete {
verify: None,
meta: None,
},
&mut app,
);
assert!(app.has_access());
}
/// The verification window expired before the live check resolved:
/// err on blocking — the deferred gate is promoted, and the freshly shown
/// paywall gets the 5s auto-lift chain.
#[test]
fn gate_verify_timeout_promotes_pending_gate() {
let mut app = test_app();
let _effs = app.impose_gate(test_gate());
assert!(app.has_access());
let effects = dispatch_task_result(
TaskResult::GateVerifyTimeout {
generation: app.gate_verify_gen,
},
&mut app,
);
assert!(!app.has_access(), "timeout — deferred gate must show");
assert!(app.pending_gate_verification.is_none());
assert!(
app.paywall_check_started.is_some(),
"promoted gate must arm the paywall auto-check chain"
);
assert!(
effects
.iter()
.any(|e| matches!(e, Effect::SchedulePaywallCheck)),
"promoted gate must schedule the 5s chain; got: {effects:?}"
);
}
/// The timeout fires after the check already resolved the gate: no-op.
#[test]
fn gate_verify_timeout_noop_when_already_resolved() {
let mut app = test_app();
let _effs = app.impose_gate(test_gate());
let generation = app.gate_verify_gen;
// Live check resolved first (access confirmed).
let meta = serde_json::to_value(kigi_shell::auth::AuthMeta::default()).unwrap();
dispatch_task_result(
TaskResult::CheckSubscriptionComplete {
verify: None,
meta: Some(meta),
},
&mut app,
);
dispatch_task_result(TaskResult::GateVerifyTimeout { generation }, &mut app);
assert!(
app.has_access(),
"stale timeout must not re-impose the gate"
);
}
/// A timeout from a SUPERSEDED verification (older generation) must not
/// promote a newer deferred gate whose own live check is still in flight.
#[test]
fn gate_verify_timeout_stale_generation_is_ignored() {
let mut app = test_app();
// First deferral resolves (access confirmed) ...
let _effs = app.impose_gate(test_gate());
let stale_gen = app.gate_verify_gen;
let meta = serde_json::to_value(kigi_shell::auth::AuthMeta::default()).unwrap();
dispatch_task_result(
TaskResult::CheckSubscriptionComplete {
verify: None,
meta: Some(meta),
},
&mut app,
);
// ... then a SECOND gate is deferred (check in flight).
let _effs = app.impose_gate(test_gate());
assert!(app.has_access());
// The FIRST deferral's timer fires now — it must not promote the
// second deferral's pending gate.
let effects = dispatch_task_result(
TaskResult::GateVerifyTimeout {
generation: stale_gen,
},
&mut app,
);
assert!(effects.is_empty());
assert!(
app.has_access(),
"stale-generation timer must not promote the newer pending gate"
);
assert!(
app.pending_gate_verification.is_some(),
"the newer verification must stay in flight"
);
}
/// `GateRefreshed` with gate-free settings while a deferred gate awaits
/// verification must drop the pending copy — the fresh settings are newer
/// than the stale snapshot that produced it — and still run the lift
/// bookkeeping (`CheckSubscription` for the JWT refresh), since the pending
/// deferral means the user was conceptually blocked.
#[test]
fn gate_refreshed_without_gate_clears_pending_verification() {
let mut app = test_app();
let _effs = app.impose_gate(test_gate());
let generation = app.gate_verify_gen;
let settings = kigi_shell::util::config::RemoteSettings::default();
let effects = dispatch_task_result(
TaskResult::GateRefreshed {
settings: Some(settings),
},
&mut app,
);
assert!(app.pending_gate_verification.is_none());
assert!(
effects
.iter()
.any(|e| matches!(e, Effect::CheckSubscription { verify: None })),
"settings-confirmed lift of a pending gate must refresh the JWT; got: {effects:?}"
);
// The still-armed timer must find nothing to promote.
dispatch_task_result(TaskResult::GateVerifyTimeout { generation }, &mut app);
assert!(
app.has_access(),
"cleared pending gate must not resurface via the timer"
);
}
/// Logout clears any deferred gate and the check debounce.
#[test]
fn logout_clears_pending_gate_verification() {
let mut app = test_app();
let _effs = app.impose_gate(test_gate());
dispatch_task_result(TaskResult::LogoutComplete, &mut app);
assert!(app.pending_gate_verification.is_none());
assert!(app.last_subscription_check_at.is_none());
}
/// `apply_setting_rollback` on a known key reverts the in-memory
/// cache without emitting any new effects.
#[test]
@@ -2002,38 +1617,16 @@ fn rollback_to_always_approve_blocked_by_policy_pin() {
assert!(!app.default_yolo);
}
// -- Degraded conversations lane (SessionListLoaded.partial) ----------
// -- SessionListLoaded ------------------------------------------------
/// A degraded conversations lane surfaces an actionable notice instead of
/// the misleading "No sessions found" toast.
/// Canary: an empty list surfaces the generic "no sessions" toast.
#[test]
fn session_list_partial_no_oauth_surfaces_login_hint() {
fn session_list_empty_shows_generic_toast() {
let mut app = test_app_with_agent();
open_session_picker_with(&mut app, vec![]);
let _ = dispatch(
Action::TaskComplete(TaskResult::SessionListLoaded {
sessions: vec![],
partial: Some(crate::app::effects::ConversationsPartial::NoOauth),
seq: 0,
query: None,
}),
&mut app,
);
assert!(
read_toast(&app).contains("/login"),
"no_oauth must point at /login"
);
}
/// Canary: an empty list without a degraded lane keeps the generic toast.
#[test]
fn session_list_empty_without_partial_keeps_generic_toast() {
let mut app = test_app_with_agent();
open_session_picker_with(&mut app, vec![]);
let _ = dispatch(
Action::TaskComplete(TaskResult::SessionListLoaded {
sessions: vec![],
partial: None,
seq: 0,
query: None,
}),
@@ -2041,95 +1634,3 @@ fn session_list_empty_without_partial_keeps_generic_toast() {
);
assert!(read_toast(&app).contains("No sessions found"));
}
/// Non-empty degraded list under chat mode (welcome-fallback branch):
/// entries land AND the retry notice surfaces; Build mode stays silent.
#[test]
fn session_list_nonempty_partial_toasts_retry_in_chat_mode_only() {
let mut app = test_app_with_agent();
app.chat_mode = true;
let _ = dispatch(
Action::TaskComplete(TaskResult::SessionListLoaded {
sessions: vec![make_conversation_entry("conv-part-1")],
partial: Some(crate::app::effects::ConversationsPartial::Timeout),
seq: 0,
query: None,
}),
&mut app,
);
assert!(
app.session_picker_entries.is_some(),
"entries must still land on a degraded lane"
);
assert!(
read_toast(&app).contains("retry"),
"timeout must surface the retry notice"
);
// Build-mode canary: stays silent on a degraded lane.
let mut app = test_app_with_agent();
let _ = dispatch(
Action::TaskComplete(TaskResult::SessionListLoaded {
sessions: vec![make_picker_entry("local-part-1", "/r")],
partial: Some(crate::app::effects::ConversationsPartial::Timeout),
seq: 0,
query: None,
}),
&mut app,
);
assert!(
app.agents[&AgentId(0)].toast.is_none(),
"Build-mode non-empty degraded list stays silent"
);
}
/// Modal variant of the non-empty degraded-lane notice: same chat-mode-only
/// gating as the welcome-fallback branch.
#[test]
fn session_list_nonempty_partial_modal_toasts_in_chat_mode_only() {
use crate::views::modal::ActiveModal;
let mut app = test_app_with_agent();
app.chat_mode = true;
open_session_picker_with(&mut app, vec![]);
let _ = dispatch(
Action::TaskComplete(TaskResult::SessionListLoaded {
sessions: vec![make_conversation_entry("conv-part-m1")],
partial: Some(crate::app::effects::ConversationsPartial::Timeout),
seq: 0,
query: None,
}),
&mut app,
);
let agent = get_active_agent(&app).expect("active agent");
assert!(
matches!(
agent.active_modal.as_ref(),
Some(ActiveModal::SessionPicker {
entries: Some(list),
..
}) if list.len() == 1
),
"entries must land in the open modal on a degraded lane"
);
assert!(
read_toast(&app).contains("retry"),
"chat-mode modal must surface the retry notice"
);
// Build-mode canary: the open modal stays silent.
let mut app = test_app_with_agent();
open_session_picker_with(&mut app, vec![]);
let _ = dispatch(
Action::TaskComplete(TaskResult::SessionListLoaded {
sessions: vec![make_picker_entry("local-part-m1", "/r")],
partial: Some(crate::app::effects::ConversationsPartial::Timeout),
seq: 0,
query: None,
}),
&mut app,
);
assert!(
app.agents[&AgentId(0)].toast.is_none(),
"Build-mode modal non-empty degraded list stays silent"
);
}