use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
pub const STATE_SCHEMA_VERSION: u32 = 1;
pub const SUPPORTED_STATE_SCHEMAS: &[u32] = &[1];
const CROCKFORD_LOWER: &[u8] = b"0123456789abcdefghjkmnpqrstvwxyz";
fn all_crockford_lower(s: &str) -> bool {
s.bytes().all(|b| CROCKFORD_LOWER.contains(&b))
}
pub fn is_run_id_prefix(s: &str) -> bool {
!s.is_empty()
&& s.len() <= RunId::LEN
&& all_crockford_lower(s)
&& matches!(s.as_bytes().first(), Some(b'0'..=b'7'))
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum IdValidationError {
#[error("invalid {kind} id {value:?}: expected {expected}")]
InvalidFormat {
kind: &'static str,
value: String,
expected: &'static str,
},
#[error("invalid {kind} id: wrong prefix, expected {expected}")]
WrongPrefix {
kind: &'static str,
expected: &'static str,
},
}
impl IdValidationError {
pub fn kind(&self) -> &'static str {
match self {
Self::InvalidFormat { kind, .. } | Self::WrongPrefix { kind, .. } => kind,
}
}
pub fn expected(&self) -> &'static str {
match self {
Self::InvalidFormat { expected, .. } | Self::WrongPrefix { expected, .. } => expected,
}
}
}
macro_rules! id_newtype {
($(#[$m:meta])* $name:ident) => {
$(#[$m])*
#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct $name(String);
impl $name {
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::str::FromStr for $name {
type Err = IdValidationError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse_str(s)
}
}
impl std::fmt::Display for $name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl std::fmt::Debug for $name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}({:?})", stringify!($name), self.0)
}
}
impl serde::Serialize for $name {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(&self.0)
}
}
impl<'de> serde::Deserialize<'de> for $name {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
let s = String::deserialize(d)?;
Self::parse_str(&s).map_err(serde::de::Error::custom)
}
}
};
}
id_newtype! {
RunId
}
impl RunId {
const EXPECTED: &'static str = "26-char lowercase Crockford base32 ULID";
pub const LEN: usize = 26;
pub fn parse_str(s: &str) -> Result<Self, IdValidationError> {
let reject = || IdValidationError::InvalidFormat {
kind: "run",
value: s.to_string(),
expected: Self::EXPECTED,
};
if s.len() != Self::LEN || !all_crockford_lower(s) {
return Err(reject());
}
if !(b'0'..=b'7').contains(&s.as_bytes()[0]) {
return Err(reject());
}
Ok(Self(s.to_string()))
}
}
id_newtype! {
NodeId
}
impl NodeId {
const EXPECTED: &'static str = "n-NNNN (n- followed by 4-10 ASCII digits)";
pub fn parse_str(s: &str) -> Result<Self, IdValidationError> {
let body = s.strip_prefix("n-").ok_or(IdValidationError::WrongPrefix {
kind: "node",
expected: Self::EXPECTED,
})?;
if (4..=10).contains(&body.len()) && body.bytes().all(|b| b.is_ascii_digit()) {
Ok(Self(s.to_string()))
} else {
Err(IdValidationError::InvalidFormat {
kind: "node",
value: s.to_string(),
expected: Self::EXPECTED,
})
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Kind {
Spinoff,
Research,
TechnicalDecision,
FanOut,
#[serde(other)]
Unknown,
}
impl Kind {
#[must_use]
pub const fn wire_name(self) -> &'static str {
match self {
Kind::Spinoff => "spinoff",
Kind::Research => "research",
Kind::TechnicalDecision => "technical-decision",
Kind::FanOut => "fan-out",
Kind::Unknown => "unknown",
}
}
pub const WIRE_NAMES: &'static [&'static str] = &[
Kind::Spinoff.wire_name(),
Kind::Research.wire_name(),
Kind::TechnicalDecision.wire_name(),
Kind::FanOut.wire_name(),
];
pub fn lifecycle(self) -> Lifecycle {
match self {
Kind::Spinoff
| Kind::Research
| Kind::TechnicalDecision
| Kind::FanOut
| Kind::Unknown => Lifecycle::Autonomous,
}
}
#[must_use]
pub fn is_autonomous_single_node_worker(self) -> bool {
match self {
Kind::Spinoff | Kind::Research | Kind::TechnicalDecision => true,
Kind::FanOut | Kind::Unknown => false,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Lifecycle {
Autonomous,
Interactive,
}
impl Lifecycle {
#[must_use]
pub fn is_interactive(self) -> bool {
matches!(self, Lifecycle::Interactive)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Status {
Pending,
Running,
Blocked,
Done,
Failed,
Cancelled,
}
impl Status {
pub fn is_terminal(self) -> bool {
matches!(self, Status::Done | Status::Failed | Status::Cancelled)
}
}
pub fn aggregate_terminal_status<I>(statuses: I) -> Option<Status>
where
I: IntoIterator<Item = Status>,
{
let mut any = false;
let mut any_failed = false;
let mut any_cancelled = false;
for s in statuses {
any = true;
match s {
Status::Done => {}
Status::Failed => any_failed = true,
Status::Cancelled => any_cancelled = true,
Status::Pending | Status::Running | Status::Blocked => return None,
}
}
if !any {
return None;
}
Some(if any_failed {
Status::Failed
} else if any_cancelled {
Status::Cancelled
} else {
Status::Done
})
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct AgentSelection {
pub schema_version: u32,
pub profile: String,
pub selection_source: String,
pub interaction: String,
pub capability: String,
pub residency: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub requested_harness: Option<String>,
pub selected: SelectedAgentCandidate,
#[serde(default)]
pub fallback: Vec<SkippedAgentCandidate>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SelectedAgentCandidate {
pub candidate_index: u8,
pub harness: String,
pub command: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub telemetry: Option<String>,
}
impl SelectedAgentCandidate {
#[must_use]
pub fn supports_worker_telemetry_v1(&self) -> bool {
self.harness == "pi" && self.telemetry.as_deref() == Some("worker-v1")
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SkippedAgentCandidate {
pub candidate_index: u8,
pub harness: String,
pub reason: String,
}
impl AgentSelection {
pub fn validate(&self) -> Result<(), String> {
if self.schema_version != 1 {
return Err(format!(
"unsupported selection schema_version {}",
self.schema_version
));
}
let name = self.profile.as_bytes();
if name.is_empty()
|| name.len() > 63
|| !name[0].is_ascii_lowercase()
|| !name
.iter()
.all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || *b == b'-')
|| self.profile.ends_with('-')
|| self.profile.contains("--")
{
return Err("invalid profile name".into());
}
if !matches!(
self.selection_source.as_str(),
"cli"
| "environment"
| "repository-per-kind"
| "user-per-kind"
| "repository-default"
| "user-default"
| "builtin-harness"
) {
return Err("invalid selection_source".into());
}
if !matches!(
self.interaction.as_str(),
"autonomous" | "explicit-interactive"
) || !matches!(
self.capability.as_str(),
"fast" | "capable" | "ultra-capable"
) || !matches!(self.residency.as_str(), "local" | "remote")
{
return Err("invalid interaction, capability, or residency".into());
}
validate_selected_candidate(&self.selected)?;
if self.interaction == "autonomous"
&& (self.selected.harness != "pi"
|| self.selected.telemetry.as_deref() != Some("worker-v1"))
{
return Err("autonomous selection requires pi with worker-v1 telemetry".into());
}
if self.selected.candidate_index >= 8
|| self.fallback.len() != usize::from(self.selected.candidate_index)
{
return Err("candidate index/count exceeds profile bound".into());
}
let mut prior = None;
for (expected_index, skipped) in self.fallback.iter().enumerate() {
if usize::from(skipped.candidate_index) != expected_index
|| prior.is_some_and(|value| skipped.candidate_index <= value)
|| !matches!(skipped.harness.as_str(), "pi" | "claude")
|| !matches!(
skipped.reason.as_str(),
"executable_missing"
| "autonomous_harness_unsupported"
| "telemetry_unsupported"
)
{
return Err("invalid fallback candidate index, harness, or reason".into());
}
if skipped.reason == "autonomous_harness_unsupported"
&& (self.interaction != "autonomous" || skipped.harness == "pi")
{
return Err("inconsistent autonomous harness skip reason".into());
}
if skipped.reason == "telemetry_unsupported"
&& (self.interaction != "autonomous" || skipped.harness != "pi")
{
return Err("inconsistent telemetry skip reason".into());
}
prior = Some(skipped.candidate_index);
}
Ok(())
}
}
fn validate_selected_candidate(candidate: &SelectedAgentCandidate) -> Result<(), String> {
if !matches!(candidate.harness.as_str(), "pi" | "claude")
|| candidate.command.is_empty()
|| candidate.command.len() > 32
|| candidate
.command
.iter()
.any(|arg| arg.is_empty() || arg.len() > 4096 || arg.contains('\0'))
|| candidate.command.iter().map(String::len).sum::<usize>() > 16_384
{
return Err("invalid selected harness or command".into());
}
match (candidate.harness.as_str(), candidate.telemetry.as_deref()) {
(_, None) | ("pi", Some("worker-v1")) => Ok(()),
_ => Err("invalid selected telemetry declaration".into()),
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Manifest {
pub schema_version: u32,
#[serde(default)]
pub applied_seq: u64,
pub run_id: RunId,
pub kind: Kind,
pub lifecycle: Lifecycle,
pub title: String,
pub status: Status,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub source_repo: Option<String>,
pub source_branch: Option<String>,
pub worktree_root: Option<String>,
#[serde(default)]
pub managed_tmux_session: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tmux_retention: Option<Box<TmuxRetentionPolicy>>,
#[serde(default)]
pub notify_cmd: Option<String>,
#[serde(default)]
pub harness: Option<String>,
#[serde(default)]
pub agent_selection: Option<AgentSelection>,
pub node_count: u32,
pub parent_run_id: Option<RunId>,
pub parent_node_id: Option<NodeId>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ChildRef {
pub run_id: RunId,
pub node_id: NodeId,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum EvidenceStatus {
Pending,
Failed,
Complete,
}
impl std::fmt::Display for EvidenceStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::Pending => "pending",
Self::Failed => "failed",
Self::Complete => "complete",
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct WorkerEvidence {
pub attempt: u32,
pub session_id: String,
pub original_cwd: String,
pub live_session_path: String,
pub status: EvidenceStatus,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub transcript_path: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub resume_path: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pane_path: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub report_path: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub transcript_sha256: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TmuxRetentionPolicy {
pub persistent: bool,
pub completed_window_ttl_secs: u64,
pub completed_window_max: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RetainedDisplay {
pub attempt: u32,
pub socket: String,
pub session: String,
pub window_id: String,
pub pane_id: String,
pub server_pid: u32,
pub server_pid_start_secs: u64,
pub server_marker: String,
pub ownership_marker: String,
pub retained_at: DateTime<Utc>,
pub expires_at: DateTime<Utc>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expired_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Node {
pub schema_version: u32,
pub node_id: NodeId,
pub run_id: RunId,
pub parent_node_id: Option<NodeId>,
pub kind: Kind,
pub status: Status,
pub task: Option<String>,
pub worktree_path: Option<String>,
pub branch: Option<String>,
#[serde(default)]
pub base_sha: Option<String>,
pub tmux_window: Option<String>,
#[serde(default)]
pub tmux_identity: Option<Box<TmuxIdentity>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub evidence: Option<WorkerEvidence>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub retained_display: Option<Box<RetainedDisplay>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub retention_unavailable: Option<String>,
pub agent_pid: Option<i32>,
pub agent_pid_start_time: Option<DateTime<Utc>>,
pub supervisor_pid: Option<i32>,
#[serde(default)]
pub children: Vec<ChildRef>,
pub started_at: Option<DateTime<Utc>>,
pub updated_at: DateTime<Utc>,
pub last_report: Option<Value>,
#[serde(default)]
pub last_processed_report_seq_by_child: Map<String, Value>,
#[serde(default)]
pub retry_attempts: u32,
#[serde(default)]
pub worker_exit: Option<WorkerExit>,
#[serde(default)]
pub pending_merge: Option<Box<MergeTxn>>,
#[serde(default)]
pub first_death_at: Option<DateTime<Utc>>,
#[serde(default)]
pub awaiting_input: Option<Box<AwaitingInput>>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AwaitingInput {
pub opened_at: DateTime<Utc>,
pub event_seq: u64,
pub discussion_items: Vec<Value>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MergeTxn {
pub op_id: String,
pub source_branch: String,
pub worker_branch: String,
pub expected_source_oid: String,
pub worker_oid: String,
#[serde(default)]
pub base_sha: Option<String>,
#[serde(default)]
pub driver_pid: Option<i32>,
#[serde(default)]
pub driver_pid_start_secs: Option<u64>,
pub started_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct WorkerExit {
#[serde(default)]
pub code: Option<i32>,
#[serde(default)]
pub signal: Option<i32>,
pub at: DateTime<Utc>,
}
impl WorkerExit {
pub fn is_clean(self) -> bool {
self.signal.is_none() && self.code == Some(0)
}
pub fn is_failure(self) -> bool {
!self.is_clean()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TmuxIdentity {
#[serde(default)]
pub socket: Option<String>,
pub session: String,
pub window_id: String,
#[serde(default)]
pub pane_id: Option<String>,
#[serde(default)]
pub server_pid: Option<u32>,
#[serde(default)]
pub server_pid_start_secs: Option<u64>,
#[serde(default)]
pub server_marker: Option<String>,
}
impl TmuxIdentity {
pub fn capture_target(&self) -> &str {
self.pane_id
.as_deref()
.filter(|id| !id.is_empty())
.unwrap_or(&self.window_id)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Event {
pub ts: DateTime<Utc>,
pub seq: u64,
pub kind: String,
pub run_id: RunId,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub node_id: Option<NodeId>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub idempotency_key: Option<String>,
#[serde(default)]
pub data: Value,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn aggregate_terminal_status_is_the_three_way_rule() {
use Status::{Blocked, Cancelled, Done, Failed, Pending, Running};
assert_eq!(aggregate_terminal_status([]), None);
for live in [Pending, Running, Blocked] {
assert_eq!(aggregate_terminal_status([Done, live]), None);
}
assert_eq!(aggregate_terminal_status([Done, Done]), Some(Done));
assert_eq!(aggregate_terminal_status([Done, Failed]), Some(Failed));
assert_eq!(aggregate_terminal_status([Failed, Cancelled]), Some(Failed));
assert_eq!(
aggregate_terminal_status([Cancelled, Cancelled]),
Some(Cancelled)
);
assert_eq!(
aggregate_terminal_status([Done, Cancelled]),
Some(Cancelled)
);
}
#[test]
fn wire_names_match_serde_round_trip() {
for &name in Kind::WIRE_NAMES {
let kind: Kind = serde_json::from_value(Value::String(name.to_string()))
.unwrap_or_else(|_| panic!("WIRE_NAMES entry {name:?} is not a valid Kind"));
assert_eq!(
serde_json::to_value(kind).unwrap(),
Value::String(name.to_string()),
"serde round-trip diverged from wire_name for {name:?}",
);
}
}
#[test]
fn autonomous_single_node_worker_set_is_exact() {
for k in [Kind::Spinoff, Kind::Research, Kind::TechnicalDecision] {
assert!(
k.is_autonomous_single_node_worker(),
"{k:?} should be retry-eligible"
);
assert_eq!(k.lifecycle(), Lifecycle::Autonomous);
}
for k in [
Kind::FanOut, Kind::Unknown, ] {
assert!(
!k.is_autonomous_single_node_worker(),
"{k:?} must NOT be retry-eligible"
);
}
}
#[test]
fn removed_kinds_deserialize_to_unknown() {
for removed in [
"code",
"orchestrate",
"orchestrated",
"bugfix",
"make-skill",
] {
let kind: Kind = serde_json::from_value(Value::String(removed.to_string()))
.expect("a removed kind must still deserialize, not fault");
assert_eq!(kind, Kind::Unknown, "{removed:?} should map to Unknown");
}
assert_eq!(
serde_json::from_value::<Kind>(Value::String("future-kind".into())).unwrap(),
Kind::Unknown
);
for &name in Kind::WIRE_NAMES {
let kind: Kind = serde_json::from_value(Value::String(name.to_string())).unwrap();
assert_ne!(kind, Kind::Unknown, "{name:?} must not fold to Unknown");
}
}
#[test]
fn tmux_identity_deserializes_legacy_state_without_pane_id() {
let absent: TmuxIdentity = serde_json::from_value(serde_json::json!({
"socket": null,
"session": "taskfleet",
"window_id": "@42",
}))
.expect("legacy identity without pane_id must deserialize");
assert_eq!(absent.pane_id, None);
assert_eq!(absent.capture_target(), "@42");
let null: TmuxIdentity = serde_json::from_value(serde_json::json!({
"socket": null,
"session": "taskfleet",
"window_id": "@42",
"pane_id": null,
}))
.expect("identity with explicit null pane_id must deserialize");
assert_eq!(null.pane_id, None);
assert_eq!(null.capture_target(), "@42");
}
#[test]
fn capture_target_prefers_nonempty_pane_id() {
let with_pane = TmuxIdentity {
socket: None,
session: "taskfleet".into(),
window_id: "@42".into(),
pane_id: Some("%7".into()),
server_pid: None,
server_pid_start_secs: None,
server_marker: None,
};
assert_eq!(with_pane.capture_target(), "%7");
let empty_pane = TmuxIdentity {
pane_id: Some(String::new()),
..with_pane.clone()
};
assert_eq!(empty_pane.capture_target(), "@42");
}
}
#[cfg(test)]
mod id_tests {
use super::*;
const TRAVERSAL_VECTORS: &[&str] = &[
"..",
"../etc",
"a/b",
"a/../b",
".hidden",
"./x",
"foo/bar.json",
"n-0001/../../etc",
"",
];
#[test]
fn run_id_accepts_generator_output_and_rejects_malformed() {
let id = crate::new_run_id();
assert!(
RunId::parse_str(&id).is_ok(),
"generator must validate: {id}"
);
for bad in [
"tooshort",
"01jxsnap0000000000000000000", "01JXSNAP000000000000000000", "01jxiiiiiiiiiiiiiiiiiiiiii", "80000000000000000000000000", "n-0001", ] {
assert!(RunId::parse_str(bad).is_err(), "expected reject: {bad:?}");
}
for bad in TRAVERSAL_VECTORS {
assert!(
RunId::parse_str(bad).is_err(),
"traversal not rejected: {bad:?}"
);
}
}
#[test]
fn node_id_accepts_canonical_and_rejects_malformed() {
for ok in ["n-0001", "n-0010", "n-123456"] {
assert!(NodeId::parse_str(ok).is_ok(), "expected accept: {ok}");
}
assert!(matches!(
NodeId::parse_str("d-0001"),
Err(IdValidationError::WrongPrefix { .. })
));
assert!(matches!(
NodeId::parse_str("0001"),
Err(IdValidationError::WrongPrefix { .. })
));
for bad in [
"n-1", "n-abcd", "n-", "n-00a1", "n-00000000000", ] {
assert!(
matches!(
NodeId::parse_str(bad),
Err(IdValidationError::InvalidFormat { .. })
),
"expected InvalidFormat: {bad:?}",
);
}
for bad in TRAVERSAL_VECTORS {
assert!(
NodeId::parse_str(bad).is_err(),
"traversal not rejected: {bad:?}"
);
}
}
#[test]
fn deserialize_rejects_malformed_ids() {
assert!(serde_json::from_str::<NodeId>("\"n-0001\"").is_ok());
assert!(serde_json::from_str::<NodeId>("\"../../etc\"").is_err());
assert!(serde_json::from_str::<NodeId>("\"n-../escape\"").is_err());
}
#[test]
fn serialize_round_trips_as_bare_string() {
let nid = NodeId::parse_str("n-0042").unwrap();
let json = serde_json::to_string(&nid).unwrap();
assert_eq!(json, "\"n-0042\"");
let back: NodeId = serde_json::from_str(&json).unwrap();
assert_eq!(back, nid);
assert_eq!(nid.as_str(), "n-0042");
assert_eq!(nid.to_string(), "n-0042");
}
#[test]
fn error_exposes_kind_and_expected() {
let err = NodeId::parse_str("n-x").unwrap_err();
assert_eq!(err.kind(), "node");
assert_eq!(err.expected(), "n-NNNN (n- followed by 4-10 ASCII digits)");
}
#[test]
fn event_deserialize_validates_envelope_ids() {
let ok = r#"{"ts":"2026-06-12T00:00:00Z","seq":1,"kind":"node.created","run_id":"01jxsnap000000000000000000","node_id":"n-0001","data":{}}"#;
assert!(serde_json::from_str::<Event>(ok).is_ok());
let bad_run = r#"{"ts":"2026-06-12T00:00:00Z","seq":1,"kind":"run.status","run_id":"not-a-ulid","data":{}}"#;
assert!(serde_json::from_str::<Event>(bad_run).is_err());
let bad_node = r#"{"ts":"2026-06-12T00:00:00Z","seq":1,"kind":"node.status","run_id":"01jxsnap000000000000000000","node_id":"n-1","data":{}}"#;
assert!(serde_json::from_str::<Event>(bad_node).is_err());
}
#[test]
fn from_str_and_ord_delegate_to_inner() {
use std::str::FromStr;
assert!(RunId::from_str("01jxsnap000000000000000000").is_ok());
assert!("n-0001".parse::<NodeId>().is_ok());
assert!("n-x".parse::<NodeId>().is_err());
let a = RunId::parse_str("01jxsnap000000000000000000").unwrap();
let b = RunId::parse_str("02jxsnap000000000000000000").unwrap();
assert!(a < b);
let mut v = vec![b.clone(), a.clone()];
v.sort();
assert_eq!(v, vec![a, b]);
}
}
#[cfg(test)]
mod agent_selection_validation_tests {
use super::*;
fn valid() -> AgentSelection {
AgentSelection {
schema_version: 1,
profile: "capable".into(),
selection_source: "cli".into(),
interaction: "autonomous".into(),
capability: "capable".into(),
residency: "remote".into(),
requested_harness: None,
selected: SelectedAgentCandidate {
candidate_index: 1,
harness: "pi".into(),
command: vec!["pi".into()],
telemetry: Some("worker-v1".into()),
},
fallback: vec![SkippedAgentCandidate {
candidate_index: 0,
harness: "claude".into(),
reason: "autonomous_harness_unsupported".into(),
}],
}
}
#[test]
fn durable_selection_rejects_impossible_state() {
assert!(valid().validate().is_ok());
let mut invalid = valid();
invalid.selected.candidate_index = 200;
assert!(invalid.validate().is_err());
let mut invalid = valid();
invalid.fallback[0].reason = "made_up".into();
assert!(invalid.validate().is_err());
let mut invalid = valid();
invalid.selected.harness = "claude".into();
assert!(invalid.validate().is_err());
}
}