test(m4): wait for a complete sink record, not a substring of one
`m4_5_initial_config_pushed_via_did_change_configuration` fails
intermittently on macOS/lua54 with a truncated payload, observed in CI as:
the daemon pushed the configured settings after initialized: {"rust":{"probe":
This is a real read-while-writing race, not a platform quirk. The wait
predicate was weaker than the assertion it guards: the pump waited for
`contains("probe")` while the assertion needs `"probe":true`, six bytes
further on. The sink is JSONL written by a separate process, so the test
could read a half-written line. Linux wins that race reliably; macOS does
not.
Wait for the trailing newline instead. `src/bin/pmacs_fake_lsp.rs` writes
the sink with `writeln!`, one record per push, so a trailing newline is
true only once a whole record has landed — it waits for exactly the unit
the assertion reads, and stays correct if the payload's field order or
spelling ever changes.
Note this cannot be falsified locally: reproducing it means losing a
scheduler race that Linux wins, so a passing local run is a regression
check rather than proof. The argument is structural — `writeln!` is the
only writer of this file.
The sibling `rooturi` sink test has the same weak-predicate shape and is
deliberately NOT changed, with a comment recording why: waiting for the
expected value there would convert a genuine regression — `rootUri`
falling back to the cwd, which its `assert_ne!`s exist to catch — into a
five-second timeout with a misleading "server didn't initialize?"
message, trading a precise diff for a vague hang. Closing it properly
means giving that sink a record terminator in the fake server, and it has
never been observed failing, so it is a separate change.
Gates: `cargo fmt --check` clean; strict workspace Clippy clean;
`m4_acceptance -- --skip basedpyright` 121 passed; the previously-racy
test 10/10 in isolation; `git diff --check` clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0126d2sikA6jZpFin3rtLCSK
This commit is contained in:
parent
ccf29e3eb5
commit
c4b759553d
|
|
@ -5483,6 +5483,18 @@ fn m4_26_auto_attach_roots_server_at_opened_files_project() {
|
|||
|
||||
// The fake writes the received rootUri on `initialize`; wait for
|
||||
// the side-channel to carry the scheme.
|
||||
//
|
||||
// NOTE: this predicate is weaker than the `assert_eq!` below, the same
|
||||
// shape that made the config sink in
|
||||
// `m4_5_initial_config_pushed_via_did_change_configuration` race. It is
|
||||
// left as-is deliberately. Waiting for the expected value instead would
|
||||
// turn a genuine regression — `rootUri` falling back to the cwd, which
|
||||
// the `assert_ne!`s below exist to catch — into a five-second timeout
|
||||
// with a misleading "server didn't initialize?" message, trading a
|
||||
// precise diff for a vague hang. Closing it properly means giving the
|
||||
// rooturi sink a record terminator in `src/bin/pmacs_fake_lsp.rs` and
|
||||
// waiting for that; it has never been observed failing, so that is a
|
||||
// separate change rather than a drive-by.
|
||||
assert!(
|
||||
pump_until_file_contains(&mut state, &sink, "file://", 5),
|
||||
"fake never recorded a rootUri (server didn't initialize?)"
|
||||
|
|
@ -6831,9 +6843,21 @@ fn m4_5_initial_config_pushed_via_did_change_configuration() {
|
|||
"fake never initialized"
|
||||
);
|
||||
// A few more ticks for the push + the server's sink write to land.
|
||||
//
|
||||
// Wait for a COMPLETE record, not for a substring of one. The sink is
|
||||
// JSONL written by a separate process (`writeln!` in
|
||||
// `src/bin/pmacs_fake_lsp.rs`, one line per push), so a substring
|
||||
// predicate can be satisfied by a half-written line and the assertion
|
||||
// below then reads the truncation. That is a real race, not a platform
|
||||
// quirk: `probe` lands six bytes before `"probe":true` is complete, and
|
||||
// macOS/lua54 lost it in CI, reporting `{"rust":{"probe":`.
|
||||
//
|
||||
// The trailing newline is the strongest available predicate because it
|
||||
// waits for exactly the unit the assertion reads, and it stays correct
|
||||
// if the payload's field order or spelling ever changes.
|
||||
let sink_probe = sink.clone();
|
||||
pump_async(&mut state, move |_| {
|
||||
std::fs::read_to_string(&sink_probe).is_ok_and(|s| s.contains("probe"))
|
||||
std::fs::read_to_string(&sink_probe).is_ok_and(|s| s.ends_with('\n'))
|
||||
});
|
||||
|
||||
let recorded = std::fs::read_to_string(&sink).unwrap_or_else(|e| {
|
||||
|
|
|
|||
Loading…
Reference in New Issue