1use std::path::{Path, PathBuf};
2
3use anyhow::Context;
4
5use crate::Result;
6
7const SETUP_DIR_ERROR: &str = "Failed to resolve setup dir (try --config-dir).";
8const INVOCATION_DIR_ERROR: &str = "Failed to resolve invocation dir for setup discovery";
9const SETUP_GRAPHQL_ERROR: &str = "failed to resolve setup/graphql";
10const SETUP_GRPC_ERROR: &str = "failed to resolve setup/grpc";
11const SETUP_WEBSOCKET_ERROR: &str = "failed to resolve setup/websocket";
12
13#[derive(Debug, Clone, Copy)]
14enum FallbackMode {
15 None,
16 Upwards(&'static str),
17 Direct(&'static str, &'static str),
18}
19
20#[derive(Debug)]
21struct SetupDiscovery<'a> {
22 cwd: &'a Path,
23 seed: PathBuf,
24 config_dir_explicit: bool,
25 files: &'static [&'static str],
26 seed_fallback: FallbackMode,
27 invocation_dir: Option<&'a Path>,
28 invocation_fallback: FallbackMode,
29}
30
31impl<'a> SetupDiscovery<'a> {
32 fn resolve(&self) -> Result<PathBuf> {
33 let seed_abs = abs_dir(self.cwd, &self.seed).context(SETUP_DIR_ERROR)?;
34
35 if let Some(dir) = find_upwards_for_files(&seed_abs, self.files) {
36 return Ok(dir);
37 }
38
39 if let FallbackMode::Upwards(subdir) = self.seed_fallback
40 && let Some(found_setup) = find_upwards_for_setup_subdir(&seed_abs, subdir)
41 {
42 return Ok(found_setup);
43 }
44
45 if self.config_dir_explicit {
46 return Ok(seed_abs);
47 }
48
49 match self.invocation_fallback {
50 FallbackMode::None => {}
51 FallbackMode::Upwards(subdir) => {
52 let invocation_dir = self
53 .invocation_dir
54 .expect("invocation_dir required for upwards fallback");
55 let invocation_abs =
56 abs_dir(self.cwd, invocation_dir).context(INVOCATION_DIR_ERROR)?;
57 if let Some(found_setup) = find_upwards_for_setup_subdir(&invocation_abs, subdir) {
58 return Ok(found_setup);
59 }
60 }
61 FallbackMode::Direct(subdir, ctx) => {
62 let invocation_dir = self
63 .invocation_dir
64 .expect("invocation_dir required for direct fallback");
65 let invocation_abs =
66 abs_dir(self.cwd, invocation_dir).context(INVOCATION_DIR_ERROR)?;
67 let fallback = invocation_abs.join(subdir);
68 if fallback.is_dir() {
69 return abs_dir(self.cwd, &fallback).context(ctx);
70 }
71 }
72 }
73
74 Ok(seed_abs)
75 }
76}
77
78fn abs_dir(base_dir: &Path, path: &Path) -> Result<PathBuf> {
79 let joined = if path.is_absolute() {
80 path.to_path_buf()
81 } else {
82 base_dir.join(path)
83 };
84
85 std::fs::canonicalize(&joined)
86 .with_context(|| format!("failed to resolve directory path: {}", joined.display()))
87}
88
89fn find_upwards_for_file(start_dir: &Path, filename: &str) -> Option<PathBuf> {
90 let mut dir = start_dir;
91 loop {
92 if dir.join(filename).is_file() {
93 return Some(dir.to_path_buf());
94 }
95
96 match dir.parent() {
97 Some(parent) if parent != dir => dir = parent,
98 _ => return None,
99 }
100 }
101}
102
103fn find_upwards_for_files(start_dir: &Path, filenames: &[&str]) -> Option<PathBuf> {
104 for filename in filenames {
105 if let Some(found) = find_upwards_for_file(start_dir, filename) {
106 return Some(found);
107 }
108 }
109 None
110}
111
112fn find_upwards_for_setup_subdir(start_dir: &Path, rel_subdir: &str) -> Option<PathBuf> {
113 let mut dir = start_dir;
114 loop {
115 let candidate = dir.join(rel_subdir);
116 if candidate.is_dir() {
117 return Some(candidate);
118 }
119
120 match dir.parent() {
121 Some(parent) if parent != dir => dir = parent,
122 _ => return None,
123 }
124 }
125}
126
127pub fn resolve_rest_setup_dir_for_call(
128 cwd: &Path,
129 invocation_dir: &Path,
130 request_file: &Path,
131 config_dir: Option<&Path>,
132) -> Result<PathBuf> {
133 let seed = match config_dir {
134 Some(dir) => dir.to_path_buf(),
135 None => request_file
136 .parent()
137 .map(PathBuf::from)
138 .unwrap_or_else(|| PathBuf::from(".")),
139 };
140
141 SetupDiscovery {
142 cwd,
143 seed,
144 config_dir_explicit: config_dir.is_some(),
145 files: &[
146 "endpoints.env",
147 "tokens.env",
148 "endpoints.local.env",
149 "tokens.local.env",
150 ],
151 seed_fallback: FallbackMode::Upwards("setup/rest"),
152 invocation_dir: Some(invocation_dir),
153 invocation_fallback: FallbackMode::Upwards("setup/rest"),
154 }
155 .resolve()
156}
157
158pub fn resolve_rest_setup_dir_for_history(
159 cwd: &Path,
160 config_dir: Option<&Path>,
161) -> Result<PathBuf> {
162 let seed = config_dir.unwrap_or_else(|| Path::new("."));
163
164 SetupDiscovery {
165 cwd,
166 seed: seed.to_path_buf(),
167 config_dir_explicit: config_dir.is_some(),
168 files: &[
169 ".rest_history",
170 "endpoints.env",
171 "tokens.env",
172 "tokens.local.env",
173 ],
174 seed_fallback: FallbackMode::Upwards("setup/rest"),
175 invocation_dir: None,
176 invocation_fallback: FallbackMode::None,
177 }
178 .resolve()
179}
180
181pub fn resolve_gql_setup_dir_for_call(
182 cwd: &Path,
183 invocation_dir: &Path,
184 operation_file: Option<&Path>,
185 config_dir: Option<&Path>,
186) -> Result<PathBuf> {
187 let seed = match config_dir {
188 Some(dir) => dir.to_path_buf(),
189 None => operation_file
190 .and_then(|p| p.parent())
191 .map(PathBuf::from)
192 .unwrap_or_else(|| PathBuf::from(".")),
193 };
194
195 SetupDiscovery {
196 cwd,
197 seed,
198 config_dir_explicit: config_dir.is_some(),
199 files: &["endpoints.env", "jwts.env", "jwts.local.env"],
200 seed_fallback: FallbackMode::None,
201 invocation_dir: Some(invocation_dir),
202 invocation_fallback: FallbackMode::Direct("setup/graphql", SETUP_GRAPHQL_ERROR),
203 }
204 .resolve()
205}
206
207pub fn resolve_gql_setup_dir_for_history(
208 cwd: &Path,
209 invocation_dir: &Path,
210 config_dir: Option<&Path>,
211) -> Result<PathBuf> {
212 let seed = config_dir.unwrap_or_else(|| Path::new("."));
213
214 SetupDiscovery {
215 cwd,
216 seed: seed.to_path_buf(),
217 config_dir_explicit: config_dir.is_some(),
218 files: &[
219 ".gql_history",
220 "endpoints.env",
221 "jwts.env",
222 "jwts.local.env",
223 ],
224 seed_fallback: FallbackMode::None,
225 invocation_dir: Some(invocation_dir),
226 invocation_fallback: FallbackMode::Direct("setup/graphql", SETUP_GRAPHQL_ERROR),
227 }
228 .resolve()
229}
230
231pub fn resolve_gql_setup_dir_for_schema(
232 cwd: &Path,
233 invocation_dir: &Path,
234 config_dir: Option<&Path>,
235) -> Result<PathBuf> {
236 let seed = config_dir.unwrap_or_else(|| Path::new("."));
237
238 SetupDiscovery {
239 cwd,
240 seed: seed.to_path_buf(),
241 config_dir_explicit: config_dir.is_some(),
242 files: &[
243 "schema.env",
244 "schema.local.env",
245 "endpoints.env",
246 "jwts.env",
247 "jwts.local.env",
248 ],
249 seed_fallback: FallbackMode::None,
250 invocation_dir: Some(invocation_dir),
251 invocation_fallback: FallbackMode::Direct("setup/graphql", SETUP_GRAPHQL_ERROR),
252 }
253 .resolve()
254}
255
256pub fn resolve_grpc_setup_dir_for_call(
257 cwd: &Path,
258 invocation_dir: &Path,
259 request_file: &Path,
260 config_dir: Option<&Path>,
261) -> Result<PathBuf> {
262 let seed = match config_dir {
263 Some(dir) => dir.to_path_buf(),
264 None => request_file
265 .parent()
266 .map(PathBuf::from)
267 .unwrap_or_else(|| PathBuf::from(".")),
268 };
269
270 SetupDiscovery {
271 cwd,
272 seed,
273 config_dir_explicit: config_dir.is_some(),
274 files: &[
275 "endpoints.env",
276 "tokens.env",
277 "endpoints.local.env",
278 "tokens.local.env",
279 ],
280 seed_fallback: FallbackMode::None,
281 invocation_dir: Some(invocation_dir),
282 invocation_fallback: FallbackMode::Direct("setup/grpc", SETUP_GRPC_ERROR),
283 }
284 .resolve()
285}
286
287pub fn resolve_grpc_setup_dir_for_history(
288 cwd: &Path,
289 invocation_dir: &Path,
290 config_dir: Option<&Path>,
291) -> Result<PathBuf> {
292 let seed = config_dir.unwrap_or_else(|| Path::new("."));
293
294 SetupDiscovery {
295 cwd,
296 seed: seed.to_path_buf(),
297 config_dir_explicit: config_dir.is_some(),
298 files: &[
299 ".grpc_history",
300 "endpoints.env",
301 "tokens.env",
302 "endpoints.local.env",
303 "tokens.local.env",
304 ],
305 seed_fallback: FallbackMode::None,
306 invocation_dir: Some(invocation_dir),
307 invocation_fallback: FallbackMode::Direct("setup/grpc", SETUP_GRPC_ERROR),
308 }
309 .resolve()
310}
311
312pub fn resolve_websocket_setup_dir_for_call(
313 cwd: &Path,
314 invocation_dir: &Path,
315 request_file: &Path,
316 config_dir: Option<&Path>,
317) -> Result<PathBuf> {
318 let seed = match config_dir {
319 Some(dir) => dir.to_path_buf(),
320 None => request_file
321 .parent()
322 .map(PathBuf::from)
323 .unwrap_or_else(|| PathBuf::from(".")),
324 };
325
326 SetupDiscovery {
327 cwd,
328 seed,
329 config_dir_explicit: config_dir.is_some(),
330 files: &[
331 "endpoints.env",
332 "tokens.env",
333 "endpoints.local.env",
334 "tokens.local.env",
335 ],
336 seed_fallback: FallbackMode::None,
337 invocation_dir: Some(invocation_dir),
338 invocation_fallback: FallbackMode::Direct("setup/websocket", SETUP_WEBSOCKET_ERROR),
339 }
340 .resolve()
341}
342
343pub fn resolve_websocket_setup_dir_for_history(
344 cwd: &Path,
345 invocation_dir: &Path,
346 config_dir: Option<&Path>,
347) -> Result<PathBuf> {
348 let seed = config_dir.unwrap_or_else(|| Path::new("."));
349
350 SetupDiscovery {
351 cwd,
352 seed: seed.to_path_buf(),
353 config_dir_explicit: config_dir.is_some(),
354 files: &[
355 ".ws_history",
356 "endpoints.env",
357 "tokens.env",
358 "endpoints.local.env",
359 "tokens.local.env",
360 ],
361 seed_fallback: FallbackMode::None,
362 invocation_dir: Some(invocation_dir),
363 invocation_fallback: FallbackMode::Direct("setup/websocket", SETUP_WEBSOCKET_ERROR),
364 }
365 .resolve()
366}
367
368#[derive(Debug, Clone)]
369pub struct ResolvedSetup {
370 pub setup_dir: PathBuf,
371 pub history_file: PathBuf,
372 pub endpoints_env: PathBuf,
373 pub endpoints_local_env: PathBuf,
374 pub tokens_env: Option<PathBuf>,
375 pub tokens_local_env: Option<PathBuf>,
376 pub jwts_env: Option<PathBuf>,
377 pub jwts_local_env: Option<PathBuf>,
378}
379
380impl ResolvedSetup {
381 pub fn rest(setup_dir: PathBuf, history_override: Option<&Path>) -> Self {
382 let history_file =
383 crate::history::resolve_history_file(&setup_dir, history_override, ".rest_history");
384 let endpoints_env = setup_dir.join("endpoints.env");
385 let endpoints_local_env = setup_dir.join("endpoints.local.env");
386 let tokens_env = setup_dir.join("tokens.env");
387 let tokens_local_env = setup_dir.join("tokens.local.env");
388 Self {
389 setup_dir,
390 history_file,
391 endpoints_env,
392 endpoints_local_env,
393 tokens_env: Some(tokens_env),
394 tokens_local_env: Some(tokens_local_env),
395 jwts_env: None,
396 jwts_local_env: None,
397 }
398 }
399
400 pub fn graphql(setup_dir: PathBuf, history_override: Option<&Path>) -> Self {
401 let history_file =
402 crate::history::resolve_history_file(&setup_dir, history_override, ".gql_history");
403 let endpoints_env = setup_dir.join("endpoints.env");
404 let endpoints_local_env = setup_dir.join("endpoints.local.env");
405 let jwts_env = setup_dir.join("jwts.env");
406 let jwts_local_env = setup_dir.join("jwts.local.env");
407 Self {
408 setup_dir,
409 history_file,
410 endpoints_env,
411 endpoints_local_env,
412 tokens_env: None,
413 tokens_local_env: None,
414 jwts_env: Some(jwts_env),
415 jwts_local_env: Some(jwts_local_env),
416 }
417 }
418
419 pub fn grpc(setup_dir: PathBuf, history_override: Option<&Path>) -> Self {
420 let history_file =
421 crate::history::resolve_history_file(&setup_dir, history_override, ".grpc_history");
422 let endpoints_env = setup_dir.join("endpoints.env");
423 let endpoints_local_env = setup_dir.join("endpoints.local.env");
424 let tokens_env = setup_dir.join("tokens.env");
425 let tokens_local_env = setup_dir.join("tokens.local.env");
426 Self {
427 setup_dir,
428 history_file,
429 endpoints_env,
430 endpoints_local_env,
431 tokens_env: Some(tokens_env),
432 tokens_local_env: Some(tokens_local_env),
433 jwts_env: None,
434 jwts_local_env: None,
435 }
436 }
437
438 pub fn websocket(setup_dir: PathBuf, history_override: Option<&Path>) -> Self {
439 let history_file =
440 crate::history::resolve_history_file(&setup_dir, history_override, ".ws_history");
441 let endpoints_env = setup_dir.join("endpoints.env");
442 let endpoints_local_env = setup_dir.join("endpoints.local.env");
443 let tokens_env = setup_dir.join("tokens.env");
444 let tokens_local_env = setup_dir.join("tokens.local.env");
445 Self {
446 setup_dir,
447 history_file,
448 endpoints_env,
449 endpoints_local_env,
450 tokens_env: Some(tokens_env),
451 tokens_local_env: Some(tokens_local_env),
452 jwts_env: None,
453 jwts_local_env: None,
454 }
455 }
456
457 pub fn endpoints_files(&self) -> Vec<&Path> {
458 if self.endpoints_env.is_file() || self.endpoints_local_env.is_file() {
459 vec![&self.endpoints_env, &self.endpoints_local_env]
460 } else {
461 Vec::new()
462 }
463 }
464
465 pub fn tokens_files(&self) -> Vec<&Path> {
466 let mut files: Vec<&Path> = Vec::new();
467 if let Some(tokens_env) = self.tokens_env.as_deref() {
468 files.push(tokens_env);
469 }
470 if let Some(tokens_local) = self.tokens_local_env.as_deref() {
471 files.push(tokens_local);
472 }
473
474 if files.iter().any(|path| path.is_file()) {
475 files
476 } else {
477 Vec::new()
478 }
479 }
480
481 pub fn jwts_files(&self) -> Vec<&Path> {
482 let mut files: Vec<&Path> = Vec::new();
483 if let Some(jwts_env) = self.jwts_env.as_deref() {
484 files.push(jwts_env);
485 }
486 if let Some(jwts_local) = self.jwts_local_env.as_deref() {
487 files.push(jwts_local);
488 }
489
490 if files.iter().any(|path| path.is_file()) {
491 files
492 } else {
493 Vec::new()
494 }
495 }
496}
497
498#[cfg(test)]
499mod tests {
500 use super::*;
501 use pretty_assertions::assert_eq;
502
503 use tempfile::TempDir;
504
505 fn write_file(path: &Path, contents: &str) {
506 std::fs::create_dir_all(path.parent().expect("parent")).expect("mkdir");
507 std::fs::write(path, contents).expect("write");
508 }
509
510 #[test]
511 fn config_rest_call_falls_back_to_upwards_setup_rest() {
512 let tmp = TempDir::new().expect("tmp");
513 let root = std::fs::canonicalize(tmp.path()).expect("root abs");
514
515 write_file(
516 &root.join("setup/rest/endpoints.env"),
517 "export REST_URL_LOCAL=http://x\n",
518 );
519 write_file(
520 &root.join("tests/requests/health.request.json"),
521 r#"{"method":"GET","path":"/health"}"#,
522 );
523
524 let setup_dir = resolve_rest_setup_dir_for_call(
525 &root,
526 &root,
527 &root.join("tests/requests/health.request.json"),
528 None,
529 )
530 .expect("resolve");
531
532 assert_eq!(setup_dir, root.join("setup/rest"));
533 }
534
535 #[test]
536 fn config_rest_call_explicit_config_dir_wins() {
537 let tmp_root = TempDir::new().expect("tmp root");
538 let root = std::fs::canonicalize(tmp_root.path()).expect("root abs");
539
540 let tmp_cfg = TempDir::new().expect("tmp cfg");
541 let cfg_root = std::fs::canonicalize(tmp_cfg.path()).expect("cfg abs");
542 std::fs::create_dir_all(cfg_root.join("custom/rest")).expect("mkdir");
543
544 write_file(
545 &cfg_root.join("req/health.request.json"),
546 r#"{"method":"GET","path":"/health"}"#,
547 );
548
549 let setup_dir = resolve_rest_setup_dir_for_call(
550 &root,
551 &root,
552 &cfg_root.join("req/health.request.json"),
553 Some(&cfg_root.join("custom/rest")),
554 )
555 .expect("resolve");
556
557 assert_eq!(setup_dir, cfg_root.join("custom/rest"));
558 }
559
560 #[test]
561 fn endpoints_files_includes_local_when_env_missing() {
562 let tmp = TempDir::new().expect("tmp");
563 let root = std::fs::canonicalize(tmp.path()).expect("root abs");
564 let setup = root.join("setup/rest");
565
566 write_file(
567 &setup.join("endpoints.local.env"),
568 "export REST_URL_LOCAL=http://localhost:1234\n",
569 );
570
571 let resolved = ResolvedSetup::rest(setup, None);
572 let files = resolved.endpoints_files();
573
574 assert_eq!(
575 files,
576 vec![
577 resolved.endpoints_env.as_path(),
578 resolved.endpoints_local_env.as_path()
579 ]
580 );
581 }
582
583 #[test]
584 fn tokens_files_handles_missing_local_path_without_panicking() {
585 let tmp = TempDir::new().expect("tmp");
586 let root = std::fs::canonicalize(tmp.path()).expect("root abs");
587 let setup = root.join("setup/rest");
588
589 let mut resolved = ResolvedSetup::rest(setup, None);
590 let tokens_env = resolved.tokens_env.as_ref().expect("tokens env").clone();
591 write_file(&tokens_env, "REST_TOKEN_DEFAULT=abc\n");
592 resolved.tokens_local_env = None;
593
594 let files: Vec<PathBuf> = resolved
595 .tokens_files()
596 .into_iter()
597 .map(Path::to_path_buf)
598 .collect();
599
600 assert_eq!(files, vec![tokens_env]);
601 }
602
603 #[test]
604 fn jwts_files_handles_missing_local_path_without_panicking() {
605 let tmp = TempDir::new().expect("tmp");
606 let root = std::fs::canonicalize(tmp.path()).expect("root abs");
607 let setup = root.join("setup/graphql");
608
609 let mut resolved = ResolvedSetup::graphql(setup, None);
610 let jwts_env = resolved.jwts_env.as_ref().expect("jwts env").clone();
611 write_file(&jwts_env, "GQL_JWT_DEFAULT=abc\n");
612 resolved.jwts_local_env = None;
613
614 let files: Vec<PathBuf> = resolved
615 .jwts_files()
616 .into_iter()
617 .map(Path::to_path_buf)
618 .collect();
619
620 assert_eq!(files, vec![jwts_env]);
621 }
622
623 #[test]
624 fn config_rest_call_falls_back_to_invocation_dir() {
625 let tmp_root = TempDir::new().expect("tmp root");
626 let root = std::fs::canonicalize(tmp_root.path()).expect("root abs");
627
628 std::fs::create_dir_all(root.join("setup/rest")).expect("mkdir");
629
630 let tmp_other = TempDir::new().expect("tmp other");
631 let other = std::fs::canonicalize(tmp_other.path()).expect("other abs");
632 write_file(
633 &other.join("place/health.request.json"),
634 r#"{"method":"GET","path":"/health"}"#,
635 );
636
637 let setup_dir = resolve_rest_setup_dir_for_call(
638 &root,
639 &root,
640 &other.join("place/health.request.json"),
641 None,
642 )
643 .expect("resolve");
644
645 assert_eq!(setup_dir, root.join("setup/rest"));
646 }
647
648 #[test]
649 fn config_gql_call_falls_back_to_setup_graphql_in_invocation_dir() {
650 let tmp = TempDir::new().expect("tmp");
651 let root = std::fs::canonicalize(tmp.path()).expect("root abs");
652
653 write_file(
654 &root.join("setup/graphql/endpoints.env"),
655 "export GQL_URL_LOCAL=http://x\n",
656 );
657 write_file(
658 &root.join("operations/countries.graphql"),
659 "query { __typename }\n",
660 );
661
662 let setup_dir = resolve_gql_setup_dir_for_call(
663 &root,
664 &root,
665 Some(&root.join("operations/countries.graphql")),
666 None,
667 )
668 .expect("resolve");
669
670 assert_eq!(setup_dir, root.join("setup/graphql"));
671 }
672
673 #[test]
674 fn config_gql_schema_discovers_schema_env_upwards() {
675 let tmp = TempDir::new().expect("tmp");
676 let root = std::fs::canonicalize(tmp.path()).expect("root abs");
677
678 write_file(
679 &root.join("setup/graphql/schema.env"),
680 "export GQL_SCHEMA_FILE=schema.gql\n",
681 );
682 std::fs::create_dir_all(root.join("setup/graphql/ops")).expect("mkdir");
683
684 let setup_dir =
685 resolve_gql_setup_dir_for_schema(&root, &root, Some(&root.join("setup/graphql/ops")))
686 .expect("resolve");
687
688 assert_eq!(setup_dir, root.join("setup/graphql"));
689 }
690
691 #[test]
692 fn config_grpc_call_falls_back_to_setup_grpc_in_invocation_dir() {
693 let tmp = TempDir::new().expect("tmp");
694 let root = std::fs::canonicalize(tmp.path()).expect("root abs");
695
696 write_file(
697 &root.join("setup/grpc/endpoints.env"),
698 "export GRPC_URL_LOCAL=127.0.0.1:50051\n",
699 );
700 write_file(
701 &root.join("requests/health.grpc.json"),
702 r#"{"method":"health.HealthService/Check","body":{}}"#,
703 );
704
705 let setup_dir = resolve_grpc_setup_dir_for_call(
706 &root,
707 &root,
708 &root.join("requests/health.grpc.json"),
709 None,
710 )
711 .expect("resolve");
712
713 assert_eq!(setup_dir, root.join("setup/grpc"));
714 }
715
716 #[test]
717 fn resolved_setup_grpc_uses_grpc_history_default() {
718 let tmp = TempDir::new().expect("tmp");
719 let root = std::fs::canonicalize(tmp.path()).expect("root abs");
720 let setup = root.join("setup/grpc");
721 std::fs::create_dir_all(&setup).expect("mkdir");
722
723 let resolved = ResolvedSetup::grpc(setup.clone(), None);
724 assert_eq!(resolved.history_file, setup.join(".grpc_history"));
725 assert!(resolved.tokens_env.is_some());
726 assert!(resolved.tokens_local_env.is_some());
727 }
728
729 #[test]
730 fn config_websocket_call_falls_back_to_setup_websocket_in_invocation_dir() {
731 let tmp = TempDir::new().expect("tmp");
732 let root = std::fs::canonicalize(tmp.path()).expect("root abs");
733
734 write_file(
735 &root.join("setup/websocket/endpoints.env"),
736 "WS_URL_LOCAL=ws://127.0.0.1:9001/ws\n",
737 );
738 write_file(
739 &root.join("requests/health.ws.json"),
740 r#"{"url":"ws://127.0.0.1:9001/ws","steps":[{"type":"send","text":"ping"},{"type":"receive"}]}"#,
741 );
742
743 let setup_dir = resolve_websocket_setup_dir_for_call(
744 &root,
745 &root,
746 &root.join("requests/health.ws.json"),
747 None,
748 )
749 .expect("resolve");
750
751 assert_eq!(setup_dir, root.join("setup/websocket"));
752 }
753
754 #[test]
755 fn resolved_setup_websocket_uses_ws_history_default() {
756 let tmp = TempDir::new().expect("tmp");
757 let root = std::fs::canonicalize(tmp.path()).expect("root abs");
758 let setup = root.join("setup/websocket");
759 std::fs::create_dir_all(&setup).expect("mkdir");
760
761 let resolved = ResolvedSetup::websocket(setup.clone(), None);
762 assert_eq!(resolved.history_file, setup.join(".ws_history"));
763 assert!(resolved.tokens_env.is_some());
764 assert!(resolved.tokens_local_env.is_some());
765 }
766}