1. download.add.json had startMode "now" against a real, large (~6 GB)
Ubuntu ISO with saveDir hardcoded to /home/sami/Downloads/Programs.
Against a real veloxd (tests/conformance/run.sh) that's a real
download into the real user's real home, every single run — it had
already happened twice. startMode -> "later" (exercises the add path,
hands nothing to the engine) and saveDir is dropped entirely (resolves
to saveTo.defaultDir instead, checked against allowedRoots the same
way). Documented the rule this fixture was breaking in
contracts/fixtures/README.md so it doesn't happen a third time.
Auditing the rest for the same shape (now real: capture.offer, D7)
found a second, subtler instance: capture.offer.take.json's "take"
admits a real, immediately-started task the same way download.add
does, and the "Programs" category's saveDir is a migration-seeded
builtin (~/Downloads/Programs) that no isolated test setup can
redirect -- so even after pointing the URL at example.org (RFC 2606),
a real ~6 GB sparse .veloxpart still landed in the real home on the
declared Content-Length alone. Shrunk to a plausible-but-small 5 MiB.
Also scoped to "transport": "uds" -- a real "take" persists an active
task, so replaying the same fixture again on the second live transport
against the same shared daemon was hitting capture.offer's own
dedupe-by-URL and failing on a missing taskId, not a bug.
download.add's other real-URL siblings (errors/*.invalid-path,
*.invalid-params, *.disk-full) all fail before admission or are
requires-gated; left alone.
2. ADR 0018: DAEMON can set a nullable field through download.update /
settings.set but never clear it back to null, because the generated
C++ parser collapses "absent" and "explicit null" to the same
std::nullopt for every optional field (contracts/codegen/gen_cpp.py,
on purpose, and correct for create-style params -- just wrong for
patch-style ones, which is the only place the schema documents
"explicit null clears"). Decision: an opt-in x-clearable schema
annotation makes just those fields std::optional<std::optional<T>> in
C++ (TS already round-trips this natively); not a blanket rule
(would retype response fields like TaskSummary.effectiveUrl that have
no clear-vs-absent distinction to make), not an explicit clear-list
field (would redesign a wire contract DAEMON already built against
just to route around a generator gap). Recorded, not implemented here
-- that's its own PROTO PR (schema annotations + gen_cpp.py + gen_ts.py
+ regeneration + a minor VERSION bump per ADR 0015), not bundled into
a fixture-safety pass. Left a pointer to the ADR at the generator
comment it concerns.
3. Re-verified every xfail entry against current deferrals.md rather
than trust the reasons already on file: D7/D8 (capture.offer/
getRules), D3d/e/f/g/h/i (rules, queue.reorder, schedule, limiter,
download.update/refreshUrl) and D9 (settings) have all closed since
the list was last pruned, so most of it was stale. Removed everything
that now cleanly passes; kept and re-reasoned everything that doesn't:
- errors/download.provideAuth.not-found.json stays, as asked: real
bug, on_download_provideAuth never checks the task exists.
- category.list.json (mimeTypes -- documented D3a gap), schedule.set.json
(nextRunAt -- documented D3f gap): unchanged in substance, reason
text was already accurate.
- download.probe/get/list/update.json, session.hello.json,
queue.start/reorder.json, category.remove.json: not bugs -- each
golden depicts a richer lifecycle/config state (a probed download,
real queue or category membership, media/grabber capabilities) than
this harness's fresh, never-started bound tasks and empty isolated
DB can produce.
- limiter.get.json: real fixture bug, not a daemon one -- applyToRunning
is a write-only instruction on limiter.set, on_limiter_get never
returns it; the golden shouldn't have had it either. Fixed the
fixture and tools/mockd's own limiter.get, which had the same field
hardcoded into its in-memory state independent of the fixture file.
- grabber.*/media.*: still genuinely stub (M4 territory).
Only remaining unexpected-pass surfaced while re-verifying
(errors/capture.offer.ignore.json, always "take" instead of "ignore")
traced to capture.minSizeBytes defaulting to 0 on a fresh daemon,
making its below-minimum-size scenario unreachable -- not a bug, so
raised the setting in run.sh's isolated seeding instead of xfailing it.
ctest -L conformance: green, 100% (2/2), ~87s.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SFeUKLbdHizrJjLBeK7ffz
tests/conformance — one suite, four runners
This is a required check on every lane's PR. It is the mechanism that makes four parallel lanes safe: the C++ daemon and the TypeScript extension are proved compatible without either having run against the other.
./tests/conformance/run.sh # starts its own mockd + veloxd
./tests/conformance/run.sh --uds /run/user/1000/velox/velox.sock --ws-port 52000
The runners
| Runner | Needs | Asserts |
|---|---|---|
check_contract.py |
python3, jsonschema | schemas parse and resolve; the documented surface matches the schema surface both ways; every method has a success fixture; every fixture validates; SettingKey and Settings agree; committed generated code is not stale |
cpp/ |
a C++23 compiler, nlohmann | every golden payload parses into the generated structs, serialises back stably, and goes through the real dispatch(); privileged methods are refused -32003 over the WebSocket |
ts/replay.ts against mockd |
node ≥ 20 | the TS client and the fixtures agree with each other — mockd always answers every fixture correctly by construction, so this cannot catch veloxd disagreeing with the contract |
ts/replay.ts against veloxd |
the above, plus a C++23 toolchain (daemon/CMakeLists.txt's deps) |
the real daemon it builds and starts, isolated (its own XDG_RUNTIME_DIR/XDG_DATA_HOME/XDG_CONFIG_HOME), answers every fixture — except ones hitting a still-stubbed handler, excused by veloxd-xfail.json (see below) |
veloxd-xfail.json
daemon/docs/deferrals.md (D1-D4b) lists the handlers still stubbed out (-32603 not implemented); the fixtures that hit them can't pass against veloxd yet and are listed
here with why, keyed by fixture path. This is a maintained allowlist, not a snapshot: a
listed fixture that unexpectedly passes is flipped back to a failure by replay.ts
(applyXfail) rather than silently accepted, so an entry has to be deleted the same PR
that closes the handler — the list can only shrink, never rot into "things nobody checks."
This is also where a real veloxd bug shows up before it reaches anyone else: an
implemented handler returning something the schema forbids (e.g. download.get with
segments: 0, which TaskSummary.segments requires >= 1) is not in the allowlist, so
it fails the run for real. That is the point of running against veloxd at all, not just
mockd.
Current status against veloxd: red, for two reasons — not just the one
As of this wiring, the veloxd runner does not pass, and shouldn't yet:
download.get/download.listcan returnsegments: 0.store::TaskRow::eff_segmentsdefaults to0andto_summarycopies it straight intoTaskSummary.segments(daemon/src/store/tasks.{hpp,cpp}), which the schema forbids (minimum 1, required). This isn't only a post-completion thing — it's any task the engine hasn't segmented yet, which includes a task the instant it's added. This is the regression this runner exists to catch, and it is deliberately not inveloxd-xfail.json.- New finding:
download.addwithstartMode: "later"always fails.lateris a valid, documentedStartMode(contracts/schema/types/StartMode.schema.json:["now", "later", "queue"]— "'later' is the File Info dialog's Download Later button"), andon_download_addstores it verbatim astasks.start_mode(daemon/src/rpc/dispatcher.cpp). But thestart_modecolumn'sCHECKconstraint (daemon/src/store/migrations/0001_initial.sql) only allows'auto','now','queue','manual'— no'later', and'manual'/'auto'aren't contract values at all. Everydownload.addwithstartMode: "later"— including this runner's own fixture-binding setup, which needs one to exist before it can replay any$taskId-referencing fixture — fails-32603on a SQLiteCHECKviolation. This is not inveloxd-xfail.jsoneither: it's not a stub (D-list), it's a real, currently-shipping bug, and it's why the veloxd run is red across most of the suite right now, not just ondownload.get. Filed to DAEMON; not fixed here (out of lane).
run.sh also runs one scenario that cannot be shown against a healthy server: with the
daemon answering slower than capture.offer's 750 ms deadline, the client must give up and
let Firefox take the download. That is the fail-open guarantee, and it is checked here.
What "passing" means
The runners check the contract, not the implementation's opinions. Results are compared by
shape and validated against the generated validators; error codes are compared exactly.
Byte-equality with a golden file is deliberately not asserted, because a live daemon
returns its own ids and its own clock — see contracts/fixtures/README.md.
Adding a method without a fixture fails check_contract.py. Regenerating and forgetting to
commit the output fails it too.
CI
Wired in as ctest -L conformance (.github/workflows/ci.yml's conformance job, PKG/QA;
see docs/adr/0014-conformance-runs-through-ctest.md), required on every branch. It starts
and stops its own mockd and its own veloxd; nothing else needs to be running.
Needs: python3 with jsonschema, a C++23 compiler, the same deps daemon/CMakeLists.txt
needs (SQLite3, nlohmann-json, libsecret — tools/bootstrap.sh installs all of it), and
Node ≥ 20.
One caveat: veloxd's single-instance lock is not isolation-aware
veloxd refuses to start a second copy for the same user — an abstract-namespace socket
keyed by UID only, not by $XDG_RUNTIME_DIR (daemon/src/main.cpp,
acquire_single_instance_lock). This run's isolated veloxd collides with that lock exactly
like any other copy would: if a real veloxd (or another worktree's integration run) is
already up for this user when run.sh starts, the veloxd step fails fast with "another
instance is already running for this user" rather than silently testing the wrong daemon.
A fresh CI runner never hits this — only concurrent local runs can. If that turns out to
bite CI in practice (two conformance jobs sharing a runner user, say), the fix belongs in
daemon/ (scope the lock name to the runtime dir), not here.