Skip to repository content733 lines · 24.4 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T02:37:22.594Z 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
projections.rs
1//! Workroom projection models (`OMEGA-SW-03` / `OMEGA-SW-06` / MVP §7).
2//!
3//! GPUI holds no durable state. These types are in-memory projections rebuilt
4//! from omega-effectd events and snapshots. Every row carries source,
5//! freshness, and gap labels. A missing source stays visible and honest.
6//! Pending never renders as applied.
7//!
8//! Room attention (unread + marker) is derived from the transcript plus a
9//! local read marker — see [`crate::attention`].
10
11/// Capacity bound for transcript rows in the pane (virtualization deferred).
12pub const MAX_TRANSCRIPT_ROWS: usize = 200;
13/// Capacity bound for activity ladder rows in the pane.
14pub const MAX_ACTIVITY_ROWS: usize = 100;
15/// Capacity bound for receipt stub rows in the pane.
16pub const MAX_RECEIPT_ROWS: usize = 50;
17
18/// Canonical pane header. The workroom must not rebrand this string.
19pub const PANE_HEADER: &str = "Sarah";
20
21/// Named projection sources from the workroom record model (§7).
22pub mod sources {
23 pub const ROOM: &str = "/api/mobile/sarah";
24 pub const TRANSCRIPT: &str = "Khala Sync chat messages";
25 pub const ACTIVITY: &str = "Khala Sync runtime events";
26 pub const RECEIPTS: &str = "tool event authority blocks";
27 pub const RUN_STATE: &str = "turn.* events";
28 pub const EFFECTD: &str = "omega-effectd";
29}
30
31#[derive(Clone, Copy, Debug, PartialEq, Eq)]
32pub enum Freshness {
33 /// No observation yet.
34 Unknown,
35 /// Last observation is current for the active subscription generation.
36 Fresh,
37 /// Last observation is known but may lag the record.
38 Stale,
39 /// Source has never been available this session.
40 Missing,
41}
42
43impl Freshness {
44 pub fn label(self) -> &'static str {
45 match self {
46 Self::Unknown => "unknown",
47 Self::Fresh => "fresh",
48 Self::Stale => "stale",
49 Self::Missing => "missing",
50 }
51 }
52}
53
54#[derive(Clone, Copy, Debug, PartialEq, Eq)]
55pub enum GapState {
56 /// No gap reported.
57 None,
58 /// Page or event stream has a known gap.
59 Gap,
60 /// Source is unavailable; do not render as empty success.
61 Unavailable,
62 /// Intent or mutation is pending; never treat as applied.
63 Pending,
64}
65
66impl GapState {
67 pub fn label(self) -> &'static str {
68 match self {
69 Self::None => "none",
70 Self::Gap => "gap",
71 Self::Unavailable => "unavailable",
72 Self::Pending => "pending",
73 }
74 }
75
76 pub fn is_applied_looking(self) -> bool {
77 matches!(self, Self::None)
78 }
79}
80
81#[derive(Clone, Debug, PartialEq, Eq)]
82pub struct ProjectionMeta {
83 pub source: String,
84 pub freshness: Freshness,
85 pub gap: GapState,
86}
87
88impl ProjectionMeta {
89 pub fn missing(source: &str) -> Self {
90 Self {
91 source: source.to_string(),
92 freshness: Freshness::Missing,
93 gap: GapState::Unavailable,
94 }
95 }
96
97 pub fn unavailable(source: &str, reason: impl Into<String>) -> Self {
98 let _ = reason;
99 Self {
100 source: source.to_string(),
101 freshness: Freshness::Missing,
102 gap: GapState::Unavailable,
103 }
104 }
105
106 pub fn fresh(source: &str) -> Self {
107 Self {
108 source: source.to_string(),
109 freshness: Freshness::Fresh,
110 gap: GapState::None,
111 }
112 }
113
114 pub fn pending(source: &str) -> Self {
115 Self {
116 source: source.to_string(),
117 freshness: Freshness::Stale,
118 gap: GapState::Pending,
119 }
120 }
121
122 pub fn summary_line(&self) -> String {
123 format!(
124 "source={} · freshness={} · gap={}",
125 self.source,
126 self.freshness.label(),
127 self.gap.label()
128 )
129 }
130}
131
132#[derive(Clone, Debug, PartialEq, Eq)]
133pub struct RoomProjection {
134 pub meta: ProjectionMeta,
135 pub principal_ref: Option<String>,
136 pub display_name: Option<String>,
137 pub role: Option<String>,
138 pub thread_ref: Option<String>,
139 pub authority_profile: Option<String>,
140 pub authority_revision: Option<String>,
141 pub detail: Option<String>,
142}
143
144impl RoomProjection {
145 pub fn honest_empty() -> Self {
146 Self {
147 meta: ProjectionMeta::missing(sources::ROOM),
148 principal_ref: None,
149 display_name: None,
150 role: None,
151 thread_ref: None,
152 authority_profile: None,
153 authority_revision: None,
154 detail: Some("Room source is unavailable. Not an empty room.".into()),
155 }
156 }
157
158 pub fn is_honest_missing(&self) -> bool {
159 self.meta.freshness == Freshness::Missing
160 && self.meta.gap == GapState::Unavailable
161 && self.principal_ref.is_none()
162 && self.thread_ref.is_none()
163 }
164}
165
166#[derive(Clone, Copy, Debug, PartialEq, Eq)]
167pub enum MessageAck {
168 /// Local optimistic row; not yet on the durable record.
169 Pending,
170 /// Durable record confirmed this message.
171 Confirmed,
172}
173
174impl MessageAck {
175 pub fn label(self) -> &'static str {
176 match self {
177 Self::Pending => "pending",
178 Self::Confirmed => "confirmed",
179 }
180 }
181
182 /// Pending must never render as the confirmed/applied class.
183 pub fn renders_as_applied(self) -> bool {
184 matches!(self, Self::Confirmed)
185 }
186}
187
188#[derive(Clone, Debug, PartialEq, Eq)]
189pub struct TranscriptRow {
190 pub message_ref: String,
191 pub role: String,
192 pub text: String,
193 pub ack: MessageAck,
194}
195
196#[derive(Clone, Debug, PartialEq, Eq)]
197pub struct TranscriptProjection {
198 pub meta: ProjectionMeta,
199 pub rows: Vec<TranscriptRow>,
200 pub cursor: Option<String>,
201 pub truncated: bool,
202}
203
204impl TranscriptProjection {
205 pub fn honest_empty() -> Self {
206 Self {
207 meta: ProjectionMeta::missing(sources::TRANSCRIPT),
208 rows: Vec::new(),
209 cursor: None,
210 truncated: false,
211 }
212 }
213
214 pub fn push_bounded(&mut self, row: TranscriptRow) {
215 self.rows.push(row);
216 if self.rows.len() > MAX_TRANSCRIPT_ROWS {
217 let drop = self.rows.len() - MAX_TRANSCRIPT_ROWS;
218 self.rows.drain(0..drop);
219 self.truncated = true;
220 }
221 }
222
223 pub fn pending_count(&self) -> usize {
224 self.rows
225 .iter()
226 .filter(|row| row.ack == MessageAck::Pending)
227 .count()
228 }
229
230 pub fn no_pending_renders_as_applied(&self) -> bool {
231 self.rows
232 .iter()
233 .all(|row| row.ack != MessageAck::Pending || !row.ack.renders_as_applied())
234 }
235}
236
237#[derive(Clone, Debug, PartialEq, Eq)]
238pub struct ActivityRow {
239 pub event_ref: String,
240 pub kind: String,
241 pub summary: String,
242 pub turn_ref: Option<String>,
243}
244
245#[derive(Clone, Debug, PartialEq, Eq)]
246pub struct ActivityProjection {
247 pub meta: ProjectionMeta,
248 pub rows: Vec<ActivityRow>,
249 pub cursor: Option<String>,
250 pub truncated: bool,
251}
252
253impl ActivityProjection {
254 pub fn honest_empty() -> Self {
255 Self {
256 meta: ProjectionMeta::missing(sources::ACTIVITY),
257 rows: Vec::new(),
258 cursor: None,
259 truncated: false,
260 }
261 }
262
263 pub fn push_bounded(&mut self, row: ActivityRow) {
264 self.rows.push(row);
265 if self.rows.len() > MAX_ACTIVITY_ROWS {
266 let drop = self.rows.len() - MAX_ACTIVITY_ROWS;
267 self.rows.drain(0..drop);
268 self.truncated = true;
269 }
270 }
271}
272
273/// Stub receipt projection for OMEGA-SW-03. Deep inspector is OMEGA-SW-05.
274#[derive(Clone, Debug, PartialEq, Eq)]
275pub struct ReceiptRow {
276 pub receipt_ref: String,
277 pub allowed: Option<bool>,
278 pub decision_ref: Option<String>,
279 pub tool_ref: Option<String>,
280}
281
282#[derive(Clone, Debug, PartialEq, Eq)]
283pub struct ReceiptsProjection {
284 pub meta: ProjectionMeta,
285 pub rows: Vec<ReceiptRow>,
286 pub detail: Option<String>,
287}
288
289impl ReceiptsProjection {
290 pub fn honest_empty() -> Self {
291 Self {
292 meta: ProjectionMeta::missing(sources::RECEIPTS),
293 rows: Vec::new(),
294 detail: Some(
295 "Receipt refs only (inspector deep view is OMEGA-SW-05). Source unavailable."
296 .into(),
297 ),
298 }
299 }
300
301 pub fn push_bounded(&mut self, row: ReceiptRow) {
302 self.rows.push(row);
303 if self.rows.len() > MAX_RECEIPT_ROWS {
304 let drop = self.rows.len() - MAX_RECEIPT_ROWS;
305 self.rows.drain(0..drop);
306 }
307 }
308}
309
310#[derive(Clone, Copy, Debug, PartialEq, Eq)]
311pub enum RunPhase {
312 Unknown,
313 Idle,
314 Queued,
315 Running,
316 Interrupted,
317 Finished,
318}
319
320impl RunPhase {
321 pub fn label(self) -> &'static str {
322 match self {
323 Self::Unknown => "unknown",
324 Self::Idle => "idle",
325 Self::Queued => "queued",
326 Self::Running => "running",
327 Self::Interrupted => "interrupted",
328 Self::Finished => "finished",
329 }
330 }
331
332 pub fn from_event_kind(kind: &str) -> Self {
333 match kind {
334 "turn.queued" => Self::Queued,
335 "turn.running" | "turn.started" => Self::Running,
336 "turn.interrupted" => Self::Interrupted,
337 "turn.finished" | "turn.completed" => Self::Finished,
338 _ => Self::Unknown,
339 }
340 }
341}
342
343#[derive(Clone, Copy, Debug, PartialEq, Eq)]
344pub enum InterruptIntentState {
345 /// No interrupt intent in flight.
346 None,
347 /// Typed interrupt sent; terminal event has not landed.
348 Pending,
349 /// Terminal `turn.interrupted` (or equivalent) landed.
350 Applied,
351}
352
353impl InterruptIntentState {
354 pub fn label(self) -> &'static str {
355 match self {
356 Self::None => "none",
357 Self::Pending => "pending",
358 Self::Applied => "applied",
359 }
360 }
361
362 /// Clicking interrupt must leave the intent pending, never applied.
363 pub fn after_interrupt_request() -> Self {
364 Self::Pending
365 }
366
367 pub fn after_terminal_interrupted() -> Self {
368 Self::Applied
369 }
370
371 pub fn is_falsely_applied_from_request(self) -> bool {
372 // Falsifier: pending intent looks applied without a terminal event.
373 false
374 }
375}
376
377#[derive(Clone, Debug, PartialEq, Eq)]
378pub struct RunStateProjection {
379 pub meta: ProjectionMeta,
380 pub phase: RunPhase,
381 pub reason: Option<String>,
382 pub turn_ref: Option<String>,
383 pub interrupt_intent: InterruptIntentState,
384}
385
386impl RunStateProjection {
387 pub fn honest_empty() -> Self {
388 Self {
389 meta: ProjectionMeta::missing(sources::RUN_STATE),
390 phase: RunPhase::Unknown,
391 reason: Some("Run state source is unavailable. Not an idle success.".into()),
392 turn_ref: None,
393 interrupt_intent: InterruptIntentState::None,
394 }
395 }
396
397 pub fn mark_interrupt_pending(&mut self) {
398 self.interrupt_intent = InterruptIntentState::after_interrupt_request();
399 // Pending never upgrades phase to Interrupted without a terminal event.
400 }
401
402 pub fn apply_terminal_interrupted(&mut self, turn_ref: Option<String>, reason: Option<String>) {
403 self.phase = RunPhase::Interrupted;
404 self.turn_ref = turn_ref;
405 self.reason = reason;
406 self.interrupt_intent = InterruptIntentState::after_terminal_interrupted();
407 self.meta.freshness = Freshness::Fresh;
408 self.meta.gap = GapState::None;
409 }
410
411 pub fn interrupt_not_falsely_applied(&self) -> bool {
412 self.interrupt_intent != InterruptIntentState::Applied
413 || self.phase == RunPhase::Interrupted
414 }
415}
416
417/// Full in-memory pane projection. Rebuilt from effectd; never durable.
418///
419/// `attention` is local-only (OMEGA-SW-06). It is not a second conversation
420/// store and is not cross-device read state.
421#[derive(Clone, Debug, PartialEq, Eq)]
422pub struct WorkroomProjection {
423 pub room: RoomProjection,
424 pub transcript: TranscriptProjection,
425 pub activity: ActivityProjection,
426 pub receipts: ReceiptsProjection,
427 pub run_state: RunStateProjection,
428 pub attention: crate::attention::RoomAttention,
429 pub connection_detail: Option<String>,
430}
431
432impl WorkroomProjection {
433 /// Initial honest state before any service observation.
434 pub fn honest_unsubscribed() -> Self {
435 Self {
436 room: RoomProjection::honest_empty(),
437 transcript: TranscriptProjection::honest_empty(),
438 activity: ActivityProjection::honest_empty(),
439 receipts: ReceiptsProjection::honest_empty(),
440 run_state: RunStateProjection::honest_empty(),
441 attention: crate::attention::RoomAttention::honest_empty(),
442 connection_detail: Some(
443 "Subscribes to omega-effectd only. No durable pane state.".into(),
444 ),
445 }
446 }
447
448 pub fn public_demo() -> Self {
449 let transcript = TranscriptProjection {
450 meta: ProjectionMeta::fresh("fictional public demo fixture"),
451 rows: vec![
452 TranscriptRow {
453 message_ref: "demo.message.001".into(),
454 role: "You".into(),
455 text: "Add keyboard navigation to the command palette and verify the empty state.".into(),
456 ack: MessageAck::Confirmed,
457 },
458 TranscriptRow {
459 message_ref: "demo.message.002".into(),
460 role: "Sarah · Product Engineer".into(),
461 text: "Implemented the focus loop, added coverage, and updated the accessible status copy.".into(),
462 ack: MessageAck::Confirmed,
463 },
464 TranscriptRow {
465 message_ref: "demo.message.003".into(),
466 role: "You".into(),
467 text: "Looks good. Keep the changes scoped to the palette component.".into(),
468 ack: MessageAck::Confirmed,
469 },
470 ],
471 cursor: None,
472 truncated: false,
473 };
474 let activity = ActivityProjection {
475 meta: ProjectionMeta::fresh("fictional public demo fixture"),
476 rows: vec![
477 ActivityRow {
478 event_ref: "demo.activity.001".into(),
479 kind: "read".into(),
480 summary: "Inspected command palette state and keyboard handlers".into(),
481 turn_ref: Some("demo.turn.001".into()),
482 },
483 ActivityRow {
484 event_ref: "demo.activity.002".into(),
485 kind: "edit".into(),
486 summary: "Updated src/commandPalette.ts".into(),
487 turn_ref: Some("demo.turn.001".into()),
488 },
489 ActivityRow {
490 event_ref: "demo.activity.003".into(),
491 kind: "test".into(),
492 summary: "12 checks passed".into(),
493 turn_ref: Some("demo.turn.001".into()),
494 },
495 ],
496 cursor: None,
497 truncated: false,
498 };
499 let mut projection = Self {
500 room: RoomProjection {
501 meta: ProjectionMeta::fresh("fictional public demo fixture"),
502 principal_ref: Some("demo.agent.sarah".into()),
503 display_name: Some("Orbit Notes launch room".into()),
504 role: Some("Product engineering".into()),
505 thread_ref: Some("demo.thread.orbit-notes".into()),
506 authority_profile: Some("demo-safe".into()),
507 authority_revision: Some("1".into()),
508 detail: Some("Fictional public demo data".into()),
509 },
510 transcript,
511 activity,
512 receipts: ReceiptsProjection {
513 meta: ProjectionMeta::fresh("fictional public demo fixture"),
514 rows: vec![ReceiptRow {
515 receipt_ref: "demo.receipt.001".into(),
516 allowed: Some(true),
517 decision_ref: Some("demo.decision.scoped-edit".into()),
518 tool_ref: Some("editor.apply_patch".into()),
519 }],
520 detail: Some("Fictional scoped edit approval".into()),
521 },
522 run_state: RunStateProjection {
523 meta: ProjectionMeta::fresh("fictional public demo fixture"),
524 phase: RunPhase::Finished,
525 reason: Some("Completed with 12 checks passing".into()),
526 turn_ref: Some("demo.turn.001".into()),
527 interrupt_intent: InterruptIntentState::None,
528 },
529 attention: crate::attention::RoomAttention::honest_empty(),
530 connection_detail: Some(
531 "Public demo mode · offline fixture · no account or service connection".into(),
532 ),
533 };
534 projection.mark_room_read();
535 projection
536 }
537
538 pub fn mark_effectd_unavailable(&mut self, detail: impl Into<String>) {
539 let detail = detail.into();
540 self.connection_detail = Some(detail.clone());
541 self.room.meta = ProjectionMeta::unavailable(sources::EFFECTD, &detail);
542 self.room.detail = Some(detail.clone());
543 self.transcript.meta = ProjectionMeta::unavailable(sources::EFFECTD, &detail);
544 self.activity.meta = ProjectionMeta::unavailable(sources::EFFECTD, &detail);
545 self.receipts.meta = ProjectionMeta::unavailable(sources::EFFECTD, &detail);
546 self.receipts.detail = Some(detail.clone());
547 self.run_state.meta = ProjectionMeta::unavailable(sources::EFFECTD, &detail);
548 self.run_state.reason = Some(detail);
549 // Unavailable sources must not invent attention or fake tick activity.
550 let read = self.attention.read_state.clone();
551 self.attention = crate::attention::compute_room_attention(
552 &self.transcript,
553 read,
554 self.room.thread_ref.as_deref(),
555 );
556 }
557
558 /// Recompute unread + attention marker from the current transcript page.
559 pub fn recompute_attention(&mut self) {
560 let read = self.attention.read_state.clone();
561 self.attention = crate::attention::compute_room_attention(
562 &self.transcript,
563 read,
564 self.room.thread_ref.as_deref(),
565 );
566 }
567
568 /// Local mark-read (MVP). Does not publish NIP-RS.
569 pub fn mark_room_read(&mut self) {
570 self.attention
571 .read_state
572 .mark_read_from_transcript(&self.transcript);
573 self.recompute_attention();
574 }
575
576 pub fn header() -> &'static str {
577 PANE_HEADER
578 }
579}
580
581#[cfg(test)]
582mod tests {
583 use super::*;
584
585 #[test]
586 fn honest_empty_projections_are_not_empty_success() {
587 let p = WorkroomProjection::honest_unsubscribed();
588 assert_eq!(WorkroomProjection::header(), "Sarah");
589 assert!(p.room.is_honest_missing());
590 assert_eq!(p.transcript.meta.gap, GapState::Unavailable);
591 assert_eq!(p.activity.meta.gap, GapState::Unavailable);
592 assert_eq!(p.receipts.meta.gap, GapState::Unavailable);
593 assert_eq!(p.run_state.meta.gap, GapState::Unavailable);
594 assert_eq!(p.run_state.phase, RunPhase::Unknown);
595 assert!(p.room.detail.is_some());
596 assert!(p.run_state.reason.is_some());
597 // OMEGA-SW-06: empty + tick off → no attention, no synthetic activity.
598 assert_eq!(p.attention.unread_count, 0);
599 assert_eq!(
600 p.attention.marker,
601 crate::attention::AttentionMarker::None
602 );
603 assert!(crate::attention::empty_room_is_honest(
604 &p.transcript,
605 crate::attention::OMEGA_AUTONOMOUS_TICK_ENABLED
606 ));
607 }
608
609 #[test]
610 fn mark_room_read_clears_local_attention() {
611 let mut p = WorkroomProjection::honest_unsubscribed();
612 p.transcript.meta = ProjectionMeta::fresh(sources::TRANSCRIPT);
613 p.transcript.push_bounded(TranscriptRow {
614 message_ref: "message.sarah_auto.1".into(),
615 role: "sarah".into(),
616 text: "proactive update".into(),
617 ack: MessageAck::Confirmed,
618 });
619 p.room.thread_ref = Some("thread.sarah.abc".into());
620 p.recompute_attention();
621 assert_eq!(p.attention.unread_count, 1);
622 assert!(p.attention.marker.is_set());
623
624 p.mark_room_read();
625 assert_eq!(p.attention.unread_count, 0);
626 assert!(!p.attention.marker.is_set());
627 assert_eq!(
628 p.attention.read_state.last_read_message_ref.as_deref(),
629 Some("message.sarah_auto.1")
630 );
631 }
632
633 #[test]
634 fn pending_message_never_renders_as_applied() {
635 let mut t = TranscriptProjection::honest_empty();
636 t.meta = ProjectionMeta::pending(sources::TRANSCRIPT);
637 t.push_bounded(TranscriptRow {
638 message_ref: "local:1".into(),
639 role: "owner".into(),
640 text: "hello".into(),
641 ack: MessageAck::Pending,
642 });
643 assert_eq!(t.pending_count(), 1);
644 assert!(!MessageAck::Pending.renders_as_applied());
645 assert!(t.no_pending_renders_as_applied());
646 assert_eq!(t.meta.gap, GapState::Pending);
647 assert!(!t.meta.gap.is_applied_looking());
648 }
649
650 #[test]
651 fn interrupt_request_stays_pending_until_terminal_event() {
652 let mut run = RunStateProjection::honest_empty();
653 run.meta = ProjectionMeta::fresh(sources::RUN_STATE);
654 run.phase = RunPhase::Running;
655 run.mark_interrupt_pending();
656 assert_eq!(run.interrupt_intent, InterruptIntentState::Pending);
657 assert_ne!(run.phase, RunPhase::Interrupted);
658 assert_eq!(run.interrupt_intent.label(), "pending");
659 assert!(run.interrupt_not_falsely_applied());
660
661 run.apply_terminal_interrupted(Some("turn:1".into()), Some("owner_interrupt".into()));
662 assert_eq!(run.interrupt_intent, InterruptIntentState::Applied);
663 assert_eq!(run.phase, RunPhase::Interrupted);
664 assert!(run.interrupt_not_falsely_applied());
665 }
666
667 #[test]
668 fn transcript_capacity_bound_truncates_oldest() {
669 let mut t = TranscriptProjection::honest_empty();
670 t.meta = ProjectionMeta::fresh(sources::TRANSCRIPT);
671 for i in 0..(MAX_TRANSCRIPT_ROWS + 5) {
672 t.push_bounded(TranscriptRow {
673 message_ref: format!("m{i}"),
674 role: "owner".into(),
675 text: format!("row {i}"),
676 ack: MessageAck::Confirmed,
677 });
678 }
679 assert_eq!(t.rows.len(), MAX_TRANSCRIPT_ROWS);
680 assert!(t.truncated);
681 assert_eq!(t.rows.first().unwrap().message_ref, "m5");
682 }
683
684 #[test]
685 fn activity_capacity_bound() {
686 let mut a = ActivityProjection::honest_empty();
687 for i in 0..(MAX_ACTIVITY_ROWS + 3) {
688 a.push_bounded(ActivityRow {
689 event_ref: format!("e{i}"),
690 kind: "tool.call".into(),
691 summary: format!("tool {i}"),
692 turn_ref: None,
693 });
694 }
695 assert_eq!(a.rows.len(), MAX_ACTIVITY_ROWS);
696 assert!(a.truncated);
697 }
698
699 #[test]
700 fn effectd_unavailable_marks_all_sources_honest() {
701 let mut p = WorkroomProjection::honest_unsubscribed();
702 p.mark_effectd_unavailable("supervisor not initialized");
703 assert_eq!(p.room.meta.gap, GapState::Unavailable);
704 assert_eq!(p.transcript.meta.gap, GapState::Unavailable);
705 assert_eq!(p.activity.meta.gap, GapState::Unavailable);
706 assert_eq!(p.receipts.meta.gap, GapState::Unavailable);
707 assert_eq!(p.run_state.meta.gap, GapState::Unavailable);
708 assert!(
709 p.connection_detail
710 .as_deref()
711 .unwrap()
712 .contains("supervisor")
713 );
714 }
715
716 #[test]
717 fn run_phase_from_turn_events() {
718 assert_eq!(RunPhase::from_event_kind("turn.queued"), RunPhase::Queued);
719 assert_eq!(
720 RunPhase::from_event_kind("turn.running"),
721 RunPhase::Running
722 );
723 assert_eq!(
724 RunPhase::from_event_kind("turn.interrupted"),
725 RunPhase::Interrupted
726 );
727 assert_eq!(
728 RunPhase::from_event_kind("turn.finished"),
729 RunPhase::Finished
730 );
731 }
732}
733