Files
samiandClaude Sonnet 5 4e177ec809 proto: stop conformance from downloading real files, ADR the null-clearing gap
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
2026-09-12 16:56:35 +04:00
..

contracts/fixtures — golden request/response pairs

Every method has at least one success fixture. A method with no fixture is not done.

These files are replayed by tests/conformance/ against both the generated C++ and a live server, which is what lets four lanes build in parallel and still be compatible: green fixtures mean the C++ daemon and the TypeScript extension agree, without either having ever run against the other. tools/mockd also answers from them, so the GUI and extension are developed against the same bytes conformance asserts.

Layout

fixtures/
├── *.json          one success fixture per method
├── errors/         error cases: auth, transport, not-found, bad path, timeout
└── events/         one fixture per server-to-client notification

Shape

{
  "name": "download.add — add an ISO for later, into the Programs category",
  "description": "Why this case is worth pinning.",
  "transport": "uds",          // optional: replay only on this transport
  "requires": "...",           // optional: a condition a plain server cannot produce
  "kind": "timeout",           // optional: the correct behaviour is *no reply*
  "request":  { "jsonrpc": "2.0", "id": 11, "method": "download.add", "params": { } },
  "response": { "jsonrpc": "2.0", "id": 11, "result": { } },
  "assertions": [ "things a runner or a reviewer should check" ]
}

An event fixture carries notification instead of request/response.

assertions are prose, for the human writing the implementation. The runners check the machine-checkable parts: schema validity, error codes, shape, and the timeout.

Placeholders

Some values cannot be pinned in a golden file. These stand in for them, and the runners treat them as "any value of the right shape":

Placeholder Means
$uuid any UUID
$isoDate any RFC 3339 date-time
$opaque a credential-shaped string (a token)
$any any value
$taskId, $taskId2 a task the runner creates during setup, and binds before replaying

$taskId exists so a fixture never depends on a task id that only happens to exist in a seeded mock. The same fixture then runs against an empty veloxd and a populated mockd.

Values are matched by shape, not by equality

A live daemon returns its own task ids and its own clock. Demanding byte-identical results would only teach the suite to lie, so the runners assert:

  • the payload passes the generated validator — this is the real cross-language check;
  • the key structure matches the golden file, with no extra and no missing fields;
  • error codes match exactly.

A null where the golden shows a value is accepted: the validator has already ruled on whether null is legal there, and a golden file shows one plausible value, not the only one.

requires: fixtures a mock cannot produce

Most error fixtures are intrinsic — a path outside the allowed roots, an out-of-range parameter, an unknown task id — and any correct server produces them from the request alone. Those are replayed everywhere.

Four are environmental: a 403 from an origin server, a full disk, a pairing lockout, a wedged daemon. They carry requires, are skipped by default, and are exercised where the condition can actually be arranged — run.sh starts a deliberately slow mockd to prove capture.offer fails open, and lane PKG/QA's tools/testserver covers the hostile-server cases in tests/integration/.

errors/capture.offer.timeout.json is the most important file in this directory. Its correct response is no response: past 750 ms the extension must abandon the offer and let Firefox download normally. A download manager that eats downloads when its daemon is down is worse than no download manager.

No fixture may pair a real external URL with startMode: "now"

This suite replays every fixture against a real, live veloxd (tests/conformance/run.sh), not just mockd. mockd never actually fetches anything, so it hid this for a while: a fixture with startMode: "now" (or "queue" into a running queue — anything that gets admitted to the scheduler right away) and a real, resolvable URL makes a real daemon actually start downloading it, for real, onto whatever machine runs the suite. This happened — twice, with download.add.json pointed at a ~6 GB Ubuntu ISO, straight into the developer's real ~/Downloads.

The fix in each case is one of:

  • startMode: "later" — exercises the add path (validation, category assignment, the event) without ever handing the task to the engine;
  • a URL under example.org/example.com (IANA-reserved for exactly this, RFC 2606) — resolvable enough to validate as a URL, never a real download source;
  • requires, if the fixture's entire point needs a real transfer to fail in a specific way (see errors/download.add.disk-full.json) — skipped by default, so it only ever runs where the condition has actually been arranged.

A real saveDir gets the same treatment for the same reason: an absolute path like /home/sami/Downloads/... only means anything on the machine that fixture was written on. Omit saveDir and let saveTo.defaultDir apply, or use a relative-feeling path under a root the runner controls.