test(session/gamelease): prove it against real processes, not just fixtures

The fixture tests establish that the parsing is right; they cannot establish that
it describes this kernel. Two tests close that gap.

procscan now scans the real /proc for a process it just started, which is the
only version that would catch wrong `stat` field ordering, a broken uid check, or
a mis-read uptime clock. Writing it found something worth keeping: a wrapper
script that `exec`s a binary outside the install dir leaves no trace of that
directory in either the image path or the command line, so the install-dir recipe
cannot see it. A real game's binary lives under its install dir, so the fixture
now models that instead — and the note is in the test for whoever wonders.

The gamelease test drives a real child from Running through a host-requested end
to Exited, and asserts the session-ending action does NOT fire for it — the
difference between "the player quit" (end the session) and "we closed it" (do
not). It needs ~12s to outlive the shim window and the exit confirmation, so it
is #[ignore]d; run it with `-- --ignored gamelease`. Verified on .21.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-26 18:28:13 +02:00
co-authored by Claude Fable 5
parent a17aa61c5a
commit 10902f57d0
2 changed files with 123 additions and 0 deletions
+52
View File
@@ -517,6 +517,58 @@ mod tests {
assert!(s.alive(&[gone]).is_empty());
}
/// Everything above runs against a fixture tree, which proves the parsing but not that the
/// parsing describes this kernel. This one scans the **real** `/proc` for a process it just
/// started — the only version of this test that would notice `/proc/<pid>/stat` field ordering,
/// the uid check, or the uptime clock being wrong on a real box.
#[test]
fn finds_a_real_process_it_just_started() {
// A real game's *binary* lives under its install dir, which is what makes the install-dir
// recipe work. So the stand-in must too: copy a long-running binary into the directory and run
// it from there. (A wrapper script that `exec`s something outside the directory would leave no
// trace of the directory in either the image path or the command line — worth knowing, and the
// reason a copied binary is the honest fixture here.)
let td = tempfile::tempdir().expect("tempdir");
let game = td.path().join("game");
std::fs::copy("/bin/sleep", &game).expect("copy a stand-in game binary");
let s = Scanner::system();
let before = s.uptime().expect("real /proc/uptime is readable");
let mut child = std::process::Command::new(&game)
.arg("20")
.spawn()
.expect("spawn the fake game");
// Give it a moment to be visible in /proc.
std::thread::sleep(std::time::Duration::from_millis(300));
let found = s.find(&DetectSpec::dir(td.path()), Some(before));
assert!(
found.iter().any(|p| p.pid == child.id()),
"scanning the real /proc did not find pid {} under {}; found {found:?}",
child.id(),
td.path().display()
);
// The same process is still alive when re-verified by (pid, start time).
assert!(!s.alive(&found).is_empty());
// The exact-exe recipe finds it too, on the same live process.
assert!(s
.find(&DetectSpec::exe(&game), Some(before))
.iter()
.any(|p| p.pid == child.id()));
// A launch reference AFTER this process started must exclude it — the rule that keeps a
// pre-existing copy of a game from being adopted, checked against a real start time.
let after = s.uptime().unwrap() + 60.0;
assert!(s.find(&DetectSpec::dir(td.path()), Some(after)).is_empty());
let _ = child.kill();
let _ = child.wait();
// Once it is gone and reaped, neither the scan nor the liveness re-check sees it.
std::thread::sleep(std::time::Duration::from_millis(300));
assert!(s.find(&DetectSpec::dir(td.path()), Some(before)).is_empty());
assert!(s.alive(&found).is_empty());
}
#[test]
fn parses_uptime_and_hostile_comm() {
let td = fake_proc_root(4321.5, &[FakeProc::new(80, 1234)]);