@team-agent/installer 0.5.116 → 0.5.118

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.
@@ -0,0 +1,566 @@
1
+ #![allow(clippy::unwrap_used, clippy::panic)]
2
+ use super::*;
3
+ use crate::model::enums::Provider;
4
+ use crate::transport::native_input::{NativeInputBinding, NativeInputRequest};
5
+ use std::sync::atomic::{AtomicUsize, Ordering};
6
+
7
+ struct Fixture {
8
+ workspace: std::path::PathBuf,
9
+ store: MessageStore,
10
+ log: EventLog,
11
+ evidence: NativeInputEvidence,
12
+ }
13
+ impl Fixture {
14
+ fn new() -> Self {
15
+ static NEXT: AtomicUsize = AtomicUsize::new(0);
16
+ let workspace = std::env::temp_dir().join(format!(
17
+ "ta-enter-rescue-{}-{}",
18
+ std::process::id(),
19
+ NEXT.fetch_add(1, Ordering::SeqCst)
20
+ ));
21
+ std::fs::create_dir(&workspace).unwrap();
22
+ let store = MessageStore::open(&workspace).unwrap();
23
+ let log = EventLog::new(&workspace);
24
+ let request = NativeInputRequest {
25
+ message_id: "msg_one".into(),
26
+ owner_team_id: "team".into(),
27
+ recipient: "worker".into(),
28
+ provider: Provider::Pi,
29
+ pane_id: "%1".into(),
30
+ pane_pid: 2,
31
+ session: "session".into(),
32
+ window: "worker".into(),
33
+ };
34
+ let evidence = NativeInputEvidence {
35
+ request,
36
+ binding: NativeInputBinding {
37
+ endpoint: "/tmp/owned-test.sock".into(),
38
+ pane_id: "%1".into(),
39
+ pane_pid: 2,
40
+ pane_height: 120,
41
+ carrier_birth: "test-birth".into(),
42
+ binding_nonce: "test-nonce".into(),
43
+ session: "session".into(),
44
+ window: "worker".into(),
45
+ },
46
+ payload_sha256: digest(&render_message("leader", None, "body", "msg_one")),
47
+ ui_identity: "gpt-6-luna".into(),
48
+ footer_sha256: digest(""),
49
+ baseline_sha256: digest("baseline"),
50
+ baseline_capsules: vec![],
51
+ phase: NativeInputPhase::Submitted,
52
+ capsule: None,
53
+ };
54
+ let fixture = Self {
55
+ workspace,
56
+ store,
57
+ log,
58
+ evidence,
59
+ };
60
+ let conn = fixture.conn();
61
+ conn.execute("insert into messages(message_id,owner_team_id,sender,recipient,content,status,created_at,updated_at,delivery_attempts) values('msg_one','team','leader','worker','body','target_resolved','old','old',1)", []).unwrap();
62
+ fixture.state("running", false);
63
+ fixture
64
+ }
65
+ fn conn(&self) -> rusqlite::Connection {
66
+ crate::db::schema::open_db(self.store.db_path()).unwrap()
67
+ }
68
+ fn state(&self, status: &str, stopped: bool) {
69
+ let state = serde_json::json!({"team_key":"team","session_name":"session","agents":{"worker":{"status":status,"explicit_stop":stopped,"provider":"pi","pane_id":"%1","pane_pid":2,"window":"worker"}}});
70
+ std::fs::write(
71
+ crate::model::paths::runtime_dir(&self.workspace).join("state.json"),
72
+ state.to_string(),
73
+ )
74
+ .unwrap();
75
+ }
76
+ fn park(&self, age: i64) {
77
+ park_at(
78
+ &self.store,
79
+ &self.evidence,
80
+ 1,
81
+ "uncertain",
82
+ &self.log,
83
+ Utc::now() - Duration::seconds(age),
84
+ )
85
+ .unwrap();
86
+ }
87
+ fn job(&self) -> (i64, String, String) {
88
+ self.conn()
89
+ .query_row(
90
+ "select id,payload_json,status from scheduled_events",
91
+ [],
92
+ |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
93
+ )
94
+ .unwrap()
95
+ }
96
+ fn message_status(&self) -> String {
97
+ self.conn()
98
+ .query_row(
99
+ "select status from messages where message_id='msg_one'",
100
+ [],
101
+ |row| row.get(0),
102
+ )
103
+ .unwrap()
104
+ }
105
+ }
106
+ impl Drop for Fixture {
107
+ fn drop(&mut self) {
108
+ let _ = std::fs::remove_dir_all(&self.workspace);
109
+ }
110
+ }
111
+
112
+ #[test]
113
+ fn enter_rescue_first_successful_key_arms_original_turn_before_acceptance_poll() {
114
+ let f = Fixture::new();
115
+ f.park(95);
116
+ super::super::delivery::with_rescue_submit_observer(
117
+ &f.workspace,
118
+ &f.store,
119
+ &f.evidence,
120
+ &f.log,
121
+ |observer| {
122
+ observer.after_physical_submit();
123
+ let state = crate::state::persist::load_runtime_state(&f.workspace).unwrap();
124
+ assert_eq!(
125
+ state["agents"]["worker"]["current_turn_message_id"],
126
+ "msg_one"
127
+ );
128
+ Ok(EnterRescueOutcome::Entered { accepted: false })
129
+ },
130
+ )
131
+ .unwrap();
132
+ assert_eq!(f.message_status(), "submitted_unverified");
133
+ }
134
+
135
+ #[test]
136
+ fn enter_rescue_post_key_owner_or_receiver_loss_is_unknown_without_cross_team_write() {
137
+ for change in [
138
+ "owner",
139
+ "receiver",
140
+ "pane",
141
+ "pane_pid",
142
+ "missing_pane_pid",
143
+ "session",
144
+ "stopped",
145
+ ] {
146
+ let f = Fixture::new();
147
+ f.park(95);
148
+ let (id, payload, _) = f.job();
149
+ let effects = AtomicUsize::new(0);
150
+ let path = crate::model::paths::runtime_dir(&f.workspace).join("state.json");
151
+ let replacement = std::cell::RefCell::new(String::new());
152
+ // Controlled post-key mutation; this is not a native TUI reproduction.
153
+ fire_with(
154
+ &f.workspace,
155
+ &f.store,
156
+ &f.log,
157
+ id,
158
+ &payload,
159
+ &|evidence, _| {
160
+ super::super::delivery::with_rescue_submit_observer(
161
+ &f.workspace,
162
+ &f.store,
163
+ evidence,
164
+ &f.log,
165
+ |observer| {
166
+ effects.fetch_add(1, Ordering::SeqCst);
167
+ let mut state =
168
+ crate::state::persist::load_runtime_state(&f.workspace).unwrap();
169
+ match change {
170
+ "owner" => {
171
+ state["team_key"] = "other".into();
172
+ state["active_team_key"] = "other".into();
173
+ state.as_object_mut().unwrap().remove("teams");
174
+ }
175
+ "receiver" => {
176
+ state["agents"].as_object_mut().unwrap().remove("worker");
177
+ }
178
+ "pane" => state["agents"]["worker"]["pane_id"] = "%2".into(),
179
+ "pane_pid" => state["agents"]["worker"]["pane_pid"] = 3.into(),
180
+ "missing_pane_pid" => {
181
+ state["agents"]["worker"]
182
+ .as_object_mut()
183
+ .unwrap()
184
+ .remove("pane_pid");
185
+ }
186
+ "session" => state["session_name"] = "other-session".into(),
187
+ "stopped" => state["agents"]["worker"]["explicit_stop"] = true.into(),
188
+ _ => unreachable!(),
189
+ }
190
+ *replacement.borrow_mut() = state.to_string();
191
+ std::fs::write(&path, replacement.borrow().as_bytes()).unwrap();
192
+ observer.after_physical_submit();
193
+ Ok(EnterRescueOutcome::Entered { accepted: true })
194
+ },
195
+ )
196
+ },
197
+ )
198
+ .unwrap();
199
+ // The rejected commit must not change even one byte of the replacement state.
200
+ let expected = replacement.borrow().clone();
201
+ assert_eq!(std::fs::read_to_string(&path).unwrap(), expected);
202
+ assert_eq!(f.job().2, "unknown", "{change}");
203
+ assert_eq!(f.message_status(), "submitted_unverified");
204
+ let state: serde_json::Value = serde_json::from_str(&expected).unwrap();
205
+ assert!(
206
+ state["agents"]["worker"]["current_turn_message_id"].is_null(),
207
+ "{change}"
208
+ );
209
+ assert!(!fire_with(
210
+ &f.workspace,
211
+ &f.store,
212
+ &f.log,
213
+ id,
214
+ &payload,
215
+ &|_, _| panic!("post-key error replay")
216
+ )
217
+ .unwrap());
218
+ assert_eq!(std::fs::read_to_string(&path).unwrap(), expected);
219
+ assert_eq!(effects.load(Ordering::SeqCst), 1);
220
+ }
221
+ }
222
+
223
+ #[test]
224
+ fn enter_rescue_owner_loss_at_state_save_hook_refuses_without_default_fallback() {
225
+ let f = Fixture::new();
226
+ f.park(95);
227
+ let path = crate::model::paths::runtime_dir(&f.workspace).join("state.json");
228
+ let replacement = serde_json::json!({"team_key":"other","active_team_key":"other","session_name":"session","agents":{"worker":{"provider":"pi","status":"running","pane_id":"%1","pane_pid":2,"window":"worker"}}}).to_string();
229
+ let hook_path = path.clone();
230
+ let hook_state = replacement.clone();
231
+ crate::state::repository::test_support::set_before_commit(move || {
232
+ std::fs::write(hook_path, hook_state).unwrap();
233
+ });
234
+ assert!(super::super::delivery::with_rescue_submit_observer(
235
+ &f.workspace,
236
+ &f.store,
237
+ &f.evidence,
238
+ &f.log,
239
+ |observer| {
240
+ observer.after_physical_submit();
241
+ Ok(EnterRescueOutcome::Entered { accepted: true })
242
+ },
243
+ )
244
+ .is_err());
245
+ assert_eq!(std::fs::read_to_string(path).unwrap(), replacement);
246
+ assert_eq!(f.message_status(), "submitted_unverified");
247
+ }
248
+
249
+ #[test]
250
+ fn enter_rescue_switched_top_projection_updates_only_retained_original_owner() {
251
+ let f = Fixture::new();
252
+ f.park(95);
253
+ super::super::delivery::with_rescue_submit_observer(
254
+ &f.workspace, &f.store, &f.evidence, &f.log, |observer| {
255
+ let original = crate::state::persist::load_runtime_state(&f.workspace).unwrap();
256
+ let mut other = original.clone();
257
+ other["team_key"] = "other".into();
258
+ other["active_team_key"] = "other".into();
259
+ other["session_name"] = "other-session".into();
260
+ other["agents"]["worker"]["pane_id"] = "%2".into();
261
+ other["agents"]["worker"]["pane_pid"] = 3.into();
262
+ let mut state = other.clone();
263
+ state["teams"] = serde_json::json!({"team": crate::state::projection::compact_team_state(&original), "other": crate::state::projection::compact_team_state(&other)});
264
+ std::fs::write(crate::model::paths::runtime_dir(&f.workspace).join("state.json"), state.to_string()).unwrap();
265
+ observer.after_physical_submit();
266
+ Ok(EnterRescueOutcome::Entered { accepted: false })
267
+ },
268
+ ).unwrap();
269
+ let state = crate::state::persist::load_runtime_state(&f.workspace).unwrap();
270
+ assert_eq!(
271
+ state["teams"]["team"]["agents"]["worker"]["current_turn_message_id"],
272
+ "msg_one"
273
+ );
274
+ assert!(state["teams"]["other"]["agents"]["worker"]["current_turn_message_id"].is_null());
275
+ assert!(state["agents"]["worker"]["current_turn_message_id"].is_null());
276
+ assert_eq!(state["agents"]["worker"]["pane_id"], "%2");
277
+ }
278
+
279
+ #[test]
280
+ fn enter_rescue_atomic_park_and_due90_no_early_effect() {
281
+ let f = Fixture::new();
282
+ f.park(0);
283
+ let (id, payload, status) = f.job();
284
+ assert_eq!(status, "pending");
285
+ assert_eq!(f.message_status(), "submitted_unverified");
286
+ let times: (String, String, String) = f
287
+ .conn()
288
+ .query_row(
289
+ "select owner_team_id,created_at,due_at from scheduled_events",
290
+ [],
291
+ |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?)),
292
+ )
293
+ .unwrap();
294
+ assert_eq!(times.0, "team");
295
+ assert_eq!(
296
+ DateTime::parse_from_rfc3339(&times.2).unwrap()
297
+ - DateTime::parse_from_rfc3339(&times.1).unwrap(),
298
+ Duration::seconds(90)
299
+ );
300
+ assert!(!fire_with(
301
+ &f.workspace,
302
+ &f.store,
303
+ &f.log,
304
+ id,
305
+ &payload,
306
+ &|_, _| panic!("not due")
307
+ )
308
+ .unwrap());
309
+ assert!(park_at(&f.store, &f.evidence, 1, "again", &f.log, Utc::now()).is_err());
310
+ assert_eq!(
311
+ f.conn()
312
+ .query_row("select count(*) from scheduled_events", [], |r| r
313
+ .get::<_, i64>(0))
314
+ .unwrap(),
315
+ 1
316
+ );
317
+ }
318
+
319
+ #[test]
320
+ fn enter_rescue_insert_failure_rolls_back_original_uncertainty() {
321
+ let f = Fixture::new();
322
+ f.conn().execute_batch("create trigger refuse_job before insert on scheduled_events begin select raise(abort,'fixture'); end;").unwrap();
323
+ assert!(park_at(&f.store, &f.evidence, 1, "uncertain", &f.log, Utc::now()).is_err());
324
+ assert_eq!(f.message_status(), "target_resolved");
325
+ }
326
+
327
+ #[test]
328
+ fn enter_rescue_no_effect_deferral_releases_only_exact_unspent_claim() {
329
+ let f = Fixture::new();
330
+ release_no_effect(&f.store, "msg_one", 2).unwrap();
331
+ assert_eq!(
332
+ f.conn()
333
+ .query_row("select delivery_attempts from messages", [], |r| r
334
+ .get::<_, i64>(0))
335
+ .unwrap(),
336
+ 1
337
+ );
338
+ release_no_effect(&f.store, "msg_one", 1).unwrap();
339
+ assert_eq!(
340
+ f.conn()
341
+ .query_row("select delivery_attempts from messages", [], |r| r
342
+ .get::<_, i64>(0))
343
+ .unwrap(),
344
+ 0
345
+ );
346
+ assert!(f.store.claim_for_delivery("msg_one").unwrap());
347
+ assert_eq!(
348
+ f.conn()
349
+ .query_row("select delivery_attempts from messages", [], |r| r
350
+ .get::<_, i64>(0))
351
+ .unwrap(),
352
+ 1
353
+ );
354
+ }
355
+
356
+ #[test]
357
+ fn enter_rescue_canonical_stopped_changed_consumed_expired_and_superseded_refuse() {
358
+ for case in [
359
+ "stopped",
360
+ "explicit_stop",
361
+ "content",
362
+ "owner",
363
+ "consumed",
364
+ "expired",
365
+ "superseded",
366
+ "job_scope",
367
+ ] {
368
+ let f = Fixture::new();
369
+ f.park(if case == "expired" { 121 } else { 95 });
370
+ match case {
371
+ "stopped" => f.state("stopped", false),
372
+ "explicit_stop" => f.state("running", true),
373
+ "content" => {
374
+ f.conn()
375
+ .execute("update messages set content='changed'", [])
376
+ .unwrap();
377
+ }
378
+ "owner" => {
379
+ f.conn()
380
+ .execute("update messages set owner_team_id='other'", [])
381
+ .unwrap();
382
+ }
383
+ "consumed" => {
384
+ f.conn()
385
+ .execute("update messages set status='acknowledged'", [])
386
+ .unwrap();
387
+ }
388
+ "superseded" => {
389
+ f.conn().execute("insert into messages(message_id,owner_team_id,recipient,status,created_at,delivery_attempts) values('msg_two','team','worker','submitted_unverified','z',1)",[]).unwrap();
390
+ }
391
+ "job_scope" => {
392
+ f.conn()
393
+ .execute("update scheduled_events set owner_team_id='other'", [])
394
+ .unwrap();
395
+ }
396
+ "expired" => {}
397
+ _ => unreachable!(),
398
+ }
399
+ let (id, payload, _) = f.job();
400
+ assert!(fire_with(
401
+ &f.workspace,
402
+ &f.store,
403
+ &f.log,
404
+ id,
405
+ &payload,
406
+ &|_, _| panic!("{case}: forbidden effect")
407
+ )
408
+ .unwrap());
409
+ assert_eq!(f.job().2, "refused", "{case}");
410
+ }
411
+ }
412
+
413
+ #[test]
414
+ fn enter_rescue_no_effect_can_reobserve_but_unknown_or_claimed_never_replays() {
415
+ let f = Fixture::new();
416
+ f.park(95);
417
+ let (id, payload, _) = f.job();
418
+ assert!(fire_with(
419
+ &f.workspace,
420
+ &f.store,
421
+ &f.log,
422
+ id,
423
+ &payload,
424
+ &|_, authorize| {
425
+ assert!(authorize());
426
+ Ok(EnterRescueOutcome::NoEffect {
427
+ reason: "busy",
428
+ reobserve: true,
429
+ })
430
+ }
431
+ )
432
+ .unwrap());
433
+ assert_eq!(f.job().2, "pending");
434
+ assert!(!fire_with(
435
+ &f.workspace,
436
+ &f.store,
437
+ &f.log,
438
+ id,
439
+ &payload,
440
+ &|_, _| panic!("not yet due")
441
+ )
442
+ .unwrap());
443
+ f.conn()
444
+ .execute("update scheduled_events set due_at='0'", [])
445
+ .unwrap();
446
+ assert!(fire_with(
447
+ &f.workspace,
448
+ &f.store,
449
+ &f.log,
450
+ id,
451
+ &payload,
452
+ &|_, authorize| {
453
+ assert!(authorize());
454
+ Ok(EnterRescueOutcome::Entered { accepted: false })
455
+ }
456
+ )
457
+ .unwrap());
458
+ assert_eq!(f.job().2, "unknown");
459
+ assert_eq!(f.message_status(), "submitted_unverified");
460
+ assert!(!fire_with(
461
+ &f.workspace,
462
+ &f.store,
463
+ &f.log,
464
+ id,
465
+ &payload,
466
+ &|_, _| panic!("unknown replay")
467
+ )
468
+ .unwrap());
469
+ f.conn()
470
+ .execute("update scheduled_events set status='claimed'", [])
471
+ .unwrap();
472
+ assert!(!fire_with(
473
+ &f.workspace,
474
+ &f.store,
475
+ &f.log,
476
+ id,
477
+ &payload,
478
+ &|_, _| panic!("crash replay")
479
+ )
480
+ .unwrap());
481
+ }
482
+
483
+ #[test]
484
+ fn enter_rescue_concurrent_claim_is_one_effect_and_updates_original_only() {
485
+ let f = Fixture::new();
486
+ f.park(95);
487
+ let (id, payload, _) = f.job();
488
+ let effects = AtomicUsize::new(0);
489
+ std::thread::scope(|scope| {
490
+ for _ in 0..2 {
491
+ scope.spawn(|| {
492
+ fire_with(
493
+ &f.workspace,
494
+ &f.store,
495
+ &f.log,
496
+ id,
497
+ &payload,
498
+ &|_, authorize| {
499
+ assert!(authorize());
500
+ effects.fetch_add(1, Ordering::SeqCst);
501
+ std::thread::sleep(std::time::Duration::from_millis(20));
502
+ Ok(EnterRescueOutcome::Entered { accepted: true })
503
+ },
504
+ )
505
+ .unwrap()
506
+ });
507
+ }
508
+ });
509
+ assert_eq!(effects.load(Ordering::SeqCst), 1);
510
+ assert_eq!(f.job().2, "done");
511
+ assert_eq!(f.message_status(), "delivered");
512
+ assert_eq!(
513
+ f.conn()
514
+ .query_row("select count(*) from messages", [], |r| r.get::<_, i64>(0))
515
+ .unwrap(),
516
+ 1
517
+ );
518
+ }
519
+
520
+ #[test]
521
+ fn enter_rescue_revalidates_at_effect_and_keeps_unknown_errors_terminal() {
522
+ let f = Fixture::new();
523
+ f.park(95);
524
+ let (id, payload, _) = f.job();
525
+ fire_with(
526
+ &f.workspace,
527
+ &f.store,
528
+ &f.log,
529
+ id,
530
+ &payload,
531
+ &|_, authorize| {
532
+ f.state("stopped", true);
533
+ assert!(!authorize());
534
+ Ok(EnterRescueOutcome::NoEffect {
535
+ reason: "canonical_changed",
536
+ reobserve: false,
537
+ })
538
+ },
539
+ )
540
+ .unwrap();
541
+ assert_eq!(f.job().2, "refused");
542
+ let other = Fixture::new();
543
+ other.park(95);
544
+ let (id, payload, _) = other.job();
545
+ fire_with(
546
+ &other.workspace,
547
+ &other.store,
548
+ &other.log,
549
+ id,
550
+ &payload,
551
+ &|_, _| {
552
+ Err(crate::transport::TransportError::Io(std::io::Error::other("may have sent")).into())
553
+ },
554
+ )
555
+ .unwrap();
556
+ assert_eq!(other.job().2, "unknown");
557
+ assert!(!fire_with(
558
+ &other.workspace,
559
+ &other.store,
560
+ &other.log,
561
+ id,
562
+ &payload,
563
+ &|_, _| panic!("error replay")
564
+ )
565
+ .unwrap());
566
+ }
@@ -107,6 +107,7 @@ pub(crate) fn parse_scheduled_kind(kind: &str) -> Result<ScheduledKind, Messagin
107
107
  "send" => Ok(ScheduledKind::Send),
108
108
  "health_ping" => Ok(ScheduledKind::HealthPing),
109
109
  "trust_retry" => Ok(ScheduledKind::TrustRetry),
110
+ "enter_rescue" => Ok(ScheduledKind::EnterRescue),
110
111
  other => Err(MessagingError::Validation(format!(
111
112
  "unknown scheduled event kind: {other}"
112
113
  ))),
@@ -64,6 +64,7 @@ use crate::transport::{PaneId, Target, Transport};
64
64
  pub mod activity;
65
65
  pub mod address;
66
66
  pub mod delivery;
67
+ mod enter_rescue;
67
68
  pub mod helpers;
68
69
  pub mod leader_channel;
69
70
  pub mod leader_receiver;
@@ -40,6 +40,13 @@ pub fn fire_due_scheduled_events(
40
40
  };
41
41
  let mut fired = Vec::new();
42
42
  for (id, kind, target, payload_json) in due_events {
43
+ if matches!(parse_scheduled_kind(&kind), Ok(ScheduledKind::EnterRescue)) {
44
+ // This kind owns its pre-effect CAS and terminal/no-effect transitions.
45
+ if super::enter_rescue::fire(workspace, store, transport, event_log, id, &payload_json)? {
46
+ fired.push(id);
47
+ }
48
+ continue;
49
+ }
43
50
  // U1 #5: per-event isolation — one poison event (bad payload / fire error) must not
44
51
  // halt the whole pass. The fire body is confined to a closure; on Err we emit
45
52
  // `scheduler.event_failed`, mark the row terminal 'failed' (no re-fire), and continue.
@@ -80,6 +87,7 @@ pub fn fire_due_scheduled_events(
80
87
  "message_id": outcome.message_id,
81
88
  })
82
89
  }
90
+ ScheduledKind::EnterRescue => return Err(MessagingError::Validation("enter rescue requires atomic dispatcher".into())),
83
91
  ScheduledKind::HealthPing => {
84
92
  event_log.write(
85
93
  "scheduled.health_ping",
@@ -117,6 +117,8 @@ fn scheduled_kind_serde_byte_locked() {
117
117
  serde_json::to_string(&ScheduledKind::TrustRetry).unwrap(),
118
118
  "\"trust_retry\""
119
119
  );
120
+ assert_eq!(serde_json::to_string(&ScheduledKind::EnterRescue).unwrap(), "\"enter_rescue\"");
121
+ assert_eq!(super::super::helpers::parse_scheduled_kind("enter_rescue").unwrap(), ScheduledKind::EnterRescue);
120
122
  }
121
123
 
122
124
  #[test]
@@ -735,7 +735,7 @@ fn resource188_batch_and_public_single_prepare_once_and_keep_fresh_leader_load()
735
735
  }
736
736
 
737
737
  #[test]
738
- fn resource188_busy_and_refused_candidates_prepare_once_without_claiming() {
738
+ fn resource188_busy_reaches_claim_but_owner_refusal_does_not() {
739
739
  use crate::messaging::delivery::preparation_probe::COUNTS;
740
740
  let mut case = Case::new("188-busy");
741
741
  case.state["agents"] = serde_json::json!({"worker": {"agent_id": "worker", "status": "busy"}});
@@ -767,14 +767,15 @@ fn resource188_busy_and_refused_candidates_prepare_once_without_claiming() {
767
767
  Some("missing-team"),
768
768
  )
769
769
  .unwrap();
770
- let before = case.message_status(&busy);
771
770
  COUNTS.with(|counts| counts.set((0, 0, 0)));
772
771
  deliver_pending_messages(&case.workspace, &case.state, &transport, &case.event_log).unwrap();
773
- assert_eq!(COUNTS.with(|counts| counts.get()), (2, 2, 0));
774
- assert_eq!(case.message_status(&busy), before);
772
+ assert_eq!(COUNTS.with(|counts| counts.get()), (2, 2, 1));
773
+ assert_ne!(case.message_status(&busy), "accepted");
775
774
  assert_ne!(case.message_status(&refused), "delivered");
775
+ // This leader-only transport has no worker target. Removing idle admission
776
+ // must not also remove target binding: the claimed row still cannot inject.
776
777
  assert_eq!(transport.inject_count(), 0);
777
- assert!(case.events().contains("send.deferred_busy"));
778
+ assert!(!case.events().contains("send.deferred_busy"));
778
779
  assert!(case.events().contains("delivery.projection_refused"));
779
780
  }
780
781