M0: compilable skeleton — Kigi 0.1.0 fork surgery
Hard fork of xai-org/grok-build (Apache-2.0) re-targeted as Kigi, an
unofficial Kimi Code CLI community build.
Rename & identity
- 72 xai-*/xai-grok-* crates -> kigi-* (explicit: xai-grok-pager-bin ->
kigi-bin [binary `kigi`], xai-grok-pager -> kigi-tui; rest mechanical);
ptyctl, ptyctl-cli, third_party/ unchanged; proto package
xai.grok.tools.v1 -> kigi.tools.v1
- Config home ~/.kigi (KIGI_SHARE_DIR override), env prefix GROK_* ->
KIGI_*, `kigi --version` carries the unofficial-community-build notice
- clap identity, help text, startup banner, prompt templates rebranded
(templates re-encrypted)
Deletions (PRD removal list #5/#6/#7/#9/#10)
- voice input (xai-grok-voice) and all TUI wiring
- telemetry: Mixpanel client, external OTel stream, Sentry, OTLP layers,
trace/GCS/S3 upload queues (kigi-file-utils halved), workspace upload
module & dc_log, heap-profile uploader, auth-diagnostics uploader,
session-analytics halves of feedback; local zero-egress observability
preserved in new kigi-log crate (unified log, --debug firehose,
subsystem file logs, opt-in instrumentation)
- announcements (crate, remote-settings fields, TUI surfaces)
- plugin marketplace (crate, sources/browse/CTA/extensions-modal tab);
direct plugin install/uninstall/update via kigi-agent git_install kept
- relay/gateway/assets endpoints and features (agent relay, headless
relay transport, gateway bridge, LeaderEnvUrls); leader IPC socket now
~/.kigi/leader.sock + KIGI_LEADER_SOCKET, no ws-url derivation
- functional types rehomed instead of deleted: PermissionMode ->
kigi-config-types, McpInitStrategy -> kigi-mcp, PrCreationSource ->
session signals, TerminalDiagnostics -> kigi-pager-render, agent_id ->
shell util
Endpoints
- kigi-env rewritten: single production KigiEndpoints {coding_api_base_url
https://api.kimi.com/coding/v1 (KIGI_CODE_BASE_URL), oauth_host
https://auth.kimi.com (KIGI_OAUTH_HOST), update_base_url (GitHub
Releases API), upgrade_page_url}; GrokBuildEnvironment enum deleted
Toolchain & workspace hygiene
- Rust 1.97.0 pinned; edition 2024; full cargo update; git2 hoisted to
workspace at 0.21 (Option->Result API migration), quick-xml 0.41
- Root Cargo.toml hand-maintained (PRD §8.1): version 0.1.0 inherited by
all members, members sorted, unused deps pruned
- cargo-deny advisories gate (deny.toml with documented transitive
exceptions); CI workflow (check/clippy/fmt/deny/test, macOS+Linux)
- cross-crate test seams re-gated behind `test-support` cargo feature;
insta snapshot baselines renamed to the kigi_tui prefix
- clippy --workspace --all-targets: zero warnings; fmt clean
Fixes surfaced by the port
- updater probe/installer divergence (bin/kigi vs bin/grok symlink set)
- idle model-metadata refresh dead under KIGI_CODE_BASE_URL override
(new is_effective_coding_endpoint_url, loopback+override aware)
- macOS symlinked-TMPDIR fixture canonicalization (foreign_sessions,
fast-worktree); RSS measurement tests serialized via serial_test
Docs & legal (Apache §4)
- NOTICE added (upstream attribution + change statement); THIRD-PARTY
notices sustained; kigi-tools ported-code notices extended; README,
CONTRIBUTING, SECURITY, AGENTS.md rewritten
Out of scope for M0 (tracked): Kimi auth/inference (M1), search/fetch,
command parity, config import (M2), Computer Hub excision & final
brand-token sweep (M2), distribution & self-update rewrite (M3).
This commit is contained in:
@@ -0,0 +1,486 @@
|
||||
//! Goal mode support — notification helpers and state formatters.
|
||||
//!
|
||||
//! The model drives goal orchestration directly via the `update_goal` tool.
|
||||
//! This module provides notification helpers used by `SessionActor` to emit
|
||||
//! `GoalUpdated` events and format elapsed time.
|
||||
|
||||
use crate::extensions::notification::{
|
||||
SessionNotification as XaiSessionNotification, SessionUpdate as XaiSessionUpdate,
|
||||
};
|
||||
use crate::session::goal_tracker::{GoalOrchestration, GoalPhase, GoalStatus, GoalTracker};
|
||||
use crate::session::persistence::PersistenceMsg;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GoalNotifySender — fire-and-forget notification sender
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Lightweight notification sender for goal progress updates.
|
||||
///
|
||||
/// Used by `SessionActor` to emit `GoalUpdated` notifications when the
|
||||
/// `update_goal` tool reports progress.
|
||||
pub(crate) struct GoalNotifySender {
|
||||
session_id: agent_client_protocol::SessionId,
|
||||
gateway: kigi_acp_lib::AcpAgentGatewaySender,
|
||||
persistence_tx: tokio::sync::mpsc::UnboundedSender<PersistenceMsg>,
|
||||
}
|
||||
|
||||
impl GoalNotifySender {
|
||||
pub(crate) fn new(
|
||||
session_id: agent_client_protocol::SessionId,
|
||||
gateway: kigi_acp_lib::AcpAgentGatewaySender,
|
||||
persistence_tx: tokio::sync::mpsc::UnboundedSender<PersistenceMsg>,
|
||||
) -> Self {
|
||||
Self {
|
||||
session_id,
|
||||
gateway,
|
||||
persistence_tx,
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a `GoalUpdated` built from the snapshot; token args come
|
||||
/// from [`SessionActor::goal_tokens`].
|
||||
pub(crate) fn emit_goal_updated(
|
||||
&self,
|
||||
tracker: &mut GoalTracker,
|
||||
tokens_used: i64,
|
||||
finished_subagent_tokens: i64,
|
||||
) {
|
||||
tracker.account_elapsed();
|
||||
let Some(o) = tracker.snapshot() else { return };
|
||||
self.send_update(build_goal_updated(o, tokens_used, finished_subagent_tokens));
|
||||
}
|
||||
|
||||
/// Like [`Self::emit_goal_updated`] but fire-and-forget to the gateway
|
||||
/// ONLY — the update is not appended to the session JSONL. Used by the
|
||||
/// high-frequency `SubagentProgress` live-token path: those ticks recur
|
||||
/// (~every `PROGRESS_PUBLISH_INTERVAL`) while a subagent runs, so
|
||||
/// persisting each one would grow the updates log without bound. The
|
||||
/// durable goal total survives via `PersistenceMsg::GoalModeState` and
|
||||
/// the next persisted state-transition `GoalUpdated`; the pager
|
||||
/// self-heals the live figure on the next tick. Mirrors the gateway-only
|
||||
/// `scheduled_task_fired` convention.
|
||||
pub(crate) fn emit_goal_updated_ephemeral(
|
||||
&self,
|
||||
tracker: &mut GoalTracker,
|
||||
tokens_used: i64,
|
||||
finished_subagent_tokens: i64,
|
||||
) {
|
||||
tracker.account_elapsed();
|
||||
let Some(o) = tracker.snapshot() else { return };
|
||||
self.dispatch_update(
|
||||
build_goal_updated(o, tokens_used, finished_subagent_tokens),
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
/// Persist + fire-and-forget a notification to the gateway. Used for
|
||||
/// snapshot-derived payloads and for the "planning…" / "Verifying…"
|
||||
/// latch updates that must not run the `send_xai_notification`
|
||||
/// rewind-window-close side effect.
|
||||
pub(crate) fn send_update(&self, update: XaiSessionUpdate) {
|
||||
self.dispatch_update(update, true);
|
||||
}
|
||||
|
||||
/// Stamp, optionally persist, and fire-and-forget a notification.
|
||||
/// `persist == false` ships the update to the gateway only (no JSONL
|
||||
/// append) for recurring/transient ticks — see
|
||||
/// [`Self::emit_goal_updated_ephemeral`].
|
||||
fn dispatch_update(&self, update: XaiSessionUpdate, persist: bool) {
|
||||
// Stamped before the persist/broadcast fork — see `ensure_event_id_meta`.
|
||||
let mut meta = None;
|
||||
crate::util::event_id::ensure_event_id_meta(&self.session_id.0, &mut meta);
|
||||
let notification = XaiSessionNotification {
|
||||
session_id: self.session_id.clone(),
|
||||
update,
|
||||
meta: meta.map(serde_json::Value::Object),
|
||||
};
|
||||
let raw = serde_json::to_value(¬ification)
|
||||
.and_then(|v| serde_json::value::to_raw_value(&v))
|
||||
.ok();
|
||||
if persist {
|
||||
let _ = self.persistence_tx.send(PersistenceMsg::Update(
|
||||
crate::session::storage::SessionUpdate::Xai(Box::new(notification)),
|
||||
));
|
||||
}
|
||||
if let Some(raw) = raw {
|
||||
let ext = agent_client_protocol::ExtNotification::new(
|
||||
"x.ai/session_notification",
|
||||
raw.into(),
|
||||
);
|
||||
self.gateway.forward_fire_and_forget(ext);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Map a `GoalEvent` to its snake_case wire name.
|
||||
fn goal_event_as_str(event: &crate::session::goal_tracker::GoalEvent) -> &'static str {
|
||||
use crate::session::goal_tracker::GoalEvent;
|
||||
match event {
|
||||
GoalEvent::GoalCreated => "goal_created",
|
||||
GoalEvent::PlanningStarted => "planning_started",
|
||||
GoalEvent::PlanningCompleted => "planning_completed",
|
||||
GoalEvent::PlanningFailed => "planning_failed",
|
||||
GoalEvent::WorkerStarted => "worker_started",
|
||||
GoalEvent::WorkerCompleted => "worker_completed",
|
||||
GoalEvent::WorkerFailed => "worker_failed",
|
||||
GoalEvent::ContextRotated => "context_rotated",
|
||||
GoalEvent::GoalPaused => "goal_paused",
|
||||
GoalEvent::GoalResumed => "goal_resumed",
|
||||
GoalEvent::GoalCompleted => "goal_completed",
|
||||
GoalEvent::GoalCleared => "goal_cleared",
|
||||
GoalEvent::BudgetExceeded => "budget_exceeded",
|
||||
GoalEvent::PrematureStopDetected => "premature_stop_detected",
|
||||
GoalEvent::Unknown => "unknown",
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a `GoalUpdated` from an orchestration snapshot. `tokens_used`
|
||||
/// already includes every subagent's marginal (live progress ticks
|
||||
/// advance the records), so `o.live_subagent_tokens` is NOT folded in —
|
||||
/// it ships only as the separate Active-subagent wire field.
|
||||
pub(crate) fn build_goal_updated(
|
||||
o: &GoalOrchestration,
|
||||
tokens_used: i64,
|
||||
finished_subagent_tokens: i64,
|
||||
) -> XaiSessionUpdate {
|
||||
let last_entry = o.history.last();
|
||||
|
||||
let status_str = match o.status {
|
||||
GoalStatus::Active => "active",
|
||||
GoalStatus::UserPaused => "user_paused",
|
||||
GoalStatus::BackOffPaused => "back_off_paused",
|
||||
GoalStatus::NoProgressPaused => "no_progress_paused",
|
||||
GoalStatus::InfraPaused => "infra_paused",
|
||||
GoalStatus::Blocked => "blocked",
|
||||
GoalStatus::BudgetLimited => "budget_limited",
|
||||
GoalStatus::Complete => "complete",
|
||||
};
|
||||
let phase_str = match o.phase {
|
||||
GoalPhase::Idle => "idle",
|
||||
GoalPhase::Planning => "planning",
|
||||
GoalPhase::Executing => "executing",
|
||||
};
|
||||
|
||||
let last_event = last_entry.map(|e| goal_event_as_str(&e.event).to_owned());
|
||||
|
||||
XaiSessionUpdate::GoalUpdated {
|
||||
goal_id: o.goal_id.clone(),
|
||||
objective: o.objective.clone(),
|
||||
status: status_str.to_owned(),
|
||||
phase: phase_str.to_owned(),
|
||||
token_budget: o.token_budget,
|
||||
tokens_used,
|
||||
elapsed_ms: o.elapsed_ms,
|
||||
total_deliverables: 0,
|
||||
completed_deliverables: 0,
|
||||
current_deliverable_id: None,
|
||||
current_deliverable_title: None,
|
||||
current_subagent_role: o.current_subagent_role.clone(),
|
||||
total_worker_rounds: o.total_worker_rounds,
|
||||
total_verify_rounds: o.total_verify_rounds,
|
||||
token_baseline: o.token_baseline,
|
||||
finished_subagent_tokens,
|
||||
live_subagent_tokens: (o.live_subagent_tokens > 0).then_some(o.live_subagent_tokens),
|
||||
// Only transmit the breakdown when ≥2 distinct models appear; a
|
||||
// single-model (or all-inherit) goal collapses to the single
|
||||
// tokens line, so a 1-element vec is never sent. This makes the
|
||||
// wire-field doc literally true and is the single source of the
|
||||
// collapse rule (the pager re-checks ≥2 as defence in depth).
|
||||
// `o.live_tokens_by_model` is a live active-subagent-window field
|
||||
// (cleared on `SubagentFinished`, like `live_subagent_tokens`); the
|
||||
// pager renders it only under the "Active subagent" block, so this
|
||||
// producer must keep its populate gate on that same axis.
|
||||
live_tokens_by_model: if o.live_tokens_by_model.len() >= 2 {
|
||||
o.live_tokens_by_model.clone()
|
||||
} else {
|
||||
Vec::new()
|
||||
},
|
||||
live_context_pct: (o.live_context_pct > 0).then_some(o.live_context_pct),
|
||||
live_turn_count: (o.live_turn_count > 0).then_some(o.live_turn_count),
|
||||
live_tool_call_count: (o.live_tool_call_count > 0).then_some(o.live_tool_call_count),
|
||||
last_event,
|
||||
last_event_detail: last_entry.and_then(|e| e.detail.clone()),
|
||||
last_event_timestamp: last_entry.map(|e| e.timestamp.clone()),
|
||||
deliverables: Vec::new(),
|
||||
pause_message: o.pause_message.clone(),
|
||||
// Suppress the counter on the wire when no classifier run has
|
||||
// happened yet — mirrors the `total_worker_rounds`-style
|
||||
// convention used elsewhere for "no activity" counters.
|
||||
classifier_runs_attempted: (o.classifier_runs_attempted > 0)
|
||||
.then_some(o.classifier_runs_attempted),
|
||||
classifier_max_runs: o.classifier_max_runs,
|
||||
last_classifier_verdict: o.last_classifier_verdict,
|
||||
last_classifier_details_path: o.last_classifier_details_path.clone(),
|
||||
// Both badges are sourced from their `*_in_flight` latches (see the
|
||||
// field docs), NOT one-shot wire flags — so the token-accounting /
|
||||
// continuation `GoalUpdated`s that fire mid-run keep carrying the
|
||||
// correct flag instead of clearing it.
|
||||
verifying_completion: o.verifying_in_flight.then_some(true),
|
||||
planning: o.planning_in_flight.then_some(true),
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a `GoalUpdated` with `status: "cleared"` to tell the pager to
|
||||
/// drop its goal state.
|
||||
pub(crate) fn build_goal_cleared() -> XaiSessionUpdate {
|
||||
XaiSessionUpdate::GoalUpdated {
|
||||
goal_id: String::new(),
|
||||
objective: String::new(),
|
||||
status: "cleared".to_owned(),
|
||||
phase: "idle".to_owned(),
|
||||
token_budget: None,
|
||||
tokens_used: 0,
|
||||
elapsed_ms: 0,
|
||||
total_deliverables: 0,
|
||||
completed_deliverables: 0,
|
||||
current_deliverable_id: None,
|
||||
current_deliverable_title: None,
|
||||
current_subagent_role: None,
|
||||
total_worker_rounds: 0,
|
||||
total_verify_rounds: 0,
|
||||
token_baseline: 0,
|
||||
finished_subagent_tokens: 0,
|
||||
live_subagent_tokens: None,
|
||||
live_tokens_by_model: Vec::new(),
|
||||
live_context_pct: None,
|
||||
live_turn_count: None,
|
||||
live_tool_call_count: None,
|
||||
last_event: None,
|
||||
last_event_detail: None,
|
||||
last_event_timestamp: None,
|
||||
deliverables: Vec::new(),
|
||||
pause_message: None,
|
||||
classifier_runs_attempted: None,
|
||||
classifier_max_runs: None,
|
||||
last_classifier_verdict: None,
|
||||
last_classifier_details_path: None,
|
||||
verifying_completion: None,
|
||||
planning: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Format elapsed milliseconds as a compact human-readable duration.
|
||||
pub(crate) fn format_elapsed(ms: u64) -> String {
|
||||
let total_secs = ms / 1000;
|
||||
let hours = total_secs / 3600;
|
||||
let mins = (total_secs % 3600) / 60;
|
||||
let secs = total_secs % 60;
|
||||
if hours > 0 {
|
||||
format!("{hours}h{mins:02}m")
|
||||
} else if mins > 0 {
|
||||
format!("{mins}m{secs:02}s")
|
||||
} else {
|
||||
format!("{secs}s")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::session::goal_tracker::make_base_orchestration;
|
||||
|
||||
#[test]
|
||||
fn format_elapsed_seconds() {
|
||||
assert_eq!(format_elapsed(5_000), "5s");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_elapsed_minutes() {
|
||||
assert_eq!(format_elapsed(125_000), "2m05s");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_elapsed_hours() {
|
||||
assert_eq!(format_elapsed(3_661_000), "1h01m");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_goal_updated_empty_orchestration() {
|
||||
let o = make_base_orchestration();
|
||||
let update = build_goal_updated(&o, 0, 0);
|
||||
match update {
|
||||
XaiSessionUpdate::GoalUpdated {
|
||||
total_deliverables,
|
||||
completed_deliverables,
|
||||
classifier_runs_attempted,
|
||||
classifier_max_runs,
|
||||
last_classifier_verdict,
|
||||
last_classifier_details_path,
|
||||
verifying_completion,
|
||||
planning,
|
||||
..
|
||||
} => {
|
||||
assert_eq!(total_deliverables, 0);
|
||||
assert_eq!(completed_deliverables, 0);
|
||||
assert_eq!(classifier_runs_attempted, None);
|
||||
assert_eq!(classifier_max_runs, None);
|
||||
assert_eq!(last_classifier_verdict, None);
|
||||
assert_eq!(last_classifier_details_path, None);
|
||||
assert_eq!(verifying_completion, None);
|
||||
assert_eq!(planning, None);
|
||||
}
|
||||
_ => panic!("expected GoalUpdated"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_goal_updated_emits_classifier_fields() {
|
||||
use crate::session::goal_tracker::GoalClassifierVerdict;
|
||||
|
||||
let mut o = make_base_orchestration();
|
||||
o.classifier_runs_attempted = 2;
|
||||
o.classifier_max_runs = Some(3);
|
||||
o.last_classifier_verdict = Some(GoalClassifierVerdict::NotAchieved);
|
||||
o.last_classifier_details_path = Some("/tmp/details.md".into());
|
||||
|
||||
let update = build_goal_updated(&o, 0, 0);
|
||||
match update {
|
||||
XaiSessionUpdate::GoalUpdated {
|
||||
classifier_runs_attempted,
|
||||
classifier_max_runs,
|
||||
last_classifier_verdict,
|
||||
last_classifier_details_path,
|
||||
verifying_completion,
|
||||
..
|
||||
} => {
|
||||
assert_eq!(classifier_runs_attempted, Some(2));
|
||||
assert_eq!(classifier_max_runs, Some(3));
|
||||
assert_eq!(
|
||||
last_classifier_verdict,
|
||||
Some(GoalClassifierVerdict::NotAchieved)
|
||||
);
|
||||
assert_eq!(
|
||||
last_classifier_details_path.as_deref(),
|
||||
Some("/tmp/details.md")
|
||||
);
|
||||
assert_eq!(verifying_completion, None);
|
||||
}
|
||||
_ => panic!("expected GoalUpdated"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_goal_updated_planning_flag_reflects_latch() {
|
||||
let mut o = make_base_orchestration();
|
||||
assert_eq!(
|
||||
planning_field(&build_goal_updated(&o, 0, 0)),
|
||||
None,
|
||||
"latch off → no badge",
|
||||
);
|
||||
o.planning_in_flight = true;
|
||||
assert_eq!(
|
||||
planning_field(&build_goal_updated(&o, 0, 0)),
|
||||
Some(true),
|
||||
"latch on → every snapshot-derived update keeps the badge",
|
||||
);
|
||||
}
|
||||
|
||||
fn planning_field(update: &XaiSessionUpdate) -> Option<bool> {
|
||||
match update {
|
||||
XaiSessionUpdate::GoalUpdated { planning, .. } => *planning,
|
||||
_ => panic!("expected GoalUpdated"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_goal_updated_verifying_flag_reflects_latch() {
|
||||
// Regression: a one-shot verifying flag flickered off the moment any
|
||||
// mid-verification GoalUpdated fired. Sourced from the latch, every
|
||||
// snapshot-derived update keeps the "Verifying…" badge.
|
||||
let mut o = make_base_orchestration();
|
||||
let verifying = |u: &XaiSessionUpdate| match u {
|
||||
XaiSessionUpdate::GoalUpdated {
|
||||
verifying_completion,
|
||||
..
|
||||
} => *verifying_completion,
|
||||
_ => panic!("expected GoalUpdated"),
|
||||
};
|
||||
assert_eq!(verifying(&build_goal_updated(&o, 0, 0)), None, "latch off");
|
||||
o.verifying_in_flight = true;
|
||||
assert_eq!(
|
||||
verifying(&build_goal_updated(&o, 0, 0)),
|
||||
Some(true),
|
||||
"latch on → badge persists across mid-verification updates",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_goal_updated_propagates_tokens_to_wire() {
|
||||
let mut o = make_base_orchestration();
|
||||
o.live_subagent_tokens = 5_000;
|
||||
let update = build_goal_updated(&o, 60_000, 40_000);
|
||||
match update {
|
||||
XaiSessionUpdate::GoalUpdated {
|
||||
tokens_used,
|
||||
finished_subagent_tokens,
|
||||
live_subagent_tokens,
|
||||
..
|
||||
} => {
|
||||
// Live tokens are already inside `tokens_used` via the
|
||||
// records; folding them in again would double-count.
|
||||
assert_eq!(tokens_used, 60_000);
|
||||
assert_eq!(finished_subagent_tokens, 40_000);
|
||||
assert_eq!(live_subagent_tokens, Some(5_000));
|
||||
}
|
||||
_ => panic!("expected GoalUpdated"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_goal_updated_gates_per_model_breakdown_on_two_or_more_models() {
|
||||
// A single-model (or all-inherit) breakdown must NOT be transmitted
|
||||
// — the wire field stays empty so the doc contract holds and we
|
||||
// don't ship a 1-element vec the pager would collapse anyway.
|
||||
let mut o = make_base_orchestration();
|
||||
o.live_tokens_by_model = vec![("grok-4".into(), 5_000)];
|
||||
match build_goal_updated(&o, 0, 0) {
|
||||
XaiSessionUpdate::GoalUpdated {
|
||||
live_tokens_by_model,
|
||||
..
|
||||
} => assert!(
|
||||
live_tokens_by_model.is_empty(),
|
||||
"single model must not be transmitted"
|
||||
),
|
||||
_ => panic!("expected GoalUpdated"),
|
||||
}
|
||||
|
||||
// ≥2 distinct models are transmitted verbatim.
|
||||
o.live_tokens_by_model = vec![("grok-4".into(), 5_000), ("grok-3".into(), 3_000)];
|
||||
match build_goal_updated(&o, 0, 0) {
|
||||
XaiSessionUpdate::GoalUpdated {
|
||||
live_tokens_by_model,
|
||||
..
|
||||
} => assert_eq!(
|
||||
live_tokens_by_model,
|
||||
vec![("grok-4".to_owned(), 5_000), ("grok-3".to_owned(), 3_000)]
|
||||
),
|
||||
_ => panic!("expected GoalUpdated"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_goal_cleared_is_cleared() {
|
||||
let update = build_goal_cleared();
|
||||
match update {
|
||||
XaiSessionUpdate::GoalUpdated {
|
||||
status,
|
||||
classifier_runs_attempted,
|
||||
classifier_max_runs,
|
||||
last_classifier_verdict,
|
||||
last_classifier_details_path,
|
||||
verifying_completion,
|
||||
live_tokens_by_model,
|
||||
..
|
||||
} => {
|
||||
assert_eq!(status, "cleared");
|
||||
assert_eq!(classifier_runs_attempted, None);
|
||||
assert_eq!(classifier_max_runs, None);
|
||||
assert_eq!(last_classifier_verdict, None);
|
||||
assert_eq!(last_classifier_details_path, None);
|
||||
assert_eq!(verifying_completion, None);
|
||||
assert!(live_tokens_by_model.is_empty());
|
||||
}
|
||||
_ => panic!("expected GoalUpdated"),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user