use chrono::{DateTime, Utc};
use serde::Serialize;
use serde_json::Value;
use octl_core::schema::TmuxIdentity;
use octl_core::{ChildRef, Node, NodeId, RunId};
use crate::run::{kind_kebab, status_kebab};
#[derive(Serialize)]
pub struct NodeView<'a> {
pub schema_version: u32,
pub node_id: &'a NodeId,
pub run_id: &'a RunId,
pub parent_node_id: Option<&'a NodeId>,
pub kind: &'static str,
pub status: &'static str,
pub task: Option<&'a str>,
pub worktree_path: Option<&'a str>,
pub branch: Option<&'a str>,
pub tmux_window: Option<&'a str>,
pub tmux_identity: &'a Option<TmuxIdentity>,
pub agent_pid: Option<i32>,
pub agent_pid_start_time: Option<DateTime<Utc>>,
pub supervisor_pid: Option<i32>,
pub children: &'a [ChildRef],
pub started_at: Option<DateTime<Utc>>,
pub updated_at: DateTime<Utc>,
pub last_report: &'a Option<Value>,
}
impl<'a> From<&'a Node> for NodeView<'a> {
fn from(n: &'a Node) -> Self {
Self {
schema_version: n.schema_version,
node_id: &n.node_id,
run_id: &n.run_id,
parent_node_id: n.parent_node_id.as_ref(),
kind: kind_kebab(n.kind),
status: status_kebab(n.status),
task: n.task.as_deref(),
worktree_path: n.worktree_path.as_deref(),
branch: n.branch.as_deref(),
tmux_window: n.tmux_window.as_deref(),
tmux_identity: &n.tmux_identity,
agent_pid: n.agent_pid,
agent_pid_start_time: n.agent_pid_start_time,
supervisor_pid: n.supervisor_pid,
children: &n.children,
started_at: n.started_at,
updated_at: n.updated_at,
last_report: &n.last_report,
}
}
}
#[derive(Serialize)]
pub struct NodeSummary {
pub node_id: String,
pub kind: String,
pub status: String,
pub parent_node_id: Option<String>,
pub updated_at: DateTime<Utc>,
pub children: u32,
}
impl From<&Node> for NodeSummary {
fn from(n: &Node) -> Self {
Self {
node_id: n.node_id.to_string(),
kind: kind_kebab(n.kind).to_string(),
status: status_kebab(n.status).to_string(),
parent_node_id: n.parent_node_id.as_ref().map(ToString::to_string),
updated_at: n.updated_at,
children: n.children.len() as u32,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use octl_core::{Kind, Status};
use serde_json::{json, Map};
fn ts() -> DateTime<Utc> {
"2024-01-01T00:00:00Z".parse().unwrap()
}
fn sample() -> Node {
Node {
schema_version: 1,
node_id: NodeId::parse_str("n-0001").unwrap(),
run_id: RunId::parse_str("01arz3ndektsv4rrffq69g5fav").unwrap(),
parent_node_id: None,
kind: Kind::Spinoff,
status: Status::Pending,
task: None,
worktree_path: Some("/tmp/seed-wt".to_string()),
branch: Some("wt/seed".to_string()),
base_sha: None,
tmux_window: Some("seed-win".to_string()),
tmux_identity: None,
agent_pid: Some(4242),
agent_pid_start_time: None,
supervisor_pid: None,
children: Vec::new(),
started_at: Some(ts()),
updated_at: ts(),
last_report: None,
last_processed_report_seq_by_child: Map::new(),
retry_attempts: 0,
}
}
#[test]
fn view_pins_wire_shape() {
let n = sample();
let got = serde_json::to_value(NodeView::from(&n)).unwrap();
assert_eq!(
got,
json!({
"schema_version": 1,
"node_id": "n-0001",
"run_id": "01arz3ndektsv4rrffq69g5fav",
"parent_node_id": null,
"kind": "spinoff",
"status": "pending",
"task": null,
"worktree_path": "/tmp/seed-wt",
"branch": "wt/seed",
"tmux_window": "seed-win",
"tmux_identity": null,
"agent_pid": 4242,
"agent_pid_start_time": null,
"supervisor_pid": null,
"children": [],
"started_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z",
"last_report": null,
})
);
}
#[test]
fn internal_cursor_does_not_leak() {
let base = serde_json::to_value(NodeView::from(&sample())).unwrap();
let mut bumped = sample();
bumped
.last_processed_report_seq_by_child
.insert("c-01arz3ndektsv4rrffq69g5fav".to_string(), json!(7));
let after = serde_json::to_value(NodeView::from(&bumped)).unwrap();
assert_eq!(base, after, "internal cursor leaked into node DTO");
assert!(
after.get("last_processed_report_seq_by_child").is_none(),
"internal cursor must be absent from the wire contract"
);
}
#[test]
fn summary_pins_wire_shape() {
let n = sample();
let got = serde_json::to_value(NodeSummary::from(&n)).unwrap();
assert_eq!(
got,
json!({
"node_id": "n-0001",
"kind": "spinoff",
"status": "pending",
"parent_node_id": null,
"updated_at": "2024-01-01T00:00:00Z",
"children": 0,
})
);
}
}