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
//! This crate's test-only helper surface, reachable cross-crate through the
//! `test-support` feature (`lib.rs:76-79`'s
//! `#[cfg(any(test, feature = "test-support"))]` gate keeps all of it absent
//! from a normal build). Two unrelated hazards live here, each documented at
//! its own definitions below: hermetic git invocation for test fixtures
//! (999.37, the module's original scope) and the 999.47 exec-visibility
//! barrier (25-11).
//!
//! ## Hermetic git invocation (999.37)
//!
//! Test fixtures build throwaway repositories in tempdirs and shell out to
//! `git` against them. Pinning the working directory is **not** sufficient to
//! keep those commands inside the fixture: git's repository-local environment
//! variables — `GIT_DIR` above all — outrank a process's working directory
//! when git resolves which repository to act on. `git -C <dir>` does not
//! override `GIT_DIR` either (only the `--git-dir` flag does), and
//! `GIT_CEILING_DIRECTORIES` does not contain it, so clearing the variables is
//! the only reliable containment.
//!
//! This matters because Rust runs a test binary's tests as threads in ONE
//! process: if the suite is launched with those variables set, every fixture
//! inherits them at once. That is exactly what happened when a `git push` from
//! a linked worktree ran the pre-push hook (git exports `GIT_DIR` to hooks
//! when the gitdir is non-default) — fixtures retargeted the real checkout,
//! setting `core.bare=true` on it, rewriting its committer identity, and
//! stacking fixture commits onto its `main` branch.
//!
//! `scripts/hooks/pre-push` clears these before running the suite, and
//! `git_env_hermeticity.rs` fails fast if they are present. Both are process
//! level. This helper is the per-command layer: a fixture built through it is
//! contained even when the environment is dirty and whatever the launch path.
// ## Exec-visibility barrier (25-11/999.47)
//
// `process_start_time`'s doc comment (`crate::agent`) is this codebase's own
// authoritative statement of the mechanism: between `Command::spawn()`
// returning and the child completing `execve`, the child is a copy of its
// parent, so `/proc/<pid>/cmdline` transiently reports the PARENT's argv,
// not the child's own. A test that spawns a child and immediately reads a
// `/proc`-cmdline census about it (via
// `crate::agent::discover_stray_devflow_processes` or its CLI-side
// equivalent) races that window, load-sensitively — 0 failures across 17
// warm local runs, 2 failures in 2 attempts under the loaded shape
// `scripts/check-in-container.sh all` runs (`25-CI-OBSERVATION.md`).
//
// `wait_for_exec_visibility` is the barrier a test must cross before
// asserting on such a census. `crate::agent::agent_running` is NOT such a
// barrier: `kill(pid, 0)` succeeds for a forked-but-unexec'd child, because
// the pid is allocated at `fork()`, well before `execve` runs — a liveness
// poll closes no window at all here.
/// Bounded default wait for [`wait_for_exec_visibility`]. The window this
/// barrier waits out is sub-millisecond in the normal case and the function
/// returns immediately once the child has exec'd, so a generous ceiling
/// costs nothing in the common path — it exists only so a pathological case
/// (a child that never execs, or a wrong `expected_argv0_basename`) fails
/// loudly within a bounded time instead of hanging a test binary.
pub const EXEC_VISIBILITY_WAIT: Duration = from_secs;
/// Poll interval for [`wait_for_exec_visibility`]. Matches the granularity
/// [`crate::agent::TERMINATE_VERIFY_POLL`] uses for the same reason: fine
/// enough that the barrier resolves promptly once the condition is true,
/// coarse enough not to busy-loop.
pub const EXEC_VISIBILITY_POLL: Duration = from_millis;
/// Poll `/proc/<pid>/cmdline` until `pid`'s argv is genuinely its OWN — not
/// its parent's, transiently inherited across the `fork()`->`execve()`
/// window — and report whether that happened within `wait`.
///
/// Returns `true` only when BOTH hold, checked on every poll:
///
/// (i) argv[0]'s basename equals `expected_argv0_basename`, via
/// [`crate::agent::argv_basename`] — the exact idiom
/// `classify_stray_layer` uses, reused rather than copied so a second
/// basename idiom cannot drift from the first.
/// (ii) the observed cmdline is not byte-identical to the caller's own
/// `/proc/self/cmdline`, captured once at call time. This is the guard
/// against the degenerate case where the caller's own argv[0] basename
/// happens to equal `expected_argv0_basename` — without it, a test
/// asserting on its OWN pid could pass merely because it started out
/// matching, which would make the barrier's answer
/// probabilistically-correct instead of unambiguous.
///
/// Parses the NUL-separated cmdline the same way
/// [`crate::agent::discover_stray_devflow_processes`] does. An unreadable
/// `/proc/<pid>/cmdline` is not itself a failure — a pid that has not yet
/// appeared, or has already exited, is exactly the kind of transient state
/// this function polls through — but a pid that is verifiably not alive
/// (checked via [`crate::agent::agent_running`] on every iteration) returns
/// `false` immediately rather than waiting out the full `wait` ceiling: a
/// dead pid can never become exec-visible, so there is nothing to wait for.
// Hermetic git invocation constants and constructors now live in
// `crate::git` — the always-compiled home (this module is gated
// `#[cfg(any(test, feature = "test-support"))]`, absent from a normal
// build, so it cannot be the canonical home for a production-reachable
// constructor — 27-01/D-01). Re-exported here so every existing fixture
// call site (~40 across both crates' test targets) keeps compiling
// unchanged, and so the two lists can never drift apart.
pub use crate;
// ## Why there is no absent-`git` (`NoGitPath`) harness in THIS crate
//
// 35-01 planned one `NoGitPath` guard per crate, so that criterion 6's tests
// could force `git` to be unresolvable and drive `phase_commit_count`'s
// could-not-measure branch from inside `devflow-core`. **That guard was built
// here, measured, and removed.** It is recorded rather than silently omitted,
// because the next author to need a failing `git` will otherwise rebuild it.
//
// A `PATH`-replacing guard mutates process-global state, and `cargo test` runs
// this crate's whole suite as threads in ONE process. `devflow-core` shells out
// to `git` from eight modules (`git`, `version`, `worktree`, `agent_result`,
// `monitor`, `ship_evidence`, `hooks`, and this one), and — decisively — its
// tests reach `git` by calling PRODUCTION code that spawns it, not only through
// fixture helpers. So no fixture-level lock can cover them: serializing this
// module's own `git()` helper still left
// `agent_result::tests::evaluate_layer2_exit_zero_no_commits_is_failed` failing,
// because its `git` call happens inside `evaluate_layer2` itself.
//
// Measured, with a control:
//
// - guard used by three regression tests ....... 1-5 unrelated failures/run
// - guard used by its own sanity test only ..... 1 failure in 8 runs
// - sanity test `#[ignore]`d (control) ......... 0 failures in 10 runs
//
// The asymmetry between the last two lines is what identifies the guard itself
// as the cause rather than a pre-existing flake. Ten clean runs is a weak bound
// on the control arm, not a proof of zero flake rate; it is enough to establish
// the direction, which is all that is being claimed.
//
// **What to use instead.** `hermetic_command` sets `cmd.current_dir(dir)`, so
// passing a path that does not exist makes the spawn itself fail and
// `.output()` return `Err` — the identical arm a missing binary produces,
// reached with no environment mutation and therefore no effect on any other
// test. `agent_result`'s `phase_commit_count_reports_none_when_git_cannot_run`
// and `evaluate_layer3_unmeasurable_count_is_unknown_not_failed` both take that
// route. It is also immune to the latent fragility of a `PATH` guard, which a
// future refactor to an absolute `git` path would disarm silently.
//
// **When that is not enough**, because the code under test must also READ a
// file from `project_root` (`evaluate_layer2` reads its exit file there, so a
// non-existent root would fail for the wrong reason), the test belongs in
// `devflow-cli`'s binary, where every `PATH` mutation goes through one
// `ENV_MUTEX` that its `git`-touching tests already hold. Criterion 6's
// layer-level and cascade-level tests live in
// `devflow-cli/src/pipeline_outcomes.rs` for exactly this reason; they call the
// same `pub` functions, so only the binary differs.