@team-agent/installer 0.5.120 → 0.5.121

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.
@@ -720,8 +720,12 @@ fn contract_b_step2_retry_delivers_queued_message_after_startup_trust_is_handled
720
720
  let ws = tmp_dir("b-step2-trust-retry");
721
721
  let mcp_config = ws.join(".team").join("runtime").join("mcp").join("w1.json");
722
722
  std::fs::create_dir_all(mcp_config.parent().unwrap()).unwrap();
723
- std::fs::write(&mcp_config, "{}").unwrap();
724
- let state = json!({
723
+ // This worker genuinely depends on MCP. Bind the controlled receipt to the
724
+ // live test carrier rather than bypassing readiness with an empty config.
725
+ let owner = "subprep-trust-retry";
726
+ let mut state = json!({
727
+ "team_key": owner,
728
+ "active_team_key": owner,
725
729
  "session_name": "team-ta-subroot-SUB-PROBE-2-send-turn-idle",
726
730
  "team_dir": ws.join("teamdir").to_string_lossy().to_string(),
727
731
  "agents": {
@@ -736,6 +740,21 @@ fn contract_b_step2_retry_delivers_queued_message_after_startup_trust_is_handled
736
740
  }
737
741
  }
738
742
  });
743
+ #[cfg(unix)]
744
+ let carrier = {
745
+ let carrier = crate::mcp_server::readiness::test_carrier_pid().unwrap();
746
+ state["agents"]["w1"]["pane_pid"] = json!(carrier);
747
+ state["agents"]["w1"]["spawned_at"] =
748
+ json!((chrono::Utc::now() - chrono::Duration::seconds(10)).to_rfc3339());
749
+ carrier
750
+ };
751
+ state["teams"] = json!({owner: state.clone()});
752
+ std::fs::write(&mcp_config, serde_json::to_vec(&json!({
753
+ "mcpServers": {"team_orchestrator": {
754
+ "command": std::env::current_exe().unwrap().canonicalize().unwrap(),
755
+ "env": {"TEAM_AGENT_WORKSPACE": ws.to_path_buf(), "TEAM_AGENT_ID": "w1", "TEAM_AGENT_OWNER_TEAM_ID": owner}
756
+ }}
757
+ })).unwrap()).unwrap();
739
758
  team_agent::state::persist::save_runtime_state(&ws, &state).unwrap();
740
759
 
741
760
  let store = MessageStore::open(&ws).unwrap();
@@ -748,7 +767,7 @@ fn contract_b_step2_retry_delivers_queued_message_after_startup_trust_is_handled
748
767
  "SUB step2 probe: reply with one sentence and call report_result exactly once.",
749
768
  None,
750
769
  true,
751
- None,
770
+ Some(owner),
752
771
  )
753
772
  .unwrap();
754
773
  let transport = RecordingTransport::new(vec![
@@ -759,6 +778,10 @@ fn contract_b_step2_retry_delivers_queued_message_after_startup_trust_is_handled
759
778
  ])
760
779
  .with_session_present(true)
761
780
  .with_windows(vec![WindowName::new("w1")]);
781
+ #[cfg(unix)]
782
+ {
783
+ transport.state.lock().unwrap().carrier = Some(carrier);
784
+ }
762
785
  let event_log = EventLog::new(&ws);
763
786
 
764
787
  let deferred =
@@ -787,6 +810,13 @@ fn contract_b_step2_retry_delivers_queued_message_after_startup_trust_is_handled
787
810
  "fixture sanity: delivery defer must be observable as delivery.deferred_startup_prompt"
788
811
  );
789
812
 
813
+ // No receipt existed during the actionable trust prompt. Once startup
814
+ // control can progress, model the successful live helper handshake before
815
+ // the coordinator retries this SAME queued row.
816
+ #[cfg(unix)]
817
+ {
818
+ crate::mcp_server::readiness::publish(&ws, owner, "w1", chrono::Utc::now()).unwrap();
819
+ }
790
820
  let coord = Coordinator::new(
791
821
  WorkspacePath::new(ws.to_path_buf()),
792
822
  Box::new(RealAdapterRegistry),
@@ -1045,6 +1075,7 @@ struct TransportState {
1045
1075
  session_present: bool,
1046
1076
  windows: Vec<WindowName>,
1047
1077
  spawn_argv: Vec<Vec<String>>,
1078
+ carrier: Option<u32>,
1048
1079
  }
1049
1080
 
1050
1081
  #[derive(Debug, Clone)]
@@ -1189,7 +1220,19 @@ impl Transport for RecordingTransport {
1189
1220
  }
1190
1221
 
1191
1222
  fn list_targets(&self) -> Result<Vec<PaneInfo>, TransportError> {
1192
- Ok(Vec::new())
1223
+ Ok(self.state.lock().unwrap().carrier.map(|carrier| PaneInfo {
1224
+ pane_id: PaneId::new("%7"),
1225
+ session: SessionName::new("team-ta-subroot-SUB-PROBE-2-send-turn-idle"),
1226
+ window_index: None,
1227
+ window_name: Some(WindowName::new("w1")),
1228
+ pane_index: None,
1229
+ tty: None,
1230
+ current_command: None,
1231
+ current_path: None,
1232
+ active: true,
1233
+ pane_pid: Some(carrier),
1234
+ leader_env: BTreeMap::new(),
1235
+ }).into_iter().collect())
1193
1236
  }
1194
1237
 
1195
1238
  fn has_session(&self, _session: &SessionName) -> Result<bool, TransportError> {
@@ -89,6 +89,8 @@ use crate::messaging::{self, DeliveryOutcome, MessageTarget, SendOptions, Truste
89
89
  pub mod helpers;
90
90
  pub(crate) mod lifecycle_tools;
91
91
  pub mod normalize;
92
+ #[cfg(unix)]
93
+ pub(crate) mod readiness;
92
94
  pub mod tools;
93
95
  pub mod types;
94
96
  pub mod wire;
@@ -0,0 +1,432 @@
1
+ //! A flushed MCP handshake is admission to first business, not client consumption.
2
+ //! Receipts are scope- and live-process-bound; no pane input or tools/call probe.
3
+
4
+ use std::fs::{File, OpenOptions};
5
+ use std::io::{Read, Write};
6
+ use std::os::unix::fs::{DirBuilderExt, MetadataExt, OpenOptionsExt};
7
+ use std::path::{Path, PathBuf};
8
+ use std::time::{Duration, Instant};
9
+
10
+ use chrono::{DateTime, Utc};
11
+ use serde::{Deserialize, Serialize};
12
+ use serde_json::Value;
13
+ use team_agent_contract::host::process::{
14
+ capture_process, fingerprint_file, process_birth_identity, sample_process, ProcessStamp,
15
+ ProcessState, MAX_NATIVE_PARENT_HOPS,
16
+ };
17
+ use team_agent_contract::host::{digest, digest_hex};
18
+
19
+ use crate::transport::{PaneInfo, Target};
20
+
21
+ type Result<T> = std::result::Result<T, &'static str>;
22
+ const MAX_RECEIPT_BYTES: u64 = 64 * 1024;
23
+ // A 73MB debug image exceeded 2s on the official host. This is a finite
24
+ // computation budget, never a sleep or a minimum startup delay.
25
+ const IMAGE_EVIDENCE_BUDGET: Duration = Duration::from_secs(10);
26
+
27
+ #[derive(Serialize, Deserialize)]
28
+ #[serde(deny_unknown_fields)]
29
+ struct Ancestor {
30
+ pid: u32,
31
+ birth: String,
32
+ }
33
+
34
+ #[derive(Serialize, Deserialize)]
35
+ #[serde(deny_unknown_fields)]
36
+ struct Handshake {
37
+ schema: u8,
38
+ workspace: PathBuf,
39
+ owner: String,
40
+ agent: String,
41
+ started_at: String,
42
+ flushed_at: String,
43
+ server: ProcessStamp,
44
+ ancestors: Vec<Ancestor>,
45
+ }
46
+
47
+ fn private_metadata(path: &Path, directory: bool) -> Result<std::fs::Metadata> {
48
+ let m = std::fs::symlink_metadata(path).map_err(|_| "mcp_handshake_missing")?;
49
+ // SAFETY: geteuid has no arguments, pointers, or side effects.
50
+ let uid = unsafe { libc::geteuid() };
51
+ if m.file_type().is_symlink()
52
+ || m.uid() != uid
53
+ || (directory && !m.is_dir())
54
+ || (!directory && (!m.is_file() || m.mode() & 0o022 != 0))
55
+ {
56
+ return Err("mcp_handshake_ownership");
57
+ }
58
+ Ok(m)
59
+ }
60
+
61
+ fn directory(workspace: &Path, create: bool) -> Result<PathBuf> {
62
+ let mut path = workspace.to_path_buf();
63
+ for component in [".team", "runtime", "mcp-handshakes"] {
64
+ path.push(component);
65
+ if create && !path.exists() {
66
+ match std::fs::DirBuilder::new().mode(0o700).create(&path) {
67
+ Ok(()) => {}
68
+ Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {}
69
+ Err(_) => return Err("mcp_handshake_directory"),
70
+ }
71
+ }
72
+ let m = private_metadata(&path, true)?;
73
+ if component == "mcp-handshakes" && m.mode() & 0o777 != 0o700 {
74
+ return Err("mcp_handshake_ownership");
75
+ }
76
+ }
77
+ Ok(path)
78
+ }
79
+
80
+ fn key(owner: &str, agent: &str) -> String {
81
+ digest_hex(digest(format!("{owner}\0{agent}").as_bytes()))
82
+ }
83
+
84
+ fn read_json(path: &Path, require_private: bool) -> Result<Value> {
85
+ let before = private_metadata(path, false)?;
86
+ if (require_private && before.mode() & 0o777 != 0o600) || before.len() > MAX_RECEIPT_BYTES {
87
+ return Err("mcp_handshake_invalid");
88
+ }
89
+ let file = OpenOptions::new()
90
+ .read(true)
91
+ .custom_flags(libc::O_NOFOLLOW)
92
+ .open(path)
93
+ .map_err(|_| "mcp_handshake_missing")?;
94
+ let opened = file.metadata().map_err(|_| "mcp_handshake_invalid")?;
95
+ if (before.dev(), before.ino()) != (opened.dev(), opened.ino()) {
96
+ return Err("mcp_handshake_replaced");
97
+ }
98
+ let mut data = Vec::new();
99
+ file.take(MAX_RECEIPT_BYTES + 1)
100
+ .read_to_end(&mut data)
101
+ .map_err(|_| "mcp_handshake_invalid")?;
102
+ if data.len() as u64 > MAX_RECEIPT_BYTES {
103
+ return Err("mcp_handshake_invalid");
104
+ }
105
+ serde_json::from_slice(&data).map_err(|_| "mcp_handshake_invalid")
106
+ }
107
+
108
+ struct ReceiptLock(File);
109
+ impl Drop for ReceiptLock {
110
+ fn drop(&mut self) {
111
+ let _ = crate::platform::file_lock::unlock(&self.0);
112
+ }
113
+ }
114
+
115
+ fn lock(path: &Path) -> Result<ReceiptLock> {
116
+ let file = OpenOptions::new()
117
+ .create(true)
118
+ .truncate(false)
119
+ .read(true)
120
+ .write(true)
121
+ .mode(0o600)
122
+ .custom_flags(libc::O_NOFOLLOW)
123
+ .open(path)
124
+ .map_err(|_| "mcp_handshake_lock")?;
125
+ private_metadata(path, false)?;
126
+ let start = Instant::now();
127
+ loop {
128
+ match crate::platform::file_lock::try_lock_once_nonblocking(&file) {
129
+ Ok(true) => return Ok(ReceiptLock(file)),
130
+ Ok(false) if start.elapsed() < Duration::from_millis(500) => {
131
+ std::thread::sleep(Duration::from_millis(10));
132
+ }
133
+ _ => return Err("mcp_handshake_lock"),
134
+ }
135
+ }
136
+ }
137
+
138
+ /// Only the actual stdio writer calls this after initialize and tools/list flush.
139
+ /// File publication failure stays unready; it never becomes an input bypass.
140
+ pub(crate) fn publish(
141
+ workspace: &Path,
142
+ owner: &str,
143
+ agent: &str,
144
+ started_at: DateTime<Utc>,
145
+ ) -> Result<()> {
146
+ if owner.is_empty() || agent.is_empty() {
147
+ return Err("mcp_handshake_scope_missing");
148
+ }
149
+ let workspace = workspace
150
+ .canonicalize()
151
+ .map_err(|_| "mcp_handshake_workspace")?;
152
+ let executable = std::env::current_exe().map_err(|_| "mcp_handshake_image")?;
153
+ let image = fingerprint_file(&executable, 512 * 1024 * 1024, IMAGE_EVIDENCE_BUDGET).map_err(
154
+ |error| {
155
+ let _ = crate::event_log::EventLog::new(&workspace).write(
156
+ "mcp.handshake_evidence_failed",
157
+ serde_json::json!({
158
+ "stage": "image_fingerprint",
159
+ "operation": error.operation,
160
+ "kind": format!("{:?}", error.kind),
161
+ "image_bytes": std::fs::metadata(&executable).ok().map(|m| m.len()),
162
+ "budget_ms": IMAGE_EVIDENCE_BUDGET.as_millis(),
163
+ }),
164
+ );
165
+ "mcp_handshake_image"
166
+ },
167
+ )?;
168
+ let server = capture_process(
169
+ std::process::id(),
170
+ &executable,
171
+ image,
172
+ IMAGE_EVIDENCE_BUDGET,
173
+ )
174
+ .map_err(|_| "mcp_handshake_process")?;
175
+ let mut ancestors = Vec::new();
176
+ let mut parent = server.parent;
177
+ for _ in 0..MAX_NATIVE_PARENT_HOPS {
178
+ if parent <= 1 {
179
+ break;
180
+ }
181
+ let birth = process_birth_identity(parent).map_err(|_| "mcp_handshake_ancestry")?;
182
+ ancestors.push(Ancestor { pid: parent, birth });
183
+ parent = crate::platform::argv::parent_pid(parent).unwrap_or(0);
184
+ }
185
+ if ancestors.is_empty() || sample_process(&server) != ProcessState::Alive {
186
+ return Err("mcp_handshake_process");
187
+ }
188
+ let receipt = Handshake {
189
+ schema: 1,
190
+ workspace: workspace.clone(),
191
+ owner: owner.to_string(),
192
+ agent: agent.to_string(),
193
+ started_at: started_at.to_rfc3339(),
194
+ flushed_at: Utc::now().to_rfc3339(),
195
+ server,
196
+ ancestors,
197
+ };
198
+ let dir = directory(&workspace, true)?;
199
+ let before = private_metadata(&dir, true)?;
200
+ let name = key(owner, agent);
201
+ let path = dir.join(format!("{name}.json"));
202
+ let _lock = lock(&dir.join(format!("{name}.lock")))?;
203
+ match std::fs::symlink_metadata(&path) {
204
+ Ok(_) => {
205
+ let old: Handshake = serde_json::from_value(read_json(&path, true)?)
206
+ .map_err(|_| "mcp_handshake_invalid")?;
207
+ if old.workspace != workspace
208
+ || old.owner != owner
209
+ || old.agent != agent
210
+ || old.schema != 1
211
+ {
212
+ return Err("mcp_handshake_scope_mismatch");
213
+ }
214
+ let old_started = DateTime::parse_from_rfc3339(&old.started_at)
215
+ .map_err(|_| "mcp_handshake_invalid")?;
216
+ if old_started > started_at {
217
+ return Ok(());
218
+ }
219
+ }
220
+ Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
221
+ Err(_) => return Err("mcp_handshake_invalid"),
222
+ }
223
+ let temp = dir.join(format!(
224
+ "{name}.{}-{}.tmp",
225
+ std::process::id(),
226
+ crate::provider::adapter::next_session_token()
227
+ ));
228
+ let result = (|| {
229
+ let mut file = OpenOptions::new()
230
+ .write(true)
231
+ .create_new(true)
232
+ .mode(0o600)
233
+ .custom_flags(libc::O_NOFOLLOW)
234
+ .open(&temp)
235
+ .map_err(|_| "mcp_handshake_write")?;
236
+ serde_json::to_writer(&mut file, &receipt).map_err(|_| "mcp_handshake_write")?;
237
+ file.write_all(b"\n").map_err(|_| "mcp_handshake_write")?;
238
+ file.sync_all().map_err(|_| "mcp_handshake_write")?;
239
+ let after = private_metadata(&dir, true)?;
240
+ if (before.dev(), before.ino()) != (after.dev(), after.ino()) {
241
+ return Err("mcp_handshake_replaced");
242
+ }
243
+ std::fs::rename(&temp, &path).map_err(|_| "mcp_handshake_write")?;
244
+ File::open(&dir)
245
+ .and_then(|file| file.sync_all())
246
+ .map_err(|_| "mcp_handshake_write")
247
+ })();
248
+ if result.is_err() {
249
+ let _ = std::fs::remove_file(&temp);
250
+ }
251
+ result
252
+ }
253
+
254
+ #[derive(Debug)]
255
+ pub(crate) struct Admission {
256
+ server: ProcessStamp,
257
+ carrier: u32,
258
+ carrier_birth: String,
259
+ pane_id: String,
260
+ }
261
+
262
+ /// Observation-only preflight, before claiming a delivery attempt. Missing facts
263
+ /// perform no input; the delivery owner releases only its exact claim for retry.
264
+ pub(crate) fn preflight(
265
+ workspace: &Path,
266
+ owner: Option<&str>,
267
+ agent_id: &str,
268
+ state: &Value,
269
+ ) -> Result<Option<Admission>> {
270
+ if agent_id == "leader" {
271
+ return Ok(None);
272
+ }
273
+ let Some(agent) = state.get("agents").and_then(|agents| agents.get(agent_id)) else {
274
+ return Ok(None);
275
+ };
276
+ let Some(config) = agent.get("mcp_config").and_then(Value::as_str) else {
277
+ return Ok(None);
278
+ };
279
+ let provider = agent
280
+ .get("provider")
281
+ .and_then(Value::as_str)
282
+ .and_then(crate::provider::wire::parse_provider)
283
+ .ok_or("mcp_handshake_provider_missing")?;
284
+ if !crate::provider::get_adapter(provider).requires_worker_mcp_handshake() {
285
+ return Ok(None);
286
+ }
287
+ let owner = owner
288
+ .filter(|s| !s.is_empty())
289
+ .ok_or("mcp_handshake_scope_missing")?;
290
+ let workspace = workspace
291
+ .canonicalize()
292
+ .map_err(|_| "mcp_handshake_workspace")?;
293
+ let dir = directory(&workspace, false)?;
294
+ let receipt: Handshake = serde_json::from_value(read_json(
295
+ &dir.join(format!("{}.json", key(owner, agent_id))),
296
+ true,
297
+ )?)
298
+ .map_err(|_| "mcp_handshake_invalid")?;
299
+ if receipt.schema != 1
300
+ || receipt.workspace != workspace
301
+ || receipt.owner != owner
302
+ || receipt.agent != agent_id
303
+ {
304
+ return Err("mcp_handshake_scope_mismatch");
305
+ }
306
+ let spawned_at = agent
307
+ .get("spawned_at")
308
+ .and_then(Value::as_str)
309
+ .and_then(|s| DateTime::parse_from_rfc3339(s).ok())
310
+ .ok_or("mcp_handshake_spawn_missing")?;
311
+ let started_at =
312
+ DateTime::parse_from_rfc3339(&receipt.started_at).map_err(|_| "mcp_handshake_invalid")?;
313
+ let flushed_at =
314
+ DateTime::parse_from_rfc3339(&receipt.flushed_at).map_err(|_| "mcp_handshake_invalid")?;
315
+ if started_at < spawned_at || flushed_at < started_at {
316
+ return Err("mcp_handshake_stale_spawn");
317
+ }
318
+ let expected_config = workspace
319
+ .join(".team/runtime/mcp")
320
+ .join(format!("{agent_id}.json"));
321
+ if Path::new(config)
322
+ .canonicalize()
323
+ .map_err(|_| "mcp_handshake_config_missing")?
324
+ != expected_config
325
+ {
326
+ return Err("mcp_handshake_config_scope");
327
+ }
328
+ private_metadata(&workspace.join(".team/runtime/mcp"), true)?;
329
+ let config = read_json(Path::new(config), false)?;
330
+ let server_config = config
331
+ .get("team_orchestrator")
332
+ .or_else(|| {
333
+ config
334
+ .get("mcpServers")
335
+ .and_then(|servers| servers.get("team_orchestrator"))
336
+ })
337
+ .ok_or("mcp_handshake_config_missing")?;
338
+ let expected = server_config
339
+ .get("command")
340
+ .and_then(Value::as_str)
341
+ .ok_or("mcp_handshake_config_missing")?;
342
+ if !Path::new(expected).is_absolute()
343
+ || Path::new(expected)
344
+ .canonicalize()
345
+ .map_err(|_| "mcp_handshake_image")?
346
+ != receipt.server.identity.executable
347
+ {
348
+ return Err("mcp_handshake_image");
349
+ }
350
+ let env = server_config
351
+ .get("env")
352
+ .ok_or("mcp_handshake_config_missing")?;
353
+ if env.get("TEAM_AGENT_ID").and_then(Value::as_str) != Some(agent_id)
354
+ || env.get("TEAM_AGENT_OWNER_TEAM_ID").and_then(Value::as_str) != Some(owner)
355
+ || env
356
+ .get("TEAM_AGENT_WORKSPACE")
357
+ .and_then(Value::as_str)
358
+ .and_then(|s| Path::new(s).canonicalize().ok())
359
+ .as_ref()
360
+ != Some(&workspace)
361
+ {
362
+ return Err("mcp_handshake_config_scope");
363
+ }
364
+ let carrier = agent
365
+ .get("pane_pid")
366
+ .and_then(Value::as_u64)
367
+ .and_then(|pid| u32::try_from(pid).ok())
368
+ .ok_or("mcp_handshake_carrier_missing")?;
369
+ let pane_id = agent
370
+ .get("pane_id")
371
+ .and_then(Value::as_str)
372
+ .filter(|id| !id.is_empty())
373
+ .ok_or("mcp_handshake_carrier_missing")?;
374
+ let carrier_birth =
375
+ process_birth_identity(carrier).map_err(|_| "mcp_handshake_carrier_missing")?;
376
+ if sample_process(&receipt.server) != ProcessState::Alive {
377
+ return Err("mcp_handshake_server_not_live");
378
+ }
379
+ let mut parent = receipt.server.parent;
380
+ let mut matched = false;
381
+ for ancestor in receipt.ancestors.iter().take(MAX_NATIVE_PARENT_HOPS) {
382
+ if ancestor.pid != parent
383
+ || process_birth_identity(parent).ok().as_deref() != Some(ancestor.birth.as_str())
384
+ {
385
+ return Err("mcp_handshake_ancestry");
386
+ }
387
+ if parent == carrier {
388
+ matched = true;
389
+ break;
390
+ }
391
+ parent = crate::platform::argv::parent_pid(parent).unwrap_or(0);
392
+ }
393
+ if !matched || sample_process(&receipt.server) != ProcessState::Alive {
394
+ return Err("mcp_handshake_ancestry");
395
+ }
396
+ Ok(Some(Admission {
397
+ server: receipt.server,
398
+ carrier,
399
+ carrier_birth,
400
+ pane_id: pane_id.to_string(),
401
+ }))
402
+ }
403
+
404
+ /// Recheck against the actual resolved target immediately before the input path.
405
+ pub(crate) fn revalidate(admission: &Admission, target: &Target, panes: &[PaneInfo]) -> Result<()> {
406
+ let Target::Pane(id) = target else {
407
+ return Err("mcp_handshake_carrier_missing");
408
+ };
409
+ let pane = panes
410
+ .iter()
411
+ .find(|pane| &pane.pane_id == id)
412
+ .ok_or("mcp_handshake_carrier_missing")?;
413
+ if id.as_str() != admission.pane_id
414
+ || pane.pane_pid != Some(admission.carrier)
415
+ || process_birth_identity(admission.carrier).ok().as_deref()
416
+ != Some(admission.carrier_birth.as_str())
417
+ || sample_process(&admission.server) != ProcessState::Alive
418
+ {
419
+ return Err("mcp_handshake_carrier_mismatch");
420
+ }
421
+ Ok(())
422
+ }
423
+
424
+ // Test construction stays in the MCP process-evidence owner, not messaging.
425
+ #[cfg(test)]
426
+ pub(crate) fn test_carrier_pid() -> Option<u32> {
427
+ crate::platform::process::current_parent_pid()
428
+ }
429
+
430
+ #[cfg(test)]
431
+ #[path = "readiness_tests.rs"]
432
+ mod tests;