@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.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/README.md +46 -1
- package/README.zh.md +46 -1
- package/crates/team-agent/src/cli/adapters.rs +11 -0
- package/crates/team-agent/src/cli/arguments.rs +401 -0
- package/crates/team-agent/src/cli/emit.rs +241 -142
- package/crates/team-agent/src/cli/intent/runtime.rs +1139 -0
- package/crates/team-agent/src/cli/intent/runtime_tests.rs +584 -0
- package/crates/team-agent/src/cli/intent/tests.rs +654 -0
- package/crates/team-agent/src/cli/intent.rs +642 -0
- package/crates/team-agent/src/cli/mod.rs +49 -5
- package/crates/team-agent/src/cli/named_address.rs +10 -1
- package/crates/team-agent/src/cli/spec.rs +22 -10
- package/crates/team-agent/src/cli/status_port/brief.rs +117 -16
- package/crates/team-agent/src/cli/tests/base.rs +6 -1
- package/crates/team-agent/src/cli/tests/parser_safety.rs +383 -0
- package/crates/team-agent/src/cli/tests/run_delegation.rs +63 -19
- package/crates/team-agent/src/cli/tests/verb_install_skill.rs +3 -2
- package/crates/team-agent/src/cli/tests/verb_profile.rs +3 -1
- package/crates/team-agent/src/contract_runtime/backend.rs +1 -1
- package/crates/team-agent/src/db/message_store.rs +10 -3
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +166 -77
- package/crates/team-agent/src/lifecycle/launch.rs +28 -0
- package/crates/team-agent/src/lifecycle/restart.rs +17 -0
- package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +1 -3
- package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +5 -15
- package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +2 -3
- package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +2 -3
- package/crates/team-agent/src/lifecycle/tests/cli_argv_routing_red.rs +10 -2
- package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +8 -1
- package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +3 -2
- package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +6 -9
- package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +7 -5
- package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +4 -2
- package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +5 -2
- package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +4 -3
- package/crates/team-agent/src/lifecycle/tests/grok_effort_argv_red.rs +2 -4
- package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +3 -4
- package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +5 -4
- package/crates/team-agent/src/lifecycle/tests/grok_restart_resume_red.rs +2 -4
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +78 -36
- package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +8 -4
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +6 -12
- package/crates/team-agent/src/lifecycle/tests/pi_fork_session_inheritance_red.rs +4 -0
- package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +1 -3
- package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +7 -0
- package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +1 -3
- package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +2 -1
- package/crates/team-agent/src/lifecycle/types.rs +10 -0
- package/crates/team-agent/src/messaging/delivery.rs +132 -39
- package/crates/team-agent/src/messaging/delivery_consumption_tests.rs +223 -0
- package/crates/team-agent/src/messaging/enter_rescue.rs +182 -154
- package/crates/team-agent/src/messaging/enter_rescue_tests.rs +344 -277
- package/crates/team-agent/src/messaging/mod.rs +1 -1
- package/crates/team-agent/src/tmux_backend/enter_rescue.rs +325 -0
- package/crates/team-agent/src/tmux_backend/enter_rescue_tests.rs +432 -0
- package/crates/team-agent/src/tmux_backend/native_input.rs +19 -0
- package/crates/team-agent/src/tmux_backend/tests.rs +37 -0
- package/crates/team-agent/src/tmux_backend.rs +35 -1
- package/crates/team-agent/src/transport/enter_rescue.rs +166 -0
- package/crates/team-agent/src/transport/native_input.rs +5 -1
- package/crates/team-agent/src/transport/test_support.rs +25 -0
- package/crates/team-agent/src/transport.rs +34 -0
- package/package.json +4 -4
- package/skills/team-agent/SKILL.md +71 -54
- package/skills/team-agent/command-coverage.json +283 -13
- package/skills/team-agent/docs/reference/team-agent-operator.md +220 -0
- package/skills/team-agent/references/bug-as-artifact-flow.md +1 -1
- package/skills/team-agent/references/recovery-runbook.md +61 -212
- package/skills/team-agent/references/team-agent-operator.md +181 -48
- 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":
|
|
623
|
+
"reason": reason,
|
|
624
|
+
"consumed": consumed,
|
|
620
625
|
}),
|
|
621
626
|
)?;
|
|
622
|
-
store.mark(message_id,
|
|
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:
|
|
625
|
-
status: DeliveryStatus::AlreadyDelivered,
|
|
626
|
-
message_status: MessageStatusShadow(
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
2782
|
-
target_session:
|
|
2783
|
-
target_window:
|
|
2784
|
-
target_pane_id:
|
|
2785
|
-
target_pane_pid: Some(
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
2846
|
-
record_rescue_turn_after_submit(self.workspace, self.sender,
|
|
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
|
-
|
|
2878
|
+
context: &crate::transport::enter_rescue::EnterRescueContext,
|
|
2861
2879
|
event_log: &EventLog,
|
|
2862
2880
|
metadata: Option<&TargetMetadata>,
|
|
2863
2881
|
) -> Result<(), MessagingError> {
|
|
2864
|
-
let 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(
|
|
2876
|
-
&& agent.get("pane_pid").and_then(serde_json::Value::as_u64) == Some(u64::from(
|
|
2877
|
-
&& request.
|
|
2878
|
-
&& agent.get("
|
|
2879
|
-
&&
|
|
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
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
///
|
|
3423
|
-
///
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
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
|
+
}
|