use shepherd_core::run::{LaneState, RunState};
const GOLDEN_WITH_UNKNOWN_KEYS: &str = r#"{
"base": "main",
"branch": "v9.0.0-dev.0",
"custom_future_field": {
"a": 1,
"b": 2
},
"kind": "sprint",
"lanes": [
{
"accepted_commit": null,
"branch": "",
"id": "l1-engine",
"merged": false,
"nested_unknown": {
"a": 2,
"z": 1
},
"plan": "lanes/l1-engine/plan.md",
"state": "pending",
"updated_at": 0,
"worktree": ""
}
],
"plan": "",
"run": "v900-dev0",
"schema_version": 1,
"seed": "",
"status": "planted",
"updated_at": 1786621458
}"#;
fn source_document() -> serde_json::Value {
serde_json::json!({
"schema_version": 1,
"run": "v900-dev0",
"kind": "sprint",
"branch": "v9.0.0-dev.0",
"base": "main",
"seed": "",
"plan": "",
"status": "planted",
"updated_at": 1_786_621_458i64,
"lanes": [{
"id": "l1-engine",
"plan": "lanes/l1-engine/plan.md",
"worktree": "",
"branch": "",
"state": "pending",
"accepted_commit": null,
"merged": false,
"updated_at": 0,
"nested_unknown": {"z": 1, "a": 2},
}],
"custom_future_field": {"b": 2, "a": 1},
})
}
fn minimal_state() -> RunState {
serde_json::from_value(serde_json::json!({"run": "v1"})).expect("minimal doc parses")
}
#[test]
fn unknown_keys_round_trip() {
let state: RunState =
serde_json::from_value(source_document()).expect("the fixture is a valid RunState");
assert_eq!(
state.extra.get("custom_future_field"),
Some(&serde_json::json!({"a": 1, "b": 2})),
"a top-level key RunState does not declare must survive parsing"
);
let lane = &state.lanes[0];
assert_eq!(
lane.extra.get("nested_unknown"),
Some(&serde_json::json!({"a": 2, "z": 1})),
"a key LaneState does not declare must survive parsing too"
);
assert_eq!(
state.to_canonical_json(),
GOLDEN_WITH_UNKNOWN_KEYS,
"canonical output must match the pinned run-state bytes"
);
let reparsed: RunState =
serde_json::from_str(&state.to_canonical_json()).expect("canonical output re-parses");
assert_eq!(reparsed, state);
}
#[test]
fn keys_are_recursively_sorted_even_with_no_unknown_keys() {
let state = RunState {
schema_version: 1,
run: "v900-dev0".into(),
kind: "sprint".into(),
branch: "v9.0.0-dev.0".into(),
base: "main".into(),
seed: String::new(),
plan: String::new(),
status: "planted".into(),
updated_at: 1_786_621_458,
lanes: vec![LaneState {
id: "l1-engine".into(),
plan: "lanes/l1-engine/plan.md".into(),
worktree: String::new(),
branch: String::new(),
state: "pending".into(),
accepted_commit: None,
merged: false,
updated_at: 0,
extra: Default::default(),
}],
extra: Default::default(),
};
let text = state.to_canonical_json();
let top_level_keys: Vec<&str> = text
.lines()
.filter(|line| line.starts_with(" \"") && line.contains("\":"))
.map(|line| {
let after_quote = &line[3..];
&after_quote[..after_quote.find('"').expect("closing quote")]
})
.collect();
assert_eq!(
top_level_keys,
vec![
"base",
"branch",
"kind",
"lanes",
"plan",
"run",
"schema_version",
"seed",
"status",
"updated_at",
]
);
}
#[test]
fn non_ascii_and_control_characters_use_pinned_wire_escapes() {
let mut state = minimal_state();
state.extra.insert(
"note".into(),
serde_json::Value::String(
"café \u{2615} \u{1F389} line1\nline2\ttab \"quote\" back\\slash".into(),
),
);
let text = state.to_canonical_json();
let expected_value =
"\"caf\\u00e9 \\u2615 \\ud83c\\udf89 line1\\nline2\\ttab \\\"quote\\\" back\\\\slash\"";
assert!(
text.contains(&format!("\"note\": {expected_value}")),
"expected the escaped literal {expected_value} in:\n{text}"
);
assert!(
text.is_ascii(),
"canonical output must be pure ASCII; got: {text}"
);
}
#[test]
fn defaults_match_the_run_state_contract() {
let state = minimal_state();
assert_eq!(state.schema_version, 1);
assert_eq!(state.run, "v1");
assert_eq!(state.kind, "sprint");
assert_eq!(state.branch, "");
assert_eq!(state.base, "");
assert_eq!(state.seed, "");
assert_eq!(state.plan, "");
assert_eq!(state.status, "planted");
assert!(state.lanes.is_empty());
assert_eq!(state.updated_at, 0);
assert!(state.extra.is_empty());
}
#[test]
fn lane_state_extra_round_trips_independently() {
let lane: LaneState = serde_json::from_value(serde_json::json!({
"id": "l9-example",
"declared_state": "in-progress",
}))
.expect("lane document parses");
assert_eq!(lane.id, "l9-example");
assert_eq!(lane.state, "pending", "LANE default, not the unknown key");
assert_eq!(
lane.extra.get("declared_state"),
Some(&serde_json::Value::String("in-progress".into()))
);
}
#[test]
fn empty_containers_do_not_expand() {
let state = minimal_state();
let text = state.to_canonical_json();
assert!(text.contains("\"lanes\": []"));
}