@team-agent/installer 0.5.118 → 0.5.120

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 (72) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/README.md +46 -1
  4. package/README.zh.md +46 -1
  5. package/crates/team-agent/src/cli/adapters.rs +11 -0
  6. package/crates/team-agent/src/cli/arguments.rs +401 -0
  7. package/crates/team-agent/src/cli/emit.rs +241 -142
  8. package/crates/team-agent/src/cli/intent/runtime.rs +1139 -0
  9. package/crates/team-agent/src/cli/intent/runtime_tests.rs +584 -0
  10. package/crates/team-agent/src/cli/intent/tests.rs +654 -0
  11. package/crates/team-agent/src/cli/intent.rs +642 -0
  12. package/crates/team-agent/src/cli/mod.rs +49 -5
  13. package/crates/team-agent/src/cli/named_address.rs +10 -1
  14. package/crates/team-agent/src/cli/spec.rs +22 -10
  15. package/crates/team-agent/src/cli/status_port/brief.rs +117 -16
  16. package/crates/team-agent/src/cli/tests/base.rs +6 -1
  17. package/crates/team-agent/src/cli/tests/parser_safety.rs +383 -0
  18. package/crates/team-agent/src/cli/tests/run_delegation.rs +63 -19
  19. package/crates/team-agent/src/cli/tests/verb_install_skill.rs +3 -2
  20. package/crates/team-agent/src/cli/tests/verb_profile.rs +3 -1
  21. package/crates/team-agent/src/contract_runtime/backend.rs +1 -1
  22. package/crates/team-agent/src/db/message_store.rs +10 -3
  23. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +166 -77
  24. package/crates/team-agent/src/lifecycle/launch.rs +28 -0
  25. package/crates/team-agent/src/lifecycle/restart.rs +17 -0
  26. package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +1 -3
  27. package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +5 -15
  28. package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +2 -3
  29. package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +2 -3
  30. package/crates/team-agent/src/lifecycle/tests/cli_argv_routing_red.rs +10 -2
  31. package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +8 -1
  32. package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +3 -2
  33. package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +6 -9
  34. package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +7 -5
  35. package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +4 -2
  36. package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +5 -2
  37. package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +4 -3
  38. package/crates/team-agent/src/lifecycle/tests/grok_effort_argv_red.rs +2 -4
  39. package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +3 -4
  40. package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +5 -4
  41. package/crates/team-agent/src/lifecycle/tests/grok_restart_resume_red.rs +2 -4
  42. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +78 -36
  43. package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +8 -4
  44. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +6 -12
  45. package/crates/team-agent/src/lifecycle/tests/pi_fork_session_inheritance_red.rs +4 -0
  46. package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +1 -3
  47. package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +7 -0
  48. package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +1 -3
  49. package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +2 -1
  50. package/crates/team-agent/src/lifecycle/types.rs +10 -0
  51. package/crates/team-agent/src/messaging/delivery.rs +132 -39
  52. package/crates/team-agent/src/messaging/delivery_consumption_tests.rs +223 -0
  53. package/crates/team-agent/src/messaging/enter_rescue.rs +182 -154
  54. package/crates/team-agent/src/messaging/enter_rescue_tests.rs +344 -277
  55. package/crates/team-agent/src/messaging/mod.rs +1 -1
  56. package/crates/team-agent/src/tmux_backend/enter_rescue.rs +325 -0
  57. package/crates/team-agent/src/tmux_backend/enter_rescue_tests.rs +432 -0
  58. package/crates/team-agent/src/tmux_backend/native_input.rs +19 -0
  59. package/crates/team-agent/src/tmux_backend/tests.rs +37 -0
  60. package/crates/team-agent/src/tmux_backend.rs +35 -1
  61. package/crates/team-agent/src/transport/enter_rescue.rs +166 -0
  62. package/crates/team-agent/src/transport/native_input.rs +5 -1
  63. package/crates/team-agent/src/transport/test_support.rs +25 -0
  64. package/crates/team-agent/src/transport.rs +34 -0
  65. package/package.json +4 -4
  66. package/skills/team-agent/SKILL.md +71 -54
  67. package/skills/team-agent/command-coverage.json +283 -13
  68. package/skills/team-agent/docs/reference/team-agent-operator.md +220 -0
  69. package/skills/team-agent/references/bug-as-artifact-flow.md +1 -1
  70. package/skills/team-agent/references/recovery-runbook.md +61 -212
  71. package/skills/team-agent/references/team-agent-operator.md +181 -48
  72. package/skills/team-agent/references/team-in-team.md +47 -80
@@ -611,21 +611,30 @@ fn deliver_prepared_message(
611
611
  });
612
612
  }
613
613
  if token_already_visible(transport, state, &message.recipient, &target, message_id) {
614
+ // Visibility prevents a second paste; it does not prove consumption.
615
+ let consumed = existing_token_consumed(transport, state, &message.recipient, &target, message_id);
616
+ let status = if consumed { "delivered" } else { "submitted_unverified" };
617
+ let reason = if consumed { "message_token_consumed" } else { "message_token_visible_without_consumption" };
614
618
  event_log.write(
615
619
  "delivery.duplicate_token_refused",
616
620
  serde_json::json!({
617
621
  "message_id": message_id,
618
622
  "recipient": message.recipient,
619
- "reason": "message_token_already_visible",
623
+ "reason": reason,
624
+ "consumed": consumed,
620
625
  }),
621
626
  )?;
622
- store.mark(message_id, "delivered", None)?;
627
+ store.mark(message_id, status, (!consumed).then_some(reason))?;
628
+ if !consumed {
629
+ bind_enter_watchdog(store, transport, state, &target, message_id, &message,
630
+ canonical_owner_team_id.as_deref(), event_log)?;
631
+ }
623
632
  return Ok(DeliveryOutcome {
624
- ok: true,
625
- status: DeliveryStatus::AlreadyDelivered,
626
- message_status: MessageStatusShadow("delivered".to_string()),
633
+ ok: consumed,
634
+ status: if consumed { DeliveryStatus::AlreadyDelivered } else { DeliveryStatus::Failed },
635
+ message_status: MessageStatusShadow(status.to_string()),
627
636
  message_id: Some(message_id.to_string()),
628
- verification: Some("message_token_already_visible".to_string()),
637
+ verification: Some(reason.to_string()),
629
638
  stage: Some(DeliveryStage::Submit),
630
639
  reason: None,
631
640
  channel: None,
@@ -819,9 +828,12 @@ fn deliver_prepared_message(
819
828
  // A successful tmux command is never provider-acceptance proof. Leader and
820
829
  // worker delivery both require post-submit evidence; leader additionally
821
830
  // waits for its authoritative provider transcript below.
822
- let verified = submit_verified;
831
+ let native_consumed = observed_token_consumed(transport, state, &message.recipient, &target, message_id);
832
+ let verified = submit_verified && native_consumed != Some(false);
823
833
  if !verified {
824
- let reason = if !readback_verified {
834
+ let reason = if native_consumed == Some(false) {
835
+ "submit_unverified:current_composer_or_consumption_unknown".to_string()
836
+ } else if !readback_verified {
825
837
  "pane_readback_unverified:capture_missing_token".to_string()
826
838
  } else {
827
839
  format!(
@@ -876,6 +888,8 @@ fn deliver_prepared_message(
876
888
  });
877
889
  }
878
890
  store.mark(message_id, "submitted_unverified", Some(&reason))?;
891
+ bind_enter_watchdog(store, transport, state, &target, message_id, &message,
892
+ canonical_owner_team_id.as_deref(), event_log)?;
879
893
  return Ok(DeliveryOutcome {
880
894
  ok: false,
881
895
  status: DeliveryStatus::Failed,
@@ -942,6 +956,8 @@ fn deliver_prepared_message(
942
956
  }),
943
957
  )?;
944
958
  store.mark(message_id, "submitted_unverified", Some(&reason))?;
959
+ bind_enter_watchdog(store, transport, state, &target, message_id, &message,
960
+ canonical_owner_team_id.as_deref(), event_log)?;
945
961
  return Ok(DeliveryOutcome {
946
962
  ok: false,
947
963
  status: DeliveryStatus::Failed,
@@ -2766,11 +2782,13 @@ pub(crate) fn record_turn_open_if_leader_to_worker(
2766
2782
  pub(super) fn with_rescue_submit_observer<R>(
2767
2783
  workspace: &Path,
2768
2784
  store: &MessageStore,
2769
- evidence: &crate::transport::native_input::NativeInputEvidence,
2785
+ context: &crate::transport::enter_rescue::EnterRescueContext,
2770
2786
  event_log: &EventLog,
2771
2787
  execute: impl FnOnce(&dyn SubmitObserver) -> Result<R, MessagingError>,
2772
2788
  ) -> Result<R, MessagingError> {
2773
- let request = &evidence.request;
2789
+ let request = context;
2790
+ let binding = context.target.as_ref().filter(|target| target.bound())
2791
+ .ok_or_else(|| MessagingError::Validation("enter_rescue: original binding missing".into()))?;
2774
2792
  let conn = crate::db::schema::open_db(store.db_path())?;
2775
2793
  let sender: String = conn.query_row(
2776
2794
  "select sender from messages where message_id=?1 and owner_team_id=?2 and recipient=?3 and status='submitted_unverified'",
@@ -2778,18 +2796,18 @@ pub(super) fn with_rescue_submit_observer<R>(
2778
2796
  |row| row.get(0),
2779
2797
  )?;
2780
2798
  let metadata = TargetMetadata {
2781
- tmux_endpoint: Some(evidence.binding.endpoint.clone()),
2782
- target_session: evidence.binding.session.clone(),
2783
- target_window: evidence.binding.window.clone(),
2784
- target_pane_id: evidence.binding.pane_id.clone(),
2785
- target_pane_pid: Some(evidence.binding.pane_pid),
2799
+ tmux_endpoint: Some(binding.endpoint.clone()),
2800
+ target_session: binding.session.clone(),
2801
+ target_window: binding.window.clone(),
2802
+ target_pane_id: binding.pane_id.clone(),
2803
+ target_pane_pid: Some(binding.pane_pid),
2786
2804
  resolved_from: "enter_rescue_owned_binding",
2787
2805
  };
2788
2806
  let mut observer = CurrentTurnSubmitObserver::new(
2789
2807
  workspace, &sender, &request.recipient, &request.message_id, event_log,
2790
2808
  Some(&request.owner_team_id), Some(&metadata),
2791
2809
  );
2792
- observer.rescue_evidence = Some(evidence);
2810
+ observer.rescue_context = Some(context);
2793
2811
  let result = execute(&observer)?;
2794
2812
  if let Some(error) = observer.take_error() { return Err(error); }
2795
2813
  Ok(result)
@@ -2803,7 +2821,7 @@ struct CurrentTurnSubmitObserver<'a> {
2803
2821
  event_log: &'a EventLog,
2804
2822
  owner_team_id: Option<&'a str>,
2805
2823
  metadata: Option<&'a TargetMetadata>,
2806
- rescue_evidence: Option<&'a crate::transport::native_input::NativeInputEvidence>,
2824
+ rescue_context: Option<&'a crate::transport::enter_rescue::EnterRescueContext>,
2807
2825
  fired: Cell<bool>,
2808
2826
  error: RefCell<Option<MessagingError>>,
2809
2827
  }
@@ -2826,7 +2844,7 @@ impl<'a> CurrentTurnSubmitObserver<'a> {
2826
2844
  event_log,
2827
2845
  owner_team_id,
2828
2846
  metadata,
2829
- rescue_evidence: None,
2847
+ rescue_context: None,
2830
2848
  fired: Cell::new(false),
2831
2849
  error: RefCell::new(None),
2832
2850
  }
@@ -2842,8 +2860,8 @@ impl SubmitObserver for CurrentTurnSubmitObserver<'_> {
2842
2860
  if self.fired.replace(true) {
2843
2861
  return;
2844
2862
  }
2845
- let result = if let Some(evidence) = self.rescue_evidence {
2846
- record_rescue_turn_after_submit(self.workspace, self.sender, evidence, self.event_log, self.metadata)
2863
+ let result = if let Some(context) = self.rescue_context {
2864
+ record_rescue_turn_after_submit(self.workspace, self.sender, context, self.event_log, self.metadata)
2847
2865
  } else {
2848
2866
  record_current_turn_after_inject_if_leader_to_worker_scoped(
2849
2867
  self.workspace, self.sender, self.recipient, self.message_id,
@@ -2857,11 +2875,13 @@ impl SubmitObserver for CurrentTurnSubmitObserver<'_> {
2857
2875
  fn record_rescue_turn_after_submit(
2858
2876
  workspace: &Path,
2859
2877
  sender: &str,
2860
- evidence: &crate::transport::native_input::NativeInputEvidence,
2878
+ context: &crate::transport::enter_rescue::EnterRescueContext,
2861
2879
  event_log: &EventLog,
2862
2880
  metadata: Option<&TargetMetadata>,
2863
2881
  ) -> Result<(), MessagingError> {
2864
- let request = &evidence.request;
2882
+ let request = context;
2883
+ let target = context.target.as_ref().filter(|target| target.bound())
2884
+ .ok_or_else(|| MessagingError::Validation("enter_rescue: original binding missing".into()))?;
2865
2885
  if !matches!(sender, "leader" | "Leader") { return Ok(()); }
2866
2886
  crate::state::repository::StateRepository::new(workspace).commit_messaging_checked(
2867
2887
  crate::state::repository::StateWriteIntent::MessagingDeliveryState {
@@ -2872,12 +2892,11 @@ fn record_rescue_turn_after_submit(
2872
2892
  let matches = agent.is_some_and(|agent| {
2873
2893
  agent.get("status").and_then(serde_json::Value::as_str) == Some("running")
2874
2894
  && agent.get("explicit_stop").and_then(serde_json::Value::as_bool) != Some(true)
2875
- && agent.get("pane_id").and_then(serde_json::Value::as_str) == Some(request.pane_id.as_str())
2876
- && agent.get("pane_pid").and_then(serde_json::Value::as_u64) == Some(u64::from(request.pane_pid))
2877
- && request.pane_pid == evidence.binding.pane_pid
2878
- && agent.get("window").and_then(serde_json::Value::as_str).unwrap_or(&request.recipient) == request.window
2879
- && agent.get("provider").cloned().and_then(|value| serde_json::from_value::<Provider>(value).ok()) == Some(request.provider)
2880
- && selected.get("session_name").and_then(serde_json::Value::as_str) == Some(request.session.as_str())
2895
+ && agent.get("pane_id").and_then(serde_json::Value::as_str) == Some(target.pane_id.as_str())
2896
+ && agent.get("pane_pid").and_then(serde_json::Value::as_u64) == Some(u64::from(target.pane_pid))
2897
+ && agent.get("window").and_then(serde_json::Value::as_str).unwrap_or(&request.recipient) == target.window
2898
+ && agent.get("provider").cloned().and_then(|value| serde_json::from_value::<Provider>(value).ok()) == Some(target.provider)
2899
+ && selected.get("session_name").and_then(serde_json::Value::as_str) == Some(target.session.as_str())
2881
2900
  });
2882
2901
  if !matches {
2883
2902
  return Err(crate::state::StateError::SaveConflict("rescue receiver changed before turn commit".into()));
@@ -3414,17 +3433,87 @@ fn text_contains_message_token(text: &str, message_id: &str) -> bool {
3414
3433
  !message_id.is_empty() && text.contains(&message_token_marker(message_id))
3415
3434
  }
3416
3435
 
3417
- /// ---
3418
- /// purpose: 同 message_id 已在 pane 或转录出现则视为已注入
3419
- /// contract:
3420
- /// provides:
3421
- /// - name: token_already_visible
3422
- /// what: capture Tail(80) 或 recipient rollout_path 尾含 `[team-agent-token:{id}]` 则为 true
3423
- /// boundary:
3424
- /// - capture 失败不当已见(倒向允许注入,避免丢信)
3425
- /// - 不请求 Full(wave-2:滚出屏的 trust 残渣不当成已见/可行动)
3426
- /// - 不把 JSON 字段里的 message_id 当 pane token
3427
- /// ---
3436
+ fn marker_occurrences(text: &str, message_id: &str) -> usize {
3437
+ text.lines().map(str::trim).collect::<String>()
3438
+ .matches(&message_token_marker(message_id)).count()
3439
+ }
3440
+
3441
+ /// Provider-specific parsing is behind an observation port, never a delivery branch.
3442
+ /// None retains existing non-native transport receipts; unknown native frames veto success.
3443
+ fn observed_token_consumed(
3444
+ transport: &dyn Transport,
3445
+ state: &serde_json::Value,
3446
+ recipient: &str,
3447
+ target: &Target,
3448
+ message_id: &str,
3449
+ ) -> Option<bool> {
3450
+ let provider = recipient_provider(state, recipient)?;
3451
+ match transport.observe_native_input(target, provider) {
3452
+ Ok(Some(frame)) => Some(frame.composer.is_empty()
3453
+ && marker_occurrences(&frame.transcript, message_id) == 1),
3454
+ Ok(None) => None,
3455
+ Err(_) => Some(false),
3456
+ }
3457
+ }
3458
+
3459
+ fn existing_token_consumed(
3460
+ transport: &dyn Transport,
3461
+ state: &serde_json::Value,
3462
+ recipient: &str,
3463
+ target: &Target,
3464
+ message_id: &str,
3465
+ ) -> bool {
3466
+ observed_token_consumed(transport, state, recipient, target, message_id)
3467
+ .unwrap_or_else(|| recipient_transcript_contains(state, recipient, &message_token_marker(message_id)))
3468
+ }
3469
+
3470
+ #[allow(clippy::too_many_arguments)]
3471
+ fn bind_enter_watchdog(
3472
+ store: &MessageStore,
3473
+ transport: &dyn Transport,
3474
+ state: &serde_json::Value,
3475
+ target: &Target,
3476
+ message_id: &str,
3477
+ message: &PendingMessage,
3478
+ owner_team_id: Option<&str>,
3479
+ event_log: &EventLog,
3480
+ ) -> Result<(), MessagingError> {
3481
+ use crate::transport::enter_rescue::{EnterRescueContext, EnterRescueTarget};
3482
+ use sha2::{Digest, Sha256};
3483
+ let rendered = render_message(&message.sender, message.task_id.as_deref(), &message.content, message_id);
3484
+ let agent = state.get("agents").and_then(|agents| agents.get(&message.recipient));
3485
+ let selected = (|| {
3486
+ let agent = agent?;
3487
+ Some(EnterRescueTarget::selected(
3488
+ recipient_provider(state, &message.recipient)?,
3489
+ agent.get("pane_id")?.as_str()?.to_string(),
3490
+ u32::try_from(agent.get("pane_pid")?.as_u64()?).ok()?,
3491
+ state.get("session_name")?.as_str()?.to_string(),
3492
+ agent.get("window").and_then(serde_json::Value::as_str)
3493
+ .unwrap_or(&message.recipient).to_string(),
3494
+ ))
3495
+ })();
3496
+ let context = EnterRescueContext {
3497
+ message_id: message_id.into(),
3498
+ owner_team_id: owner_team_id.unwrap_or_default().into(),
3499
+ recipient: message.recipient.clone(),
3500
+ payload_sha256: format!("{:x}", Sha256::digest(rendered.as_bytes())),
3501
+ target: selected,
3502
+ proof: None,
3503
+ };
3504
+ match transport.snapshot_enter_rescue(target, &context, &rendered) {
3505
+ Ok(snapshot) => { super::enter_rescue::bind_pending_context(store, &snapshot)?; }
3506
+ Err(error) => {
3507
+ event_log.write("enter_rescue.snapshot_unavailable", serde_json::json!({
3508
+ "message_id": message_id, "reason": error.to_string(),
3509
+ }))?;
3510
+ }
3511
+ }
3512
+ Ok(())
3513
+ }
3514
+
3515
+ /// Visibility prevents a duplicate paste; only a separate consumption receipt permits success.
3516
+ /// Read failures do not manufacture visibility. Capture stays bounded to Tail(80).
3428
3517
  pub(crate) fn token_already_visible(
3429
3518
  transport: &dyn Transport,
3430
3519
  state: &serde_json::Value,
@@ -3551,6 +3640,10 @@ fn rollout_tail_contains_result(
3551
3640
  Ok(haystack.contains(needle))
3552
3641
  }
3553
3642
 
3643
+ #[cfg(test)]
3644
+ #[path = "delivery_consumption_tests.rs"]
3645
+ mod consumption_tests;
3646
+
3554
3647
  #[cfg(test)]
3555
3648
  mod paste_floor_tests {
3556
3649
  use super::*;
@@ -0,0 +1,223 @@
1
+ #![allow(clippy::unwrap_used)]
2
+
3
+ use super::*;
4
+ use crate::transport::{native_input::InputFrame, test_support::OfflineTransport};
5
+ use std::sync::atomic::{AtomicU64, Ordering};
6
+
7
+ struct Fixture {
8
+ workspace: std::path::PathBuf,
9
+ store: MessageStore,
10
+ state: serde_json::Value,
11
+ }
12
+ impl Fixture {
13
+ fn new(provider: &str) -> Self {
14
+ static NEXT: AtomicU64 = AtomicU64::new(0);
15
+ let workspace = std::env::temp_dir().join(format!(
16
+ "ta-consumption-{}-{}",
17
+ std::process::id(),
18
+ NEXT.fetch_add(1, Ordering::Relaxed)
19
+ ));
20
+ std::fs::create_dir_all(&workspace).unwrap();
21
+ let store = MessageStore::open(&workspace).unwrap();
22
+ let conn = crate::db::schema::open_db(store.db_path()).unwrap();
23
+ conn.execute("insert into messages(message_id,owner_team_id,sender,recipient,content,status,created_at,updated_at,delivery_attempts) values('msg_one','team','operator','worker','body','accepted','old','old',0)", []).unwrap();
24
+ let state = serde_json::json!({"team_key":"team","session_name":"session","agents":{"worker":{
25
+ "status":"running","provider":provider,"pane_id":"%1","pane_pid":2,"window":"worker"
26
+ }}});
27
+ Self {
28
+ workspace,
29
+ store,
30
+ state,
31
+ }
32
+ }
33
+ fn transport(&self) -> OfflineTransport {
34
+ OfflineTransport::new()
35
+ .with_session_present(true)
36
+ .with_targets(vec![PaneInfo {
37
+ pane_id: PaneId::new("%1"),
38
+ session: SessionName::new("session"),
39
+ window_index: Some(1),
40
+ window_name: Some(WindowName::new("worker")),
41
+ pane_index: Some(0),
42
+ tty: None,
43
+ current_command: None,
44
+ current_path: Some(self.workspace.clone()),
45
+ active: true,
46
+ pane_pid: Some(2),
47
+ leader_env: Default::default(),
48
+ }])
49
+ .with_default_liveness(crate::transport::PaneLiveness::Live)
50
+ }
51
+ fn deliver(&self, transport: &OfflineTransport) -> DeliveryOutcome {
52
+ let message = message_for_delivery(&self.store, "msg_one")
53
+ .unwrap()
54
+ .unwrap();
55
+ deliver_prepared_message(
56
+ &self.workspace,
57
+ &self.store,
58
+ transport,
59
+ "msg_one",
60
+ &EventLog::new(&self.workspace),
61
+ &self.state,
62
+ message,
63
+ PreparedMessageState {
64
+ scoped: None,
65
+ canonical_owner_team_id: Some("team".into()),
66
+ },
67
+ )
68
+ .unwrap()
69
+ }
70
+ fn receipt(&self) -> (String, i64) {
71
+ let conn = crate::db::schema::open_db(self.store.db_path()).unwrap();
72
+ (
73
+ conn.query_row(
74
+ "select status from messages where message_id='msg_one'",
75
+ [],
76
+ |r| r.get(0),
77
+ )
78
+ .unwrap(),
79
+ conn.query_row(
80
+ "select count(*) from scheduled_events where kind='enter_rescue'",
81
+ [],
82
+ |r| r.get(0),
83
+ )
84
+ .unwrap(),
85
+ )
86
+ }
87
+ }
88
+ impl Drop for Fixture {
89
+ fn drop(&mut self) {
90
+ let _ = std::fs::remove_dir_all(&self.workspace);
91
+ }
92
+ }
93
+ fn frame(composer: &str, transcript: &str) -> InputFrame {
94
+ InputFrame {
95
+ composer: composer.into(),
96
+ transcript: transcript.into(),
97
+ identity: "known-current-ui".into(),
98
+ footer: "owned cwd".into(),
99
+ busy: false,
100
+ capsule: None,
101
+ }
102
+ }
103
+
104
+ #[test]
105
+ fn current_token_in_composer_is_not_duplicate_delivered_or_repasted() {
106
+ let f = Fixture::new("cursor_agent");
107
+ let marker = message_token_marker("msg_one");
108
+ let t = f
109
+ .transport()
110
+ .with_capture_for_pane("%1", &marker)
111
+ .with_native_input_observation(frame(&marker, &marker));
112
+ let out = f.deliver(&t);
113
+ assert!(!out.ok);
114
+ assert_eq!(f.receipt(), ("submitted_unverified".into(), 1));
115
+ assert!(t.inject_payloads().is_empty());
116
+ }
117
+
118
+ #[test]
119
+ fn successful_transport_report_does_not_override_unconsumed_composer() {
120
+ for composer in [
121
+ "[team-agent-token:msg_one]",
122
+ "[Pasted text #1 +25 lines]",
123
+ "foreign draft",
124
+ ] {
125
+ let f = Fixture::new("cursor_agent");
126
+ let t = f
127
+ .transport()
128
+ .with_native_input_observation(frame(composer, ""));
129
+ let out = f.deliver(&t);
130
+ assert!(!out.ok, "composer={composer}");
131
+ assert_eq!(f.receipt(), ("submitted_unverified".into(), 1));
132
+ assert_eq!(t.inject_payloads().len(), 1);
133
+ }
134
+ }
135
+
136
+ #[test]
137
+ fn unknown_current_native_surface_is_unverified_not_success() {
138
+ let f = Fixture::new("pi");
139
+ let t = f
140
+ .transport()
141
+ .with_native_input_error("native_surface_unknown");
142
+ assert!(!f.deliver(&t).ok);
143
+ assert_eq!(f.receipt(), ("submitted_unverified".into(), 1));
144
+ }
145
+
146
+ #[test]
147
+ fn pane_visibility_without_an_observation_protocol_is_not_duplicate_acceptance() {
148
+ let f = Fixture::new("fake");
149
+ let t = f
150
+ .transport()
151
+ .with_capture_for_pane("%1", message_token_marker("msg_one"));
152
+ assert!(!f.deliver(&t).ok);
153
+ assert_eq!(f.receipt(), ("submitted_unverified".into(), 1));
154
+ assert!(t.inject_payloads().is_empty());
155
+ }
156
+
157
+ #[test]
158
+ fn current_empty_composer_and_unique_transcript_token_prove_consumption() {
159
+ let f = Fixture::new("cursor_agent");
160
+ let marker = message_token_marker("msg_one");
161
+ let t = f
162
+ .transport()
163
+ .with_capture_for_pane("%1", &marker)
164
+ .with_native_input_observation(frame("", &marker));
165
+ assert!(f.deliver(&t).ok);
166
+ assert_eq!(f.receipt(), ("delivered".into(), 0));
167
+ assert!(t.inject_payloads().is_empty());
168
+ }
169
+
170
+ #[test]
171
+ fn busy_answers_or_duplicate_history_tokens_do_not_prove_consumption() {
172
+ let f = Fixture::new("cursor_agent");
173
+ let target = Target::Pane(PaneId::new("%1"));
174
+ for transcript in [
175
+ "answer is 5535",
176
+ "[team-agent-token:msg_one]\n[team-agent-token:msg_one]",
177
+ ] {
178
+ let mut view = frame("", transcript);
179
+ view.busy = true;
180
+ let t = f.transport().with_native_input_observation(view);
181
+ assert_eq!(
182
+ observed_token_consumed(&t, &f.state, "worker", &target, "msg_one"),
183
+ Some(false)
184
+ );
185
+ }
186
+ }
187
+
188
+ #[test]
189
+ fn unsupported_observation_keeps_existing_positive_transport_receipt() {
190
+ let f = Fixture::new("fake");
191
+ assert!(f.deliver(&f.transport()).ok);
192
+ assert_eq!(f.receipt(), ("delivered".into(), 0));
193
+ }
194
+
195
+ #[test]
196
+ fn mark_unverified_schedules_once_and_insert_failure_rolls_back_status() {
197
+ let f = Fixture::new("fake");
198
+ f.store
199
+ .mark("msg_one", "submitted_unverified", Some("opaque no proof"))
200
+ .unwrap();
201
+ f.store
202
+ .mark("msg_one", "submitted_unverified", Some("again"))
203
+ .unwrap();
204
+ assert_eq!(f.receipt(), ("submitted_unverified".into(), 1));
205
+ let conn = crate::db::schema::open_db(f.store.db_path()).unwrap();
206
+ let payload: String = conn
207
+ .query_row("select payload_json from scheduled_events", [], |r| {
208
+ r.get(0)
209
+ })
210
+ .unwrap();
211
+ let job: serde_json::Value = serde_json::from_str(&payload).unwrap();
212
+ assert!(job["context"]["target"].is_null());
213
+ assert!(job["context"]["proof"].is_null());
214
+
215
+ let g = Fixture::new("fake");
216
+ let conn = crate::db::schema::open_db(g.store.db_path()).unwrap();
217
+ conn.execute_batch("create trigger refuse_rescue before insert on scheduled_events begin select raise(abort,'fixture failure'); end;").unwrap();
218
+ assert!(g
219
+ .store
220
+ .mark("msg_one", "submitted_unverified", None)
221
+ .is_err());
222
+ assert_eq!(g.receipt(), ("accepted".into(), 0));
223
+ }