Files
samiandClaude Sonnet 5 1d359af5a3 pkg: wire GUI's DoD harness into CI, mark live-veloxd conformance required
gui-dod (per-PR: scroll-60fps + unhappy-path) and gui-dod-nightly (rss-flat,
schedule/workflow_dispatch) are live in ci.yml, driving GUI's newly-landed
gui/tests/dod/run.sh + gui-dod-harness. No Xvfb step: run.sh already runs
QT_QPA_PLATFORM=offscreen itself.

Each gate forced red once before being trusted (tests/integration/README.md
has the transcripts): VELOX_DOD_FRAME_BUDGET_MS=0.01 for scroll-60fps,
VELOX_DOD_RSS_SLACK_KIB=-999999999 for rss-flat, and — since run.sh always
starts a working mockd — a direct gui-dod-harness invocation against an
unreachable socket for unhappy-path, which hit the harness's own 75s
watchdog exactly as documented.

Recorded GUI's live finding (gui/docs/proto-requests-m1.md) that mockd
--drop-connection is a no-op over the UDS transport, so unhappy-path's
drop-connection phase can't yet exercise a real drop — coordinating with
PROTO on the fix rather than working around it locally. gui-dod stays
required regardless: its other two phases and the crash/hang/watchdog paths
still catch real regressions.

Added gui-dod to BRANCH_PROTECTION.md's required-checks table.

ADR 0019: the live-veloxd conformance runner (run.sh step 3b, already
unconditional inside the already-required conformance job) stays required
as PROTO's xfail list shrinks (18 entries now, down from 34; 57/57 fixtures
passing on main). No CI change needed — it was already inside a required
check; this records the decision not to carve out an exception for it.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01RBPR7iM3YPyxrjWsVtZDPJ
2026-09-12 22:04:09 +04:00

150 lines
9.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# tests/integration — cross-lane runtime gates
Owned by PKG/QA. Real binaries against `tools/mockd` / `tools/testserver`, headless.
Unit tests live in each lane; this tree is for behaviour that only shows up when the
pieces run together (throughput, memory over time, reconnect).
## Nightly integration run
`tests/integration/nightly_run.py`, wired as the `nightly-integration` job in `ci.yml`
(`schedule: '17 3 * * *'`, plus `workflow_dispatch` for an on-demand run). Real `veloxd`
+ `tools/testserver`, 50 concurrent downloads mixing hostile testserver modes
(`flaky-reset`, `throttled`, `no-range`, mostly `plain`), asserting:
1. every completed file's SHA-256 matches testserver's own `/<mode>/sha256/<size>` route
(computed by this script re-hashing the file on disk — never trusting veloxd's own
claim of success);
2. every task reaches a terminal state inside the timeout (a task stuck retrying forever
is a failure, not a hang for CI's `timeout(1)` to paper over);
3. `veloxd`'s own open-FD count (`/proc/<pid>/fd`) returns to within a small fixed
tolerance of its pre-run baseline, checked after a settle window.
`veloxd` runs isolated: `XDG_RUNTIME_DIR`, `XDG_DATA_HOME` and `XDG_CONFIG_HOME` all
point into a fresh `mkdtemp()` (not this session's scratch dir — its path is long enough
to overflow `AF_UNIX`'s ~108-byte `sun_path`; verified live via "File name too long"
before switching to `tempfile.mkdtemp()`). `saveTo.allowedRoots` is seeded straight into
`velox.db` after a migrations-only warm-up start: `settings.set` returns `-32603 "not
implemented in this build"` on the `veloxd` this job builds — verified live, not assumed
— so direct DB seeding is the only entry point that currently exists, not a workaround
for a wrong contract. Every task also gets its own `filename` override on `download.add`:
tasks share `(mode, size)` pairs by design (testserver's content is a pure function of
path, not of who's asking), and without distinct filenames they raced each other for the
same destination path — this was caught live on the first real run of this script (48/50
"passed" with io_errors and checksum mismatches on the collided tasks) before the
`filename` override was added.
### What makes each check go red, proven once
Per the standing note in this repo's history (four green checks that didn't look where
the bug was — an always-false guard, a `ctest` label matching zero tests, `--check`
validating pkg-config instead of apt names, conformance validating only fixtures): every
assertion here was forced red once, on purpose, before being trusted.
| Assertion | Forced via | Observed |
|---|---|---|
| Checksum match | `VDM_NIGHTLY_FORCE_BAD_HASH=1` (substitutes a wrong hash for task 0's comparison only, after the real download and hash succeed) | `FAIL: task 0 (plain, 256K) checksum mismatch: got 6f4c254c…, testserver says 0000…0000`, exit 1 |
| FD-leak tolerance | `VDM_NIGHTLY_FORCE_FD_LEAK=1` (adds 25 to the post-run FD count) | `FAIL: veloxd leaked file descriptors: 19 -> 49 (tolerance 10)`, exit 1 |
| A task that can't succeed still fails the run | `VDM_NIGHTLY_FORCE_HOSTILE_STALL=1` (task 0's URL points at 192.0.2.1, TEST-NET-1 — unroutable, so the connection just hangs) | `FAIL: task 0 (plain, 256K) ended in state 'failed', error={'code': 'timeout', ...}`, exit 1 |
The first version of the `HOSTILE_STALL` hook used testserver's `416-always` mode,
expecting it to never yield a 2xx to a Range probe — but `veloxd` correctly falls back
to a plain full GET on a 416 and the task completed fine, so that attempt proved
nothing (a real finding in itself: worth knowing the daemon handles this correctly).
Re-run any of the three whenever the corresponding assertion changes, to re-prove it
still catches what it claims to — that's the point of the hooks living in the script
rather than being one-off manual edits.
### Proven: the harness cannot leak its daemon
A run of this script was itself killed hard (its own harness process, not a graceful
stop) mid-download and left `veloxd` running under `systemd --user` for 4h40m — the
`finally:` teardown never got to run, because nothing runs after `SIGKILL`. Fixed by
giving every child (`veloxd` and `testserver.py`) `PR_SET_PDEATHSIG` (via a `preexec_fn`
calling `prctl` through `ctypes`) plus its own process group (`start_new_session=True`):
the kernel now delivers `SIGKILL` to a child the instant its parent dies, by any means,
without the harness needing to run any code at all. `finally:` still does the graceful
SIGTERM-then-SIGKILL `killpg` for the normal-exit path; `PR_SET_PDEATHSIG` is what
covers the path `finally:` cannot reach.
Proven live: started the harness, waited for its "baseline FDs" log line (proof the
real `veloxd` and `testserver.py` were both already up as separate process groups
under it), then `kill -9`'d the harness itself and confirmed both children were gone
within 1.5 s — nothing left running, nothing to clean up by hand.
### Known limitation: single-instance lock is per-euid, not per-XDG-tree
`veloxd`'s single-instance guard binds an abstract-namespace socket keyed only by
`geteuid()` (`daemon/src/main.cpp`), so `XDG_RUNTIME_DIR` isolation does not let two
`veloxd` processes for the same Unix user run side by side — confirmed live: a second
instance exits with "another instance is already running for this user" even with fully
distinct `XDG_*` dirs. Harmless on a real CI runner (one job, one user, one `veloxd`) but
means this script cannot run concurrently with another `veloxd` on the same machine —
worth knowing before parallelizing this job or running it by hand next to another lane's
manual testing.
## GUI M1 definition-of-done gates (R3)
`gui/docs/pkg-qa-requests-m1.md` R3: three GUI DoD items are not unit tests and have
nowhere to run. GUI owns the harness; PKG/QA owns the CI job. This is the wiring contract
so the two halves meet without another round trip.
### What GUI built, and the invocation contract
`gui/tests/dod/run.sh <gate> [--json <path>]` (`gate` one of `scroll-60fps` / `rss-flat`
/ `unhappy-path`), backed by `gui/tests/dod/dod_harness.cpp` (target `gui-dod-harness`).
Headless by default — `run.sh` sets `QT_QPA_PLATFORM=offscreen` itself, so the CI jobs
below need no Xvfb.
| `<gate>` | Pass / fail condition | Budget |
|---|---|---|
| `scroll-60fps` | `mockd --tasks 10000`, scripted fling scroll; **fail** if p99 frame > 16.6 ms (×4 under an ASan/UBSan build — `VELOX_DOD_FRAME_BUDGET_MS` overrides outright) | per-PR |
| `rss-flat` | `mockd --tasks 10000` + progress events, 10 min; **fail** if RSS growth past warm-up exceeds a 20 MiB slack (`VELOX_DOD_RSS_SLACK_KIB` overrides) | nightly |
| `unhappy-path` | Three phases (`--slow` / `--flaky <f>` / `--drop-connection <s>`), one `mockd` restart each; **fail** on crash, on the harness's own watchdog firing (75 s), or if connection state never (re)reaches `Connected` | per-PR |
Contract, as built: exit `0` pass, non-zero fail; a hang is the harness's own watchdog
converting itself into a non-zero exit (`3`), never something CI needs `timeout(1)`
around; `--json` writes one result object per gate (`unhappy-path` merges its three
phases into one file); no network, no writes outside a tempdir, no leaked child process
on any exit path (`run.sh`'s `trap cleanup EXIT INT TERM`).
### CI jobs — wired
`gui-dod` (per-PR: `scroll-60fps` + `unhappy-path`) and `gui-dod-nightly` (`rss-flat`,
`schedule`/`workflow_dispatch` only) are live in `ci.yml`. Both bootstrap, build
`gui-dod-harness`, `npm ci` in `tools/mockd`, run `gui/tests/dod/run.sh`, and upload the
`--json` output as an artifact — no Xvfb step, since `run.sh` already runs offscreen.
### Forced red, once per gate, before wiring it required
| Gate | Forced via | Observed |
|---|---|---|
| `scroll-60fps` | `VELOX_DOD_FRAME_BUDGET_MS=0.01` | `FAIL: p99=50.73 ms mean=31.34 ms max=52.88 ms budget=0.01 ms over 240 steps, 10000 rows`, exit 1 |
| `rss-flat` | `VELOX_DOD_RSS_SLACK_KIB=-999999999` (guarantees `growthKiB > slack` regardless of actual RSS behavior that run) | `FAIL: growth=13644 KiB slack=-999999999 KiB over 15s (warmup 1s), 10000 rows`, exit 1 |
| `unhappy-path` | ran `gui-dod-harness` directly (bypassing `run.sh`, which always starts a working `mockd`) against a `--sock` path with nothing listening | 75 s of `Connecting`/`Reconnecting`, then `FAIL: dod_harness watchdog fired — hung past 75s`, exit 3 |
The real (non-forced) runs all pass live: `scroll-60fps` p99 51.28 ms against a 66.4 ms
budget (ASan/UBSan build), `rss-flat` growth 13504 KiB against the 20480 KiB slack over a
15 s smoke duration, `unhappy-path` all three phases `PASS` with `finalConnected=true`.
### Known gap: `unhappy-path`'s drop-connection phase doesn't drop anything
Filed by GUI in `gui/docs/proto-requests-m1.md`: `mockd --drop-connection` only works
over WebSocket — `startUds()` never wires the periodic-drop timer `startWs()` has, so the
UDS transport (GUI/CLI/nmhost's only one) never sees a connection actually die. GUI's own
live verification: 45 s observing a `--drop-connection 5` mockd over UDS, `stateChanged`
never fires. The phase still runs and its JSON honestly records `sawDisruption: false`
rather than silently passing as if it proved something — that's what the real run above
shows, `pass: true` alongside `sawDisruption: false`, so a reviewer reading the JSON sees
exactly how much this phase currently covers.
This is PROTO's fix, not PKG/QA's or GUI's to route around locally — coordinating on it
rather than patching mockd from this lane. Once PROTO lands `dropEverySec` on `startUds`,
`unhappy-path`'s drop-connection phase starts exercising a real drop and this note comes
out; until then `gui-dod` stays required as specified (crash/hang/never-reconnects still
catch real regressions), just not yet catching a swallowed real disconnect.
### Status
Wired and required: `gui-dod` runs per-PR, `gui-dod-nightly` on schedule. Revisit once
PROTO's UDS `--drop-connection` fix lands (see above).