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
//! Adversarial audit — VECTOR 10 (COHERENCE).
//!
//! These are black-box CLI tests: they spawn the real `keyhog` binary and
//! assert that the numbers/labels it advertises to operators agree with the
//! numbers/labels the same binary actually emits, and with the documented
//! contract under `docs/`. Every assertion is drift-proof: it derives the
//! "expected" value from a live source of truth (the binary's own JSON output,
//! or the committed docs), never from a hardcoded constant — so the test stays
//! correct after the corpus grows or shrinks again.
//!
//! Each `#[test]` documents one finding: the drift, the file:line evidence,
//! and the expected fix.
use PathBuf;
use Command;
/// Resolve the keyhog binary under test.
///
/// `CARGO_BIN_EXE_keyhog` is injected by Cargo for integration tests and is the
/// canonical handle. We fall back to the prebuilt release-fast artifact if, for
/// some reason, the env handle points at a path that does not exist.
/// Count top-level JSON objects in `keyhog detectors --json` output without a
/// serde dependency: every detector object opens with the canonical first key
/// `"companions"` (the detectors subcommand serialises keys in a fixed order,
/// `companions` first). This is robust to the array growing/shrinking and to
/// pretty-printing, and avoids brittle brace-counting.
///
/// We cross-check against the array's structural shape: the output must start
/// with `[` and end with `]`.
/// AUD-coherence-1 — `keyhog detectors --help` undercounts the embedded corpus.
///
/// FINDING: The detector count is 899 everywhere it is computed at runtime:
/// - `detectors/*.toml` on disk: 899 files
/// - `keyhog detectors --json` array length: 899
/// - `keyhog detectors` summary header: "Loaded 899 detectors"
/// - tests/docker/scenarios.sh:95 expects "Loaded 899 detectors"
/// - README.md:19/50/60/80 say "899 detectors"
/// BUT the `--search` help text hardcodes a stale "894-strong corpus":
/// - crates/cli/src/args.rs:372 (`/// ... finding detectors in the 894-strong corpus`)
/// - crates/cli/src/subcommands/detectors.rs:45 (comment "The 894-strong corpus")
/// - crates/cli/src/orchestrator/mod.rs:188 (comment "all 894 regexes")
/// So an operator reading `keyhog detectors --help` is told there are 894
/// detectors while the same binary loads and lists 899.
///
/// This test is DRIFT-PROOF: it parses the number cited in `--help`
/// (`<N>-strong`) and asserts it EQUALS the live `--json` array length. It
/// fails today (894 != 899) and will pass once the help string is made dynamic
/// (or the constant is corrected) so the cited count tracks the real corpus.
///
/// EXPECTED FIX: replace the hardcoded "894-strong" literal in args.rs:372 with
/// the actual embedded detector count (e.g. injected at build time or rendered
/// at runtime), and update the stale comments. The existing tests
/// crates/cli/tests/gap/detectors_search_help_detector_count_drift.rs and
/// crates/cli/tests/stress/detectors_help_detector_count_drift.rs hardcode
/// "894-strong" and must also become dynamic (assert help count == actual
/// count) — but those are NOT edited here.
/// AUD-coherence-2 — `--help` EXIT CODES omits a documented exit-4 producer.
///
/// FINDING: The committed exit-code contract in
/// docs/src/reference/exit-codes.md defines exit `4` as a *Health/self-test
/// failure* covering THREE producers:
/// "`keyhog doctor` unhealthy, `keyhog repair` could not restore a working
/// binary, `keyhog backend` self-test failed."
/// The source agrees that exit 4 has multiple producers:
/// - crates/cli/src/subcommands/repair.rs:17 `const EXIT_REPAIR_FAILED: u8 = 4;`
/// - crates/cli/src/subcommands/backend.rs:23 `const EXIT_SELF_TEST_FAILED: u8 = 4;`
/// BUT the `--help` EXIT CODES block (crates/cli/src/args.rs:15) describes exit
/// 4 as ONLY "`backend --self-test` failed (GPU/SIMD probe error)", silently
/// dropping the `keyhog repair` producer that the docs and source both have.
/// An operator who reads `keyhog --help` is given an incomplete exit-4 contract
/// that does not match the documentation they are pointed at (README.md:83).
///
/// This test is drift-proof against the docs: it reads the live `--help` exit-4
/// line and asserts it acknowledges the `repair` producer that
/// docs/src/reference/exit-codes.md documents. It fails today (the help line
/// mentions only `backend`/self-test) and passes once the help text is widened
/// to match the documented contract.
///
/// EXPECTED FIX: update the exit-4 line in the `after_help` EXIT CODES block in
/// crates/cli/src/args.rs:15 to describe the full exit-4 contract (doctor /
/// repair / backend self-test), matching docs/src/reference/exit-codes.md.
/// AUD-coherence-3 — `--help` calls exit 2 "Runtime error"; docs call it "user error".
///
/// FINDING: The same exit code 2 is labelled two contradictory ways:
/// - `keyhog --help` (crates/cli/src/args.rs:15):
/// "2 Runtime error (e.g., config error, unreadable path)"
/// - docs/src/reference/exit-codes.md:
/// "`2` User error: unknown CLI flag, `.keyhog.toml` parse failure, bad `--baseline`."
/// - docs/src/first-scan.md:
/// "`2` User error - bad config, bad path, unsupported flag"
/// The binary's behaviour matches the DOCS' framing: an unknown flag exits 2
/// and a nonexistent path exits 2 — both are *user* errors, distinct from the
/// "System error" reserved for exit 3. Calling exit 2 a "Runtime error" in
/// `--help` while the docs and `main.rs` (`EXIT_USER_ERROR: u8 = 2`) call it a
/// user error is a label disagreement on the same code: the two sources of
/// truth an operator consults give different names for the same exit.
///
/// This test verifies the behaviour (unknown flag -> 2) AND that the `--help`
/// label for exit 2 agrees with the documented "user error" wording. It fails
/// today because `--help` says "Runtime error", not "user error".
///
/// EXPECTED FIX: relabel the exit-2 line in the EXIT CODES `after_help` block in
/// crates/cli/src/args.rs:15 to "User error" to match docs/src/reference/
/// exit-codes.md, docs/src/first-scan.md, and the `EXIT_USER_ERROR` constant
/// name in crates/cli/src/main.rs:26.