1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
//! Tests for `spawn_startup_tasks` (#474).
//!
//! Why: `spawn_startup_tasks` populates `AppState::pin_project_map` in the
//! background so handlers can resolve a palace id to a project path without
//! a filesystem walk at request time (issue #470). Moved out of `main.rs`
//! into its own file (pure code motion, no logic change) to keep `main.rs`
//! under the 500 SLOC production cap — see issue #2522 review.
//!
//! What: exercises the full wiring — a real `AppState` with a real temp
//! search root, a pinned project, and the async background task.
//!
//! Test: this file.
use super::*;
use std::fs;
use trusty_memory::project_root::{write_project_pin, ProjectPin, PIN_SCHEMA_VERSION};
/// Why: the pin scan inside `spawn_startup_tasks` must populate
/// `AppState::pin_project_map` so handlers can resolve a palace id to a
/// project path without a filesystem walk at request time (issue #470).
/// This test verifies the full wiring: a real `AppState` with a real temp
/// search root, a pinned project, and the async background task.
/// What: creates a project with a pin file under a temp search root; then
/// calls `spawn_startup_tasks` and yields to the tokio runtime until the
/// task completes; asserts the pin map contains the expected entry.
/// Test: itself (issue #474 regression guard).
#[tokio::test]
async fn spawn_startup_tasks_populates_pin_map() {
// Build a temp search root with one pinned project.
let tmp = tempfile::tempdir().expect("tempdir");
let search_root = tmp.path().join("Projects");
let project_dir = search_root.join("my-project");
fs::create_dir_all(&project_dir).expect("create project dir");
write_project_pin(
&project_dir,
&ProjectPin {
schema_version: PIN_SCHEMA_VERSION,
palace: "my-palace".to_string(),
note: None,
},
)
.expect("write pin");
// Override HOME so `default_search_dirs()` points at our temp root.
// SAFETY: single-threaded test; env var only affects this process.
let prev_home = std::env::var_os("HOME");
unsafe {
std::env::set_var("HOME", tmp.path());
}
// Also bypass palace-slug enforcement so AppState::new doesn't
// need a real project root.
unsafe {
std::env::set_var("TRUSTY_SKIP_PALACE_ENFORCEMENT", "1");
}
let state_root = tmp.path().join("data");
fs::create_dir_all(&state_root).expect("create data dir");
let state = AppState::new(state_root);
// Fire the background task.
spawn_startup_tasks(&state);
// Yield to the tokio runtime repeatedly until the task populates the
// pin map or a timeout is reached (50 × 10 ms = 500 ms ceiling).
let deadline = std::time::Instant::now() + std::time::Duration::from_millis(500);
loop {
if state.pin_project_map.contains_key("my-palace") {
break;
}
if std::time::Instant::now() >= deadline {
panic!(
"pin_project_map was not populated within 500 ms; \
spawn_startup_tasks may not be running the pin scan"
);
}
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
}
// Restore HOME.
match prev_home {
Some(v) => unsafe { std::env::set_var("HOME", v) },
None => unsafe { std::env::remove_var("HOME") },
}
let found = state.pin_project_map.get("my-palace").map(|e| e.clone());
assert!(
found.is_some(),
"pin_project_map must contain 'my-palace' after spawn_startup_tasks"
);
// Canonicalize to handle macOS /private symlinks.
let actual = fs::canonicalize(found.unwrap()).expect("canonicalize actual");
let expected = fs::canonicalize(&project_dir).expect("canonicalize expected");
assert_eq!(
actual, expected,
"pin_project_map entry must point to the project directory"
);
}