@team-agent/installer 0.5.117 → 0.5.119
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/crates/team-agent/src/cli/mod.rs +20 -0
- package/crates/team-agent/src/db/message_store.rs +10 -3
- package/crates/team-agent/src/leader/start.rs +27 -47
- package/crates/team-agent/src/lifecycle/tests/pi_session_delivery_red.rs +2 -7
- package/crates/team-agent/src/messaging/delivery.rs +157 -136
- 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/messaging/tests/leader_inject_acceptance.rs +6 -5
- package/crates/team-agent/src/messaging/tests/spine.rs +27 -28
- 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/native_input_tests.rs +141 -4
- package/crates/team-agent/src/tmux_backend.rs +27 -0
- package/crates/team-agent/src/transport/enter_rescue.rs +166 -0
- package/crates/team-agent/src/transport/native_input.rs +136 -10
- 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
|
@@ -1,64 +1,58 @@
|
|
|
1
1
|
#![allow(clippy::unwrap_used, clippy::panic)]
|
|
2
2
|
use super::*;
|
|
3
3
|
use crate::model::enums::Provider;
|
|
4
|
-
use crate::transport::
|
|
4
|
+
use crate::transport::enter_rescue::{EnterRescueProof, EnterRescueTarget};
|
|
5
|
+
use std::cell::Cell;
|
|
5
6
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
6
7
|
|
|
7
8
|
struct Fixture {
|
|
8
9
|
workspace: std::path::PathBuf,
|
|
9
10
|
store: MessageStore,
|
|
10
11
|
log: EventLog,
|
|
11
|
-
|
|
12
|
+
context: EnterRescueContext,
|
|
12
13
|
}
|
|
13
14
|
impl Fixture {
|
|
14
15
|
fn new() -> Self {
|
|
15
16
|
static NEXT: AtomicUsize = AtomicUsize::new(0);
|
|
16
17
|
let workspace = std::env::temp_dir().join(format!(
|
|
17
|
-
"ta-
|
|
18
|
+
"ta-independent-rescue-{}-{}",
|
|
18
19
|
std::process::id(),
|
|
19
20
|
NEXT.fetch_add(1, Ordering::SeqCst)
|
|
20
21
|
));
|
|
21
22
|
std::fs::create_dir(&workspace).unwrap();
|
|
22
23
|
let store = MessageStore::open(&workspace).unwrap();
|
|
23
24
|
let log = EventLog::new(&workspace);
|
|
24
|
-
let
|
|
25
|
+
let context = EnterRescueContext {
|
|
25
26
|
message_id: "msg_one".into(),
|
|
26
27
|
owner_team_id: "team".into(),
|
|
27
28
|
recipient: "worker".into(),
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
window: "worker".into(),
|
|
33
|
-
};
|
|
34
|
-
let evidence = NativeInputEvidence {
|
|
35
|
-
request,
|
|
36
|
-
binding: NativeInputBinding {
|
|
37
|
-
endpoint: "/tmp/owned-test.sock".into(),
|
|
29
|
+
payload_sha256: digest(&render_message("leader", None, "body", "msg_one")),
|
|
30
|
+
target: Some(EnterRescueTarget {
|
|
31
|
+
provider: Provider::Pi,
|
|
32
|
+
endpoint: "/tmp/owned.sock".into(),
|
|
38
33
|
pane_id: "%1".into(),
|
|
39
34
|
pane_pid: 2,
|
|
40
35
|
pane_height: 120,
|
|
41
|
-
carrier_birth: "
|
|
42
|
-
binding_nonce: "
|
|
36
|
+
carrier_birth: "original-birth".into(),
|
|
37
|
+
binding_nonce: "original-nonce".into(),
|
|
43
38
|
session: "session".into(),
|
|
44
39
|
window: "worker".into(),
|
|
45
|
-
},
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
40
|
+
}),
|
|
41
|
+
proof: Some(EnterRescueProof {
|
|
42
|
+
ui_identity: "pi-model".into(),
|
|
43
|
+
footer_sha256: digest("footer"),
|
|
44
|
+
visible_marker: true,
|
|
45
|
+
transcript_marker_absent: true,
|
|
46
|
+
owned_capsule: None,
|
|
47
|
+
}),
|
|
53
48
|
};
|
|
54
49
|
let fixture = Self {
|
|
55
50
|
workspace,
|
|
56
51
|
store,
|
|
57
52
|
log,
|
|
58
|
-
|
|
53
|
+
context,
|
|
59
54
|
};
|
|
60
|
-
|
|
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();
|
|
55
|
+
fixture.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','submitted_unverified','old','old',1)",[]).unwrap();
|
|
62
56
|
fixture.state("running", false);
|
|
63
57
|
fixture
|
|
64
58
|
}
|
|
@@ -73,23 +67,21 @@ impl Fixture {
|
|
|
73
67
|
)
|
|
74
68
|
.unwrap();
|
|
75
69
|
}
|
|
76
|
-
fn
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
&self.
|
|
83
|
-
|
|
84
|
-
)
|
|
85
|
-
.unwrap();
|
|
70
|
+
fn schedule(&self, age: i64, bound: bool) {
|
|
71
|
+
let mut conn = self.conn();
|
|
72
|
+
let tx = conn.transaction().unwrap();
|
|
73
|
+
schedule_in_tx(&tx, "msg_one", Utc::now() - Duration::seconds(age)).unwrap();
|
|
74
|
+
tx.commit().unwrap();
|
|
75
|
+
if bound {
|
|
76
|
+
assert!(bind_pending_context(&self.store, &self.context).unwrap());
|
|
77
|
+
}
|
|
86
78
|
}
|
|
87
79
|
fn job(&self) -> (i64, String, String) {
|
|
88
80
|
self.conn()
|
|
89
81
|
.query_row(
|
|
90
82
|
"select id,payload_json,status from scheduled_events",
|
|
91
83
|
[],
|
|
92
|
-
|
|
|
84
|
+
|r| Ok((r.get(0)?, r.get(1)?, r.get(2)?)),
|
|
93
85
|
)
|
|
94
86
|
.unwrap()
|
|
95
87
|
}
|
|
@@ -98,10 +90,22 @@ impl Fixture {
|
|
|
98
90
|
.query_row(
|
|
99
91
|
"select status from messages where message_id='msg_one'",
|
|
100
92
|
[],
|
|
101
|
-
|
|
|
93
|
+
|r| r.get(0),
|
|
102
94
|
)
|
|
103
95
|
.unwrap()
|
|
104
96
|
}
|
|
97
|
+
fn fire(&self, execute: &Executor<'_>) -> bool {
|
|
98
|
+
let (id, payload, _) = self.job();
|
|
99
|
+
fire_with(
|
|
100
|
+
&self.workspace,
|
|
101
|
+
&self.store,
|
|
102
|
+
&self.log,
|
|
103
|
+
id,
|
|
104
|
+
&payload,
|
|
105
|
+
execute,
|
|
106
|
+
)
|
|
107
|
+
.unwrap()
|
|
108
|
+
}
|
|
105
109
|
}
|
|
106
110
|
impl Drop for Fixture {
|
|
107
111
|
fn drop(&mut self) {
|
|
@@ -112,11 +116,11 @@ impl Drop for Fixture {
|
|
|
112
116
|
#[test]
|
|
113
117
|
fn enter_rescue_first_successful_key_arms_original_turn_before_acceptance_poll() {
|
|
114
118
|
let f = Fixture::new();
|
|
115
|
-
f.
|
|
119
|
+
f.schedule(95, true);
|
|
116
120
|
super::super::delivery::with_rescue_submit_observer(
|
|
117
121
|
&f.workspace,
|
|
118
122
|
&f.store,
|
|
119
|
-
&f.
|
|
123
|
+
&f.context,
|
|
120
124
|
&f.log,
|
|
121
125
|
|observer| {
|
|
122
126
|
observer.after_physical_submit();
|
|
@@ -144,7 +148,7 @@ fn enter_rescue_post_key_owner_or_receiver_loss_is_unknown_without_cross_team_wr
|
|
|
144
148
|
"stopped",
|
|
145
149
|
] {
|
|
146
150
|
let f = Fixture::new();
|
|
147
|
-
f.
|
|
151
|
+
f.schedule(95, true);
|
|
148
152
|
let (id, payload, _) = f.job();
|
|
149
153
|
let effects = AtomicUsize::new(0);
|
|
150
154
|
let path = crate::model::paths::runtime_dir(&f.workspace).join("state.json");
|
|
@@ -156,11 +160,11 @@ fn enter_rescue_post_key_owner_or_receiver_loss_is_unknown_without_cross_team_wr
|
|
|
156
160
|
&f.log,
|
|
157
161
|
id,
|
|
158
162
|
&payload,
|
|
159
|
-
&|
|
|
163
|
+
&|context, _| {
|
|
160
164
|
super::super::delivery::with_rescue_submit_observer(
|
|
161
165
|
&f.workspace,
|
|
162
166
|
&f.store,
|
|
163
|
-
|
|
167
|
+
context,
|
|
164
168
|
&f.log,
|
|
165
169
|
|observer| {
|
|
166
170
|
effects.fetch_add(1, Ordering::SeqCst);
|
|
@@ -196,7 +200,7 @@ fn enter_rescue_post_key_owner_or_receiver_loss_is_unknown_without_cross_team_wr
|
|
|
196
200
|
},
|
|
197
201
|
)
|
|
198
202
|
.unwrap();
|
|
199
|
-
//
|
|
203
|
+
// A rejected post-key commit must preserve every byte of replacement state.
|
|
200
204
|
let expected = replacement.borrow().clone();
|
|
201
205
|
assert_eq!(std::fs::read_to_string(&path).unwrap(), expected);
|
|
202
206
|
assert_eq!(f.job().2, "unknown", "{change}");
|
|
@@ -223,7 +227,7 @@ fn enter_rescue_post_key_owner_or_receiver_loss_is_unknown_without_cross_team_wr
|
|
|
223
227
|
#[test]
|
|
224
228
|
fn enter_rescue_owner_loss_at_state_save_hook_refuses_without_default_fallback() {
|
|
225
229
|
let f = Fixture::new();
|
|
226
|
-
f.
|
|
230
|
+
f.schedule(95, true);
|
|
227
231
|
let path = crate::model::paths::runtime_dir(&f.workspace).join("state.json");
|
|
228
232
|
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
233
|
let hook_path = path.clone();
|
|
@@ -234,7 +238,7 @@ fn enter_rescue_owner_loss_at_state_save_hook_refuses_without_default_fallback()
|
|
|
234
238
|
assert!(super::super::delivery::with_rescue_submit_observer(
|
|
235
239
|
&f.workspace,
|
|
236
240
|
&f.store,
|
|
237
|
-
&f.
|
|
241
|
+
&f.context,
|
|
238
242
|
&f.log,
|
|
239
243
|
|observer| {
|
|
240
244
|
observer.after_physical_submit();
|
|
@@ -249,9 +253,9 @@ fn enter_rescue_owner_loss_at_state_save_hook_refuses_without_default_fallback()
|
|
|
249
253
|
#[test]
|
|
250
254
|
fn enter_rescue_switched_top_projection_updates_only_retained_original_owner() {
|
|
251
255
|
let f = Fixture::new();
|
|
252
|
-
f.
|
|
256
|
+
f.schedule(95, true);
|
|
253
257
|
super::super::delivery::with_rescue_submit_observer(
|
|
254
|
-
&f.workspace, &f.store, &f.
|
|
258
|
+
&f.workspace, &f.store, &f.context, &f.log, |observer| {
|
|
255
259
|
let original = crate::state::persist::load_runtime_state(&f.workspace).unwrap();
|
|
256
260
|
let mut other = original.clone();
|
|
257
261
|
other["team_key"] = "other".into();
|
|
@@ -277,102 +281,177 @@ fn enter_rescue_switched_top_projection_updates_only_retained_original_owner() {
|
|
|
277
281
|
}
|
|
278
282
|
|
|
279
283
|
#[test]
|
|
280
|
-
fn
|
|
284
|
+
fn every_unverified_row_registers_independently_of_native_receipts() {
|
|
281
285
|
let f = Fixture::new();
|
|
282
|
-
f.
|
|
283
|
-
let (
|
|
286
|
+
f.schedule(0, false);
|
|
287
|
+
let (_, payload, status) = f.job();
|
|
288
|
+
let job: RescueJob = serde_json::from_str(&payload).unwrap();
|
|
284
289
|
assert_eq!(status, "pending");
|
|
285
|
-
|
|
286
|
-
|
|
290
|
+
assert!(job.context.target.is_none());
|
|
291
|
+
assert!(job.context.proof.is_none());
|
|
292
|
+
assert!(!payload.contains("evidence"));
|
|
293
|
+
assert!(!payload.contains("NativeInput"));
|
|
294
|
+
let due: String = f
|
|
287
295
|
.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
|
-
)
|
|
296
|
+
.query_row("select due_at from scheduled_events", [], |r| r.get(0))
|
|
293
297
|
.unwrap();
|
|
294
|
-
assert_eq!(times.0, "team");
|
|
295
298
|
assert_eq!(
|
|
296
|
-
|
|
297
|
-
- DateTime::parse_from_rfc3339(×.1).unwrap(),
|
|
299
|
+
timestamp(&due).unwrap() - timestamp(&job.registered_at).unwrap(),
|
|
298
300
|
Duration::seconds(90)
|
|
299
301
|
);
|
|
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
302
|
assert_eq!(
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
.get::<_, i64>(0))
|
|
314
|
-
.unwrap(),
|
|
315
|
-
1
|
|
303
|
+
timestamp(&job.expires_at).unwrap() - timestamp(&job.registered_at).unwrap(),
|
|
304
|
+
Duration::seconds(120)
|
|
316
305
|
);
|
|
317
306
|
}
|
|
318
307
|
|
|
319
308
|
#[test]
|
|
320
|
-
fn
|
|
309
|
+
fn even_missing_owner_or_recipient_gets_a_job_then_refuses_without_keys() {
|
|
321
310
|
let f = Fixture::new();
|
|
322
|
-
f.conn()
|
|
323
|
-
|
|
324
|
-
|
|
311
|
+
f.conn()
|
|
312
|
+
.execute("update messages set owner_team_id=null,recipient=null", [])
|
|
313
|
+
.unwrap();
|
|
314
|
+
f.schedule(95, false);
|
|
315
|
+
assert_eq!(f.job().2, "pending");
|
|
316
|
+
assert!(f.fire(&|_, _| panic!("unowned registration is not key authority")));
|
|
317
|
+
assert_eq!(f.job().2, "refused");
|
|
325
318
|
}
|
|
326
319
|
|
|
327
320
|
#[test]
|
|
328
|
-
fn
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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!(
|
|
321
|
+
fn registration_is_durable_and_never_resets_a_claimed_or_unknown_attempt() {
|
|
322
|
+
for status in ["pending", "claimed", "unknown", "done", "refused"] {
|
|
323
|
+
let f = Fixture::new();
|
|
324
|
+
f.schedule(95, false);
|
|
340
325
|
f.conn()
|
|
341
|
-
.
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
0
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
326
|
+
.execute("update scheduled_events set status=?1", [status])
|
|
327
|
+
.unwrap();
|
|
328
|
+
let original = f.job();
|
|
329
|
+
f.schedule(0, false);
|
|
330
|
+
assert_eq!(f.job(), original);
|
|
331
|
+
let count: i64 = f
|
|
332
|
+
.conn()
|
|
333
|
+
.query_row("select count(*) from scheduled_events", [], |r| r.get(0))
|
|
334
|
+
.unwrap();
|
|
335
|
+
assert_eq!(count, 1);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
#[test]
|
|
340
|
+
fn unbound_job_is_observed_then_refused_without_executor() {
|
|
341
|
+
let f = Fixture::new();
|
|
342
|
+
f.schedule(95, false);
|
|
343
|
+
assert!(f.fire(&|_, _| panic!("unbound job must not reach keys")));
|
|
344
|
+
assert_eq!(f.job().2, "refused");
|
|
345
|
+
assert_eq!(f.message_status(), "submitted_unverified");
|
|
346
|
+
let result: String = f
|
|
347
|
+
.conn()
|
|
348
|
+
.query_row("select result_json from scheduled_events", [], |r| r.get(0))
|
|
349
|
+
.unwrap();
|
|
350
|
+
assert!(result.contains("proof_missing"));
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
#[test]
|
|
354
|
+
fn supplement_cannot_change_original_clock_scope_digest_or_bound_carrier() {
|
|
355
|
+
let f = Fixture::new();
|
|
356
|
+
f.schedule(95, false);
|
|
357
|
+
let before: (String, String) = f
|
|
358
|
+
.conn()
|
|
359
|
+
.query_row("select due_at,created_at from scheduled_events", [], |r| {
|
|
360
|
+
Ok((r.get(0)?, r.get(1)?))
|
|
361
|
+
})
|
|
362
|
+
.unwrap();
|
|
363
|
+
assert!(bind_pending_context(&f.store, &f.context).unwrap());
|
|
364
|
+
let after: (String, String) = f
|
|
365
|
+
.conn()
|
|
366
|
+
.query_row("select due_at,created_at from scheduled_events", [], |r| {
|
|
367
|
+
Ok((r.get(0)?, r.get(1)?))
|
|
368
|
+
})
|
|
369
|
+
.unwrap();
|
|
370
|
+
assert_eq!(before, after);
|
|
371
|
+
for field in [
|
|
372
|
+
"owner",
|
|
373
|
+
"recipient",
|
|
374
|
+
"digest",
|
|
375
|
+
"pid",
|
|
376
|
+
"birth",
|
|
377
|
+
"nonce",
|
|
378
|
+
"endpoint",
|
|
379
|
+
] {
|
|
380
|
+
let mut other = f.context.clone();
|
|
381
|
+
match field {
|
|
382
|
+
"owner" => other.owner_team_id = "other".into(),
|
|
383
|
+
"recipient" => other.recipient = "other".into(),
|
|
384
|
+
"digest" => other.payload_sha256 = digest("different"),
|
|
385
|
+
"pid" => other.target.as_mut().unwrap().pane_pid = 3,
|
|
386
|
+
"birth" => other.target.as_mut().unwrap().carrier_birth = "reused".into(),
|
|
387
|
+
"nonce" => other.target.as_mut().unwrap().binding_nonce = "replacement".into(),
|
|
388
|
+
_ => other.target.as_mut().unwrap().endpoint = "/tmp/other.sock".into(),
|
|
389
|
+
}
|
|
390
|
+
assert!(!bind_pending_context(&f.store, &other).unwrap(), "{field}");
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
#[test]
|
|
395
|
+
fn early_expired_or_extended_jobs_have_no_effect() {
|
|
396
|
+
for (age, extend) in [(0, false), (121, false), (95, true)] {
|
|
397
|
+
let f = Fixture::new();
|
|
398
|
+
f.schedule(age, true);
|
|
399
|
+
let (id, payload, _) = f.job();
|
|
400
|
+
let payload = if extend {
|
|
401
|
+
let mut j: RescueJob = serde_json::from_str(&payload).unwrap();
|
|
402
|
+
j.expires_at = (Utc::now() + Duration::hours(1)).to_rfc3339();
|
|
403
|
+
let p = serde_json::to_string(&j).unwrap();
|
|
404
|
+
f.conn()
|
|
405
|
+
.execute("update scheduled_events set payload_json=?1", [&p])
|
|
406
|
+
.unwrap();
|
|
407
|
+
p
|
|
408
|
+
} else {
|
|
409
|
+
payload
|
|
410
|
+
};
|
|
348
411
|
f.conn()
|
|
349
|
-
.
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
412
|
+
.execute(
|
|
413
|
+
"update scheduled_events set due_at='1970-01-01T00:00:00+00:00'",
|
|
414
|
+
[],
|
|
415
|
+
)
|
|
416
|
+
.unwrap();
|
|
417
|
+
assert!(fire_with(
|
|
418
|
+
&f.workspace,
|
|
419
|
+
&f.store,
|
|
420
|
+
&f.log,
|
|
421
|
+
id,
|
|
422
|
+
&payload,
|
|
423
|
+
&|_, _| panic!("invalid clock")
|
|
424
|
+
)
|
|
425
|
+
.unwrap());
|
|
426
|
+
assert_eq!(f.job().2, "refused");
|
|
427
|
+
}
|
|
354
428
|
}
|
|
355
429
|
|
|
356
430
|
#[test]
|
|
357
|
-
fn
|
|
358
|
-
for
|
|
359
|
-
"
|
|
360
|
-
"explicit_stop",
|
|
431
|
+
fn current_canonical_message_and_original_owner_recipient_remain_required() {
|
|
432
|
+
for change in [
|
|
433
|
+
"ack",
|
|
361
434
|
"content",
|
|
362
435
|
"owner",
|
|
363
|
-
"
|
|
364
|
-
"
|
|
365
|
-
"
|
|
366
|
-
"
|
|
436
|
+
"recipient",
|
|
437
|
+
"stop",
|
|
438
|
+
"pid",
|
|
439
|
+
"provider",
|
|
440
|
+
"session",
|
|
441
|
+
"window",
|
|
442
|
+
"newer",
|
|
367
443
|
] {
|
|
368
444
|
let f = Fixture::new();
|
|
369
|
-
f.
|
|
370
|
-
match
|
|
371
|
-
"
|
|
372
|
-
|
|
445
|
+
f.schedule(95, true);
|
|
446
|
+
match change {
|
|
447
|
+
"ack" => {
|
|
448
|
+
f.conn()
|
|
449
|
+
.execute("update messages set status='acknowledged'", [])
|
|
450
|
+
.unwrap();
|
|
451
|
+
}
|
|
373
452
|
"content" => {
|
|
374
453
|
f.conn()
|
|
375
|
-
.execute("update messages set content='
|
|
454
|
+
.execute("update messages set content='edited'", [])
|
|
376
455
|
.unwrap();
|
|
377
456
|
}
|
|
378
457
|
"owner" => {
|
|
@@ -380,187 +459,175 @@ fn enter_rescue_canonical_stopped_changed_consumed_expired_and_superseded_refuse
|
|
|
380
459
|
.execute("update messages set owner_team_id='other'", [])
|
|
381
460
|
.unwrap();
|
|
382
461
|
}
|
|
383
|
-
"
|
|
462
|
+
"recipient" => {
|
|
384
463
|
f.conn()
|
|
385
|
-
.execute("update messages set
|
|
464
|
+
.execute("update messages set recipient='other'", [])
|
|
386
465
|
.unwrap();
|
|
387
466
|
}
|
|
388
|
-
"
|
|
389
|
-
|
|
467
|
+
"stop" => f.state("running", true),
|
|
468
|
+
"newer" => {
|
|
469
|
+
f.conn().execute("insert into messages(message_id,owner_team_id,recipient,created_at,delivery_attempts) values('msg_new','team','worker','z',1)",[]).unwrap();
|
|
390
470
|
}
|
|
391
|
-
|
|
392
|
-
f.
|
|
393
|
-
|
|
394
|
-
.unwrap();
|
|
471
|
+
field => {
|
|
472
|
+
let p = crate::model::paths::runtime_dir(&f.workspace).join("state.json");
|
|
473
|
+
let mut state: serde_json::Value =
|
|
474
|
+
serde_json::from_str(&std::fs::read_to_string(&p).unwrap()).unwrap();
|
|
475
|
+
match field {
|
|
476
|
+
"pid" => state["agents"]["worker"]["pane_pid"] = 3.into(),
|
|
477
|
+
"provider" => state["agents"]["worker"]["provider"] = "cursor_agent".into(),
|
|
478
|
+
"session" => state["session_name"] = "other".into(),
|
|
479
|
+
_ => state["agents"]["worker"]["window"] = "other".into(),
|
|
480
|
+
};
|
|
481
|
+
std::fs::write(p, state.to_string()).unwrap();
|
|
395
482
|
}
|
|
396
|
-
"expired" => {}
|
|
397
|
-
_ => unreachable!(),
|
|
398
483
|
}
|
|
399
|
-
|
|
400
|
-
|
|
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}");
|
|
484
|
+
assert!(f.fire(&|_, _| panic!("canonical change {change}")));
|
|
485
|
+
assert_eq!(f.job().2, "refused", "{change}");
|
|
410
486
|
}
|
|
411
487
|
}
|
|
412
488
|
|
|
413
489
|
#[test]
|
|
414
|
-
fn
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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| {
|
|
490
|
+
fn one_cas_and_ambiguous_effect_or_error_never_replay() {
|
|
491
|
+
for error in [false, true] {
|
|
492
|
+
let f = Fixture::new();
|
|
493
|
+
f.schedule(95, true);
|
|
494
|
+
let calls = Cell::new(0);
|
|
495
|
+
assert!(f.fire(&|_, authorize| {
|
|
496
|
+
calls.set(calls.get() + 1);
|
|
453
497
|
assert!(authorize());
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
)
|
|
468
|
-
.unwrap());
|
|
498
|
+
if error {
|
|
499
|
+
Err(MessagingError::Validation("may have sent".into()))
|
|
500
|
+
} else {
|
|
501
|
+
Ok(EnterRescueOutcome::Entered { accepted: false })
|
|
502
|
+
}
|
|
503
|
+
}));
|
|
504
|
+
assert_eq!(f.job().2, "unknown");
|
|
505
|
+
assert!(!f.fire(&|_, _| panic!("unknown replay")));
|
|
506
|
+
assert_eq!(calls.get(), 1);
|
|
507
|
+
assert_eq!(f.message_status(), "submitted_unverified");
|
|
508
|
+
}
|
|
509
|
+
let f = Fixture::new();
|
|
510
|
+
f.schedule(95, true);
|
|
469
511
|
f.conn()
|
|
470
512
|
.execute("update scheduled_events set status='claimed'", [])
|
|
471
513
|
.unwrap();
|
|
472
|
-
assert!(!
|
|
473
|
-
&f.workspace,
|
|
474
|
-
&f.store,
|
|
475
|
-
&f.log,
|
|
476
|
-
id,
|
|
477
|
-
&payload,
|
|
478
|
-
&|_, _| panic!("crash replay")
|
|
479
|
-
)
|
|
480
|
-
.unwrap());
|
|
514
|
+
assert!(!f.fire(&|_, _| panic!("crash replay")));
|
|
481
515
|
}
|
|
482
516
|
|
|
483
517
|
#[test]
|
|
484
|
-
fn
|
|
518
|
+
fn no_effect_busy_reobserves_within_original_window_only() {
|
|
485
519
|
let f = Fixture::new();
|
|
486
|
-
f.
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
520
|
+
f.schedule(95, true);
|
|
521
|
+
assert!(f.fire(&|_, _| Ok(EnterRescueOutcome::NoEffect {
|
|
522
|
+
reason: "busy",
|
|
523
|
+
reobserve: true
|
|
524
|
+
})));
|
|
525
|
+
assert_eq!(f.job().2, "pending");
|
|
526
|
+
let due: String = f
|
|
527
|
+
.conn()
|
|
528
|
+
.query_row("select due_at from scheduled_events", [], |r| r.get(0))
|
|
529
|
+
.unwrap();
|
|
530
|
+
assert!(timestamp(&due).unwrap() > Utc::now());
|
|
531
|
+
let f = Fixture::new();
|
|
532
|
+
f.schedule(119, true);
|
|
533
|
+
assert!(f.fire(&|_, _| Ok(EnterRescueOutcome::NoEffect {
|
|
534
|
+
reason: "busy",
|
|
535
|
+
reobserve: true
|
|
536
|
+
})));
|
|
537
|
+
assert_eq!(f.job().2, "refused");
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
#[test]
|
|
541
|
+
fn consumed_or_single_accepted_enter_updates_only_original_unverified_row() {
|
|
542
|
+
for consumed in [false, true] {
|
|
543
|
+
let f = Fixture::new();
|
|
544
|
+
f.schedule(95, true);
|
|
545
|
+
assert!(f.fire(&|_, _| Ok(if consumed {
|
|
546
|
+
EnterRescueOutcome::Consumed
|
|
547
|
+
} else {
|
|
548
|
+
EnterRescueOutcome::Entered { accepted: true }
|
|
549
|
+
})));
|
|
550
|
+
assert_eq!(f.job().2, "done");
|
|
551
|
+
assert_eq!(f.message_status(), "delivered");
|
|
552
|
+
assert!(!f.fire(&|_, _| panic!("done replay")));
|
|
553
|
+
}
|
|
554
|
+
let f = Fixture::new();
|
|
555
|
+
f.schedule(95, true);
|
|
556
|
+
assert!(f.fire(&|_, _| {
|
|
513
557
|
f.conn()
|
|
514
|
-
.
|
|
515
|
-
.unwrap()
|
|
516
|
-
|
|
517
|
-
);
|
|
558
|
+
.execute("update messages set status='acknowledged'", [])
|
|
559
|
+
.unwrap();
|
|
560
|
+
Ok(EnterRescueOutcome::Entered { accepted: true })
|
|
561
|
+
}));
|
|
562
|
+
assert_eq!(f.message_status(), "acknowledged");
|
|
518
563
|
}
|
|
519
564
|
|
|
520
565
|
#[test]
|
|
521
|
-
fn
|
|
566
|
+
fn final_authorizer_detects_canonical_changes_during_transport_checks() {
|
|
522
567
|
let f = Fixture::new();
|
|
523
|
-
f.
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
assert!(!authorize());
|
|
534
|
-
Ok(EnterRescueOutcome::NoEffect {
|
|
535
|
-
reason: "canonical_changed",
|
|
536
|
-
reobserve: false,
|
|
537
|
-
})
|
|
538
|
-
},
|
|
539
|
-
)
|
|
540
|
-
.unwrap();
|
|
568
|
+
f.schedule(95, true);
|
|
569
|
+
assert!(f.fire(&|_, authorize| {
|
|
570
|
+
assert!(authorize());
|
|
571
|
+
f.state("stopped", true);
|
|
572
|
+
assert!(!authorize());
|
|
573
|
+
Ok(EnterRescueOutcome::NoEffect {
|
|
574
|
+
reason: "canonical_changed",
|
|
575
|
+
reobserve: false,
|
|
576
|
+
})
|
|
577
|
+
}));
|
|
541
578
|
assert_eq!(f.job().2, "refused");
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
#[test]
|
|
582
|
+
fn revoked_scheduler_claim_is_not_key_authority() {
|
|
583
|
+
let f = Fixture::new();
|
|
584
|
+
f.schedule(95, true);
|
|
585
|
+
assert!(f.fire(&|_, authorize| {
|
|
586
|
+
assert!(authorize());
|
|
587
|
+
f.conn()
|
|
588
|
+
.execute("update scheduled_events set status='refused'", [])
|
|
589
|
+
.unwrap();
|
|
590
|
+
assert!(!authorize());
|
|
591
|
+
Ok(EnterRescueOutcome::NoEffect {
|
|
592
|
+
reason: "claim_revoked",
|
|
593
|
+
reobserve: false,
|
|
594
|
+
})
|
|
595
|
+
}));
|
|
596
|
+
assert_eq!(f.job().2, "refused");
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
#[test]
|
|
600
|
+
fn tampered_event_scope_or_untrusted_proof_cannot_execute() {
|
|
601
|
+
for change in ["scope", "capsule", "capsule_scope", "missing"] {
|
|
602
|
+
let f = Fixture::new();
|
|
603
|
+
f.schedule(95, false);
|
|
604
|
+
let mut context = f.context.clone();
|
|
605
|
+
if change == "capsule" || change == "capsule_scope" {
|
|
606
|
+
let scope_sha256 = if change == "capsule_scope" {
|
|
607
|
+
digest("foreign receipt")
|
|
608
|
+
} else {
|
|
609
|
+
context.scope_sha256().unwrap()
|
|
610
|
+
};
|
|
611
|
+
let proof = context.proof.as_mut().unwrap();
|
|
612
|
+
proof.visible_marker = false;
|
|
613
|
+
proof.owned_capsule = Some(crate::transport::enter_rescue::OwnedCapsule {
|
|
614
|
+
scope_sha256,
|
|
615
|
+
label: "[paste #1 +30 lines]".into(),
|
|
616
|
+
baseline_sha256: digest("baseline"),
|
|
617
|
+
baseline_capsules: vec![],
|
|
618
|
+
observed_after_owned_paste: change == "capsule_scope",
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
if change == "missing" {
|
|
622
|
+
context.proof = None;
|
|
623
|
+
}
|
|
624
|
+
assert!(bind_pending_context(&f.store, &context).unwrap());
|
|
625
|
+
if change == "scope" {
|
|
626
|
+
f.conn()
|
|
627
|
+
.execute("update scheduled_events set owner_team_id='other'", [])
|
|
628
|
+
.unwrap();
|
|
629
|
+
}
|
|
630
|
+
assert!(f.fire(&|_, _| panic!("untrusted job")));
|
|
631
|
+
assert_eq!(f.job().2, "refused");
|
|
632
|
+
}
|
|
566
633
|
}
|