Move state update proof to core
This commit is contained in:
parent
fa222584e4
commit
b95b95c11d
6 changed files with 533 additions and 652 deletions
|
|
@ -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()?;
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
|
|
|
|||
|
|
@ -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
646
vendor/codex/codex-rs/core/src/codex_thread.rs
vendored
646
vendor/codex/codex-rs/core/src/codex_thread.rs
vendored
|
|
@ -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 {
|
||||
|
|
|
|||
Reference in a new issue