Skip to repository content744 lines · 30.1 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T05:03:53.289Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
thread_run_link.rs
1//! What a thread may say about the engine run it is linked to.
2//! `OMEGA-DELTA-0030`.
3//!
4//! omega#77 gave every thread an executor line, so a thread executed by an
5//! `omega-effectd` lane already names its run. Naming a run is not showing
6//! one: the reader is told a reference and left to go and look somewhere else
7//! for whether the work was verified, refused, or merely claimed. omega#80
8//! closes that — the linked run renders its state and its receipt chain in the
9//! thread, through the same inspector grammar the receipt pane uses.
10//!
11//! # The engine stays the sole run authority
12//!
13//! Nothing here holds run state. [`project_thread_run_link`] is a pure
14//! function of (the thread's disclosure record, the engine's own records, the
15//! clock), so the thread *projects* a run and can never become a second
16//! opinion about one. Three consequences, each of them checked below:
17//!
18//! - The reference comes from [`ExecutorDisclosure::run_ref`], the typed
19//! record omega#77 stores, not from a string a surface assembled.
20//! - The engine's answer expires. Past [`THREAD_RUN_LINK_MAX_AGE_MS`] the link
21//! renders `host_unavailable` instead of the last chain it saw, so a thread
22//! cannot outlive the authority it is projecting. A stale complete chain
23//! held on screen is precisely a panel entity becoming the source of truth.
24//! - A state Omega does not recognise is not translated into the nearest one.
25//! An acknowledgement is not a completion, and a run that says `acknowledged`
26//! reports no state at all rather than being read as finished.
27//!
28//! # A broken chain is shown, not hidden
29//!
30//! The chain is produced by
31//! [`workroom_receipts::project_issue31_evidence_pair`] — the same producer
32//! that builds the phone's projection, so the desktop and the phone cannot
33//! hold two opinions about one run. When it refuses, the refusal is rendered
34//! with its reason, never as an absence and never as completion. A single
35//! unshowable run must not blank the surface: one malformed record stopping
36//! every device is a bug this workspace has already shipped once.
37
38use serde_json::Value;
39use workroom_receipts::{
40 InspectorField, Issue31EvidenceChain, Issue31EvidenceUnavailableReason,
41 Issue31FullAutoLifecycle, PublicRef, project_issue31_evidence_pair,
42};
43
44use omega_front_door::{ExecutorClass, ExecutorDisclosure};
45
46/// How long the engine's answer about a run stays showable.
47///
48/// The panel re-reads every three seconds, so five missed reads is a host that
49/// has stopped answering rather than one that is briefly busy. Rendering the
50/// last known chain past that point would be the thread asserting a run state
51/// on the engine's behalf.
52pub const THREAD_RUN_LINK_MAX_AGE_MS: u64 = 15_000;
53
54/// One reading of the engine's records for a linked run.
55///
56/// Raw host answers plus the instant they were read. Deliberately not a
57/// projected view: caching the *conclusion* is what lets a surface disagree
58/// with the engine, whereas caching the engine's own words and re-deriving
59/// cannot.
60#[derive(Clone, Debug, Default, PartialEq, Eq)]
61pub struct ThreadRunRecords {
62 /// When the engine answered, on the same clock as `now_ms`.
63 pub read_at_ms: u64,
64 /// The `get_run` record, when the engine answered.
65 pub run: Option<Value>,
66 /// The `get_report` record, when the engine answered.
67 pub report: Option<Value>,
68 /// The `get_receipt` record, when the engine answered.
69 pub receipt: Option<Value>,
70}
71
72/// A thread's projection of the engine run that executed it.
73#[derive(Clone, Debug, PartialEq, Eq)]
74pub struct ThreadRunLink {
75 /// The run, named by the thread's own disclosure record.
76 pub run_ref: PublicRef,
77 /// The agent the run delegated the work to, kept from the disclosure.
78 pub agent_id: String,
79 /// The run's lifecycle, when the engine reported one this contract models.
80 ///
81 /// `None` says the engine has not told this surface where the run stands.
82 /// It is never a stand-in for "queued" or "running": showing a state the
83 /// host did not report is the failure the whole surface exists to avoid.
84 pub state: Option<Issue31FullAutoLifecycle>,
85 /// The receipt chain, complete or unavailable-with-a-reason.
86 pub chain: Issue31EvidenceChain,
87}
88
89impl ThreadRunLink {
90 /// Whether this run's completion is backed by a resolvable receipt chain.
91 ///
92 /// omega#80's falsifier is "a completed run is claimed without its receipt
93 /// refs resolvable". This is the predicate that makes such a claim
94 /// impossible to make by accident: a terminal state is not enough, and an
95 /// authority decision that refused is not a completion either.
96 #[must_use]
97 pub fn is_receipted(&self) -> bool {
98 matches!(
99 self.chain,
100 Issue31EvidenceChain::Complete {
101 authority_allowed: true,
102 ..
103 }
104 )
105 }
106
107 /// Why the chain is not showable, when it is not.
108 #[must_use]
109 pub fn unavailable_reason(&self) -> Option<Issue31EvidenceUnavailableReason> {
110 match self.chain {
111 Issue31EvidenceChain::Unavailable { reason, .. } => Some(reason),
112 Issue31EvidenceChain::Complete { .. } => None,
113 }
114 }
115
116 /// The inspector rows this link renders.
117 ///
118 /// Same label/value grammar as `workroom_receipts::render_receipt_detail`,
119 /// so the thread's chain reads like the receipt pane's rather than like a
120 /// second format. The unavailable case still produces rows — `chain:
121 /// unavailable` and `chain_reason: …` — because a surface that renders
122 /// nothing has told the reader nothing, and silence reads as "no run".
123 #[must_use]
124 pub fn fields(&self) -> Vec<InspectorField> {
125 let mut fields = vec![
126 InspectorField::new("run_ref", self.run_ref.as_str()),
127 InspectorField::new("executed_by", self.agent_id.as_str()),
128 InspectorField::new(
129 "run_state",
130 match self.state {
131 Some(state) => lifecycle_token(state),
132 None => "not reported by the engine",
133 },
134 ),
135 ];
136 match &self.chain {
137 Issue31EvidenceChain::Complete {
138 authority_allowed,
139 hops,
140 ..
141 } => {
142 fields.push(InspectorField::new("chain", "complete"));
143 fields.push(InspectorField::new(
144 "authority_allowed",
145 if *authority_allowed { "true" } else { "false" },
146 ));
147 for hop in hops {
148 fields.push(InspectorField::new(
149 hop.kind.token(),
150 match &hop.detail {
151 Some(detail) => format!("{} · {detail}", hop.reference.as_str()),
152 None => hop.reference.as_str().to_owned(),
153 },
154 ));
155 }
156 }
157 Issue31EvidenceChain::Unavailable {
158 reason, broken_at, ..
159 } => {
160 fields.push(InspectorField::new("chain", "unavailable"));
161 fields.push(InspectorField::new("chain_reason", reason.token()));
162 if let Some(broken_at) = broken_at {
163 fields.push(InspectorField::new("chain_broken_at", broken_at.token()));
164 }
165 }
166 }
167 fields
168 }
169}
170
171/// The wire token for a lifecycle, for rendering.
172fn lifecycle_token(state: Issue31FullAutoLifecycle) -> &'static str {
173 match state {
174 Issue31FullAutoLifecycle::Queued => "queued",
175 Issue31FullAutoLifecycle::Running => "running",
176 Issue31FullAutoLifecycle::Pausing => "pausing",
177 Issue31FullAutoLifecycle::Paused => "paused",
178 Issue31FullAutoLifecycle::Stopping => "stopping",
179 Issue31FullAutoLifecycle::Retrying => "retrying",
180 Issue31FullAutoLifecycle::Stalled => "stalled",
181 Issue31FullAutoLifecycle::Succeeded => "succeeded",
182 Issue31FullAutoLifecycle::Failed => "failed",
183 Issue31FullAutoLifecycle::Stopped => "stopped",
184 Issue31FullAutoLifecycle::Expired => "expired",
185 }
186}
187
188fn lifecycle_from_host(state: &str) -> Option<Issue31FullAutoLifecycle> {
189 let token = crate::issue31_adjunct::lifecycle_for_host_state(state)?;
190 serde_json::from_value(Value::String(token.to_owned())).ok()
191}
192
193fn record_run_ref(record: &Value) -> Option<&str> {
194 record.get("runRef")?.as_str()
195}
196
197/// Project a thread's linked run from the engine's own records.
198///
199/// Returns `None` when the thread is not engine-lane work — a native or
200/// external ACP thread has no run authority to project, and inventing an empty
201/// run panel for it would suggest one exists.
202///
203/// An incoherent disclosure also returns `None`. A `native_loop` record
204/// carrying a run reference is a routed result wearing the wrong name, and
205/// `OMEGA-AGENT-AC-05` exists to stop exactly that from being rendered as a
206/// run this thread owns.
207#[must_use]
208pub fn project_thread_run_link(
209 disclosure: &ExecutorDisclosure,
210 records: Option<&ThreadRunRecords>,
211 now_ms: u64,
212) -> Option<ThreadRunLink> {
213 if disclosure.class != ExecutorClass::EngineLane || !disclosure.is_coherent() {
214 return None;
215 }
216 // The reference is the one the typed disclosure record holds. Nothing here
217 // parses it back out of the rendered line.
218 let run_ref = PublicRef::new(disclosure.run_ref.as_ref()?)?;
219
220 let fresh = records
221 .filter(|records| now_ms.saturating_sub(records.read_at_ms) <= THREAD_RUN_LINK_MAX_AGE_MS);
222
223 // A stale or absent reading is the host not answering, whatever it said
224 // last time. `state` follows the chain into `None` rather than surviving
225 // it: half a stale reading is still a stale reading.
226 let Some(records) = fresh else {
227 return Some(ThreadRunLink {
228 run_ref: run_ref.clone(),
229 agent_id: disclosure.agent_id.clone(),
230 state: None,
231 chain: Issue31EvidenceChain::Unavailable {
232 run_ref,
233 reason: Issue31EvidenceUnavailableReason::HostUnavailable,
234 broken_at: None,
235 },
236 });
237 };
238
239 let state = records
240 .run
241 .as_ref()
242 .filter(|run| record_run_ref(run) == Some(run_ref.as_str()))
243 .and_then(|run| run.get("state"))
244 .and_then(Value::as_str)
245 .and_then(lifecycle_from_host);
246
247 let chain = project_chain(&run_ref, records);
248
249 Some(ThreadRunLink {
250 run_ref,
251 agent_id: disclosure.agent_id.clone(),
252 state,
253 chain,
254 })
255}
256
257fn project_chain(run_ref: &PublicRef, records: &ThreadRunRecords) -> Issue31EvidenceChain {
258 let unavailable = |reason| Issue31EvidenceChain::Unavailable {
259 run_ref: run_ref.clone(),
260 reason,
261 broken_at: None,
262 };
263
264 let (Some(report), Some(receipt)) = (records.report.as_ref(), records.receipt.as_ref()) else {
265 return unavailable(Issue31EvidenceUnavailableReason::HostUnavailable);
266 };
267
268 // A pair belonging to another run is not this run's proof. Checked before
269 // the producer, because the producer is entitled to assume it was handed
270 // one run's records and would otherwise report a foreign but internally
271 // consistent chain as this thread's.
272 if record_run_ref(report) != Some(run_ref.as_str())
273 || record_run_ref(receipt) != Some(run_ref.as_str())
274 {
275 return unavailable(Issue31EvidenceUnavailableReason::HopMismatched);
276 }
277
278 match project_issue31_evidence_pair(report, receipt, Some(run_ref.as_str())) {
279 Ok(chain) if chain.run_ref() == run_ref => chain,
280 // The producer refuses to relabel a complete chain, so a chain naming
281 // another run reaching here means the records contradict themselves.
282 Ok(_) => unavailable(Issue31EvidenceUnavailableReason::HopMismatched),
283 // The two reachable decoder errors are both "the host produced this and
284 // this surface may not carry it": a hop reference outside the
285 // public-reference character class, and a detail past the carried
286 // bound. `hop_private` is the reason for exactly that situation.
287 Err(_) => unavailable(Issue31EvidenceUnavailableReason::HopPrivate),
288 }
289}
290
291#[cfg(test)]
292mod tests {
293 use super::*;
294 use serde_json::json;
295 use workroom_receipts::{ISSUE31_EVIDENCE_HOPS, Issue31EvidenceHopKind};
296
297 const NOW: u64 = 1_784_894_400_000;
298
299 fn engine_lane() -> ExecutorDisclosure {
300 ExecutorDisclosure {
301 class: ExecutorClass::EngineLane,
302 agent_id: "codex-acp".into(),
303 provider: None,
304 model: None,
305 run_ref: Some("run.fa.80".into()),
306 // A thread restored from before the router landed carries no
307 // recorded route. omega#78 says that is "not routed", not a route,
308 // and it must not stop the run behind it from being inspectable.
309 route: None,
310 }
311 }
312
313 fn run_record(state: &str) -> Value {
314 json!({
315 "runRef": "run.fa.80",
316 "title": "Receipts in thread",
317 "state": state,
318 })
319 }
320
321 fn evidence_pair() -> (Value, Value) {
322 (
323 json!({
324 "runRef": "run.fa.80",
325 "evidence": {
326 "objectiveRef": "objective.fa.80",
327 "turnRef": "turn.fa.80.11",
328 "changeRef": "change.fa.80.11",
329 "projectGeneration": "project.generation.9",
330 "diffSummary": "3 files changed, 210 insertions",
331 "testCommand": "cargo test -p full_auto_ui",
332 "testOutcome": "passed",
333 "verificationRef": "verification.host.fa.80.11",
334 "hostExecuted": true
335 }
336 }),
337 json!({
338 "runRef": "run.fa.80",
339 "objectiveRef": "objective.fa.80",
340 "turnRef": "turn.fa.80.11",
341 "changeRef": "change.fa.80.11",
342 "verificationRef": "verification.host.fa.80.11",
343 "decisionRef": "decision.authority.fa.80.11",
344 "authorityReceiptRef": "receipt.authority.fa.80.11",
345 "allowed": true
346 }),
347 )
348 }
349
350 fn records(state: &str) -> ThreadRunRecords {
351 let (report, receipt) = evidence_pair();
352 ThreadRunRecords {
353 read_at_ms: NOW,
354 run: Some(run_record(state)),
355 report: Some(report),
356 receipt: Some(receipt),
357 }
358 }
359
360 fn link(records: Option<&ThreadRunRecords>, now_ms: u64) -> ThreadRunLink {
361 project_thread_run_link(&engine_lane(), records, now_ms).expect("an engine lane links")
362 }
363
364 #[test]
365 fn a_finished_run_renders_its_chain_in_the_normative_order() {
366 let records = records("completed");
367 let link = link(Some(&records), NOW);
368 assert_eq!(link.state, Some(Issue31FullAutoLifecycle::Succeeded));
369 assert!(link.is_receipted());
370
371 match &link.chain {
372 Issue31EvidenceChain::Complete { hops, .. } => {
373 let kinds: Vec<Issue31EvidenceHopKind> = hops.iter().map(|hop| hop.kind).collect();
374 assert_eq!(kinds, ISSUE31_EVIDENCE_HOPS.to_vec());
375 }
376 Issue31EvidenceChain::Unavailable { .. } => panic!("expected a complete chain"),
377 }
378
379 let lines: Vec<String> = link.fields().iter().map(InspectorField::line).collect();
380 let text = lines.join("\n");
381 assert!(text.contains("run_ref: run.fa.80"), "{text}");
382 assert!(text.contains("executed_by: codex-acp"), "{text}");
383 assert!(text.contains("run_state: succeeded"), "{text}");
384 assert!(text.contains("chain: complete"), "{text}");
385 assert!(text.contains("authority_allowed: true"), "{text}");
386 assert!(
387 text.contains("host_verification: verification.host.fa.80.11"),
388 "{text}"
389 );
390 assert!(
391 text.contains("receipt: receipt.authority.fa.80.11"),
392 "{text}"
393 );
394 assert!(
395 text.contains("test: verification.host.fa.80.11 · cargo test -p full_auto_ui"),
396 "the hop detail is the command the host ran: {text}"
397 );
398 assert!(!text.contains("/Users/"), "{text}");
399 }
400
401 /// Only engine-lane work has a run to project.
402 #[test]
403 fn a_thread_with_no_run_authority_has_no_run_link() {
404 for class in [ExecutorClass::NativeLoop, ExecutorClass::ExternalAcp] {
405 let disclosure = ExecutorDisclosure {
406 class,
407 run_ref: None,
408 ..engine_lane()
409 };
410 assert!(disclosure.is_coherent());
411 assert!(
412 project_thread_run_link(&disclosure, Some(&records("running")), NOW).is_none(),
413 "{class:?} threads own no run"
414 );
415 }
416 }
417
418 /// A routed result wearing the first-party name projects nothing.
419 ///
420 /// The incoherent record is the one omega#77 already refuses to call
421 /// coherent; this proves the run surface refuses it too, rather than
422 /// rendering a run panel on a thread that never had run authority.
423 #[test]
424 fn an_incoherent_disclosure_never_becomes_a_run_link() {
425 let native_claiming_a_run = ExecutorDisclosure {
426 class: ExecutorClass::NativeLoop,
427 run_ref: Some("run.fa.80".into()),
428 ..engine_lane()
429 };
430 assert!(!native_claiming_a_run.is_coherent());
431 assert!(
432 project_thread_run_link(&native_claiming_a_run, Some(&records("running")), NOW)
433 .is_none()
434 );
435
436 let engine_without_a_run = ExecutorDisclosure {
437 run_ref: None,
438 ..engine_lane()
439 };
440 assert!(!engine_without_a_run.is_coherent());
441 assert!(
442 project_thread_run_link(&engine_without_a_run, Some(&records("running")), NOW)
443 .is_none()
444 );
445
446 // omega#78: a fallback route means the router could not place the
447 // turn and it ran on the native loop. A record claiming both a
448 // fallback and an engine lane is two stories about one thread.
449 let fallback_claiming_a_lane = ExecutorDisclosure {
450 route: Some(omega_front_door::RouteReason::EngineUnreachable),
451 ..engine_lane()
452 };
453 assert!(!fallback_claiming_a_lane.is_coherent());
454 assert!(
455 project_thread_run_link(&fallback_claiming_a_lane, Some(&records("running")), NOW)
456 .is_none()
457 );
458 }
459
460 /// A run reference this surface may not repeat is not repeated.
461 #[test]
462 fn a_private_run_reference_is_never_rendered() {
463 let disclosure = ExecutorDisclosure {
464 run_ref: Some("/Users/owner/.omega/runs/80".into()),
465 ..engine_lane()
466 };
467 assert!(disclosure.is_coherent());
468 assert!(project_thread_run_link(&disclosure, Some(&records("running")), NOW).is_none());
469 }
470
471 /// The engine's silence is rendered as silence, not as its last answer.
472 ///
473 /// This is the "engine is the sole run authority" property with teeth: a
474 /// complete chain seen once must not stay on screen after the host stops
475 /// answering, or the thread has become the authority for it.
476 #[test]
477 fn a_stale_reading_stops_being_shown_as_the_runs_state() {
478 let records = records("completed");
479 assert!(link(Some(&records), NOW + THREAD_RUN_LINK_MAX_AGE_MS).is_receipted());
480
481 let stale = link(Some(&records), NOW + THREAD_RUN_LINK_MAX_AGE_MS + 1);
482 assert!(!stale.is_receipted());
483 assert_eq!(stale.state, None);
484 assert_eq!(
485 stale.unavailable_reason(),
486 Some(Issue31EvidenceUnavailableReason::HostUnavailable)
487 );
488 // The reference still shows: the thread knows which run it belongs to
489 // even when the engine is not answering about it.
490 assert_eq!(stale.run_ref.as_str(), "run.fa.80");
491 }
492
493 /// A host that has not answered at all is named as such.
494 #[test]
495 fn an_unanswered_host_renders_a_reason_rather_than_an_absence() {
496 let never_read = link(None, NOW);
497 assert_eq!(
498 never_read.unavailable_reason(),
499 Some(Issue31EvidenceUnavailableReason::HostUnavailable)
500 );
501 assert_eq!(never_read.state, None);
502
503 let no_receipt = ThreadRunRecords {
504 receipt: None,
505 ..records("running")
506 };
507 let partial = link(Some(&no_receipt), NOW);
508 assert_eq!(
509 partial.unavailable_reason(),
510 Some(Issue31EvidenceUnavailableReason::HostUnavailable)
511 );
512 // The half the host did answer is still shown. A missing receipt does
513 // not blank the run's state.
514 assert_eq!(partial.state, Some(Issue31FullAutoLifecycle::Running));
515 }
516
517 /// Every refusal path, watched refusing, and none of them silent.
518 #[test]
519 fn every_broken_chain_is_shown_with_its_reason() {
520 let reason = |mutate: &dyn Fn(&mut Value, &mut Value)| {
521 let (mut report, mut receipt) = evidence_pair();
522 mutate(&mut report, &mut receipt);
523 let records = ThreadRunRecords {
524 read_at_ms: NOW,
525 run: Some(run_record("completed")),
526 report: Some(report),
527 receipt: Some(receipt),
528 };
529 let link = link(Some(&records), NOW);
530 assert!(
531 !link.is_receipted(),
532 "a broken chain must never be claimed as receipted"
533 );
534 assert!(
535 !link.fields().is_empty(),
536 "a broken chain must still render rows"
537 );
538 link.unavailable_reason().expect("a named refusal")
539 };
540
541 assert_eq!(
542 reason(&|report, _| {
543 report["evidence"]
544 .as_object_mut()
545 .expect("evidence object")
546 .remove("projectGeneration");
547 }),
548 Issue31EvidenceUnavailableReason::HopMissing
549 );
550 assert_eq!(
551 reason(&|_, receipt| receipt["changeRef"] = json!("change.someone-elses-work")),
552 Issue31EvidenceUnavailableReason::HopMismatched
553 );
554 assert_eq!(
555 reason(&|report, _| {
556 report["evidence"]["testCommand"] = json!("cat /Users/owner/.codex/auth.json");
557 }),
558 Issue31EvidenceUnavailableReason::HopPrivate
559 );
560 assert_eq!(
561 reason(&|report, _| report["evidence"]["hostExecuted"] = json!(false)),
562 Issue31EvidenceUnavailableReason::SelfReported
563 );
564 // A chain the host holds but this surface's bound refuses to carry.
565 assert_eq!(
566 reason(&|report, _| {
567 report["evidence"]["testCommand"] =
568 json!(format!("cargo test -p {}", "x".repeat(300)));
569 }),
570 Issue31EvidenceUnavailableReason::HopPrivate
571 );
572 }
573
574 /// Another run's proof is not this run's proof.
575 ///
576 /// Both shapes are checked. A *complete* foreign chain is caught because
577 /// the producer refuses to relabel it. A *broken* foreign chain is not:
578 /// its refusal would arrive already wearing this run's reference, and
579 /// without the pair check the reader would be told this run has a missing
580 /// hop when in fact they are looking at somebody else's records.
581 #[test]
582 fn a_chain_belonging_to_another_run_is_refused_as_mismatched() {
583 let foreign = |mutate: &dyn Fn(&mut Value, &mut Value)| {
584 let (mut report, mut receipt) = evidence_pair();
585 report["runRef"] = json!("run.fa.81");
586 receipt["runRef"] = json!("run.fa.81");
587 mutate(&mut report, &mut receipt);
588 let records = ThreadRunRecords {
589 read_at_ms: NOW,
590 run: Some(run_record("completed")),
591 report: Some(report),
592 receipt: Some(receipt),
593 };
594 let link = link(Some(&records), NOW);
595 assert!(!link.is_receipted());
596 assert_eq!(link.run_ref.as_str(), "run.fa.80");
597 link.unavailable_reason()
598 };
599
600 assert_eq!(
601 foreign(&|_, _| {}),
602 Some(Issue31EvidenceUnavailableReason::HopMismatched),
603 "a complete chain proving another run is not this run's proof"
604 );
605 assert_eq!(
606 foreign(&|report, _| {
607 report["evidence"]
608 .as_object_mut()
609 .expect("evidence object")
610 .remove("turnRef");
611 }),
612 Some(Issue31EvidenceUnavailableReason::HopMismatched),
613 "another run's broken records are a mismatch, not this run's \
614 missing hop"
615 );
616 }
617
618 /// A run record about a different run tells this thread nothing.
619 #[test]
620 fn a_state_from_another_runs_record_is_not_this_runs_state() {
621 let records = ThreadRunRecords {
622 run: Some(json!({"runRef": "run.fa.81", "state": "completed"})),
623 ..records("running")
624 };
625 assert_eq!(link(Some(&records), NOW).state, None);
626 }
627
628 /// An acknowledgement is not a completed command.
629 ///
630 /// A relay or a UI can report that it accepted a request. That is a
631 /// statement about a message, not about work, and it must not be
632 /// translated into the nearest lifecycle this contract does model.
633 #[test]
634 fn an_acknowledgement_is_never_read_as_a_run_state() {
635 for claimed in [
636 "acknowledged",
637 "accepted",
638 "sent",
639 "dispatched",
640 "delivered",
641 "ok",
642 "done",
643 "complete",
644 ] {
645 let records = ThreadRunRecords {
646 run: Some(json!({
647 "runRef": "run.fa.80",
648 "state": claimed,
649 "acknowledged": true,
650 "acknowledgedAtMs": NOW,
651 })),
652 ..records("running")
653 };
654 assert_eq!(
655 link(Some(&records), NOW).state,
656 None,
657 "{claimed:?} is not a lifecycle this contract models, and \
658 guessing the nearest one is how a stalled run reads as done"
659 );
660 }
661 }
662
663 /// A terminal state is not a receipt.
664 ///
665 /// omega#80's falsifier: "a completed run is claimed without its receipt
666 /// refs resolvable". A succeeded run whose chain is unavailable renders
667 /// both facts, and `is_receipted` stays false.
668 #[test]
669 fn a_succeeded_run_without_a_chain_is_not_claimed_as_receipted() {
670 let records = ThreadRunRecords {
671 report: None,
672 receipt: None,
673 ..records("completed")
674 };
675 let link = link(Some(&records), NOW);
676 assert_eq!(link.state, Some(Issue31FullAutoLifecycle::Succeeded));
677 assert!(!link.is_receipted());
678
679 let text: Vec<String> = link.fields().iter().map(InspectorField::line).collect();
680 let text = text.join("\n");
681 assert!(text.contains("run_state: succeeded"), "{text}");
682 assert!(text.contains("chain: unavailable"), "{text}");
683 assert!(text.contains("chain_reason: host_unavailable"), "{text}");
684 }
685
686 /// A complete chain whose authority decision refused is not a receipt.
687 ///
688 /// The chain resolves — every hop is there and host-verified — and the
689 /// answer at the end is "no". Reading that as a receipted completion would
690 /// turn a refusal into an approval, which is the most consequential
691 /// misreading available on this surface.
692 #[test]
693 fn a_refused_authority_decision_is_complete_but_not_receipted() {
694 let (report, mut receipt) = evidence_pair();
695 receipt["allowed"] = json!(false);
696 let records = ThreadRunRecords {
697 read_at_ms: NOW,
698 run: Some(run_record("completed")),
699 report: Some(report),
700 receipt: Some(receipt),
701 };
702 let link = link(Some(&records), NOW);
703 assert!(matches!(link.chain, Issue31EvidenceChain::Complete { .. }));
704 assert!(link.unavailable_reason().is_none());
705 assert!(
706 !link.is_receipted(),
707 "an allowed:false decision is a resolvable chain and a refusal"
708 );
709
710 let text: Vec<String> = link.fields().iter().map(InspectorField::line).collect();
711 assert!(
712 text.join("\n").contains("authority_allowed: false"),
713 "{text:?}"
714 );
715 }
716
717 /// The link is a function of its inputs, which is what keeps the engine
718 /// the authority.
719 ///
720 /// Two projections from one reading are identical, and the record holds no
721 /// field a caller could set to change what a run appears to be doing.
722 #[test]
723 fn the_link_is_a_projection_and_holds_no_authority_of_its_own() {
724 let records = records("running");
725 assert_eq!(link(Some(&records), NOW), link(Some(&records), NOW));
726
727 let dumped = format!("{:?}", link(Some(&records), NOW));
728 for absent in ["cached", "override", "assumed", "last_known"] {
729 assert!(!dumped.contains(absent), "{dumped}");
730 }
731
732 // Changing only the engine's record changes the projection. Nothing in
733 // between remembers the old answer.
734 let stopped = ThreadRunRecords {
735 run: Some(run_record("stopped")),
736 ..records
737 };
738 assert_eq!(
739 link(Some(&stopped), NOW).state,
740 Some(Issue31FullAutoLifecycle::Stopped)
741 );
742 }
743}
744