Move state update proof to core

This commit is contained in:
Meta Vens 2026-05-17 20:52:00 +01:00
commit b95b95c11d
6 changed files with 533 additions and 652 deletions

View file

@ -1964,6 +1964,60 @@ mod tests {
Ok(())
}
#[test]
fn heartbeat_launch_plan_leaves_lifecycle_to_runtime_links() {
let launch_plan = plan_runtime_spine_heartbeat_launch(
&EpiphanyThreadState::default(),
RuntimeSpineHeartbeatLaunchPlanOptions {
binding_id: "modeling-checkpoint-worker".to_string(),
kind: EpiphanyJobKind::Specialist,
scope: "role-scoped modeling/checkpoint maintenance".to_string(),
owner_role: "epiphany-modeler".to_string(),
authority_scope: "epiphany.role.modeling".to_string(),
linked_subgoal_ids: vec!["phase-6".to_string()],
linked_graph_node_ids: vec!["runtime-spine".to_string()],
instruction: "Model the target before implementation.".to_string(),
launch_document: EpiphanyWorkerLaunchDocument::Role(
crate::EpiphanyRoleWorkerLaunchDocument {
thread_id: "thread-1".to_string(),
role_id: "modeling".to_string(),
state_revision: 7,
objective: Some("keep state typed".to_string()),
active_subgoal_id: None,
active_subgoals: Vec::new(),
active_graph_node_ids: vec!["runtime-spine".to_string()],
investigation_checkpoint: None,
scratch: None,
invariants: Vec::new(),
graphs: None,
recent_evidence: Vec::new(),
recent_observations: Vec::new(),
graph_frontier: None,
graph_checkpoint: None,
planning: None,
churn: None,
},
),
output_contract_id: "epiphany.worker.role_result.v0".to_string(),
max_runtime_seconds: Some(60),
runtime_job_id: "turn-1".to_string(),
},
)
.expect("launch planning should build binding and runtime link");
assert_eq!(
launch_plan.binding.authority_scope.as_deref(),
Some("epiphany.role.modeling")
);
assert_eq!(
launch_plan.runtime_link.id,
"runtime-link-modeling-checkpoint-worker-turn-1"
);
assert_eq!(launch_plan.runtime_link.runtime_job_id, "turn-1");
assert_eq!(launch_plan.runtime_link.runtime_result_id, None);
assert_eq!(launch_plan.runtime_link.role_id, "epiphany-modeler");
}
#[test]
fn runtime_spine_emits_cultnet_hello_frame() -> Result<()> {
let temp = tempdir()?;

View file

@ -231,6 +231,479 @@ pub fn apply_epiphany_state_update(
state.last_updated_turn_id = reference_turn_id;
}
#[cfg(test)]
mod tests {
use super::*;
use epiphany_state_model::EpiphanyBacklogItem;
use epiphany_state_model::EpiphanyGraph;
use epiphany_state_model::EpiphanyGraphNode;
use epiphany_state_model::EpiphanyJobKind;
use epiphany_state_model::EpiphanyObjectiveDraft;
use epiphany_state_model::EpiphanyPlanningCapture;
use epiphany_state_model::EpiphanyPlanningPriority;
use epiphany_state_model::EpiphanyPlanningSourceRef;
use epiphany_state_model::EpiphanyRoadmapStream;
fn evidence(id: &str) -> EpiphanyEvidenceRecord {
EpiphanyEvidenceRecord {
id: id.to_string(),
kind: "verification".to_string(),
status: "ok".to_string(),
summary: "Evidence summary".to_string(),
code_refs: Vec::new(),
}
}
fn observation(id: &str, evidence_ids: Vec<&str>) -> EpiphanyObservation {
EpiphanyObservation {
id: id.to_string(),
summary: "Observation summary".to_string(),
source_kind: "smoke".to_string(),
status: "ok".to_string(),
code_refs: Vec::new(),
evidence_ids: evidence_ids.into_iter().map(str::to_string).collect(),
}
}
fn job_binding(id: &str) -> EpiphanyJobBinding {
EpiphanyJobBinding {
id: id.to_string(),
kind: EpiphanyJobKind::Specialist,
scope: "role-scoped specialist work".to_string(),
owner_role: "epiphany-harness".to_string(),
authority_scope: Some("epiphany.specialist".to_string()),
linked_subgoal_ids: vec!["phase-6".to_string()],
linked_graph_node_ids: vec!["job-surface".to_string()],
blocking_reason: None,
}
}
fn acceptance_receipt(
id: &str,
result_id: &str,
evidence_id: &str,
) -> EpiphanyAcceptanceReceipt {
EpiphanyAcceptanceReceipt {
id: id.to_string(),
result_id: result_id.to_string(),
job_id: "runtime-job-1".to_string(),
binding_id: "modeling".to_string(),
surface: "roleAccept".to_string(),
role_id: "modeling".to_string(),
status: "accepted".to_string(),
accepted_at: "2026-05-12T00:00:00Z".to_string(),
accepted_observation_id: Some("obs-modeling".to_string()),
accepted_evidence_id: Some(evidence_id.to_string()),
summary: Some("Accepted modeling result.".to_string()),
}
}
#[test]
fn state_update_replaces_typed_fields_and_prepends_evidence() {
let mut state = EpiphanyThreadState {
revision: 3,
recent_evidence: vec![EpiphanyEvidenceRecord {
id: "old-evidence".to_string(),
kind: "research".to_string(),
status: "ok".to_string(),
summary: "Older finding".to_string(),
code_refs: Vec::new(),
}],
..Default::default()
};
apply_epiphany_state_update(
&mut state,
EpiphanyStateUpdate {
objective: Some("Keep the map honest".to_string()),
investigation_checkpoint: Some(EpiphanyInvestigationCheckpoint {
checkpoint_id: "ix-1".to_string(),
kind: "slice_planning".to_string(),
focus: "Keep the durable packet small and explicit.".to_string(),
next_action: Some(
"Resume from the packet instead of the ghost transcript.".to_string(),
),
..Default::default()
}),
evidence: vec![EpiphanyEvidenceRecord {
id: "new-evidence".to_string(),
kind: "verification".to_string(),
status: "ok".to_string(),
summary: "New finding".to_string(),
code_refs: Vec::new(),
}],
churn: Some(EpiphanyChurnState {
understanding_status: "grounded".to_string(),
diff_pressure: "low".to_string(),
..Default::default()
}),
..Default::default()
},
Some("turn-1".to_string()),
);
assert_eq!(state.revision, 4);
assert_eq!(state.objective.as_deref(), Some("Keep the map honest"));
assert_eq!(state.last_updated_turn_id.as_deref(), Some("turn-1"));
assert_eq!(
state
.investigation_checkpoint
.as_ref()
.map(|checkpoint| checkpoint.checkpoint_id.as_str()),
Some("ix-1")
);
assert_eq!(state.recent_evidence[0].id, "new-evidence");
assert_eq!(state.recent_evidence[1].id, "old-evidence");
assert_eq!(
state
.churn
.as_ref()
.map(|churn| churn.diff_pressure.as_str()),
Some("low")
);
}
#[test]
fn state_update_replaces_job_bindings_and_prepends_acceptance_receipts() {
let mut state = EpiphanyThreadState {
revision: 2,
recent_evidence: vec![evidence("ev-new")],
job_bindings: vec![job_binding("old")],
acceptance_receipts: vec![acceptance_receipt("accept-old", "result-old", "ev-old")],
..Default::default()
};
apply_epiphany_state_update(
&mut state,
EpiphanyStateUpdate {
job_bindings: Some(vec![job_binding("new")]),
acceptance_receipts: vec![acceptance_receipt("accept-new", "result-new", "ev-new")],
..Default::default()
},
Some("turn-jobs".to_string()),
);
assert_eq!(state.revision, 3);
assert_eq!(state.job_bindings.len(), 1);
assert_eq!(state.job_bindings[0].id, "new");
assert_eq!(
state.job_bindings[0].authority_scope.as_deref(),
Some("epiphany.specialist")
);
assert_eq!(state.acceptance_receipts[0].id, "accept-new");
assert_eq!(state.acceptance_receipts[1].id, "accept-old");
}
#[test]
fn state_update_validation_accepts_and_rejects_planning_state() {
let valid = EpiphanyStateUpdate {
planning: Some(EpiphanyPlanningState {
captures: vec![EpiphanyPlanningCapture {
id: "capture-github-42".to_string(),
title: "Import issue backlog".to_string(),
confidence: "medium".to_string(),
status: "new".to_string(),
source: EpiphanyPlanningSourceRef {
kind: "github_issue".to_string(),
provider: Some("github".to_string()),
repo: Some("GameCult/Epiphany".to_string()),
issue_number: Some(42),
..Default::default()
},
..Default::default()
}],
backlog_items: vec![EpiphanyBacklogItem {
id: "backlog-planning-api".to_string(),
title: "Expose planning projection".to_string(),
kind: "feature".to_string(),
summary: "Make planning state queryable by the GUI.".to_string(),
status: "ready".to_string(),
horizon: "now".to_string(),
priority: EpiphanyPlanningPriority {
value: "p1".to_string(),
rationale: "Unblocks planning operations.".to_string(),
..Default::default()
},
confidence: "high".to_string(),
product_area: "gui".to_string(),
lane_hints: vec!["imagination".to_string()],
..Default::default()
}],
roadmap_streams: vec![EpiphanyRoadmapStream {
id: "stream-gui".to_string(),
title: "GUI Operator Surface".to_string(),
purpose: "Let the human inspect and steer planning.".to_string(),
status: "active".to_string(),
item_ids: vec!["backlog-planning-api".to_string()],
..Default::default()
}],
objective_drafts: vec![EpiphanyObjectiveDraft {
id: "objdraft-planning-api".to_string(),
title: "Build planning API slice".to_string(),
summary: "Land typed planning state and read-only projection.".to_string(),
source_item_ids: vec!["backlog-planning-api".to_string()],
acceptance_criteria: vec!["Projection returns planning counts.".to_string()],
status: "draft".to_string(),
..Default::default()
}],
..Default::default()
}),
..Default::default()
};
assert!(
epiphany_state_update_validation_errors(&EpiphanyThreadState::default(), &valid)
.is_empty()
);
let invalid = EpiphanyStateUpdate {
planning: Some(EpiphanyPlanningState {
captures: vec![EpiphanyPlanningCapture {
id: "capture-bad-github".to_string(),
title: "Missing repo issue".to_string(),
confidence: "medium".to_string(),
status: "new".to_string(),
source: EpiphanyPlanningSourceRef {
kind: "github_issue".to_string(),
issue_number: None,
..Default::default()
},
..Default::default()
}],
backlog_items: vec![EpiphanyBacklogItem {
id: "backlog-1".to_string(),
title: "Backlog without priority rationale".to_string(),
kind: "feature".to_string(),
summary: "Invalid on purpose.".to_string(),
status: "ready".to_string(),
horizon: "now".to_string(),
priority: EpiphanyPlanningPriority {
value: "p1".to_string(),
rationale: String::new(),
..Default::default()
},
confidence: "high".to_string(),
product_area: "gui".to_string(),
..Default::default()
}],
roadmap_streams: vec![EpiphanyRoadmapStream {
id: "stream-gui".to_string(),
title: "GUI Operator Surface".to_string(),
purpose: "Let the human inspect and steer planning.".to_string(),
status: "active".to_string(),
item_ids: vec!["missing-backlog".to_string()],
..Default::default()
}],
objective_drafts: vec![EpiphanyObjectiveDraft {
id: "objdraft-empty".to_string(),
title: "Empty acceptance draft".to_string(),
summary: "Invalid on purpose.".to_string(),
source_item_ids: vec!["missing-backlog".to_string()],
status: "draft".to_string(),
..Default::default()
}],
..Default::default()
}),
..Default::default()
};
let errors =
epiphany_state_update_validation_errors(&EpiphanyThreadState::default(), &invalid);
assert!(
errors
.iter()
.any(|error| error.contains("repo is required for github_issue"))
);
assert!(
errors
.iter()
.any(|error| error.contains("priority.rationale"))
);
assert!(
errors
.iter()
.any(|error| error.contains("references missing backlog item"))
);
assert!(
errors
.iter()
.any(|error| error.contains("must include at least one acceptance criterion"))
);
}
#[test]
fn state_update_validation_rejects_bad_evidence_observations_acceptance_and_bindings() {
let state = EpiphanyThreadState {
observations: vec![observation("obs-existing", vec!["ev-existing"])],
recent_evidence: vec![evidence("ev-existing"), evidence("ev-new")],
acceptance_receipts: vec![acceptance_receipt(
"accept-existing",
"result-existing",
"ev-existing",
)],
..Default::default()
};
let update = EpiphanyStateUpdate {
observations: vec![
observation("obs-existing", vec!["ev-new"]),
observation("obs-dup", vec!["ev-new"]),
observation("obs-dup", vec!["ev-missing"]),
EpiphanyObservation {
id: String::new(),
..observation("unused", vec!["ev-new"])
},
],
evidence: vec![
evidence("ev-existing"),
evidence("ev-new"),
evidence("ev-new"),
EpiphanyEvidenceRecord {
id: String::new(),
..evidence("unused")
},
],
acceptance_receipts: vec![
acceptance_receipt("accept-new", "result-existing", "ev-new"),
acceptance_receipt("accept-new", "result-new", "ev-new"),
],
job_bindings: Some(vec![
job_binding("dup"),
job_binding("dup"),
EpiphanyJobBinding {
id: String::new(),
kind: EpiphanyJobKind::Verification,
scope: String::new(),
owner_role: String::new(),
authority_scope: Some(String::new()),
linked_subgoal_ids: Vec::new(),
linked_graph_node_ids: Vec::new(),
blocking_reason: Some(String::new()),
},
]),
..Default::default()
};
let errors = epiphany_state_update_validation_errors(&state, &update);
assert!(
errors
.iter()
.any(|error| error.contains("evidence id \"ev-existing\" already exists"))
);
assert!(
errors
.iter()
.any(|error| error.contains("duplicate evidence id \"ev-new\""))
);
assert!(
errors
.iter()
.any(|error| error.contains("observation id \"obs-existing\" already exists"))
);
assert!(
errors
.iter()
.any(|error| error.contains("duplicate observation id \"obs-dup\""))
);
assert!(
errors
.iter()
.any(|error| error.contains("cites missing evidence id"))
);
assert!(
errors
.iter()
.any(|error| error.contains("runtime result \"result-existing\" already has"))
);
assert!(
errors
.iter()
.any(|error| error.contains("acceptance receipt id \"accept-new\" is duplicated"))
);
assert!(
errors
.iter()
.any(|error| error.contains("duplicate job binding id"))
);
assert!(
errors
.iter()
.any(|error| error.contains("job_binding.id must not be empty"))
);
assert!(
errors
.iter()
.any(|error| error.contains("job_binding.scope must not be empty"))
);
assert!(
errors
.iter()
.any(|error| error.contains("job_binding.owner_role must not be empty"))
);
assert!(
errors
.iter()
.any(|error| error.contains("job_binding.authority_scope must not be empty"))
);
assert!(
errors
.iter()
.any(|error| error.contains("job_binding.blocking_reason must not be empty"))
);
}
#[test]
fn state_update_validation_rejects_structural_replacements_and_checkpoint_evidence_gap() {
let state = EpiphanyThreadState {
graphs: EpiphanyGraphs {
architecture: EpiphanyGraph {
nodes: vec![EpiphanyGraphNode {
id: "state".to_string(),
title: "State".to_string(),
purpose: "Carry the explicit map".to_string(),
..Default::default()
}],
edges: Vec::new(),
},
..Default::default()
},
..Default::default()
};
let frontier_update = EpiphanyStateUpdate {
graph_frontier: Some(EpiphanyGraphFrontier {
active_node_ids: vec!["missing".to_string()],
..Default::default()
}),
..Default::default()
};
let errors = epiphany_state_update_validation_errors(&state, &frontier_update);
assert!(
errors
.iter()
.any(|error| error.contains("graph frontier references missing node"))
);
let checkpoint_update = EpiphanyStateUpdate {
investigation_checkpoint: Some(EpiphanyInvestigationCheckpoint {
checkpoint_id: "ix-missing".to_string(),
kind: "source_gathering".to_string(),
focus: "Trace the compaction seam.".to_string(),
next_action: Some("Re-gather source before implementation.".to_string()),
evidence_ids: vec!["ev-missing".to_string()],
..Default::default()
}),
..Default::default()
};
let errors = epiphany_state_update_validation_errors(
&EpiphanyThreadState::default(),
&checkpoint_update,
);
assert!(
errors
.iter()
.any(|error| error.contains("investigation checkpoint cites missing evidence id"))
);
}
}
fn validate_epiphany_runtime_links(
existing: &[EpiphanyRuntimeLink],
links: &[EpiphanyRuntimeLink],

View file

@ -40,6 +40,7 @@ remembers doctrine; the branch remembers the blade.
- Agent self-memory review/application now has typed `AgentSelfPatch` core APIs. JSON-value selfPatch functions are compatibility wrappers for CLI/artifact ingress; native callers should parse once at the edge and then call `review_agent_self_patch_document` / `apply_agent_self_patch_document`.
- `epiphany-openai-codex-spine` no longer uses Codex's Responses client, model-provider layer, `codex-api`, `codex-model-provider`, `codex-model-provider-info`, or `codex-protocol` for outbound model requests. It reaches `codex-login` only through `epiphany-openai-auth-spine` so credential loading/refresh stays Codex-compatible while request/status/event/runtime surfaces stay typed Epiphany documents.
- Durable Epiphany state types now live in root crate `epiphany-state-model`. `codex-protocol` re-exports them only so old rollout/API callers keep compiling; do not add new durable `Epiphany*` state structs under `vendor/codex`. `epiphany-core` imports the native model directly and no longer depends on `codex-protocol`. Pressure derives from native `EpiphanyTokenUsageSnapshot`; Codex token telemetry is mapped at the bridge. Codex rollout/event reconstruction now lives in `codex-core::epiphany_rollout`, where the Codex `RolloutItem` / `EventMsg` types belong. Exact retrieval path matching is native to `epiphany-core`, so `epiphany-core` has no direct vendored Codex crate dependency.
- State-update contract proof now lives in native `epiphany-core`. The old giant `codex_thread.rs` Epiphany state-update test fixture was cut; vendored `CodexThread` may call the native mutation law for compatibility, but it must not regain ownership of state-update semantics or heartbeat launch-plan policy.
- The May 2026 foundation migration in `notes/epiphany-ideal-architecture-rebuild-plan.md` is complete enough to defend. Read `notes/epiphany-architectural-teardown.md` before touching Epiphany control-plane code, but do not treat it as an open-ended excuse to keep shaving the altar. The rebuild moved scene projection, freshness derivation, pressure policy, targeted context shards, bounded graph traversal, job/progress view derivation, planning view derivation, reorientation resume/regather verdict policy, pure CRRC recommendation policy, fixed-lane coordinator decision policy, role-board projection policy, role/reorient result interpretation, role self-persistence review policy, and role/reorient acceptance bundle policy into `epiphany-core`; typed acceptance receipts replaced summary-string identity for live accept paths, runtime links are dual-written on launch, and result read-back now prefers runtime links.
- Read `notes/epiphany-ideal-architecture-rebuild-plan.md` immediately after the teardown. It defines the smallest coherent replacement architecture: durable `EpiphanyState`, runtime-spine-owned `RuntimeState`, separate role memory/heartbeat state, `epiphany-core` as policy owner, app-server as adapter, Aquarium as reflector, typed acceptance receipts, and derived view lenses instead of a protocol verb zoo.
- Do not continue Aquarium UI, bridge, Face, or dogfood expansion until the teardown has a source-grounded cleanup slice plan. Epiphany is the foundation; patches on patches are not a purification rite, they are how the altar becomes load-bearing garbage.

Binary file not shown.

File diff suppressed because one or more lines are too long

View file

@ -20,24 +20,8 @@ use codex_protocol::models::ResponseInputItem;
use codex_protocol::models::ResponseItem;
use codex_protocol::openai_models::ReasoningEffort;
use codex_protocol::protocol::AskForApproval;
#[cfg(test)]
use codex_protocol::protocol::EpiphanyAcceptanceReceipt;
#[cfg(test)]
use codex_protocol::protocol::EpiphanyChurnState;
#[cfg(test)]
use codex_protocol::protocol::EpiphanyEvidenceRecord;
#[cfg(test)]
use codex_protocol::protocol::EpiphanyGraphFrontier;
#[cfg(test)]
use codex_protocol::protocol::EpiphanyGraphs;
#[cfg(test)]
use codex_protocol::protocol::EpiphanyInvestigationCheckpoint;
use codex_protocol::protocol::EpiphanyJobBinding;
use codex_protocol::protocol::EpiphanyJobKind;
#[cfg(test)]
use codex_protocol::protocol::EpiphanyObservation;
#[cfg(test)]
use codex_protocol::protocol::EpiphanyPlanningState;
use codex_protocol::protocol::EpiphanyRetrievalState;
use codex_protocol::protocol::EpiphanyStateItem;
use codex_protocol::protocol::EpiphanyThreadState;
@ -784,636 +768,6 @@ fn clear_epiphany_job_binding_backend(
bindings
}
#[cfg(test)]
mod epiphany_update_tests {
use super::*;
use codex_protocol::protocol::EpiphanyBacklogItem;
use codex_protocol::protocol::EpiphanyGraph;
use codex_protocol::protocol::EpiphanyGraphNode;
use codex_protocol::protocol::EpiphanyJobKind;
use codex_protocol::protocol::EpiphanyPlanningCapture;
use codex_protocol::protocol::EpiphanyPlanningPriority;
use codex_protocol::protocol::EpiphanyPlanningSourceRef;
fn evidence(id: &str) -> EpiphanyEvidenceRecord {
EpiphanyEvidenceRecord {
id: id.to_string(),
kind: "verification".to_string(),
status: "ok".to_string(),
summary: "Evidence summary".to_string(),
code_refs: Vec::new(),
}
}
fn observation(id: &str, evidence_ids: Vec<&str>) -> EpiphanyObservation {
EpiphanyObservation {
id: id.to_string(),
summary: "Observation summary".to_string(),
source_kind: "smoke".to_string(),
status: "ok".to_string(),
code_refs: Vec::new(),
evidence_ids: evidence_ids.into_iter().map(str::to_string).collect(),
}
}
fn job_binding(id: &str) -> EpiphanyJobBinding {
EpiphanyJobBinding {
id: id.to_string(),
kind: EpiphanyJobKind::Specialist,
scope: "role-scoped specialist work".to_string(),
owner_role: "epiphany-harness".to_string(),
authority_scope: Some("epiphany.specialist".to_string()),
linked_subgoal_ids: vec!["phase-6".to_string()],
linked_graph_node_ids: vec!["job-surface".to_string()],
blocking_reason: None,
}
}
fn acceptance_receipt(
id: &str,
result_id: &str,
evidence_id: &str,
) -> EpiphanyAcceptanceReceipt {
EpiphanyAcceptanceReceipt {
id: id.to_string(),
result_id: result_id.to_string(),
job_id: "runtime-job-1".to_string(),
binding_id: "modeling".to_string(),
surface: "roleAccept".to_string(),
role_id: "modeling".to_string(),
status: "accepted".to_string(),
accepted_at: "2026-05-12T00:00:00Z".to_string(),
accepted_observation_id: Some("obs-modeling".to_string()),
accepted_evidence_id: Some(evidence_id.to_string()),
summary: Some("Accepted modeling result.".to_string()),
}
}
#[test]
fn apply_epiphany_state_update_replaces_typed_fields_and_prepends_evidence() {
let mut state = EpiphanyThreadState {
revision: 3,
recent_evidence: vec![EpiphanyEvidenceRecord {
id: "old-evidence".to_string(),
kind: "research".to_string(),
status: "ok".to_string(),
summary: "Older finding".to_string(),
code_refs: Vec::new(),
}],
..Default::default()
};
apply_epiphany_state_update(
&mut state,
EpiphanyStateUpdate {
objective: Some("Keep the map honest".to_string()),
investigation_checkpoint: Some(EpiphanyInvestigationCheckpoint {
checkpoint_id: "ix-1".to_string(),
kind: "slice_planning".to_string(),
focus: "Keep the durable packet small and explicit.".to_string(),
next_action: Some(
"Resume from the packet instead of the ghost transcript.".to_string(),
),
..Default::default()
}),
evidence: vec![EpiphanyEvidenceRecord {
id: "new-evidence".to_string(),
kind: "verification".to_string(),
status: "ok".to_string(),
summary: "New finding".to_string(),
code_refs: Vec::new(),
}],
churn: Some(EpiphanyChurnState {
understanding_status: "grounded".to_string(),
diff_pressure: "low".to_string(),
..Default::default()
}),
..Default::default()
},
Some("turn-1".to_string()),
);
assert_eq!(state.revision, 4);
assert_eq!(state.objective.as_deref(), Some("Keep the map honest"));
assert_eq!(state.last_updated_turn_id.as_deref(), Some("turn-1"));
assert_eq!(
state
.investigation_checkpoint
.as_ref()
.map(|checkpoint| checkpoint.checkpoint_id.as_str()),
Some("ix-1")
);
assert_eq!(state.recent_evidence[0].id, "new-evidence");
assert_eq!(state.recent_evidence[1].id, "old-evidence");
assert_eq!(
state
.churn
.as_ref()
.map(|churn| churn.diff_pressure.as_str()),
Some("low")
);
}
#[test]
fn apply_epiphany_state_update_replaces_job_bindings() {
let mut state = EpiphanyThreadState {
revision: 2,
job_bindings: vec![job_binding("old")],
..Default::default()
};
apply_epiphany_state_update(
&mut state,
EpiphanyStateUpdate {
job_bindings: Some(vec![job_binding("new")]),
..Default::default()
},
Some("turn-jobs".to_string()),
);
assert_eq!(state.revision, 3);
assert_eq!(state.job_bindings.len(), 1);
assert_eq!(state.job_bindings[0].id, "new");
assert_eq!(
state.job_bindings[0].authority_scope.as_deref(),
Some("epiphany.specialist")
);
assert_eq!(state.last_updated_turn_id.as_deref(), Some("turn-jobs"));
}
#[test]
fn apply_epiphany_state_update_prepends_acceptance_receipts() {
let mut state = EpiphanyThreadState {
revision: 2,
recent_evidence: vec![evidence("ev-new")],
acceptance_receipts: vec![acceptance_receipt("accept-old", "result-old", "ev-old")],
..Default::default()
};
apply_epiphany_state_update(
&mut state,
EpiphanyStateUpdate {
acceptance_receipts: vec![acceptance_receipt("accept-new", "result-new", "ev-new")],
..Default::default()
},
Some("turn-accept".to_string()),
);
assert_eq!(state.revision, 3);
assert_eq!(state.acceptance_receipts[0].id, "accept-new");
assert_eq!(state.acceptance_receipts[1].id, "accept-old");
}
#[test]
fn validate_epiphany_state_update_accepts_planning_state() {
let update = EpiphanyStateUpdate {
planning: Some(EpiphanyPlanningState {
captures: vec![EpiphanyPlanningCapture {
id: "capture-github-42".to_string(),
title: "Import issue backlog".to_string(),
confidence: "medium".to_string(),
status: "new".to_string(),
source: EpiphanyPlanningSourceRef {
kind: "github_issue".to_string(),
provider: Some("github".to_string()),
repo: Some("GameCult/Epiphany".to_string()),
issue_number: Some(42),
..Default::default()
},
..Default::default()
}],
backlog_items: vec![EpiphanyBacklogItem {
id: "backlog-planning-api".to_string(),
title: "Expose planning projection".to_string(),
kind: "feature".to_string(),
summary: "Make planning state queryable by the GUI.".to_string(),
status: "ready".to_string(),
horizon: "now".to_string(),
priority: EpiphanyPlanningPriority {
value: "p1".to_string(),
rationale: "Unblocks planning operations.".to_string(),
..Default::default()
},
confidence: "high".to_string(),
product_area: "gui".to_string(),
lane_hints: vec!["imagination".to_string()],
..Default::default()
}],
roadmap_streams: vec![codex_protocol::protocol::EpiphanyRoadmapStream {
id: "stream-gui".to_string(),
title: "GUI Operator Surface".to_string(),
purpose: "Let the human inspect and steer planning.".to_string(),
status: "active".to_string(),
item_ids: vec!["backlog-planning-api".to_string()],
..Default::default()
}],
objective_drafts: vec![codex_protocol::protocol::EpiphanyObjectiveDraft {
id: "objdraft-planning-api".to_string(),
title: "Build planning API slice".to_string(),
summary: "Land typed planning state and read-only projection.".to_string(),
source_item_ids: vec!["backlog-planning-api".to_string()],
acceptance_criteria: vec!["Projection returns planning counts.".to_string()],
status: "draft".to_string(),
..Default::default()
}],
..Default::default()
}),
..Default::default()
};
assert!(
epiphany_state_update_validation_errors(&EpiphanyThreadState::default(), &update)
.is_empty()
);
}
#[test]
fn validate_epiphany_state_update_rejects_invalid_planning_state() {
let update = EpiphanyStateUpdate {
planning: Some(EpiphanyPlanningState {
captures: vec![EpiphanyPlanningCapture {
id: "capture-bad-github".to_string(),
title: "Missing repo issue".to_string(),
confidence: "medium".to_string(),
status: "new".to_string(),
source: EpiphanyPlanningSourceRef {
kind: "github_issue".to_string(),
issue_number: None,
..Default::default()
},
..Default::default()
}],
backlog_items: vec![EpiphanyBacklogItem {
id: "backlog-1".to_string(),
title: "Backlog without priority rationale".to_string(),
kind: "feature".to_string(),
summary: "Invalid on purpose.".to_string(),
status: "ready".to_string(),
horizon: "now".to_string(),
priority: EpiphanyPlanningPriority {
value: "p1".to_string(),
rationale: String::new(),
..Default::default()
},
confidence: "high".to_string(),
product_area: "gui".to_string(),
..Default::default()
}],
roadmap_streams: vec![codex_protocol::protocol::EpiphanyRoadmapStream {
id: "stream-gui".to_string(),
title: "GUI Operator Surface".to_string(),
purpose: "Let the human inspect and steer planning.".to_string(),
status: "active".to_string(),
item_ids: vec!["missing-backlog".to_string()],
..Default::default()
}],
objective_drafts: vec![codex_protocol::protocol::EpiphanyObjectiveDraft {
id: "objdraft-empty".to_string(),
title: "Empty acceptance draft".to_string(),
summary: "Invalid on purpose.".to_string(),
source_item_ids: vec!["missing-backlog".to_string()],
status: "draft".to_string(),
..Default::default()
}],
..Default::default()
}),
..Default::default()
};
let errors =
epiphany_state_update_validation_errors(&EpiphanyThreadState::default(), &update);
assert!(
errors
.iter()
.any(|error| error.contains("repo is required for github_issue"))
);
assert!(
errors
.iter()
.any(|error| error.contains("priority.rationale"))
);
assert!(
errors
.iter()
.any(|error| error.contains("references missing backlog item"))
);
assert!(
errors
.iter()
.any(|error| error.contains("must include at least one acceptance criterion"))
);
}
#[test]
fn validate_epiphany_state_update_accepts_observation_linked_to_existing_evidence() {
let state = EpiphanyThreadState {
recent_evidence: vec![evidence("ev-existing")],
..Default::default()
};
let update = EpiphanyStateUpdate {
observations: vec![observation("obs-new", vec!["ev-existing"])],
..Default::default()
};
assert!(epiphany_state_update_validation_errors(&state, &update).is_empty());
}
#[test]
fn validate_epiphany_state_update_rejects_observation_with_missing_evidence() {
let update = EpiphanyStateUpdate {
observations: vec![observation("obs-new", vec!["ev-missing"])],
..Default::default()
};
let errors =
epiphany_state_update_validation_errors(&EpiphanyThreadState::default(), &update);
assert!(
errors
.iter()
.any(|error| error.contains("cites missing evidence id"))
);
}
#[test]
fn validate_epiphany_state_update_rejects_empty_and_duplicate_append_ids() {
let state = EpiphanyThreadState {
observations: vec![observation("obs-existing", vec!["ev-existing"])],
recent_evidence: vec![evidence("ev-existing")],
..Default::default()
};
let update = EpiphanyStateUpdate {
observations: vec![
observation("obs-existing", vec!["ev-new"]),
observation("obs-dup", vec!["ev-new"]),
observation("obs-dup", vec!["ev-new"]),
EpiphanyObservation {
id: String::new(),
..observation("unused", vec!["ev-new"])
},
],
evidence: vec![
evidence("ev-existing"),
evidence("ev-new"),
evidence("ev-new"),
EpiphanyEvidenceRecord {
id: String::new(),
..evidence("unused")
},
],
..Default::default()
};
let errors = epiphany_state_update_validation_errors(&state, &update);
assert!(
errors
.iter()
.any(|error| error.contains("evidence id \"ev-existing\" already exists"))
);
assert!(
errors
.iter()
.any(|error| error.contains("duplicate evidence id \"ev-new\""))
);
assert!(
errors
.iter()
.any(|error| error.contains("observation id \"obs-existing\" already exists"))
);
assert!(
errors
.iter()
.any(|error| error.contains("duplicate observation id \"obs-dup\""))
);
assert!(
errors
.iter()
.any(|error| error.contains("patch.evidence.id must not be empty"))
);
assert!(
errors
.iter()
.any(|error| error.contains("patch.observations.id must not be empty"))
);
}
#[test]
fn validate_epiphany_state_update_rejects_duplicate_acceptance_result() {
let state = EpiphanyThreadState {
acceptance_receipts: vec![acceptance_receipt(
"accept-existing",
"result-existing",
"ev-existing",
)],
recent_evidence: vec![evidence("ev-existing"), evidence("ev-new")],
..Default::default()
};
let update = EpiphanyStateUpdate {
acceptance_receipts: vec![
acceptance_receipt("accept-new", "result-existing", "ev-new"),
acceptance_receipt("accept-new", "result-new", "ev-new"),
],
..Default::default()
};
let errors = epiphany_state_update_validation_errors(&state, &update);
assert!(
errors
.iter()
.any(|error| error.contains("runtime result \"result-existing\" already has"))
);
assert!(
errors
.iter()
.any(|error| error.contains("acceptance receipt id \"accept-new\" is duplicated"))
);
}
#[test]
fn validate_epiphany_state_update_rejects_structurally_invalid_replacements() {
let state = EpiphanyThreadState {
graphs: EpiphanyGraphs {
architecture: EpiphanyGraph {
nodes: vec![EpiphanyGraphNode {
id: "state".to_string(),
title: "State".to_string(),
purpose: "Carry the explicit map".to_string(),
..Default::default()
}],
edges: Vec::new(),
},
..Default::default()
},
..Default::default()
};
let update = EpiphanyStateUpdate {
graph_frontier: Some(EpiphanyGraphFrontier {
active_node_ids: vec!["missing".to_string()],
..Default::default()
}),
..Default::default()
};
let errors = epiphany_state_update_validation_errors(&state, &update);
assert!(
errors
.iter()
.any(|error| error.contains("graph frontier references missing node"))
);
}
#[test]
fn validate_epiphany_state_update_rejects_investigation_checkpoint_with_missing_evidence() {
let update = EpiphanyStateUpdate {
investigation_checkpoint: Some(EpiphanyInvestigationCheckpoint {
checkpoint_id: "ix-missing".to_string(),
kind: "source_gathering".to_string(),
focus: "Trace the compaction seam.".to_string(),
next_action: Some("Re-gather source before implementation.".to_string()),
evidence_ids: vec!["ev-missing".to_string()],
..Default::default()
}),
..Default::default()
};
let errors =
epiphany_state_update_validation_errors(&EpiphanyThreadState::default(), &update);
assert!(
errors
.iter()
.any(|error| error.contains("investigation checkpoint cites missing evidence id"))
);
}
#[test]
fn validate_epiphany_state_update_rejects_invalid_job_bindings() {
let update = EpiphanyStateUpdate {
job_bindings: Some(vec![
job_binding("dup"),
job_binding("dup"),
EpiphanyJobBinding {
id: String::new(),
kind: EpiphanyJobKind::Verification,
scope: String::new(),
owner_role: String::new(),
authority_scope: Some(String::new()),
linked_subgoal_ids: Vec::new(),
linked_graph_node_ids: Vec::new(),
blocking_reason: Some(String::new()),
},
]),
..Default::default()
};
let errors =
epiphany_state_update_validation_errors(&EpiphanyThreadState::default(), &update);
assert!(
errors
.iter()
.any(|error| error.contains("duplicate job binding id"))
);
assert!(
errors
.iter()
.any(|error| error.contains("job_binding.id must not be empty"))
);
assert!(
errors
.iter()
.any(|error| error.contains("job_binding.scope must not be empty"))
);
assert!(
errors
.iter()
.any(|error| error.contains("job_binding.owner_role must not be empty"))
);
assert!(
errors
.iter()
.any(|error| error.contains("job_binding.authority_scope must not be empty"))
);
assert!(
errors
.iter()
.any(|error| error.contains("job_binding.blocking_reason must not be empty"))
);
}
#[test]
fn plan_epiphany_job_launch_leaves_lifecycle_to_runtime_links() {
let request = EpiphanyJobLaunchRequest {
expected_revision: Some(7),
binding_id: "modeling-checkpoint-worker".to_string(),
kind: EpiphanyJobKind::Specialist,
scope: "role-scoped modeling/checkpoint maintenance".to_string(),
owner_role: "epiphany-modeler".to_string(),
authority_scope: "epiphany.role.modeling".to_string(),
linked_subgoal_ids: vec!["phase-6".to_string()],
linked_graph_node_ids: vec!["runtime-spine".to_string()],
instruction: "Model the target before implementation.".to_string(),
launch_document: EpiphanyWorkerLaunchDocument::Role(
epiphany_core::EpiphanyRoleWorkerLaunchDocument {
thread_id: "thread-1".to_string(),
role_id: "modeling".to_string(),
state_revision: 7,
objective: Some("keep state typed".to_string()),
active_subgoal_id: None,
active_subgoals: Vec::new(),
active_graph_node_ids: vec!["runtime-spine".to_string()],
investigation_checkpoint: None,
scratch: None,
invariants: Vec::new(),
graphs: None,
recent_evidence: Vec::new(),
recent_observations: Vec::new(),
graph_frontier: None,
graph_checkpoint: None,
planning: None,
churn: None,
},
),
output_contract_id: "epiphany.worker.role_result.v0".to_string(),
max_runtime_seconds: Some(60),
};
let launch_plan = plan_runtime_spine_heartbeat_launch(
&EpiphanyThreadState::default(),
RuntimeSpineHeartbeatLaunchPlanOptions {
binding_id: request.binding_id,
kind: request.kind,
scope: request.scope,
owner_role: request.owner_role,
authority_scope: request.authority_scope,
linked_subgoal_ids: request.linked_subgoal_ids,
linked_graph_node_ids: request.linked_graph_node_ids,
instruction: request.instruction,
launch_document: request.launch_document,
output_contract_id: request.output_contract_id,
max_runtime_seconds: request.max_runtime_seconds,
runtime_job_id: "turn-1".to_string(),
},
)
.expect("launch planning should build binding and runtime link");
assert_eq!(
launch_plan.binding.authority_scope.as_deref(),
Some("epiphany.role.modeling")
);
assert_eq!(
launch_plan.runtime_link.id,
"runtime-link-modeling-checkpoint-worker-turn-1"
);
assert_eq!(launch_plan.runtime_link.runtime_job_id, "turn-1");
assert_eq!(launch_plan.runtime_link.runtime_result_id, None);
assert_eq!(launch_plan.runtime_link.role_id, "epiphany-modeler");
}
}
fn pending_message_input_item(message: &ResponseItem) -> CodexResult<ResponseInputItem> {
match message {
ResponseItem::Message { role, content, .. } => Ok(ResponseInputItem::Message {