@team-agent/installer 0.5.53 → 0.5.54

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/emit.rs +94 -3
  4. package/crates/team-agent/src/cli/send/persist.rs +1 -0
  5. package/crates/team-agent/src/cli/send/presentation.rs +1 -0
  6. package/crates/team-agent/src/cli/send.rs +1 -0
  7. package/crates/team-agent/src/cli/spec.rs +1 -1
  8. package/crates/team-agent/src/cli/tests/lane_c.rs +3 -3
  9. package/crates/team-agent/src/cli/tests/leader_watch.rs +1 -0
  10. package/crates/team-agent/src/cli/tests/named_address.rs +1 -0
  11. package/crates/team-agent/src/cli/tests/status_send.rs +1 -0
  12. package/crates/team-agent/src/cli/types.rs +1 -0
  13. package/crates/team-agent/src/coordinator/tests/basics.rs +4 -4
  14. package/crates/team-agent/src/db/message_store.rs +31 -2
  15. package/crates/team-agent/src/db/migration.rs +7 -6
  16. package/crates/team-agent/src/db/schema.rs +18 -5
  17. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +1 -0
  18. package/crates/team-agent/src/mcp_server/normalize.rs +8 -0
  19. package/crates/team-agent/src/mcp_server/tests/wire.rs +1 -1
  20. package/crates/team-agent/src/mcp_server/tools.rs +71 -0
  21. package/crates/team-agent/src/mcp_server/types.rs +4 -0
  22. package/crates/team-agent/src/mcp_server/wire.rs +42 -4
  23. package/crates/team-agent/src/messaging/delivery.rs +2 -0
  24. package/crates/team-agent/src/messaging/helpers.rs +1 -0
  25. package/crates/team-agent/src/messaging/leader_receiver.rs +32 -0
  26. package/crates/team-agent/src/messaging/mod.rs +2 -1
  27. package/crates/team-agent/src/messaging/persist.rs +68 -2
  28. package/crates/team-agent/src/messaging/presentation.rs +307 -0
  29. package/crates/team-agent/src/messaging/results.rs +130 -3
  30. package/crates/team-agent/src/messaging/selftest.rs +1 -0
  31. package/crates/team-agent/src/messaging/send.rs +54 -2
  32. package/crates/team-agent/src/messaging/tests/runtime.rs +51 -1
  33. package/crates/team-agent/src/messaging/types.rs +2 -0
  34. package/crates/team-agent/src/messaging/watchers.rs +1 -0
  35. package/crates/team-agent/src/provider/session/capture.rs +74 -0
  36. package/crates/team-agent/src/provider/session_scan/common/tests.rs +112 -0
  37. package/crates/team-agent/src/provider/session_scan/common.rs +14 -86
  38. package/package.json +4 -4
  39. package/schemas/result-envelope.schema.json +10 -0
  40. package/skills/team-agent/SKILL.md +3 -0
@@ -178,6 +178,34 @@ impl TeamOrchestratorTools {
178
178
  requires_ack: Option<bool>,
179
179
  scope_override: Option<Scope>,
180
180
  ) -> Result<SendOutcome, ToolError> {
181
+ self.send_message_with_presentation(
182
+ to,
183
+ content,
184
+ task_id,
185
+ requires_ack,
186
+ scope_override,
187
+ None,
188
+ )
189
+ }
190
+
191
+ pub fn send_message_with_presentation(
192
+ &self,
193
+ to: &MessageTarget,
194
+ content: &str,
195
+ task_id: Option<&str>,
196
+ requires_ack: Option<bool>,
197
+ scope_override: Option<Scope>,
198
+ presentation: Option<&Value>,
199
+ ) -> Result<SendOutcome, ToolError> {
200
+ let (presentation, presentation_error) =
201
+ crate::messaging::presentation::normalize_presentation(presentation);
202
+ if let Some(error) = presentation_error {
203
+ return Err(ToolError::new(
204
+ ToolErrorReason::InvalidToolArguments,
205
+ format!("invalid presentation: {error}"),
206
+ "PresentationError",
207
+ ));
208
+ }
181
209
  let canonical_owner_team = self.canonical_owner_team_key()?;
182
210
  if matches!(scope_override, Some(Scope::Workspace)) {
183
211
  return Err(self.rpc_scope_refused(
@@ -227,6 +255,7 @@ impl TeamOrchestratorTools {
227
255
  sender,
228
256
  requires_ack: ack,
229
257
  team: canonical_owner_team,
258
+ presentation,
230
259
  ..SendOptions::default()
231
260
  };
232
261
  if is_worker_recipient(to) {
@@ -322,6 +351,36 @@ impl TeamOrchestratorTools {
322
351
  next_actions: Option<&[Value]>,
323
352
  task_id: Option<&str>,
324
353
  agent_id: Option<&str>,
354
+ ) -> ToolResult {
355
+ self.report_result_with_presentation(
356
+ envelope,
357
+ summary,
358
+ status,
359
+ changes,
360
+ tests,
361
+ risks,
362
+ artifacts,
363
+ next_actions,
364
+ task_id,
365
+ agent_id,
366
+ None,
367
+ )
368
+ }
369
+
370
+ #[allow(clippy::too_many_arguments)]
371
+ pub fn report_result_with_presentation(
372
+ &self,
373
+ envelope: Option<&Value>,
374
+ summary: Option<&str>,
375
+ status: ResultStatus,
376
+ changes: Option<&[Value]>,
377
+ tests: Option<&[Value]>,
378
+ risks: Option<&[Value]>,
379
+ artifacts: Option<&[Value]>,
380
+ next_actions: Option<&[Value]>,
381
+ task_id: Option<&str>,
382
+ agent_id: Option<&str>,
383
+ presentation: Option<&Value>,
325
384
  ) -> ToolResult {
326
385
  if let Some(envelope) = envelope {
327
386
  self.validate_rpc_scope_args("report_result", envelope)?;
@@ -331,6 +390,11 @@ impl TeamOrchestratorTools {
331
390
  .unwrap_or_else(|| Value::Object(serde_json::Map::new()));
332
391
  ensure_object(&mut base);
333
392
  if let Some(obj) = base.as_object_mut() {
393
+ if !obj.contains_key("presentation") {
394
+ if let Some(presentation) = presentation {
395
+ obj.insert("presentation".to_string(), presentation.clone());
396
+ }
397
+ }
334
398
  if !obj.contains_key("summary") {
335
399
  obj.insert(
336
400
  "summary".to_string(),
@@ -459,6 +523,13 @@ impl TeamOrchestratorTools {
459
523
  self.note_unknown_result_status(&raw);
460
524
  }
461
525
  let normalized = normalize_report_envelope(&base);
526
+ if let Some(error) = normalized.presentation_error.as_deref() {
527
+ return Err(ToolError::new(
528
+ ToolErrorReason::InvalidToolArguments,
529
+ format!("invalid presentation: {error}"),
530
+ "PresentationError",
531
+ ));
532
+ }
462
533
  let warnings = report_result_integrity_warnings(&base, &normalized);
463
534
  let mut env_value = normalized_envelope_value(&normalized);
464
535
  copy_report_attribution_fields(&base, &mut env_value);
@@ -5,6 +5,7 @@ use serde_json::Value;
5
5
  use thiserror::Error;
6
6
 
7
7
  // ── REUSE: step 2 model (ids + normalized-envelope value enums) ─────────────
8
+ use crate::messaging::presentation::PresentationDecision;
8
9
  use crate::model::enums::{ChangeKind, ResultStatus, RiskSeverity, TestStatus};
9
10
  use crate::model::ids::{AgentId, TaskId, TeamKey};
10
11
 
@@ -367,6 +368,9 @@ pub struct NormalizedReportEnvelope {
367
368
  pub risks: Vec<NormalizedRisk>,
368
369
  pub artifacts: Vec<NormalizedArtifact>,
369
370
  pub next_actions: Vec<NormalizedNextAction>,
371
+ pub presentation: PresentationDecision,
372
+ #[serde(skip_serializing_if = "Option::is_none")]
373
+ pub presentation_error: Option<String>,
370
374
  }
371
375
 
372
376
  /// `changes[]` (`normalize.py:126-142`): path + regularized [`ChangeKind`] +
@@ -369,11 +369,14 @@ fn python_repr(value: &str) -> String {
369
369
  fn tool_contract(tool: McpTool) -> Value {
370
370
  let (description, required) = match tool {
371
371
  McpTool::SendMessage => (
372
- "Send a message to a teammate, the leader, or '*' for all other team members. Provide only target and content; Team Agent fills sender, task id, ack policy, and delivery metadata.",
372
+ "Send a message to a teammate, the leader, or '*' for all other team members. Team Agent fills identity and delivery metadata; optional presentation routing is durable and never drops the message.",
373
373
  vec!["to", "content"],
374
374
  ),
375
375
  McpTool::AssignTask => ("Assign or update a task in the team graph and deliver it to its assignee.", vec!["task"]),
376
- McpTool::ReportResult => ("Report task completion with a result envelope.", Vec::new()),
376
+ McpTool::ReportResult => (
377
+ "Report task completion with a durable result envelope. Optional presentation routing controls live leader display, not persistence.",
378
+ Vec::new(),
379
+ ),
377
380
  McpTool::UpdateState => ("Append a note to team state and rewrite team_state.md.", vec!["note"]),
378
381
  McpTool::GetTeamStatus => ("Return machine-readable team status.", Vec::new()),
379
382
  McpTool::StopAgent => ("Stop a running worker.", vec!["agent_id"]),
@@ -419,6 +422,11 @@ fn tool_properties(tool: McpTool) -> serde_json::Map<String, Value> {
419
422
  string_property("Target agent id, 'leader', or '*' for broadcast."),
420
423
  );
421
424
  insert_property(&mut properties, "content", string_property("Message body."));
425
+ insert_property(
426
+ &mut properties,
427
+ "presentation",
428
+ presentation_property("Optional durable presentation routing."),
429
+ );
422
430
  }
423
431
  McpTool::ReportResult => {
424
432
  insert_property(
@@ -467,6 +475,11 @@ fn tool_properties(tool: McpTool) -> serde_json::Map<String, Value> {
467
475
  "agent_id",
468
476
  string_property("Optional reporting agent id override."),
469
477
  );
478
+ insert_property(
479
+ &mut properties,
480
+ "presentation",
481
+ presentation_property("Optional durable presentation routing."),
482
+ );
470
483
  }
471
484
  McpTool::UpdateState => {
472
485
  insert_property(
@@ -590,6 +603,23 @@ fn object_property(description: &str) -> Value {
590
603
  serde_json::json!({"type": "object", "description": description, "additionalProperties": true})
591
604
  }
592
605
 
606
+ fn presentation_property(description: &str) -> Value {
607
+ serde_json::json!({
608
+ "type": "object",
609
+ "description": description,
610
+ "properties": {
611
+ "sink": {"type": "string", "enum": ["leader", "casefile", "silent"]},
612
+ "class": {"type": "string", "enum": [
613
+ "message", "progress", "stage_result", "stage_pass", "bounce",
614
+ "blocking", "final_review", "timeout"
615
+ ]},
616
+ "case_id": {"type": "string"}
617
+ },
618
+ "required": ["sink", "class"],
619
+ "additionalProperties": false
620
+ })
621
+ }
622
+
593
623
  fn array_property(description: &str) -> Value {
594
624
  serde_json::json!({"type": "array", "description": description, "items": {"type": "object", "additionalProperties": true}})
595
625
  }
@@ -610,7 +640,14 @@ pub(crate) fn dispatch_tool(
610
640
  McpTool::SendMessage => {
611
641
  let target = message_target_from_value(args.get("to"));
612
642
  let content = args.get("content").and_then(Value::as_str).unwrap_or("");
613
- let outcome = tools.send_message(&target, content, None, None, None)?;
643
+ let outcome = tools.send_message_with_presentation(
644
+ &target,
645
+ content,
646
+ None,
647
+ None,
648
+ None,
649
+ args.get("presentation"),
650
+ )?;
614
651
  match outcome {
615
652
  SendOutcome::WorkerAccepted { .. } => Ok(ToolOk {
616
653
  fields: object_fields(outcome.to_value()),
@@ -618,7 +655,7 @@ pub(crate) fn dispatch_tool(
618
655
  SendOutcome::Direct(ok) => Ok(ok),
619
656
  }
620
657
  }
621
- McpTool::ReportResult => tools.report_result(
658
+ McpTool::ReportResult => tools.report_result_with_presentation(
622
659
  args.get("envelope"),
623
660
  args.get("summary").and_then(Value::as_str),
624
661
  // cr verdict (T3-1 refined): an unknown status literal normalizes to
@@ -650,6 +687,7 @@ pub(crate) fn dispatch_tool(
650
687
  .map(Vec::as_slice),
651
688
  args.get("task_id").and_then(Value::as_str),
652
689
  args.get("agent_id").and_then(Value::as_str),
690
+ args.get("presentation"),
653
691
  ),
654
692
  McpTool::UpdateState => {
655
693
  tools.update_state(args.get("note").and_then(Value::as_str).unwrap_or(""))
@@ -57,6 +57,7 @@ pub fn deliver_stored_message(
57
57
  requires_ack,
58
58
  None,
59
59
  super::InitialDisposition::Accepted,
60
+ None,
60
61
  )?
61
62
  else {
62
63
  unreachable!("internal delivery does not accept caller-supplied ids")
@@ -1828,6 +1829,7 @@ pub fn deliver_pending_messages(
1828
1829
  let store = MessageStore::open(workspace)?;
1829
1830
  let message_ids = {
1830
1831
  let conn = crate::db::schema::open_db(store.db_path())?;
1832
+ // `stored_only` is intentionally absent: C6 chose durable presentation without C5.
1831
1833
  let mut stmt = conn.prepare(
1832
1834
  "select message_id from messages
1833
1835
  where status in (
@@ -25,6 +25,7 @@ static RESULT_COUNTER: AtomicU64 = AtomicU64::new(0);
25
25
  pub(crate) fn status_wire(status: DeliveryStatus) -> &'static str {
26
26
  match status {
27
27
  DeliveryStatus::Delivered => "delivered",
28
+ DeliveryStatus::StoredOnly => "stored_only",
28
29
  DeliveryStatus::Failed => "failed",
29
30
  DeliveryStatus::Queued => "queued",
30
31
  DeliveryStatus::Blocked => "blocked",
@@ -14,6 +14,7 @@ use crate::transport::{
14
14
 
15
15
  use super::helpers::MessageStatusShadow;
16
16
  use super::persist::persist_internal_send;
17
+ use super::presentation::PresentationDecision;
17
18
  use super::{
18
19
  DeliveryOutcome, DeliveryRefusal, DeliveryStage, DeliveryStatus, InitialDisposition,
19
20
  InternalSendKind, MessagingError, PersistResolution,
@@ -69,6 +70,35 @@ pub fn send_to_leader_receiver_with_message_id(
69
70
  result_id: Option<&str>,
70
71
  requested_message_id: Option<&str>,
71
72
  event_log: &EventLog,
73
+ ) -> Result<DeliveryOutcome, MessagingError> {
74
+ send_to_leader_receiver_with_presentation(
75
+ workspace,
76
+ state,
77
+ leader_id,
78
+ content,
79
+ task_id,
80
+ sender,
81
+ requires_ack,
82
+ result_id,
83
+ requested_message_id,
84
+ &PresentationDecision::default(),
85
+ event_log,
86
+ )
87
+ }
88
+
89
+ #[allow(clippy::too_many_arguments)]
90
+ pub fn send_to_leader_receiver_with_presentation(
91
+ workspace: &Path,
92
+ state: &serde_json::Value,
93
+ leader_id: &str,
94
+ content: &str,
95
+ task_id: Option<&TaskId>,
96
+ sender: &str,
97
+ requires_ack: bool,
98
+ result_id: Option<&str>,
99
+ requested_message_id: Option<&str>,
100
+ presentation: &PresentationDecision,
101
+ event_log: &EventLog,
72
102
  ) -> Result<DeliveryOutcome, MessagingError> {
73
103
  let store = MessageStore::open(workspace)?;
74
104
  let owner_team = active_team_key(workspace, state);
@@ -90,6 +120,7 @@ pub fn send_to_leader_receiver_with_message_id(
90
120
  false,
91
121
  requested_message_id,
92
122
  InitialDisposition::Accepted,
123
+ Some(presentation),
93
124
  )? {
94
125
  PersistResolution::Duplicate(requested) => {
95
126
  return Ok(DeliveryOutcome {
@@ -643,6 +674,7 @@ pub fn enqueue_leader_mailbox_until_attach(
643
674
  false,
644
675
  None,
645
676
  InitialDisposition::QueuedUntilLeaderAttach,
677
+ None,
646
678
  )?
647
679
  else {
648
680
  unreachable!("offline mailbox does not accept caller-supplied ids")
@@ -68,6 +68,7 @@ pub mod leader_channel;
68
68
  pub mod leader_receiver;
69
69
  pub mod peers;
70
70
  pub mod persist;
71
+ pub mod presentation;
71
72
  pub mod results;
72
73
  pub mod scheduler;
73
74
  pub mod selftest;
@@ -94,7 +95,7 @@ pub use leader_channel::{
94
95
  pub use leader_receiver::{
95
96
  deliver_to_leader_fallback_pane, enqueue_leader_mailbox_until_attach,
96
97
  mirror_peer_message_to_leader, send_to_leader_receiver,
97
- send_to_leader_receiver_with_message_id,
98
+ send_to_leader_receiver_with_message_id, send_to_leader_receiver_with_presentation,
98
99
  };
99
100
  pub use peers::allow_peer_talk;
100
101
  pub use persist::{
@@ -5,6 +5,7 @@ use std::path::{Path, PathBuf};
5
5
  use crate::db::message_store::{MessageRowStatus, MessageStore, PersistMessageInput};
6
6
  use crate::model::ids::{AgentId, TaskId, TeamKey};
7
7
 
8
+ use super::presentation::PresentationDecision;
8
9
  use super::send::TrustedSender;
9
10
  use super::MessagingError;
10
11
 
@@ -64,6 +65,7 @@ impl DeliveryBlocker {
64
65
  #[derive(Debug, Clone, Copy, PartialEq, Eq)]
65
66
  pub enum InitialDisposition {
66
67
  Accepted,
68
+ StoredOnly,
67
69
  QueuedUntilLeaderAttach,
68
70
  Blocked(DeliveryBlocker),
69
71
  }
@@ -72,6 +74,7 @@ impl InitialDisposition {
72
74
  fn row_status(self) -> MessageRowStatus {
73
75
  match self {
74
76
  Self::Accepted => MessageRowStatus::Accepted,
77
+ Self::StoredOnly => MessageRowStatus::StoredOnly,
75
78
  Self::QueuedUntilLeaderAttach => MessageRowStatus::QueuedUntilLeaderAttach,
76
79
  Self::Blocked(DeliveryBlocker::CoordinatorUnavailable) => {
77
80
  MessageRowStatus::QueuedCoordinatorUnavailable
@@ -82,7 +85,7 @@ impl InitialDisposition {
82
85
  fn error(self) -> Option<&'static str> {
83
86
  match self {
84
87
  Self::Blocked(blocker) => Some(blocker.as_str()),
85
- Self::Accepted | Self::QueuedUntilLeaderAttach => None,
88
+ Self::Accepted | Self::StoredOnly | Self::QueuedUntilLeaderAttach => None,
86
89
  }
87
90
  }
88
91
  }
@@ -100,6 +103,7 @@ pub struct ResolvedSendIntent {
100
103
  pub requires_ack: bool,
101
104
  pub requested_message_id: Option<String>,
102
105
  pub initial_disposition: InitialDisposition,
106
+ pub presentation: PresentationDecision,
103
107
  }
104
108
 
105
109
  impl ResolvedSendIntent {
@@ -128,6 +132,7 @@ impl ResolvedSendIntent {
128
132
  requires_ack,
129
133
  requested_message_id,
130
134
  initial_disposition: InitialDisposition::Accepted,
135
+ presentation: PresentationDecision::default(),
131
136
  }
132
137
  }
133
138
  }
@@ -158,6 +163,7 @@ pub fn persist_resolved_send(
158
163
  }
159
164
  }
160
165
  let status = intent.initial_disposition.row_status();
166
+ let presentation = serde_json::to_string(&intent.presentation)?;
161
167
  let message_id = store.persist_message(PersistMessageInput {
162
168
  message_id: intent.requested_message_id.as_deref(),
163
169
  owner_team_id: intent.owner_team_id.as_ref().map(TeamKey::as_str),
@@ -168,6 +174,7 @@ pub fn persist_resolved_send(
168
174
  requires_ack: intent.requires_ack,
169
175
  status,
170
176
  content: &intent.content,
177
+ presentation: &presentation,
171
178
  error: intent.initial_disposition.error(),
172
179
  })?;
173
180
  Ok(PersistResolution::Persisted(PersistedSend {
@@ -177,7 +184,9 @@ pub fn persist_resolved_send(
177
184
  row_status: status,
178
185
  blocker: match intent.initial_disposition {
179
186
  InitialDisposition::Blocked(blocker) => Some(blocker),
180
- InitialDisposition::Accepted | InitialDisposition::QueuedUntilLeaderAttach => None,
187
+ InitialDisposition::Accepted
188
+ | InitialDisposition::StoredOnly
189
+ | InitialDisposition::QueuedUntilLeaderAttach => None,
181
190
  },
182
191
  }))
183
192
  }
@@ -195,6 +204,7 @@ pub(crate) fn persist_internal_send(
195
204
  requires_ack: bool,
196
205
  requested_message_id: Option<&str>,
197
206
  initial_disposition: InitialDisposition,
207
+ presentation: Option<&PresentationDecision>,
198
208
  ) -> Result<PersistResolution, MessagingError> {
199
209
  let mut intent = ResolvedSendIntent::accepted(
200
210
  SendOrigin::Internal(kind),
@@ -209,6 +219,9 @@ pub(crate) fn persist_internal_send(
209
219
  requested_message_id.map(ToOwned::to_owned),
210
220
  );
211
221
  intent.initial_disposition = initial_disposition;
222
+ if let Some(presentation) = presentation {
223
+ intent.presentation = presentation.clone();
224
+ }
212
225
  persist_resolved_send(&intent)
213
226
  }
214
227
 
@@ -266,6 +279,59 @@ mod tests {
266
279
  assert_eq!(error.as_deref(), Some("coordinator_unavailable"));
267
280
  }
268
281
 
282
+ #[test]
283
+ fn stored_only_persists_requested_presentation_without_delivery_eligibility() {
284
+ let workspace = workspace("stored-only");
285
+ let mut intent = ResolvedSendIntent::accepted(
286
+ SendOrigin::Mcp,
287
+ &workspace,
288
+ Some(TeamKey::new("team-a")),
289
+ LogicalRecipient::Leader,
290
+ TrustedSender::from_runtime_identity(AgentId::new("worker_a")),
291
+ None,
292
+ "stage evidence",
293
+ None,
294
+ false,
295
+ None,
296
+ );
297
+ intent.initial_disposition = InitialDisposition::StoredOnly;
298
+ intent.presentation = super::super::presentation::decide_presentation(
299
+ &super::super::presentation::PresentationRequest {
300
+ sink: super::super::presentation::PresentationSink::Casefile,
301
+ class: super::super::presentation::PresentationClass::StageResult,
302
+ case_id: Some("case-7".to_string()),
303
+ },
304
+ super::super::presentation::PresentationSource::Send,
305
+ );
306
+ let PersistResolution::Persisted(persisted) = persist_resolved_send(&intent).unwrap()
307
+ else {
308
+ panic!("fresh intent must persist")
309
+ };
310
+ assert_eq!(persisted.row_status, MessageRowStatus::StoredOnly);
311
+ let store = MessageStore::open(&workspace).unwrap();
312
+ let connection = crate::db::schema::open_db(store.db_path()).unwrap();
313
+ let (status, presentation): (String, String) = connection
314
+ .query_row(
315
+ "select status, presentation from messages where message_id = ?1",
316
+ [&persisted.message_id],
317
+ |row| Ok((row.get(0)?, row.get(1)?)),
318
+ )
319
+ .unwrap();
320
+ assert_eq!(status, "stored_only");
321
+ assert_eq!(
322
+ serde_json::from_str::<serde_json::Value>(&presentation).unwrap(),
323
+ serde_json::json!({
324
+ "sink": "casefile",
325
+ "class": "stage_result",
326
+ "case_id": "case-7",
327
+ "requested_sink": "casefile",
328
+ "effective_sink": "casefile",
329
+ "policy_reason": "requested_sink:casefile",
330
+ "policy_version": "team-presentation-v1"
331
+ })
332
+ );
333
+ }
334
+
269
335
  #[test]
270
336
  fn production_row_creation_is_owned_by_persisted_primitive() {
271
337
  for (name, source) in [