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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
//! Unit tests for `core::worktree_index` (#5060).
//!
//! Why: split into a sibling file (declared `#[path = "worktree_index_tests.rs"]
//! mod tests;`) so `worktree_index.rs` stays under the 500-SLOC production cap.
//!
//! What: drives [`super::index_new_worktree`] synchronously against REAL git
//! repositories built in a hermetic temp dir, so the worktree-vs-clone
//! distinction is proven against git's actual on-disk shape (`.git` file vs
//! `.git` directory) rather than a hand-rolled stand-in. No test here touches
//! the operator's trusty-search daemon: `trusty_common::search_index` refuses
//! every daemon write from a test process (#4255), so the registration branch
//! runs its derivation and returns without issuing a request.
//!
//! Test: `cargo test -p trusty-mpm -- core::worktree_index`
use std::path::{Path, PathBuf};
use std::process::Command;
use trusty_common::search_index::IndexRegistration;
use super::{WorktreeIndexOutcome, index_new_worktree, index_new_worktree_in_background};
/// Run `git <args>` inside `dir`, returning whether it succeeded.
fn git_ok(dir: &Path, args: &[&str]) -> bool {
Command::new("git")
.arg("-C")
.arg(dir)
.args(args)
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
/// Build a real git repository with one commit, or `None` when git is
/// unavailable / unusable in this environment (CI sandboxes without a git
/// identity), so the tests skip rather than fail spuriously.
fn init_repo(base: &Path) -> Option<()> {
std::fs::create_dir_all(base).ok()?;
if !git_ok(base, &["init", "--initial-branch=main"]) {
return None;
}
let _ = git_ok(base, &["config", "user.email", "ci@test.invalid"]);
let _ = git_ok(base, &["config", "user.name", "CI"]);
std::fs::write(base.join("README.md"), "# fixture\n").ok()?;
if !git_ok(base, &["add", "."]) {
return None;
}
if !git_ok(base, &["commit", "-m", "seed"]) {
return None;
}
Some(())
}
/// A base clone plus one worktree cut from it, both under a hermetic temp dir.
struct Fixture {
_dir: tempfile::TempDir,
base: PathBuf,
worktree: PathBuf,
}
fn fixture(worktree_name: &str) -> Option<Fixture> {
let dir = crate::test_support::hermetic_temp_dir();
let base = dir.path().join("base-repo");
init_repo(&base)?;
let worktree = dir.path().join(worktree_name);
if !git_ok(
&base,
&[
"worktree",
"add",
"-b",
"feature",
worktree.to_str().expect("utf8 path"),
],
) {
return None;
}
Some(Fixture {
_dir: dir,
base,
worktree,
})
}
/// A path that does not exist is refused, not registered.
///
/// Why: the caller is a `git worktree add` success branch, but a TOCTOU
/// removal (or a caller passing the wrong path) must not reach the daemon and
/// register an index for a directory that is not there — the exact shape that
/// filled the registry with dead roots and stalled warm boot (#4255).
/// Test: this test.
#[test]
fn skips_missing_path() {
let dir = crate::test_support::hermetic_temp_dir();
let missing = dir.path().join("no-such-worktree");
assert_eq!(
index_new_worktree(&missing),
WorktreeIndexOutcome::PathMissing
);
}
/// A PLAIN CLONE is refused — only worktrees are indexed here.
///
/// Why (regression): this is the guard that keeps worktree creation from
/// making the operator's opt-in decision for them. Relaxing
/// `is_git_worktree` from "`.git` is a FILE" to "`.git` EXISTS" — the obvious
/// simplification, since both shapes are "a git repo" — silently turns this
/// module into one that registers an index for any checkout handed to it.
/// That mutation is invisible without this assertion, because the permissive
/// version still returns `Registered` and still "works".
/// Test: this test.
#[test]
fn skips_path_that_is_not_a_worktree() {
let dir = crate::test_support::hermetic_temp_dir();
let base = dir.path().join("plain-clone");
let Some(()) = init_repo(&base) else {
eprintln!("skipping: git unavailable in this environment");
return;
};
assert!(
base.join(".git").is_dir(),
"fixture precondition: a primary clone's .git is a directory"
);
assert_eq!(
index_new_worktree(&base),
WorktreeIndexOutcome::NotAWorktree,
"a primary clone must not be auto-indexed by worktree creation"
);
}
/// A directory with no git repository at all is refused.
#[test]
fn skips_plain_directory() {
let dir = crate::test_support::hermetic_temp_dir();
let plain = dir.path().join("not-a-repo");
std::fs::create_dir_all(&plain).expect("create dir");
assert_eq!(
index_new_worktree(&plain),
WorktreeIndexOutcome::NotAWorktree
);
}
/// A real worktree registers under its OWN basename, not the base repo's.
///
/// Why (regression): the index id must identify the WORKTREE. Deriving it from
/// the enclosing repository instead — the mistake a `resolve_project_root`
/// walk invites — would make every worktree of a repo collide on one id, so
/// the last worktree created would silently overwrite the previous one's root
/// and every earlier worktree's `search` would answer from the wrong tree.
/// Both trees are real checkouts of the same project, so the results would
/// look plausible; only the id assertion catches it.
/// Test: this test.
#[test]
fn registers_index_for_real_worktree() {
let Some(fx) = fixture("feat-index-on-create") else {
eprintln!("skipping: git unavailable in this environment");
return;
};
assert!(
fx.worktree.join(".git").is_file(),
"fixture precondition: a worktree's .git is a file"
);
assert_eq!(
index_new_worktree(&fx.worktree),
WorktreeIndexOutcome::RegistrationUnconfirmed {
index_id: "feat-index-on-create".to_string(),
reason: IndexRegistration::SkippedUnderTest,
},
"index id must be the worktree's own basename, not {:?}",
fx.base.file_name()
);
}
/// A registration the daemon never confirmed reports as UNCONFIRMED, not as
/// `Registered` (#5065 review).
///
/// Why: `ensure_project_indexed_with` returns `Some(id)` whether the daemon
/// answered 2xx, answered 500, or was never contacted at all. Mapping that
/// `Some` straight onto `Registered` made this module log `worktree index
/// registered` for a call that never left the process — announcing a success it
/// never observed, in the one enum whose stated purpose (`WorktreeIndexOutcome`
/// doc) is to keep outcomes distinguishable. #5045 measured the daemon-absent
/// case at ~94% of worktrees, so the un-distinguished outcome was the COMMON
/// one.
/// What: runs the real entry point under the #4255 test harness, where the
/// daemon write is suppressed and therefore provably unconfirmed, and asserts
/// the outcome is `RegistrationUnconfirmed` carrying the reason — while still
/// carrying the derived id, since the fail-open contract is unchanged.
/// `registers_index_for_real_worktree` above pins the same thing from the
/// id-derivation angle.
/// Test: this test.
#[test]
fn reports_unconfirmed_rather_than_registered_when_the_daemon_never_answered() {
let Some(fx) = fixture("feat-unconfirmed") else {
eprintln!("skipping: git unavailable in this environment");
return;
};
let outcome = index_new_worktree(&fx.worktree);
assert!(
!matches!(outcome, WorktreeIndexOutcome::Registered(_)),
"nothing confirmed this index — `Registered` would be a success we \
never observed, got {outcome:?}"
);
let WorktreeIndexOutcome::RegistrationUnconfirmed { index_id, reason } = outcome else {
panic!("expected RegistrationUnconfirmed, got {outcome:?}");
};
assert_eq!(index_id, "feat-unconfirmed", "the id is still reported");
assert_eq!(reason, IndexRegistration::SkippedUnderTest);
}
/// Re-running against an already-registered worktree is a cheap, stable no-op.
///
/// Why: the hook fires on every worktree creation and the daemon's
/// `POST /indexes` is find-or-create, so a repeat call must return the SAME id
/// rather than minting a second registration or erroring. Without this, a
/// retried provisioning step could fork the index identity.
/// Test: this test.
#[test]
fn is_idempotent_for_same_worktree() {
let Some(fx) = fixture("feat-idempotent") else {
eprintln!("skipping: git unavailable in this environment");
return;
};
let first = index_new_worktree(&fx.worktree);
let second = index_new_worktree(&fx.worktree);
assert_eq!(first, second);
assert_eq!(
first,
WorktreeIndexOutcome::RegistrationUnconfirmed {
index_id: "feat-idempotent".to_string(),
reason: IndexRegistration::SkippedUnderTest,
}
);
}
/// The spawn helper returns BEFORE its work finishes (regression, THE hard
/// constraint).
///
/// Why: a cold BM25+KG build of this workspace measured 35.0 s. If a future
/// edit joined the spawned thread — the natural "let's return the outcome to
/// the caller" refactor — `git worktree add` and every session launch behind
/// it would block for that long, and a joined version still produces a correct
/// index so nothing else in the suite would go red.
/// What: hands `spawn_detached` work that sleeps far longer than the
/// assertion's ceiling, so a `.join()` cannot pass. Asserting against
/// [`index_new_worktree_in_background`] directly would NOT catch that: under
/// the test harness the daemon write is refused instantly (#4255), so the real
/// work finishes in microseconds whether joined or not. The test therefore
/// pins the property on the helper that owns it, and
/// `background_entry_point_routes_through_spawn_detached` pins that the entry
/// point still uses that helper.
/// Test: this test.
#[test]
fn spawn_detached_returns_before_work_finishes() {
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
let done = Arc::new(AtomicBool::new(false));
let flag = Arc::clone(&done);
let started = std::time::Instant::now();
super::spawn_detached(move || {
std::thread::sleep(std::time::Duration::from_secs(3));
flag.store(true, Ordering::SeqCst);
});
let elapsed = started.elapsed();
assert!(
elapsed < std::time::Duration::from_millis(500),
"spawn must not wait on its work; returned after {elapsed:?}"
);
assert!(
!done.load(Ordering::SeqCst),
"work must still be running — a joined spawn would have completed it"
);
}
/// The public entry point returns promptly for a real worktree.
///
/// Why: `spawn_detached_returns_before_work_finishes` proves the helper is
/// detached; this proves the entry point actually goes through it rather than
/// doing the walk inline. Weaker than that test on its own (the harness makes
/// the real work fast), so the two are complementary, not redundant.
/// Test: this test.
#[test]
fn background_entry_point_routes_through_spawn_detached() {
let Some(fx) = fixture("feat-nonblocking") else {
eprintln!("skipping: git unavailable in this environment");
return;
};
let started = std::time::Instant::now();
index_new_worktree_in_background(fx.worktree.clone());
let elapsed = started.elapsed();
assert!(
elapsed < std::time::Duration::from_millis(500),
"worktree creation must never wait on indexing; returned after {elapsed:?}"
);
}
/// `is_git_worktree` distinguishes the two on-disk shapes.
#[test]
fn is_git_worktree_matches_git_on_disk_shapes() {
let Some(fx) = fixture("feat-shapes") else {
eprintln!("skipping: git unavailable in this environment");
return;
};
assert!(
super::is_git_worktree(&fx.worktree),
"worktree: .git is a file"
);
assert!(
!super::is_git_worktree(&fx.base),
"clone: .git is a directory"
);
}