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:
2026-07-23 16:55:39 -04:00
parent ff0fb56c67
commit a02b555e66
1458 changed files with 10729 additions and 21750 deletions
@@ -1,9 +1,6 @@
//! ACP stdio clients for testing kigi sessions end-to-end: the typed
//! [`KigiStdioClient`] (`agent-client-protocol::ClientSideConnection` —
//! authentication, session lifecycle, permissions, notification streaming) and
//! the raw-wire [`RawStdioClient`] (verbatim JSON-RPC lines for shapes the
//! typed client can't produce), plus the shared subprocess spawn/stderr-capture
//! plumbing used by every harness in this crate.
//! ACP stdio clients for driving `kigi agent stdio` end-to-end: the typed
//! [`KigiStdioClient`] and the raw-wire [`RawStdioClient`], which emits
//! verbatim JSON-RPC lines for shapes the typed client cannot produce.
use std::path::Path;
use std::sync::Arc;
@@ -20,12 +17,10 @@ use crate::headless::stderr_tail;
use crate::mock_server::MockInferenceServer;
use crate::process::spawn_piped_with_stderr_capture;
/// Spawn `kigi agent stdio` with the canonical hermetic test env: the sandbox
/// from [`test_env_cmd_tokio`] plus the debug-logging kill-list, so the
/// hermeticity setup exists exactly once for the typed ([`KigiStdioClient`])
/// and raw ([`RawStdioClient`]) harnesses. `leading_args` go before the
/// `agent stdio` subcommand (global flags); `extra_env` is applied after the
/// kill-list so a test can still set e.g. `KIGI_DEBUG_LOG=1` explicitly.
/// Spawn `kigi agent stdio` with the hermetic test env. `leading_args` go
/// before the `agent stdio` subcommand (global flags); `extra_env` is applied
/// after the debug-logging kill-list so a test can still set e.g.
/// `KIGI_DEBUG_LOG=1` explicitly.
fn spawn_agent_process(
server: &MockInferenceServer,
cwd: &Path,
@@ -40,9 +35,8 @@ fn spawn_agent_process(
.args(["agent", "stdio"])
.current_dir(cwd);
test_env_cmd_tokio(&mut cmd, &server.url(), home);
// Hermetic firehose env: clear inherited debug-logging knobs so a test
// controls logging only via `extra_env` / `leading_args` (mirrors the
// headless `debug_cmd`).
// Clear inherited debug-logging knobs so a test controls logging only via
// `extra_env` / `leading_args`.
for k in [
"KIGI_DEBUG_LOG",
"KIGI_LOG_FILE",
@@ -64,7 +58,7 @@ struct TextCapture {
notification_count: AtomicU32,
}
/// ACP client impl: auto-approves permissions, captures text chunks.
/// ACP client that auto-approves every permission request.
struct TestAcpClient {
capture: Arc<TextCapture>,
}
@@ -75,7 +69,6 @@ impl acp::Client for TestAcpClient {
&self,
args: acp::RequestPermissionRequest,
) -> acp::Result<acp::RequestPermissionResponse> {
// Auto-approve: pick AllowOnce if available, otherwise first option.
let outcome = args
.options
.iter()
@@ -107,10 +100,9 @@ impl acp::Client for TestAcpClient {
}
}
/// Drives `kigi agent stdio` via the ACP protocol over pipes.
///
/// Handles the full lifecycle: spawn → initialize → authenticate → session → prompt.
/// Child process is killed on drop.
/// Drives `kigi agent stdio` via the ACP protocol over pipes: spawn →
/// initialize → authenticate → session → prompt. The child process is killed
/// on drop.
pub struct KigiStdioClient {
conn: acp::ClientSideConnection,
_child: tokio::process::Child,
@@ -130,8 +122,7 @@ impl KigiStdioClient {
}
/// Like [`spawn_with_home`] but applies extra environment variables to the
/// child process (after the standard test env). Used by tests that toggle
/// behavior via env vars (e.g. the vendor-compat suite).
/// child process, after the standard test env.
pub async fn spawn_with_home_and_env(
server: &MockInferenceServer,
cwd: &Path,
@@ -142,8 +133,8 @@ impl KigiStdioClient {
}
/// Like [`spawn_with_home_and_env`] but also prepends `leading_args` before
/// the `agent stdio` subcommand. Used to drive top-level global flags (e.g.
/// `--debug`) so a test can exercise the flag's master switch, not just env.
/// the `agent stdio` subcommand, so a test can exercise top-level global
/// flags such as `--debug` rather than only their env equivalents.
pub async fn spawn_with_home_env_and_args(
server: &MockInferenceServer,
cwd: &Path,
@@ -237,7 +228,6 @@ impl KigiStdioClient {
resp.session_id
}
/// Create a session with a specific model pre-selected.
pub async fn create_session_with_model(&self, cwd: &Path, model_id: &str) -> acp::SessionId {
let resp = self
.conn
@@ -255,7 +245,6 @@ impl KigiStdioClient {
resp.session_id
}
/// Switch model on an existing session via the typed ACP `session/set_model`.
pub async fn set_model(
&self,
session_id: &acp::SessionId,
@@ -301,7 +290,6 @@ impl KigiStdioClient {
self.home.take().expect("test home already taken")
}
/// Return the home directory path (for cache invalidation between phases).
pub fn home_path(&self) -> &std::path::Path {
self.home.as_ref().expect("test home already taken").path()
}
@@ -401,7 +389,7 @@ impl KigiStdioClient {
/// Exists for wire shapes the typed [`KigiStdioClient`] (`ClientSideConnection`,
/// integer ids) can never produce — e.g. Xcode's Swift/Foundation `JSONEncoder`
/// output: escaped-slash methods (`"session\/prompt"`) and string UUID request
/// ids. Child process is killed on drop.
/// ids. The child process is killed on drop.
pub struct RawStdioClient {
stdin: tokio::process::ChildStdin,
stdout: tokio::io::BufReader<tokio::process::ChildStdout>,
@@ -431,7 +419,7 @@ impl RawStdioClient {
String::from_utf8_lossy(&self.stderr.lock().unwrap()).into_owned()
}
/// Write `line` verbatim followed by `\n`, and flush.
/// Write `line` verbatim — no re-encoding — followed by `\n`, then flush.
pub async fn send_line(&mut self, line: &str) {
use tokio::io::AsyncWriteExt as _;
@@ -444,13 +432,13 @@ impl RawStdioClient {
}
/// Read stdout lines until the response to `id` arrives (no `method` key +
/// exact string-id match) — returning IS the id-echo assertion: an id
/// echoed with different bytes or as a different JSON type never matches
/// and surfaces in the timeout diagnostics instead. Notifications are
/// skipped; any agent→client request is refused with a JSON-RPC error so a
/// turn can never hang on this capability-less client. On timeout the
/// panic reports how much non-matching traffic was seen (0 = true
/// silence, the acp-0.6 escaped-method symptom) plus the last few lines.
/// exact string-id match). Returning IS the id-echo assertion: an id echoed
/// with different bytes or as a different JSON type never matches and
/// surfaces in the timeout diagnostics instead. Any agent→client request is
/// refused with a JSON-RPC error so a turn can never hang on this
/// capability-less client. The timeout panic reports how much non-matching
/// traffic was seen — 0 means true silence, the acp-0.6 escaped-method
/// symptom.
pub async fn response_for_id(
&mut self,
id: &str,
@@ -504,7 +492,7 @@ impl RawStdioClient {
}
/// Record a non-matching line for [`RawStdioClient::response_for_id`]'s timeout
/// diagnostics: bump the count, keep the last 3 lines (truncated).
/// diagnostics.
fn push_skipped_tail(skipped: &mut usize, tail: &mut Vec<String>, line: &str) {
*skipped += 1;
if tail.len() == 3 {
@@ -1,6 +1,6 @@
//! Minimal connection-counting HTTP/1.1 server for wire-level tests that need
//! to assert TCP connection reuse (e.g. shared-client pooling): it counts
//! accepted connections and records each request's header block.
//! Minimal keep-alive HTTP/1.1 server for wire-level tests that assert TCP
//! connection reuse (e.g. shared-client pooling): it counts accepted
//! connections and records each request's header block.
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
@@ -8,8 +8,7 @@ use std::sync::{Arc, Mutex};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpListener;
/// Minimal keep-alive HTTP/1.1 server: counts accepted connections and
/// records each request's header block.
/// Returns the base URL, the accepted-connection count, and the header blocks.
pub async fn spawn_counting_server() -> (String, Arc<AtomicUsize>, Arc<Mutex<Vec<String>>>) {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let base_url = format!("http://{}/v1", listener.local_addr().unwrap());
+4 -5
View File
@@ -20,8 +20,7 @@ pub struct EnvGuard {
}
impl EnvGuard {
/// Set `key` to `value` for the guard's lifetime. Accepts `&str`, `&Path`,
/// `String`, etc. via `AsRef<OsStr>`.
/// Set `key` to `value` for the guard's lifetime.
pub fn set(key: &'static str, value: impl AsRef<OsStr>) -> Self {
let prior = std::env::var_os(key);
// SAFETY: callers are `#[serial]`, so no other thread touches the env.
@@ -29,7 +28,6 @@ impl EnvGuard {
Self { key, prior }
}
/// Unset `key` for the guard's lifetime.
pub fn unset(key: &'static str) -> Self {
let prior = std::env::var_os(key);
// SAFETY: see [`EnvGuard::set`].
@@ -95,7 +93,8 @@ fn ensure_local_kigi_binary(binary: &Path) {
);
}
/// Resolve kigi binary: `KIGI_BINARY` env (CI) or a locally built `kigi-tui` binary.
/// Resolve the kigi binary, building `kigi-tui` on demand if neither
/// `KIGI_BINARY` (CI) nor cargo's own path points at an existing file.
pub fn kigi_binary() -> PathBuf {
if let Ok(path) = std::env::var("KIGI_BINARY") {
let p = PathBuf::from(path);
@@ -137,7 +136,7 @@ pub fn git_workdir() -> TempDir {
}
run_git(&["init"], path);
// Configure git user for commits (required in CI where no global config exists)
// CI has no global git identity, so commits need one configured here.
run_git(&["config", "user.email", "test@test.com"], path);
run_git(&["config", "user.name", "Test"], path);
@@ -1,6 +1,5 @@
//! Headless mode (`kigi -p`) test runner.
//!
//! Runs the kigi binary as a subprocess with the mock server, captures output.
//! Headless mode (`kigi -p`) test runner: runs the kigi binary as a subprocess
//! against the mock server and captures its output.
use std::path::Path;
use std::process::ExitStatus;
@@ -21,8 +20,8 @@ pub struct HeadlessResult {
const HEADLESS_TIMEOUT_SECS: u64 = 60;
/// Run `kigi` with the given args against the mock server, with a 60s timeout.
/// Uses an isolated HOME and disables telemetry.
/// Run `kigi` with the given args against the mock server in an isolated HOME,
/// with telemetry disabled.
pub async fn run_headless(
server: &MockInferenceServer,
args: &[&str],
@@ -114,12 +113,10 @@ const CRASH_PATTERNS: &[&str] = &[
"cannot open shared object",
];
/// Diagnostic helper: format the tail of stderr for assertion messages.
pub fn stderr_tail(stderr: &str, max_chars: usize) -> &str {
&stderr[stderr.len().saturating_sub(max_chars)..]
}
/// Assert that a headless run succeeded (non-timeout, zero exit code).
pub fn assert_headless_success(
result: &HeadlessResult,
label: &str,
@@ -141,7 +138,6 @@ pub fn assert_headless_success(
);
}
/// Panic if stderr contains any crash/linking-failure indicators.
pub fn assert_no_crashes(stderr: &str) {
let lower = stderr.to_lowercase();
for pattern in CRASH_PATTERNS {
@@ -35,19 +35,14 @@ fn role_binary(env_key: &str) -> PathBuf {
kigi_binary()
}
/// Binary for the leader-electing side of a version-skew test
/// (`KIGI_BINARY_LEADER`, else the shared [`kigi_binary`] resolution).
pub fn leader_binary() -> PathBuf {
role_binary(LEADER_BINARY_ENV)
}
/// Binary for the client side of a version-skew test (`KIGI_BINARY_CLIENT`,
/// else the shared [`kigi_binary`] resolution).
pub fn client_binary() -> PathBuf {
role_binary(CLIENT_BINARY_ENV)
}
/// Capture for notifications + reconnect signals.
#[derive(Default)]
pub struct Capture {
chunks: std::sync::Mutex<Vec<String>>,
@@ -296,10 +291,11 @@ pub fn read_leader_pid(home: &Path) -> Option<u32> {
}
pub fn pid_alive(pid: u32) -> bool {
// SAFETY: `kill` with signal 0 delivers nothing and only probes whether the
// process exists; every `pid` value is a valid argument.
unsafe { libc::kill(pid as i32, 0) == 0 }
}
/// Wait until the leader lock file contains a live PID, return it.
pub async fn wait_for_live_leader(home: &Path, timeout: Duration) -> Option<u32> {
let deadline = tokio::time::Instant::now() + timeout;
while tokio::time::Instant::now() < deadline {
@@ -313,7 +309,6 @@ pub async fn wait_for_live_leader(home: &Path, timeout: Duration) -> Option<u32>
None
}
/// Wait until the leader lock file contains a live PID *different* from `old_pid`.
pub async fn wait_for_new_leader(home: &Path, old_pid: u32, timeout: Duration) -> Option<u32> {
let deadline = tokio::time::Instant::now() + timeout;
while tokio::time::Instant::now() < deadline {
+3 -12
View File
@@ -6,19 +6,10 @@
dead_code
)]
//! Shared test utilities for kigi crates: mock inference server, SSE
//! generators, ACP stdio client, headless runner, env sandbox.
//! generators, ACP stdio clients, headless runner, env sandbox.
//!
//! Provides:
//! - [`MockInferenceServer`] — Mock /v1/chat/completions + /v1/responses with request logging
//! - [`KigiStdioClient`] — ACP client that drives `kigi agent stdio` as a subprocess
//! - [`RawStdioClient`] — raw-wire ACP driver for bytes the typed client can't
//! produce (Foundation `\/` methods, string UUID ids)
//! - [`leader::LeaderStdioClient`] — ACP client that drives `kigi agent --leader stdio` (unix)
//! - [`run_headless`] — Run `kigi -p` against the mock server and capture output
//! - [`git_workdir`] — Create a temp directory with git repo (forces libgit2 init)
//! - [`kigi_binary`] — Resolve the kigi binary path (KIGI_BINARY env or cargo_bin)
//! - [`spawn_counting_server`] — Connection-counting HTTP/1.1 server for wire/pooling tests
//! - [`uds_proxy::UdsProxy`] — Frame-aware fault-injection proxy for leader IPC sockets (unix)
//! [`RawStdioClient`] complements [`KigiStdioClient`] for wire bytes the typed
//! client cannot produce (Foundation `\/` methods, string UUID ids).
pub mod acp_client;
pub mod counting_server;
pub mod env;
@@ -35,7 +35,6 @@ pub struct LogEntry {
pub method: String,
pub path: String,
pub body: Option<Value>,
/// Value of the `Authorization` header, if present.
pub authorization: Option<String>,
/// Request headers (lowercase names, arrival order), captured on the
/// inference POST endpoints; the GET endpoints log an empty list.
@@ -90,13 +89,11 @@ type ScriptQueues = Arc<std::sync::Mutex<HashMap<String, VecDeque<ScriptedRespon
/// A model entry for the mock `/v1/models` endpoint.
#[derive(Debug, Clone)]
pub struct MockModelEntry {
/// Model ID (e.g. `"test-model"`).
pub id: String,
/// Optional agent type (e.g. `"cursor"`).
/// Emitted as `agentType` inside `_meta` when set.
/// Emitted as `agentType` inside `_meta` when set (e.g. `"cursor"`).
pub agent_type: Option<String>,
/// Optional API backend (e.g. `"messages"`). Emitted as `apiBackend`
/// when set; absent means the shell's default backend.
/// Emitted as `apiBackend` when set (e.g. `"messages"`); absent means the
/// shell's default backend.
pub api_backend: Option<String>,
/// Emitted as `supportsBackendSearch` when true.
pub supports_backend_search: bool,
@@ -251,7 +248,6 @@ fn paced_events(
if let Some(d) = delay {
tokio::time::sleep(d).await;
}
// Hold the terminal event until the gate is released.
if idx == last_idx
&& let Some(gate) = gate.as_deref()
{
@@ -273,7 +269,6 @@ pub struct StorageUpload {
pub size: usize,
/// Request body when `size <= 256 KiB`; empty for larger payloads.
pub body: Vec<u8>,
/// `Authorization` header value as sent (e.g. `Bearer …`).
pub authorization: Option<String>,
}
@@ -384,7 +379,6 @@ impl MockInferenceServer {
.unwrap();
});
// Wait for server readiness — try connecting instead of a fixed sleep.
let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
while tokio::net::TcpStream::connect(addr).await.is_err() {
if tokio::time::Instant::now() >= deadline {
@@ -546,7 +540,6 @@ impl MockInferenceServer {
.any(|e| e.path.contains("responses"))
}
/// Number of `POST /v1/messages` requests received so far.
pub fn messages_request_count(&self) -> usize {
self.log
.entries
@@ -557,7 +550,6 @@ impl MockInferenceServer {
.count()
}
/// Format the request log for diagnostic output on test failures.
pub fn request_log_summary(&self) -> String {
let entries = self.log.entries.lock().unwrap();
if entries.is_empty() {
@@ -571,7 +563,6 @@ impl MockInferenceServer {
.join("\n")
}
/// Get the system prompt from the most recent inference request.
pub fn last_system_prompt(&self) -> Option<String> {
let entries = self.log.entries.lock().unwrap();
entries
@@ -579,15 +570,15 @@ impl MockInferenceServer {
.rev()
.find(|e| e.path.contains("chat/completions") || e.path.contains("responses"))
.and_then(|e| e.body.as_ref())
// Chat completions carries the system prompt as the first message;
// the Responses API carries it in `instructions` instead.
.and_then(|body| {
// Chat completions format: messages[0].content (system message)
body.get("messages")
.and_then(|m| m.as_array())
.and_then(|msgs| msgs.first())
.and_then(|msg| msg.get("content"))
.and_then(|c| c.as_str())
.map(String::from)
// Responses API format: instructions field
.or_else(|| {
body.get("instructions")
.and_then(|s| s.as_str())
@@ -1115,7 +1106,6 @@ mod tests {
const MERMAID_TEXT: &str =
"Here is a flow:\n\n```mermaid\nflowchart TD\n A --> B\n```\n\nDone.\n";
/// Payloads of all `data:` lines in an SSE body, minus the `[DONE]` marker.
fn sse_data_payloads(body: &str) -> Vec<String> {
body.lines()
.filter_map(|l| l.strip_prefix("data:"))
@@ -1124,7 +1114,6 @@ mod tests {
.collect()
}
/// Concatenation of all chat-completion content deltas in an SSE body.
fn chat_stream_text(body: &str) -> String {
sse_data_payloads(body)
.iter()
@@ -1140,7 +1129,6 @@ mod tests {
.collect()
}
/// Concatenation of all responses-API output_text deltas in an SSE body.
fn responses_stream_text(body: &str) -> String {
sse_data_payloads(body)
.iter()
@@ -1150,7 +1138,6 @@ mod tests {
.collect()
}
/// Concatenation of all Anthropic Messages text deltas in an SSE body.
fn messages_stream_text(body: &str) -> String {
sse_data_payloads(body)
.iter()
@@ -1184,7 +1171,7 @@ mod tests {
let body = post_chat(&server, "ping pong").await.text().await.unwrap();
assert_eq!(chat_stream_text(&body), "Echo: ping pong");
// Echo mode keeps its historical whitespace-collapsing semantics.
// Echo mode collapses whitespace by design; only fixed mode is exact.
let body = post_chat(&server, "a b\nc").await.text().await.unwrap();
assert_eq!(chat_stream_text(&body), "Echo: a b c");
}
@@ -1337,8 +1324,8 @@ mod tests {
assert_eq!(resp.status(), 401);
}
/// Scripted response headers reach the client (the phase-2 script format's
/// named consumer: 429 + Retry-After error injection).
/// Scripted headers must reach the client: 429 + `Retry-After` injection
/// depends on them.
#[tokio::test]
async fn scripted_response_headers_reach_the_client() {
let server = MockInferenceServer::start().await.unwrap();
@@ -11,7 +11,6 @@ use axum::response::{IntoResponse, Response};
use futures_util::stream;
use serde_json::Value;
/// One SSE event as data: optional `event:` name plus the `data:` payload.
#[derive(Debug, Clone)]
pub struct SseEvent {
pub event: Option<String>,
@@ -19,7 +18,6 @@ pub struct SseEvent {
}
impl SseEvent {
/// Event with a `data:` payload only.
pub fn data(data: impl Into<String>) -> Self {
Self {
event: None,
@@ -27,7 +25,6 @@ impl SseEvent {
}
}
/// Event with an `event:` name and a `data:` payload.
pub fn with_event(event: impl Into<String>, data: impl Into<String>) -> Self {
Self {
event: Some(event.into()),
@@ -36,12 +33,11 @@ impl SseEvent {
}
}
/// Body of a [`ScriptedResponse`].
#[derive(Debug, Clone)]
pub enum ScriptedBody {
Json(Value),
Sse(Vec<SseEvent>),
/// Raw body bytes, served verbatim (byte-controllable malformed SSE etc.).
/// Served verbatim, so the caller controls every byte (malformed SSE etc.).
Raw(String),
}
@@ -56,7 +52,6 @@ pub struct ScriptedResponse {
}
impl ScriptedResponse {
/// 200 SSE response built from an event list.
pub fn sse(events: Vec<SseEvent>) -> Self {
Self {
status: 200,
@@ -65,7 +60,6 @@ impl ScriptedResponse {
}
}
/// JSON body with the given status.
pub fn json(status: u16, body: Value) -> Self {
Self {
status,
@@ -74,7 +68,6 @@ impl ScriptedResponse {
}
}
/// Raw text body with the given status.
pub fn text(status: u16, body: impl Into<String>) -> Self {
Self {
status,
@@ -93,10 +86,9 @@ impl ScriptedResponse {
}
}
/// Render to HTTP with SSE events paced by `delay` (sleep before each
/// event, mirroring the fixed/echo `paced_events` pacing) so
/// `set_chunk_delay` also holds scripted turns open. `None` streams
/// instantly. Non-SSE bodies ignore the delay.
/// SSE events are paced by sleeping `delay` before each one, mirroring the
/// fixed/echo `paced_events` pacing so `set_chunk_delay` also holds
/// scripted turns open. `None` streams instantly; non-SSE bodies ignore it.
pub(crate) fn into_response_paced(self, delay: Option<std::time::Duration>) -> Response {
use futures_util::StreamExt as _;
let mut resp = match self.body {
+59 -136
View File
@@ -1,15 +1,12 @@
//! SSE stream generators for mock inference endpoints.
//!
//! These produce the exact wire format that the kigi sampling client expects,
//! validated against the real sampling client.
//! SSE stream generators for mock inference endpoints, producing the exact
//! wire format the real kigi sampling client parses.
use axum::response::sse::Event;
use serde_json::json;
use crate::scripted::SseEvent;
/// Generate Anthropic Messages SSE events: one text block streamed as a
/// single delta, terminated by a `message_delta` carrying `stop_reason`.
/// Anthropic Messages: one text block streamed as a single delta.
pub fn messages_api_events(text: &str, model: &str, stop_reason: &str) -> Vec<Event> {
vec![
Event::default().data(
@@ -43,30 +40,27 @@ pub fn messages_api_events(text: &str, model: &str, stop_reason: &str) -> Vec<Ev
]
}
/// Generate ChatCompletions SSE events that stream `text` word-by-word
/// (whitespace-collapsing; use [`chat_completion_events_exact`] when the
/// receiver must reconstruct `text` byte-for-byte).
/// Streams `text` word-by-word, collapsing whitespace runs; use
/// [`chat_completion_events_exact`] when the receiver must reconstruct `text`
/// byte-for-byte.
pub fn chat_completion_events(text: &str, model: &str) -> Vec<Event> {
chat_completion_events_from_deltas(&space_prefixed_deltas(text.split_whitespace()), model)
}
/// Like [`chat_completion_events`] but byte-exact: concatenating the deltas
/// reproduces `text` byte-for-byte (newlines and whitespace runs preserved).
/// Fenced code blocks (mermaid etc.) need their newlines to parse as a block,
/// which `split_whitespace` would destroy.
/// reproduces `text` byte-for-byte. Fenced code blocks (mermaid etc.) need
/// their newlines to parse as a block, which `split_whitespace` destroys.
pub fn chat_completion_events_exact(text: &str, model: &str) -> Vec<Event> {
chat_completion_events_from_deltas(&chat_completion_deltas(text), model)
}
/// Split `text` into deltas that reconstruct it byte-for-byte: the first
/// carries no leading space; each subsequent one is ` {word}` (split on
/// single spaces only, so newlines/tabs stay inside the words).
/// Splits on single spaces only, so newlines and tabs stay inside the words
/// and concatenating the deltas reconstructs `text` byte-for-byte.
fn chat_completion_deltas(text: &str) -> Vec<String> {
space_prefixed_deltas(text.split(' '))
}
/// Shape words into chat deltas: first word bare, each subsequent one ` {word}`
/// — the source iterator decides collapsing (echo) vs byte-exact (fixed).
/// The caller's iterator decides collapsing (echo) vs byte-exact (fixed).
fn space_prefixed_deltas<'a>(words: impl Iterator<Item = &'a str>) -> Vec<String> {
words
.enumerate()
@@ -119,7 +113,6 @@ fn chat_completion_events_from_deltas(deltas: &[String], model: &str) -> Vec<Eve
events.push(Event::default().data(chunk.to_string()));
}
// Usage chunk
events.push(
Event::default().data(
json!({
@@ -141,9 +134,9 @@ fn chat_completion_events_from_deltas(deltas: &[String], model: &str) -> Vec<Eve
events
}
/// Generate Responses API SSE events that stream `text` word-by-word
/// (whitespace-collapsing; use [`responses_api_events_exact`] when the
/// receiver must reconstruct `text` byte-for-byte).
/// Streams `text` word-by-word, collapsing whitespace runs; use
/// [`responses_api_events_exact`] when the receiver must reconstruct `text`
/// byte-for-byte.
pub fn responses_api_events(text: &str, model: &str) -> Vec<Event> {
let deltas: Vec<String> = text
.split_whitespace()
@@ -153,7 +146,7 @@ pub fn responses_api_events(text: &str, model: &str) -> Vec<Event> {
}
/// Like [`responses_api_events`] but byte-exact: concatenating the deltas
/// reproduces `text` byte-for-byte (newlines and whitespace runs preserved).
/// reproduces `text` byte-for-byte.
pub fn responses_api_events_exact(text: &str, model: &str) -> Vec<Event> {
responses_api_events_from_deltas(&responses_api_deltas(text), text, model)
}
@@ -170,7 +163,6 @@ fn responses_api_events_from_deltas(deltas: &[String], text: &str, model: &str)
let mut events = Vec::new();
let mut seq = 0;
// response.created
events.push(
Event::default().data(
json!({
@@ -190,7 +182,6 @@ fn responses_api_events_from_deltas(deltas: &[String], text: &str, model: &str)
);
seq += 1;
// Text deltas
for chunk in deltas {
events.push(
Event::default().data(
@@ -208,7 +199,6 @@ fn responses_api_events_from_deltas(deltas: &[String], text: &str, model: &str)
seq += 1;
}
// response.completed
events.push(
Event::default().data(
json!({
@@ -247,22 +237,19 @@ fn responses_api_events_from_deltas(deltas: &[String], text: &str, model: &str)
events
}
/// Generate Responses API SSE events for a reasoning-only completion: the
/// model streams reasoning summary deltas and finishes with a `reasoning`
/// output item but NO message / output-text and no tool call. The shell's
/// collector synthesizes an empty assistant, so the response classifies as
/// `EmptyReason::ReasoningOnly` — the trigger that makes the sampler resample
/// (the model doomloop).
/// Reasoning-only completion: reasoning summary deltas and a `reasoning`
/// output item, with no message, output text or tool call.
/// `response_to_conversation_items` appends an empty assistant, yielding
/// `[Reasoning, Assistant("")]`, so the turn classifies as
/// `EmptyReason::ReasoningOnly` and the sampler resamples (the model doomloop).
///
/// Returns [`SseEvent`]s (not axum `Event`s) for direct use with
/// [`crate::ScriptedResponse::sse`] / `enqueue_response`: reasoning-only is a
/// scripted scenario, not an echo/fixed response mode, so it is not wired into
/// the `mock_server` mode handlers.
/// Returns [`SseEvent`]s rather than axum `Event`s because this is a scripted
/// scenario ([`crate::ScriptedResponse::sse`] / `enqueue_response`), not an
/// echo/fixed response mode wired into the `mock_server` handlers.
pub fn responses_api_reasoning_only_events(reasoning: &str, model: &str) -> Vec<SseEvent> {
let mut events = Vec::new();
let mut seq = 0;
// response.created
events.push(SseEvent::data(
json!({
"type": "response.created",
@@ -280,7 +267,6 @@ pub fn responses_api_reasoning_only_events(reasoning: &str, model: &str) -> Vec<
));
seq += 1;
// Reasoning summary deltas — the only content the model streams.
for word in reasoning.split_whitespace() {
events.push(SseEvent::data(
json!({
@@ -296,9 +282,6 @@ pub fn responses_api_reasoning_only_events(reasoning: &str, model: &str) -> Vec<
seq += 1;
}
// response.completed: a single `reasoning` output item carrying the full
// summary and NO message item. `response_to_conversation_items` appends an
// empty assistant, yielding `[Reasoning, Assistant("")]` → reasoning_only.
events.push(SseEvent::data(
json!({
"type": "response.completed",
@@ -333,14 +316,10 @@ pub fn responses_api_reasoning_only_events(reasoning: &str, model: &str) -> Vec<
events
}
/// Generate Responses API SSE events for a completion that streams reasoning
/// summary deltas FIRST and then a normal text answer: the shape a
/// Reasoning summary deltas first, then a normal text answer: the shape a
/// reasoning-capable model produces on an ordinary turn. `response.completed`
/// carries both output items (`reasoning` + `message`), so the collector
/// yields `[Reasoning, Assistant(text)]` — a full, non-empty turn.
///
/// Returns [`SseEvent`]s for direct use with [`crate::ScriptedResponse::sse`]
/// / `enqueue_response`, mirroring [`responses_api_reasoning_only_events`].
/// carries both output items, so the collector yields
/// `[Reasoning, Assistant(text)]` — a full, non-empty turn.
pub fn responses_api_reasoning_and_text_events(
reasoning: &str,
text: &str,
@@ -349,7 +328,6 @@ pub fn responses_api_reasoning_and_text_events(
let mut events = Vec::new();
let mut seq = 0;
// response.created
events.push(SseEvent::data(
json!({
"type": "response.created",
@@ -367,7 +345,6 @@ pub fn responses_api_reasoning_and_text_events(
));
seq += 1;
// Reasoning summary deltas stream before any answer text.
for word in reasoning.split_whitespace() {
events.push(SseEvent::data(
json!({
@@ -383,7 +360,6 @@ pub fn responses_api_reasoning_and_text_events(
seq += 1;
}
// Then the visible answer.
for word in text.split_whitespace() {
events.push(SseEvent::data(
json!({
@@ -399,7 +375,6 @@ pub fn responses_api_reasoning_and_text_events(
seq += 1;
}
// response.completed with BOTH items: reasoning + the assistant message.
events.push(SseEvent::data(
json!({
"type": "response.completed",
@@ -448,14 +423,12 @@ pub fn responses_api_reasoning_and_text_events(
}
/// SSE `event:` name and payload `type` of the non-standard doom-loop check
/// event (`kigi_sampling_types::DOOM_LOOP_CHECK_EVENT_TYPE`). Hardcoded
/// like every other wire string in this file; the shell integration tests
/// pin the two spellings against each other by absorbing built frames
/// through the real client.
/// event, duplicating `kigi_sampling_types::DOOM_LOOP_CHECK_EVENT_TYPE` like
/// every other wire string here; the shell integration tests pin the two
/// spellings against each other by absorbing built frames through the real
/// client.
const DOOM_LOOP_CHECK_EVENT: &str = "response.doom_loop_check";
/// One named `response.doom_loop_check` frame carrying the (cumulative)
/// trigger set, in the inference API's wire shape.
fn doom_loop_check_frame(triggers: &[&str], seq: u64) -> SseEvent {
SseEvent::with_event(
DOOM_LOOP_CHECK_EVENT,
@@ -469,11 +442,11 @@ fn doom_loop_check_frame(triggers: &[&str], seq: u64) -> SseEvent {
}
/// Inject `doom_loop_check.triggers` into a turn's terminal
/// `response.completed` object — the single home for terminal-field emission,
/// the dual of the mid-stream [`doom_loop_check_frame`]. Composes over any
/// turn builder (re-serialization may reorder JSON keys; clients and shape
/// tests parse, never byte-compare, these frames). Panics when the turn has
/// no completed frame: every builder emits one, so a miss is a script bug.
/// `response.completed` object — the dual of the mid-stream
/// [`doom_loop_check_frame`]. Composes over any turn builder; re-serialization
/// may reorder JSON keys, which is safe because clients and shape tests parse
/// these frames rather than byte-compare them. Every builder emits a completed
/// frame, so a miss is a script bug.
fn with_terminal_doom_loop_field(mut events: Vec<SseEvent>, triggers: &[&str]) -> Vec<SseEvent> {
let patched = events.iter_mut().any(|e| {
if e.data == "[DONE]" {
@@ -496,16 +469,12 @@ fn with_terminal_doom_loop_field(mut events: Vec<SseEvent>, triggers: &[&str]) -
events
}
/// Generate Responses API SSE events for a server-detected doom loop: a
/// reasoning-only stream (the doomed signature — the model loops in its
/// thinking and never answers) followed by named `response.doom_loop_check`
/// frames re-sent with the growing **cumulative** trigger set (one frame per
/// prefix of `triggers`, mirroring how the server re-emits as new triggers
/// appear), and a terminal `response.completed` whose response object carries
/// the full set under `doom_loop_check.triggers`.
///
/// Returns [`SseEvent`]s for direct use with [`crate::ScriptedResponse::sse`]
/// / `enqueue_response`, mirroring [`responses_api_reasoning_only_events`].
/// Server-detected doom loop: a reasoning-only stream (the doomed signature —
/// the model loops in its thinking and never answers) followed by named
/// `response.doom_loop_check` frames carrying the growing **cumulative**
/// trigger set (one frame per prefix of `triggers`, mirroring how the server
/// re-emits as new triggers appear), and a terminal `response.completed` whose
/// response object carries the full set under `doom_loop_check.triggers`.
pub fn responses_api_doom_loop_check_events(
triggers: &[&str],
reasoning: &str,
@@ -524,10 +493,9 @@ pub fn responses_api_doom_loop_check_events(
with_terminal_doom_loop_field(events, triggers)
}
/// Generate Responses API SSE events for an ordinary reasoning + text turn
/// (mirroring [`responses_api_reasoning_and_text_events`]) whose terminal
/// `response.completed` object carries `doom_loop_check.triggers` with NO
/// mid-stream check frame — the terminal-only copy of the signal.
/// An ordinary reasoning + text turn whose terminal `response.completed`
/// object carries `doom_loop_check.triggers` with NO mid-stream check frame —
/// the terminal-only copy of the signal.
pub fn responses_api_doom_loop_terminal_only_events(
triggers: &[&str],
reasoning: &str,
@@ -543,8 +511,8 @@ pub fn responses_api_doom_loop_terminal_only_events(
/// Splice ONE named `response.doom_loop_check` frame with an arbitrary
/// `data:` payload — a byte-exact wire fixture or a malformed variant — into
/// an otherwise-normal reasoning + text turn, right after `response.created`.
/// The payload's own `sequence_number` (if any) is its business; clients
/// never validate sequence continuity.
/// The payload's own `sequence_number` (if any) is its business: clients never
/// validate sequence continuity.
pub fn responses_api_with_doom_loop_frame(
check_frame_data: &str,
reasoning: &str,
@@ -559,16 +527,11 @@ pub fn responses_api_with_doom_loop_frame(
events
}
/// Generate Responses API SSE events for a turn that streams reasoning
/// summary deltas FIRST and then issues one `function_call` — the shape a
/// reasoning-capable model produces when it thinks before its first tool
/// call. `response.completed` carries both output items (`reasoning` +
/// `function_call`) and no message, so the collector yields
/// `[Reasoning, ToolCall]` (tool calls keep the turn non-empty — no
/// `EmptyReason::ReasoningOnly` resample).
///
/// Returns [`SseEvent`]s for direct use with [`crate::ScriptedResponse::sse`]
/// / `enqueue_response`, mirroring [`responses_api_reasoning_only_events`].
/// Reasoning summary deltas first, then one `function_call` — the shape a
/// reasoning-capable model produces when it thinks before its first tool call.
/// `response.completed` carries both output items and no message, so the
/// collector yields `[Reasoning, ToolCall]`; the tool call keeps the turn
/// non-empty, so there is no `EmptyReason::ReasoningOnly` resample.
pub fn responses_api_reasoning_then_tool_call_events(
reasoning: &str,
call_id: &str,
@@ -579,7 +542,6 @@ pub fn responses_api_reasoning_then_tool_call_events(
let mut events = Vec::new();
let mut seq = 0;
// response.created
events.push(SseEvent::data(
json!({
"type": "response.created",
@@ -597,7 +559,6 @@ pub fn responses_api_reasoning_then_tool_call_events(
));
seq += 1;
// Reasoning summary deltas stream before the tool call.
for word in reasoning.split_whitespace() {
events.push(SseEvent::data(
json!({
@@ -613,7 +574,6 @@ pub fn responses_api_reasoning_then_tool_call_events(
seq += 1;
}
// Then the tool invocation.
events.push(SseEvent::data(
json!({
"type": "response.function_call_arguments.delta",
@@ -626,7 +586,6 @@ pub fn responses_api_reasoning_then_tool_call_events(
));
seq += 1;
// response.completed: the reasoning item plus the function_call item.
events.push(SseEvent::data(
json!({
"type": "response.completed",
@@ -746,11 +705,9 @@ pub fn chat_completions_reasoning_then_tool_call_events(
mod tests {
use super::*;
/// Both byte-exact delta encoders must reconstruct a multi-line response
/// (incl. a ```mermaid fence) byte-for-byte. This is load-bearing:
/// `split_whitespace` would collapse the fence's newlines onto one line,
/// so a client would never parse it as a code block and diagram detection
/// would silently fail.
/// Load-bearing: `split_whitespace` would collapse the fence's newlines
/// onto one line, so a client would never parse it as a code block and
/// diagram detection would silently fail.
#[test]
fn deltas_reconstruct_multiline_response_byte_for_byte() {
let text = "Here is a flow:\n\n```mermaid\nflowchart TD\n A --> B\n B --> C\n```\n\nDone rendering.\n";
@@ -758,8 +715,6 @@ mod tests {
assert_eq!(chat_completion_deltas(text).concat(), text);
assert_eq!(responses_api_deltas(text).concat(), text);
// The reconstruction preserves the fence as a real, newline-delimited
// code block (the property diagram detection depends on).
assert!(
chat_completion_deltas(text)
.concat()
@@ -767,8 +722,6 @@ mod tests {
);
}
/// Multiple consecutive spaces and a trailing newline survive too (no
/// `split_whitespace`-style collapsing).
#[test]
fn deltas_preserve_runs_of_whitespace() {
let text = "a b\tc\n";
@@ -776,21 +729,16 @@ mod tests {
assert_eq!(responses_api_deltas(text).concat(), text);
}
/// Shape guard for the reasoning-only builder: parse each event back to JSON
/// and assert the structural tags/fields the shell collector keys on — at
/// least one `response.reasoning_summary_text.delta` carrying text, no
/// `response.output_text.delta`, and a `response.completed` whose output
/// holds a `reasoning` item (with summary text) and no `message` item,
/// terminated by `[DONE]`. A full round-trip through `rs::ResponseStreamEvent`
/// would pin the async-openai types directly, but that crate is not a
/// dependency here; the integration test deserializes these events through
/// the real client, covering the wire contract end-to-end.
/// Structural shape guard only: a round-trip through
/// `rs::ResponseStreamEvent` would pin the async-openai types directly,
/// but that crate is not a dependency here. The shell integration test
/// deserializes these events through the real client, covering the wire
/// contract end-to-end.
#[test]
fn reasoning_only_events_carry_reasoning_and_no_output_text() {
let events = responses_api_reasoning_only_events("alpha beta gamma", "m");
assert_eq!(events.last().map(|e| e.data.as_str()), Some("[DONE]"));
// Parse every non-terminal event into JSON and key off the `type` tag.
let parsed: Vec<serde_json::Value> = events
.iter()
.filter(|e| e.data != "[DONE]")
@@ -841,10 +789,6 @@ mod tests {
);
}
/// Shape guard for the reasoning+text builder: reasoning summary deltas
/// stream before the output-text deltas, and `response.completed` carries
/// BOTH a `reasoning` item and a `message` item, terminated by `[DONE]` —
/// the ordinary reasoning-model turn (never `EmptyReason::ReasoningOnly`).
#[test]
fn reasoning_and_text_events_carry_both_items() {
let events = responses_api_reasoning_and_text_events("alpha beta", "the answer", "m");
@@ -860,7 +804,6 @@ mod tests {
.map(|v| v["type"].as_str().expect("each event has a type tag"))
.collect();
// Reasoning streams strictly before the visible answer.
let first_reasoning = types
.iter()
.position(|t| *t == "response.reasoning_summary_text.delta")
@@ -893,11 +836,6 @@ mod tests {
);
}
/// Shape guard for the reasoning+tool-call builder: reasoning summary
/// deltas stream before the function-call args delta, no output text
/// anywhere, and `response.completed` carries a `reasoning` item plus a
/// `function_call` item (no `message`), terminated by `[DONE]` — the
/// think-then-call turn whose tool call keeps it non-empty.
#[test]
fn reasoning_then_tool_call_events_carry_reasoning_and_function_call() {
let events = responses_api_reasoning_then_tool_call_events(
@@ -919,7 +857,6 @@ mod tests {
.map(|v| v["type"].as_str().expect("each event has a type tag"))
.collect();
// Reasoning streams strictly before the tool invocation; no text.
let first_reasoning = types
.iter()
.position(|t| *t == "response.reasoning_summary_text.delta")
@@ -958,10 +895,6 @@ mod tests {
);
}
/// Shape guard for the Chat Completions twin: `reasoning_content` deltas
/// stream first, then exactly one `tool_calls` delta carrying the call
/// id/name/arguments, then a `finish_reason: "tool_calls"` chunk, with no
/// visible `content` anywhere, terminated by `[DONE]`.
#[test]
fn chat_reasoning_then_tool_call_events_carry_reasoning_then_tool_call() {
let events = chat_completions_reasoning_then_tool_call_events(
@@ -1010,10 +943,6 @@ mod tests {
);
}
/// Shape guard for the doom-loop builder: one NAMED check frame per
/// cumulative prefix of `triggers` (each frame re-sends every trigger so
/// far), a reasoning-only output (no message item — the doomed
/// signature), and the terminal response object carrying the full set.
#[test]
fn doom_loop_check_events_send_growing_named_frames_and_terminal_field() {
let events = responses_api_doom_loop_check_events(
@@ -1058,9 +987,6 @@ mod tests {
);
}
/// Shape guard for the terminal-only variant: no named check frame
/// anywhere; the completed response carries both output items (the turn
/// is a normal answer) plus `doom_loop_check.triggers`.
#[test]
fn doom_loop_terminal_only_events_carry_field_without_mid_stream_frame() {
let events = responses_api_doom_loop_terminal_only_events(
@@ -1089,9 +1015,6 @@ mod tests {
assert!(output.iter().any(|o| o["type"] == "reasoning"));
}
/// Shape guard for the splice helper: the named frame lands right after
/// `response.created` with the caller's payload byte-for-byte (this is
/// how byte-exact fixtures and malformed variants ride a normal turn).
#[test]
fn with_doom_loop_frame_splices_payload_verbatim() {
let payload = r#"{"type":"response.doom_loop_check","doom_loop_check":{"triggers":42}}"#;
@@ -21,7 +21,6 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt, ReadHalf, WriteHalf};
use tokio::net::{UnixListener, UnixStream};
use tokio_util::sync::CancellationToken;
/// Which pump direction a [`FaultPlan`] applies to.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum FaultDirection {
#[default]
@@ -99,8 +98,6 @@ pub struct UdsProxy {
}
impl UdsProxy {
/// Bind `proxy_path` and forward each accepted connection to
/// `upstream_path`, applying `plan` per connection.
pub async fn spawn(
proxy_path: impl Into<PathBuf>,
upstream_path: impl AsRef<Path>,
@@ -144,7 +141,6 @@ impl UdsProxy {
self.handle.clone()
}
/// Stop accepting and sever active connections.
pub fn shutdown(&self) {
self.handle.sever_now();
self.cancel.cancel();
@@ -327,7 +323,6 @@ mod tests {
Ok(body)
}
/// Upstream that echoes every frame back to the sender.
fn spawn_echo_upstream(path: PathBuf) {
let listener = UnixListener::bind(&path).unwrap();
tokio::spawn(async move {
@@ -397,7 +392,6 @@ mod tests {
client_write_frame(&mut client, b"second").await;
client_write_frame(&mut client, b"third").await;
// The echo of "second" never arrives; "third" comes straight after "first".
assert_eq!(client_read_frame(&mut client).await.unwrap(), b"first");
assert_eq!(client_read_frame(&mut client).await.unwrap(), b"third");
}
@@ -444,7 +438,7 @@ mod tests {
client_write_frame(&mut client, b"never-delivered").await;
// The upstream got 2 bytes of a length prefix and then a close, so it
// echoes nothing; the client's next read observes the sever.
// echoes nothing and the client's next read observes the sever.
let read = client_read_frame(&mut client).await;
assert!(
read.is_err(),