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
//! `ridl --version` and `ridl mcp`'s `serverInfo.version` report the build
//! version: the `editor-v*` tag when the release workflow set
//! `RIDL_BUILD_VERSION` (`build.rs`), else the crate version — the same
//! string from both, so a bug report names one build regardless of which
//! server the reporter queried.
//!
//! The version is baked in at compile time — `src/main.rs` reads it through
//! `env!("RIDL_BUILD_VERSION")` — so the injected branch is only observable
//! by rebuilding the binary with the variable set. Comparing the already-
//! built `CARGO_BIN_EXE_ridl` against `std::env::var` at test-run time (as a
//! first draft of this test did) cannot exercise that branch under `just
//! test`, which never sets the variable: the comparison passes whether or
//! not the injection code exists at all, because both sides independently
//! fall back to the crate version. This test instead rebuilds the binary
//! itself, with the variable set and then removed, and checks each build's
//! output.
//!
//! `build.rs` declares `cargo:rerun-if-env-changed=RIDL_BUILD_VERSION`, so a
//! changed value invalidates only the `ridl` crate's own build script and
//! compile unit — every dependency stays cached, and a rebuild costs a
//! couple of seconds, not a workspace recompile. That is true for a plain
//! shell invocation; a cargo build spawned *from inside a running test*
//! inherits this process's own environment, which cargo has already
//! populated with this package's `CARGO_MANIFEST_DIR`, `CARGO_PKG_*`, and
//! `OUT_DIR` (the variables it sets for every test binary of a package that
//! has a build script). A couple of `ridl-core`'s optional dependencies
//! (`ureq`, and transitively `rustls`/`ring`) read some of those in their
//! own build scripts; inherited instead of values scoped to the nested
//! build, they read as "changed" and force an unrelated rebuild of that
//! whole chain (confirmed with `cargo build -v`: `Dirty ring: the env
//! variable CARGO_MANIFEST_DIR changed`). `rebuild_and_run_version` strips
//! the leaked variables so the nested build sees the same environment a
//! plain shell invocation would, and reliably costs only the couple of
//! seconds `ridl` itself takes to recompile.
//!
//! The nested build also matches this test binary's own profile
//! (`--release` when `cargo test --release` compiled this file, nothing
//! otherwise): `CARGO_BIN_EXE_ridl` always names the binary for *that*
//! profile, and a nested build with no `--profile` of its own always
//! rebuilds `target/debug/ridl` regardless, which would silently rebuild
//! the wrong binary under `cargo test --release`. `just test` is
//! debug-only, so this is latent rather than observed, but it is real.
use ;
use ;
use mpsc;
use Duration;
/// A value distinct from the crate's own version, so the injected and
/// fallback cases cannot pass by coincidence.
const INJECTED_VERSION: &str = "editor-v9.9.9-test";
/// How long `ridl mcp` is given to answer the one request this file sends
/// it. Generous, because a loaded CI machine is slow; bounded, because
/// `cargo test` has no per-test timeout and a hung server would otherwise
/// hang the whole run — the same rationale and value
/// `crates/ridl/tests/servers.rs`'s own `TIMEOUT` uses.
const MCP_TIMEOUT: Duration = from_secs;
/// Rebuilds the `ridl` binary with `RIDL_BUILD_VERSION` set to `version` (or
/// removed, for `None`) and returns `ridl --version`'s trimmed stdout.
///
/// Uses `CARGO_MANIFEST_DIR` (this crate's own directory) rather than relying
/// on the test process's current directory, and `CARGO` (the path to the
/// cargo binary running this test, which cargo always sets for a spawned
/// test) rather than assuming `cargo` is on `PATH`.
/// Kills and reaps the wrapped child on drop — including when the drop runs
/// while unwinding past a panic. `std::process::Child`'s own `Drop` does
/// neither, so a test failure between spawning `ridl mcp` and reading its
/// response would otherwise leak the process.
;
/// Spawns `ridl mcp` from the currently-built `CARGO_BIN_EXE_ridl` (so it
/// reflects whichever `rebuild_and_run_version` call ran last), sends one
/// `initialize` request, and returns the `serverInfo.version` the response
/// carries.
///
/// This is the minimal roundtrip needed to read `serverInfo` back — not the
/// full handshake and clean-shutdown sequence
/// `crates/ridl/tests/servers.rs` performs for its own, more thorough MCP
/// coverage. The response is read on a worker thread so the wait for it can
/// be bounded by [`MCP_TIMEOUT`]: an unresponsive `ridl mcp` fails this test
/// within that bound instead of hanging the run until the CI job's own
/// timeout, which is what a plain blocking `read_line` would do.
/// Pins the actual injection mechanism: rebuilding with `RIDL_BUILD_VERSION`
/// set changes the reported version to exactly that value — in both
/// `ridl --version` and `ridl mcp`'s `serverInfo.version` — and rebuilding
/// again with the variable unset reports exactly the crate version — so
/// either half of `build.rs`'s fallback (`unwrap_or_else`) being broken, or
/// `src/main.rs` no longer reading `RIDL_BUILD_VERSION` at all for either
/// server, fails this test.
///
/// The MCP check has to live here rather than as a standalone assertion:
/// `crates/ridl/tests/servers.rs`'s own `serverInfo.version` assertion
/// compares against `env!("RIDL_BUILD_VERSION")`, which is *this build's*
/// compile-time value — a meaningful check when the whole test run was
/// invoked with the variable already set, but not under plain `just test`,
/// where it never is: `ridl` and `ridl-mcp` share the workspace version `0.0.0`
/// (`version.workspace = true`), so under an unset variable,
/// `ridl-mcp`'s own `CARGO_PKG_VERSION` and `ridl`'s `RIDL_BUILD_VERSION`
/// fallback are numerically the same value regardless of whether `run_mcp`
/// actually wires one into the other. Only a build with a *distinct*,
/// non-default value — this test's own rebuild — tells the two apart.
///
/// The injected-version half runs inside `catch_unwind` so the fallback
/// rebuild below always runs exactly once, whether or not an assertion in
/// that half panics: skipping it on a panic would still self-heal on the
/// next unrelated rebuild (`build.rs`'s `rerun-if-env-changed`), but only
/// then — in the meantime, `target/debug/ridl` stays stamped
/// `editor-v9.9.9-test`, which can make an unrelated later test
/// (`servers.rs`'s own version assertion, or a person running the binary by
/// hand) fail in a way that looks like a fresh regression. The fallback
/// rebuild also leaves `target/debug/ridl` — the same binary
/// `CARGO_BIN_EXE_ridl` and every other integration test in this crate
/// spawns — back in its ordinary, un-injected state.