use chappi_core::{CognitiveState, TransitionKind, validate};
use chappi_syntax::parse_document;
fn decode(source: &str) -> CognitiveState {
CognitiveState::try_from(parse_document(source).expect("fixture syntax should parse"))
.expect("fixture semantics should decode")
}
#[test]
fn decodes_focus_streams_in_canonical_order_and_defaults_node_revisions() {
let state = decode(
r#"(focus memory 1 self associated 0 (fg c2) (bg self) (residue c1) (trigger recalled))
(chappi 1)
(claim c2 self working 60 likely (second))
(entity self self persistent 100 person (self))
(focus memory 0 self evoked none (fg c1) (bg c2) (residue) (trigger noticed))
(claim c1 self working 70 likely (first))
(focus work 0 self evoked none (fg c2) (bg) (residue c1) (trigger assigned))
"#,
);
assert!(state.nodes.iter().all(|node| node.revision == 0));
assert_eq!(
state
.focus_streams
.iter()
.map(|stream| stream.id.as_str())
.collect::<Vec<_>>(),
["memory", "work"]
);
assert_eq!(
state.focus_streams[0]
.frames
.iter()
.map(|frame| frame.sequence)
.collect::<Vec<_>>(),
[0, 1]
);
assert_eq!(
state.focus_streams[0].frames[1].transition,
TransitionKind::Associated
);
assert_eq!(state.focus_streams[0].frames[1].previous_sequence, Some(0));
assert_eq!(state.focus_streams[0].frames[1].foreground, ["c2"]);
assert!(validate(&state).is_empty());
}
#[test]
fn decodes_every_focus_transition_kind() {
let state = decode(
r#"(chappi 1)
(entity self self persistent 100 person (self))
(claim c self working 70 likely (claim))
(focus thought 0 self evoked none (fg c) (bg) (residue) (trigger start))
(focus thought 1 self associated 0 (fg c) (bg) (residue) (trigger link))
(focus thought 2 self interrupted 1 (fg self) (bg c) (residue) (trigger noise))
(focus thought 3 self returned 2 (fg c) (bg self) (residue) (trigger quiet))
(focus thought 4 self reframed 3 (fg c) (bg) (residue self) (trigger insight))
(focus thought 5 self resolved 4 (fg) (bg c) (residue self) (trigger done))
"#,
);
assert_eq!(
state.focus_streams[0]
.frames
.iter()
.map(|frame| frame.transition)
.collect::<Vec<_>>(),
[
TransitionKind::Evoked,
TransitionKind::Associated,
TransitionKind::Interrupted,
TransitionKind::Returned,
TransitionKind::Reframed,
TransitionKind::Resolved,
]
);
assert!(validate(&state).is_empty());
}
#[test]
fn reports_focus_sequence_reference_and_bucket_invariant_failures() {
let state = decode(
r#"(chappi 1)
(entity self self persistent 100 person (self))
(claim c self working 70 likely (claim))
(focus bad-start 1 self associated 0 (fg c c) (bg c missing) (residue self) (trigger bad))
(focus bad-next 0 self evoked none (fg c) (bg) (residue) (trigger start))
(focus bad-next 2 self returned 0 (fg c) (bg) (residue) (trigger skipped))
(focus bad-owner 0 missing evoked none (fg c) (bg) (residue) (trigger absent))
"#,
);
let diagnostics = validate(&state);
let codes = diagnostics
.iter()
.map(|diagnostic| diagnostic.code.as_str())
.collect::<Vec<_>>();
assert!(codes.contains(&"invalid-focus-start"));
assert!(codes.contains(&"duplicate-focus-reference"));
assert!(codes.contains(&"overlapping-focus-reference"));
assert!(codes.contains(&"undefined-focus-reference"));
assert!(codes.contains(&"invalid-focus-sequence"));
assert!(codes.contains(&"undefined-focus-owner"));
}
#[test]
fn distinguishes_bucket_duplicates_from_cross_bucket_overlap() {
let state = decode(
r#"(chappi 1)
(entity self self persistent 100 person (self))
(claim c self working 70 likely (claim))
(focus work 0 self evoked none (fg c c) (bg) (residue) (trigger duplicate))
"#,
);
let diagnostics = validate(&state);
assert_eq!(
diagnostics
.iter()
.filter(|item| item.code == "duplicate-focus-reference")
.count(),
1
);
assert!(
!diagnostics
.iter()
.any(|item| item.code == "overlapping-focus-reference")
);
}
#[test]
fn rejects_duplicate_streams_and_frames_attached_to_the_wrong_stream() {
let mut state = decode(
r#"(chappi 1)
(entity self self persistent 100 person (self))
(focus work 0 self evoked none (fg self) (bg) (residue) (trigger start))
"#,
);
let duplicate = state.focus_streams[0].clone();
state.focus_streams.push(duplicate);
state.focus_streams[0].frames[0].stream_id = "other".into();
let diagnostics = validate(&state);
assert!(
diagnostics
.iter()
.any(|item| item.code == "duplicate-focus-stream")
);
assert!(
diagnostics
.iter()
.any(|item| item.code == "focus-stream-mismatch")
);
}
#[test]
fn rejects_an_empty_programmatic_focus_stream() {
let mut state = decode("(chappi 1)\n(entity self self persistent 100 person (self))\n");
state.focus_streams.push(chappi_core::FocusStream {
id: "empty".into(),
frames: Vec::new(),
});
let diagnostics = validate(&state);
assert!(
diagnostics
.iter()
.any(|item| item.code == "empty-focus-stream")
);
}