use serde::{Deserialize, Serialize};
pub const SCHEMA_VERSION: &str = "0.1";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SessionRecord {
pub schema_version: String,
pub name: String,
pub project: String,
pub harness: String,
pub token: String,
pub created: String,
pub updated: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum SessionPhase {
Create,
Continue,
}
impl SessionPhase {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
SessionPhase::Create => "create",
SessionPhase::Continue => "continue",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SessionPlan {
pub phase: SessionPhase,
pub resume_token: Option<String>,
}
impl SessionPlan {
#[must_use]
pub fn decide(existing: Option<&SessionRecord>) -> SessionPlan {
match existing {
Some(record) => SessionPlan {
phase: SessionPhase::Continue,
resume_token: Some(record.token.clone()),
},
None => SessionPlan {
phase: SessionPhase::Create,
resume_token: None,
},
}
}
}
#[must_use]
pub fn harness_conflict<'a>(existing: Option<&'a SessionRecord>, harness: &str) -> Option<&'a str> {
existing
.filter(|record| record.harness != harness)
.map(|record| record.harness.as_str())
}
#[cfg(test)]
mod tests {
use super::*;
fn record(token: &str, harness: &str) -> SessionRecord {
SessionRecord {
schema_version: SCHEMA_VERSION.to_string(),
name: "greet".to_string(),
project: "/p".to_string(),
harness: harness.to_string(),
token: token.to_string(),
created: "2026-07-10T00:00:00Z".to_string(),
updated: "2026-07-10T00:00:00Z".to_string(),
}
}
#[test]
fn decide_creates_without_a_record() {
let plan = SessionPlan::decide(None);
assert_eq!(plan.phase, SessionPhase::Create);
assert_eq!(plan.resume_token, None);
}
#[test]
fn decide_continues_a_stored_token() {
let existing = record("sess-1", "claude-code");
let plan = SessionPlan::decide(Some(&existing));
assert_eq!(plan.phase, SessionPhase::Continue);
assert_eq!(plan.resume_token.as_deref(), Some("sess-1"));
}
#[test]
fn phase_strings_are_stable() {
assert_eq!(SessionPhase::Create.as_str(), "create");
assert_eq!(SessionPhase::Continue.as_str(), "continue");
}
#[test]
fn harness_conflict_flags_a_mismatch_only() {
let claude = record("s", "claude-code");
assert_eq!(harness_conflict(None, "claude-code"), None);
assert_eq!(harness_conflict(Some(&claude), "claude-code"), None);
assert_eq!(
harness_conflict(Some(&claude), "codex"),
Some("claude-code")
);
}
#[test]
fn record_round_trips_through_json() {
let original = record("sess-9", "opencode");
let text = serde_json::to_string(&original).unwrap();
let parsed: SessionRecord = serde_json::from_str(&text).unwrap();
assert_eq!(original, parsed);
}
}