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:
2026-07-17 05:31:01 -04:00
commit d6c20fc13f
2612 changed files with 1353757 additions and 0 deletions
@@ -0,0 +1,832 @@
//! Backend trait abstracting how subagent operations are dispatched.
//!
//! `SubagentBackend` decouples the tool implementations (`TaskTool`,
//! `TaskOutputTool`, `KillTaskTool`) from the transport mechanism used to
//! communicate with the subagent coordinator.
//!
//! Two implementations are planned:
//!
//! - [`ChannelBackend`] — wraps in-process `tokio::mpsc` channels used by
//! the local host shell. This is the only implementation today.
//! - `RemoteBackend` (future) — dispatches over a remote transport to an
//! out-of-process spawner.
use std::sync::Arc;
use tokio::sync::{mpsc, oneshot};
use super::types::{
SubagentCancelOutcome, SubagentCancelRequest, SubagentCancelTarget, SubagentDescribeOutcome,
SubagentDescribeRequest, SubagentEvent, SubagentQueryRequest, SubagentRequest, SubagentResult,
SubagentSnapshot, SubagentValidateTypeOutcome, SubagentValidateTypeRequest,
};
use crate::register_resource;
use kigi_tool_runtime::ToolError;
/// Abstraction over the mechanism used to spawn, query, and cancel subagents.
///
/// Injected into `Resources` as [`SubagentBackendResource`] so that
/// `TaskTool`, `TaskOutputTool`, and `KillTaskTool` can operate
/// identically regardless of the underlying transport.
#[async_trait::async_trait]
pub trait SubagentBackend: Send + Sync + 'static {
/// Spawn a subagent and await its result.
///
/// For blocking mode the caller awaits the returned future directly.
/// For background mode the caller spawns a tokio task around this call
/// and drops the receiver immediately.
async fn spawn(&self, request: SubagentRequest) -> Result<SubagentResult, ToolError>;
/// Query the current state of a subagent by ID.
///
/// When `block` is true the backend waits (up to `timeout_ms`) for the
/// subagent to reach a terminal state before responding.
async fn query(
&self,
id: &str,
block: bool,
timeout_ms: Option<u64>,
) -> Option<SubagentSnapshot>;
/// Request cancellation of a subagent by ID.
async fn cancel(&self, id: &str) -> SubagentCancelOutcome;
/// Validate a subagent type synchronously before spawning.
/// Returns `ValidationUnavailable` on channel close / responder drop / timeout.
async fn validate_type(
&self,
subagent_type: &str,
parent_session_id: &str,
) -> SubagentValidateTypeOutcome;
/// Describe a subagent type's resolved toolset (tool names + capability
/// flags) before spawning. Read-only: builds the agent definition and
/// applies the same parent-dependent toolset re-selection a spawn would,
/// then reports the result without starting a child session.
///
/// Returns [`SubagentDescribeOutcome::Unavailable`] on channel close /
/// responder drop / timeout (modeled exactly on [`Self::validate_type`]).
///
/// `harness_agent_type` is the `/goal`-only harness override (see
/// [`super::types::SubagentRuntimeOverrides::harness_agent_type`]); the
/// coordinator resolves the toolset for `(subagent_type,
/// harness_agent_type)`. `None` (every non-goal caller) defers the flavor
/// to the parent agent.
async fn describe_subagent_type(
&self,
subagent_type: &str,
harness_agent_type: Option<&str>,
parent_session_id: &str,
) -> SubagentDescribeOutcome;
}
/// Resource wrapper injected into every session's `Resources`.
///
/// Wraps an `Arc<dyn SubagentBackend>` so the backend can be shared across
/// concurrent tool invocations within the same session.
#[derive(Clone)]
pub struct SubagentBackendResource(pub Arc<dyn SubagentBackend>);
impl SubagentBackendResource {
pub fn backend(&self) -> &dyn SubagentBackend {
self.0.as_ref()
}
}
impl std::fmt::Debug for SubagentBackendResource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SubagentBackendResource").finish()
}
}
register_resource!(
"grok_build",
"SubagentBackendResource",
SubagentBackendResource
);
/// In-process channel-based backend for the local host shell.
///
/// Wraps a single `mpsc::UnboundedSender<SubagentEvent>` that carries
/// spawn, query, and cancel messages to the coordinator. The oneshot for
/// `spawn` is created inside the backend so callers never manage it.
pub struct ChannelBackend {
tx: mpsc::UnboundedSender<SubagentEvent>,
}
impl ChannelBackend {
pub fn new(tx: mpsc::UnboundedSender<SubagentEvent>) -> Self {
Self { tx }
}
}
#[async_trait::async_trait]
impl SubagentBackend for ChannelBackend {
async fn spawn(&self, request: SubagentRequest) -> Result<SubagentResult, ToolError> {
let (result_tx, result_rx) = oneshot::channel();
// Replace the dummy oneshot with our fresh one. Using struct update
// syntax (`..request`) ensures new fields added to `SubagentRequest`
// are forwarded automatically — a field-by-field copy would silently
// drop them.
self.tx
.send(SubagentEvent::Spawn(Box::new(SubagentRequest {
result_tx,
..request
})))
.map_err(|_| {
ToolError::custom(
"channel_closed",
"Subagent coordinator channel closed — cannot spawn subagent",
)
})?;
result_rx.await.map_err(|_| {
ToolError::custom(
"channel_closed",
"Subagent result channel dropped — child session may have crashed",
)
})
}
async fn query(
&self,
id: &str,
block: bool,
timeout_ms: Option<u64>,
) -> Option<SubagentSnapshot> {
let (respond_to, response_rx) = oneshot::channel();
let sent = self.tx.send(SubagentEvent::Query(SubagentQueryRequest {
subagent_id: id.to_string(),
block,
timeout_ms,
respond_to,
}));
if sent.is_err() {
return None;
}
response_rx.await.ok().flatten()
}
async fn cancel(&self, id: &str) -> SubagentCancelOutcome {
let (respond_to, response_rx) = oneshot::channel();
let sent = self.tx.send(SubagentEvent::Cancel(SubagentCancelRequest {
target: SubagentCancelTarget::SubagentId(id.to_string()),
respond_to,
}));
if sent.is_err() {
return SubagentCancelOutcome::NotFound;
}
response_rx.await.unwrap_or(SubagentCancelOutcome::NotFound)
}
async fn validate_type(
&self,
subagent_type: &str,
parent_session_id: &str,
) -> SubagentValidateTypeOutcome {
let (respond_to, response_rx) = oneshot::channel();
if self
.tx
.send(SubagentEvent::ValidateType(SubagentValidateTypeRequest {
subagent_type: subagent_type.to_string(),
parent_session_id: parent_session_id.to_string(),
respond_to,
}))
.is_err()
{
tracing::warn!(
subagent_type,
"coordinator validation channel closed, treating as ValidationUnavailable",
);
return SubagentValidateTypeOutcome::ValidationUnavailable;
}
let timeout = validate_type_timeout();
match tokio::time::timeout(timeout, response_rx).await {
Ok(Ok(outcome)) => outcome,
Ok(Err(_)) => {
tracing::warn!(
subagent_type,
"coordinator validation responder dropped, treating as ValidationUnavailable",
);
SubagentValidateTypeOutcome::ValidationUnavailable
}
Err(_) => {
tracing::warn!(
subagent_type,
timeout_ms = timeout.as_millis() as u64,
"coordinator validation timed out, treating as ValidationUnavailable",
);
SubagentValidateTypeOutcome::ValidationUnavailable
}
}
}
async fn describe_subagent_type(
&self,
subagent_type: &str,
harness_agent_type: Option<&str>,
parent_session_id: &str,
) -> SubagentDescribeOutcome {
let (respond_to, response_rx) = oneshot::channel();
if self
.tx
.send(SubagentEvent::DescribeType(SubagentDescribeRequest {
subagent_type: subagent_type.to_string(),
harness_agent_type: harness_agent_type.map(str::to_string),
parent_session_id: parent_session_id.to_string(),
respond_to,
}))
.is_err()
{
tracing::warn!(
subagent_type,
"coordinator describe channel closed, treating as Unavailable",
);
return SubagentDescribeOutcome::Unavailable;
}
let timeout = validate_type_timeout();
match tokio::time::timeout(timeout, response_rx).await {
Ok(Ok(outcome)) => outcome,
Ok(Err(_)) => {
tracing::warn!(
subagent_type,
"coordinator describe responder dropped, treating as Unavailable",
);
SubagentDescribeOutcome::Unavailable
}
Err(_) => {
tracing::warn!(
subagent_type,
timeout_ms = timeout.as_millis() as u64,
"coordinator describe timed out, treating as Unavailable",
);
SubagentDescribeOutcome::Unavailable
}
}
}
}
/// Default `validate_type` timeout. Override via [`VALIDATE_TYPE_TIMEOUT_ENV_VAR`].
pub const VALIDATE_TYPE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(2);
/// Env-var override for [`VALIDATE_TYPE_TIMEOUT`] (positive milliseconds).
pub const VALIDATE_TYPE_TIMEOUT_ENV_VAR: &str = "XAI_VALIDATE_TYPE_TIMEOUT_MS";
/// Validation timeout, honoring the env-var override.
pub(crate) fn validate_type_timeout() -> std::time::Duration {
let raw = std::env::var(VALIDATE_TYPE_TIMEOUT_ENV_VAR).ok();
parse_timeout_ms(raw.as_deref())
.map(std::time::Duration::from_millis)
.unwrap_or(VALIDATE_TYPE_TIMEOUT)
}
/// Parse a positive `u64` millisecond value; `None` for unset, invalid, or zero.
pub(crate) fn parse_timeout_ms(value: Option<&str>) -> Option<u64> {
value?.parse::<u64>().ok().filter(|&ms| ms > 0)
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
use tokio::sync::mpsc;
/// Helper: receive the next event, match the expected variant, or panic.
macro_rules! recv_event {
($rx:expr, Spawn) => {{
let event = $rx.recv().await.unwrap();
match event {
SubagentEvent::Spawn(inner) => *inner,
_ => panic!("Expected SubagentEvent::Spawn, got different variant"),
}
}};
($rx:expr, $variant:ident) => {{
let event = $rx.recv().await.unwrap();
match event {
SubagentEvent::$variant(inner) => inner,
_ => panic!(
"Expected SubagentEvent::{}, got different variant",
stringify!($variant)
),
}
}};
}
#[tokio::test]
async fn channel_backend_spawn_success() {
let (tx, mut rx) = mpsc::unbounded_channel::<SubagentEvent>();
let backend = ChannelBackend::new(tx);
let handle = tokio::spawn(async move {
let req = recv_event!(rx, Spawn);
assert_eq!(req.id, "test-id");
assert_eq!(req.prompt, "do something");
req.result_tx
.send(SubagentResult {
success: true,
output: Arc::from("done"),
subagent_id: "test-id".to_string(),
child_session_id: "test-id".to_string(),
tool_calls: 3,
turns: 1,
duration_ms: 500,
..Default::default()
})
.unwrap();
});
let (dummy_tx, _dummy_rx) = oneshot::channel();
let request = SubagentRequest {
id: "test-id".to_string(),
prompt: "do something".to_string(),
description: "test".to_string(),
subagent_type: "general-purpose".to_string(),
parent_session_id: "parent".to_string(),
parent_prompt_id: None,
resume_from: None,
cwd: None,
runtime_overrides: Default::default(),
run_in_background: false,
surface_completion: true,
fork_context: false,
result_tx: dummy_tx,
};
let result = backend.spawn(request).await.unwrap();
assert!(result.success);
assert_eq!(result.subagent_id, "test-id");
assert_eq!(result.tool_calls, 3);
handle.await.unwrap();
}
#[tokio::test]
async fn channel_backend_spawn_closed_channel() {
let (tx, rx) = mpsc::unbounded_channel::<SubagentEvent>();
drop(rx);
let backend = ChannelBackend::new(tx);
let (dummy_tx, _dummy_rx) = oneshot::channel();
let request = SubagentRequest {
id: "test-id".to_string(),
prompt: "do something".to_string(),
description: "test".to_string(),
subagent_type: "general-purpose".to_string(),
parent_session_id: "parent".to_string(),
parent_prompt_id: None,
resume_from: None,
cwd: None,
runtime_overrides: Default::default(),
run_in_background: false,
surface_completion: true,
fork_context: false,
result_tx: dummy_tx,
};
let err = backend.spawn(request).await.unwrap_err();
assert!(err.to_string().contains("channel closed"));
}
#[tokio::test]
async fn channel_backend_query_found() {
let (tx, mut rx) = mpsc::unbounded_channel::<SubagentEvent>();
let backend = ChannelBackend::new(tx);
let handle = tokio::spawn(async move {
let req = recv_event!(rx, Query);
assert_eq!(req.subagent_id, "sub-1");
assert!(req.block);
assert_eq!(req.timeout_ms, Some(5000));
req.respond_to
.send(Some(SubagentSnapshot {
subagent_id: "sub-1".to_string(),
description: "find bugs".to_string(),
subagent_type: "explore".to_string(),
status: super::super::types::SubagentSnapshotStatus::Completed {
output: "result".to_string(),
tool_calls: 2,
turns: 1,
worktree_path: None,
},
started_at_epoch_ms: 1000,
duration_ms: 200,
persona: Some("reviewer".to_string()),
}))
.unwrap();
});
let snap = backend.query("sub-1", true, Some(5000)).await;
let snap = snap.expect("snapshot should be present");
assert_eq!(snap.subagent_id, "sub-1");
assert_eq!(snap.description, "find bugs");
assert_eq!(snap.subagent_type, "explore");
assert_eq!(snap.started_at_epoch_ms, 1000);
assert_eq!(snap.duration_ms, 200);
assert_eq!(snap.persona.as_deref(), Some("reviewer"));
match &snap.status {
super::super::types::SubagentSnapshotStatus::Completed {
output,
tool_calls,
turns,
worktree_path,
} => {
assert_eq!(output, "result");
assert_eq!(*tool_calls, 2);
assert_eq!(*turns, 1);
assert!(worktree_path.is_none());
}
other => panic!("Expected Completed, got {:?}", other),
}
handle.await.unwrap();
}
#[tokio::test]
async fn channel_backend_query_non_blocking_passes_through() {
let (tx, mut rx) = mpsc::unbounded_channel::<SubagentEvent>();
let backend = ChannelBackend::new(tx);
let handle = tokio::spawn(async move {
let req = recv_event!(rx, Query);
assert_eq!(req.subagent_id, "sub-nb");
assert!(!req.block, "block should be false");
assert_eq!(req.timeout_ms, None, "timeout_ms should be None");
req.respond_to.send(None).unwrap();
});
let snap = backend.query("sub-nb", false, None).await;
assert!(snap.is_none());
handle.await.unwrap();
}
#[tokio::test]
async fn channel_backend_query_not_found() {
let (tx, mut rx) = mpsc::unbounded_channel::<SubagentEvent>();
let backend = ChannelBackend::new(tx);
let handle = tokio::spawn(async move {
let req = recv_event!(rx, Query);
req.respond_to.send(None).unwrap();
});
let snap = backend.query("nonexistent", false, None).await;
assert!(snap.is_none());
handle.await.unwrap();
}
#[tokio::test]
async fn channel_backend_cancel_success() {
let (tx, mut rx) = mpsc::unbounded_channel::<SubagentEvent>();
let backend = ChannelBackend::new(tx);
let handle = tokio::spawn(async move {
let req = recv_event!(rx, Cancel);
match &req.target {
SubagentCancelTarget::SubagentId(id) => assert_eq!(id, "sub-cancel"),
other => panic!("Expected SubagentId, got {:?}", other),
}
req.respond_to
.send(SubagentCancelOutcome::Cancelled)
.unwrap();
});
let outcome = backend.cancel("sub-cancel").await;
assert!(matches!(outcome, SubagentCancelOutcome::Cancelled));
handle.await.unwrap();
}
#[tokio::test]
async fn channel_backend_cancel_closed_channel() {
let (tx, rx) = mpsc::unbounded_channel::<SubagentEvent>();
drop(rx);
let backend = ChannelBackend::new(tx);
let outcome = backend.cancel("sub-cancel").await;
assert!(matches!(outcome, SubagentCancelOutcome::NotFound));
}
#[tokio::test]
async fn channel_backend_spawn_result_dropped() {
let (tx, mut rx) = mpsc::unbounded_channel::<SubagentEvent>();
let backend = ChannelBackend::new(tx);
let handle = tokio::spawn(async move {
let req = recv_event!(rx, Spawn);
drop(req.result_tx);
});
let (dummy_tx, _dummy_rx) = oneshot::channel();
let request = SubagentRequest {
id: "drop-test".to_string(),
prompt: "test".to_string(),
description: "test".to_string(),
subagent_type: "general-purpose".to_string(),
parent_session_id: "parent".to_string(),
parent_prompt_id: None,
resume_from: None,
cwd: None,
runtime_overrides: Default::default(),
run_in_background: false,
surface_completion: true,
fork_context: false,
result_tx: dummy_tx,
};
let err = backend.spawn(request).await.unwrap_err();
assert!(
err.to_string().contains("result channel dropped"),
"error: {err}"
);
handle.await.unwrap();
}
#[tokio::test]
async fn channel_backend_query_closed_channel() {
let (tx, rx) = mpsc::unbounded_channel::<SubagentEvent>();
drop(rx);
let backend = ChannelBackend::new(tx);
let snap = backend.query("sub-1", false, None).await;
assert!(snap.is_none());
}
// ── validate_type ────────────────────────────────────────────────
#[tokio::test]
async fn channel_backend_validate_type_round_trips_outcome() {
let (tx, mut rx) = mpsc::unbounded_channel::<SubagentEvent>();
let backend = ChannelBackend::new(tx);
let handle = tokio::spawn(async move {
let event = rx.recv().await.unwrap();
match event {
SubagentEvent::ValidateType(req) => {
assert_eq!(req.subagent_type, "explore");
assert_eq!(req.parent_session_id, "parent-1");
req.respond_to
.send(SubagentValidateTypeOutcome::Ok)
.unwrap();
}
_ => panic!("Expected ValidateType event"),
}
});
let outcome = backend.validate_type("explore", "parent-1").await;
assert!(matches!(outcome, SubagentValidateTypeOutcome::Ok));
handle.await.unwrap();
}
#[tokio::test]
async fn channel_backend_validate_type_propagates_unknown_outcome() {
let (tx, mut rx) = mpsc::unbounded_channel::<SubagentEvent>();
let backend = ChannelBackend::new(tx);
let handle = tokio::spawn(async move {
if let Some(SubagentEvent::ValidateType(req)) = rx.recv().await {
req.respond_to
.send(SubagentValidateTypeOutcome::Unknown {
available: vec!["explore".into(), "plan".into()],
})
.unwrap();
}
});
let outcome = backend.validate_type("invented", "p").await;
match outcome {
SubagentValidateTypeOutcome::Unknown { available } => {
assert_eq!(available, vec!["explore".to_string(), "plan".to_string()]);
}
other => panic!("expected Unknown, got {other:?}"),
}
handle.await.unwrap();
}
#[tokio::test]
async fn channel_backend_validate_type_returns_validation_unavailable_when_channel_closed() {
let (tx, rx) = mpsc::unbounded_channel::<SubagentEvent>();
drop(rx);
let backend = ChannelBackend::new(tx);
let outcome = backend.validate_type("explore", "p").await;
assert!(matches!(
outcome,
SubagentValidateTypeOutcome::ValidationUnavailable
));
}
#[tokio::test]
async fn channel_backend_validate_type_returns_validation_unavailable_when_responder_dropped() {
let (tx, mut rx) = mpsc::unbounded_channel::<SubagentEvent>();
let backend = ChannelBackend::new(tx);
let handle = tokio::spawn(async move {
if let Some(SubagentEvent::ValidateType(req)) = rx.recv().await {
drop(req.respond_to);
}
});
let outcome = backend.validate_type("explore", "p").await;
assert!(matches!(
outcome,
SubagentValidateTypeOutcome::ValidationUnavailable,
));
handle.await.unwrap();
}
use super::super::types::test_capture;
#[tokio::test(start_paused = true)]
async fn channel_backend_validate_type_logs_warn_on_timeout() {
let captured = test_capture::capture();
let (tx, mut rx) = mpsc::unbounded_channel::<SubagentEvent>();
let backend = ChannelBackend::new(tx);
// Coordinator receives but never replies; keeps the responder
// alive so the timeout arm fires (not responder-dropped).
let holder = tokio::spawn(async move {
if let Some(SubagentEvent::ValidateType(req)) = rx.recv().await {
std::mem::forget(req.respond_to);
std::future::pending::<()>().await;
}
});
let validate = tokio::spawn(async move { backend.validate_type("explore", "p").await });
tokio::time::advance(VALIDATE_TYPE_TIMEOUT + std::time::Duration::from_millis(1)).await;
let outcome = validate.await.unwrap();
assert!(matches!(
outcome,
SubagentValidateTypeOutcome::ValidationUnavailable
));
let mut events_rx = captured.events_rx;
let mut saw_timeout_warn = false;
while let Ok(event) = events_rx.try_recv() {
if event.level == tracing::Level::WARN
&& event.fields.contains("coordinator validation timed out")
&& event.fields.contains("subagent_type=explore")
&& event.fields.contains("timeout_ms=")
{
saw_timeout_warn = true;
break;
}
}
assert!(saw_timeout_warn, "must emit WARN with timeout_ms field");
holder.abort();
}
// ── describe_subagent_type ───────────────────────────────────────
#[tokio::test]
async fn channel_backend_describe_round_trips_summary() {
use super::super::types::{SubagentDescribeOutcome, SubagentTypeSummary};
use crate::types::tool::ToolKind;
let (tx, mut rx) = mpsc::unbounded_channel::<SubagentEvent>();
let backend = ChannelBackend::new(tx);
let handle = tokio::spawn(async move {
match rx.recv().await.unwrap() {
SubagentEvent::DescribeType(req) => {
assert_eq!(req.subagent_type, "explore");
assert_eq!(req.harness_agent_type.as_deref(), Some("cursor"));
assert_eq!(req.parent_session_id, "parent-1");
let mut summary = SubagentTypeSummary {
can_read: true,
can_search: true,
..Default::default()
};
summary
.tool_names
.insert(ToolKind::Read, "read_file".to_string());
req.respond_to
.send(SubagentDescribeOutcome::Ok(summary))
.unwrap();
}
_ => panic!("Expected DescribeType event"),
}
});
let outcome = backend
.describe_subagent_type("explore", Some("cursor"), "parent-1")
.await;
match outcome {
SubagentDescribeOutcome::Ok(summary) => {
assert!(summary.can_read && summary.can_search && !summary.can_execute);
assert_eq!(
summary.tool_names.get(&ToolKind::Read).unwrap(),
"read_file"
);
}
other => panic!("expected Ok, got {other:?}"),
}
handle.await.unwrap();
}
#[tokio::test]
async fn channel_backend_describe_propagates_not_allowed_outcome() {
use super::super::types::SubagentDescribeOutcome;
let (tx, mut rx) = mpsc::unbounded_channel::<SubagentEvent>();
let backend = ChannelBackend::new(tx);
let handle = tokio::spawn(async move {
if let Some(SubagentEvent::DescribeType(req)) = rx.recv().await {
req.respond_to
.send(SubagentDescribeOutcome::NotAllowed {
allowed: vec!["explore".into()],
})
.unwrap();
}
});
match backend.describe_subagent_type("plan", None, "p").await {
SubagentDescribeOutcome::NotAllowed { allowed } => {
assert_eq!(allowed, vec!["explore".to_string()]);
}
other => panic!("expected NotAllowed, got {other:?}"),
}
handle.await.unwrap();
}
#[tokio::test]
async fn channel_backend_describe_returns_unavailable_when_channel_closed() {
use super::super::types::SubagentDescribeOutcome;
let (tx, rx) = mpsc::unbounded_channel::<SubagentEvent>();
drop(rx);
let backend = ChannelBackend::new(tx);
assert!(matches!(
backend.describe_subagent_type("explore", None, "p").await,
SubagentDescribeOutcome::Unavailable
));
}
#[tokio::test]
async fn channel_backend_describe_returns_unavailable_when_responder_dropped() {
use super::super::types::SubagentDescribeOutcome;
let (tx, mut rx) = mpsc::unbounded_channel::<SubagentEvent>();
let backend = ChannelBackend::new(tx);
let handle = tokio::spawn(async move {
if let Some(SubagentEvent::DescribeType(req)) = rx.recv().await {
drop(req.respond_to);
}
});
assert!(matches!(
backend.describe_subagent_type("explore", None, "p").await,
SubagentDescribeOutcome::Unavailable
));
handle.await.unwrap();
}
#[tokio::test(start_paused = true)]
async fn channel_backend_describe_returns_unavailable_on_timeout() {
use super::super::types::SubagentDescribeOutcome;
let (tx, mut rx) = mpsc::unbounded_channel::<SubagentEvent>();
let backend = ChannelBackend::new(tx);
let holder = tokio::spawn(async move {
if let Some(SubagentEvent::DescribeType(req)) = rx.recv().await {
std::mem::forget(req.respond_to);
std::future::pending::<()>().await;
}
});
let describe =
tokio::spawn(async move { backend.describe_subagent_type("explore", None, "p").await });
tokio::time::advance(VALIDATE_TYPE_TIMEOUT + std::time::Duration::from_millis(1)).await;
assert!(matches!(
describe.await.unwrap(),
SubagentDescribeOutcome::Unavailable
));
holder.abort();
}
#[test]
fn parse_timeout_ms_returns_none_for_unset() {
assert_eq!(parse_timeout_ms(None), None);
}
#[test]
fn parse_timeout_ms_returns_none_for_unparseable() {
assert_eq!(parse_timeout_ms(Some("not-a-number")), None);
assert_eq!(parse_timeout_ms(Some("")), None);
assert_eq!(parse_timeout_ms(Some("3.14")), None);
assert_eq!(parse_timeout_ms(Some("-100")), None);
}
#[test]
fn parse_timeout_ms_returns_none_for_zero() {
assert_eq!(parse_timeout_ms(Some("0")), None);
}
#[test]
fn parse_timeout_ms_returns_value_for_positive_integer() {
assert_eq!(parse_timeout_ms(Some("5000")), Some(5000));
assert_eq!(parse_timeout_ms(Some("1")), Some(1));
}
}