Files
vdm/tests/integration/README.md
T
samiandClaude Sonnet 5 efe76c319d pkg: nightly integration run — real veloxd + testserver, 50 concurrent
Adds tests/integration/nightly_run.py, wired as the nightly-integration job in
ci.yml (schedule + workflow_dispatch). Real veloxd + tools/testserver, 50
concurrent downloads mixing flaky-reset/throttled/no-range/plain, asserting:
every completed file's SHA-256 against testserver's own /sha256/ route (never
trusting veloxd's own success claim), every task reaching a terminal state
inside the timeout, and veloxd's own open-FD count settling back to baseline.

veloxd runs isolated (XDG_RUNTIME_DIR/XDG_DATA_HOME/XDG_CONFIG_HOME under a
fresh mkdtemp — not the session scratch dir, whose path overflows AF_UNIX's
sun_path). saveTo.allowedRoots is seeded directly into velox.db after a
migrations-only warm-up start, since settings.set returns -32603 'not
implemented in this build' on the veloxd this job builds (verified live).

Every task gets its own filename override on download.add: tasks sharing
(mode, size) share a URL, and without distinct filenames they raced each
other's rename on the first real run (48/50 'passed' with io_errors and
checksum mismatches on the collided tasks) before this fix.

Every assertion was forced red once on purpose and the transcript recorded in
tests/integration/README.md, per this repo's history of green checks that
didn't look where the bug was.

Every spawned child (veloxd, testserver.py) gets PR_SET_PDEATHSIG plus its own
process group, so a hard-killed harness can't strand a daemon the way a prior
run did (4h40m under systemd --user, because SIGKILL never reaches a
finally: block). Proven by kill -9'ing a running harness mid-download and
confirming both children exit with it.

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

9.2 KiB

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 PKG/QA needs from GUI

A driver invoked as gui/tests/dod/run.sh <gate> [--json <path>] (exact path TBD by GUI), headless-capable (Xvfb or offscreen QT_QPA_PLATFORM), with:

<gate> Pass / fail condition Budget
scroll-60fps mockd --tasks 10000, scripted fling scroll; fail if p99 frame > 16.6 ms per-PR
rss-flat mockd --tasks 10000 + progress events, 10 min; fail if RSS growth > a fixed slack (GUI picks the number, states it) nightly
unhappy-path mockd --slow / --flaky <f> / --drop-connection <s>; fail on crash, on watchdog-detected hang, or if connection state never returns to Connected per-PR

Contract:

  • exit 0 pass, non-zero fail; a hang is the harness's own watchdog to catch and turn into a non-zero exit, not something CI should have to timeout(1) around.
  • --json writes one machine-readable result file (measured p99, RSS series, recovery time) so the job can upload it as an artifact and a regression is a diff, not a re-run.
  • no network, no writes outside a tempdir, no leaked child processes on failure.

CI job — pre-drafted, add once the harness path is fixed

  gui-dod:
    # Per-PR GUI gates. The 10-minute rss-flat gate is in gui-dod-nightly, not here.
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Bootstrap toolchain
        run: sudo ./tools/bootstrap.sh
      - uses: actions/setup-node@v4
        with:
          node-version: '22'          # tools/mockd
      - name: Configure + build
        run: |
          cmake --preset dev
          cmake --build --preset dev --target velox-gui
      - name: Install mockd
        run: cd tools/mockd && npm ci
      - name: Xvfb + gates
        run: |
          sudo apt-get install -y --no-install-recommends xvfb
          xvfb-run -a gui/tests/dod/run.sh scroll-60fps   --json scroll.json    # TODO(GUI): path
          xvfb-run -a gui/tests/dod/run.sh unhappy-path    --json unhappy.json
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: gui-dod-${{ github.run_id }}
          path: "*.json"

  gui-dod-nightly:
    if: github.event_name == 'schedule'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Bootstrap toolchain
        run: sudo ./tools/bootstrap.sh
      - uses: actions/setup-node@v4
        with:
          node-version: '22'
      - name: Configure + build
        run: |
          cmake --preset dev
          cmake --build --preset dev --target velox-gui
      - run: cd tools/mockd && npm ci
      - name: RSS soak (10 min)
        run: |
          sudo apt-get install -y --no-install-recommends xvfb
          xvfb-run -a gui/tests/dod/run.sh rss-flat --json rss.json
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: gui-dod-rss-${{ github.run_id }}
          path: rss.json

gui-dod-nightly needs if: github.event_name == 'schedule' (the nightly-integration job below already added that trigger to ci.ymlcron: '17 3 * * *' — so this no longer needs its own).

Status

Blocked on GUI's harness. Not urgent (GUI M1 DoD, not M0). When GUI files the follow-up with the real run.sh path and the rss-flat slack number, PKG/QA drops the TODO(GUI) markers and marks gui-dod required. The schedule: trigger gui-dod-nightly needs is already in ci.yml.