Files
vdm/contracts/fixtures
samiandClaude Sonnet 5 5e3e21543a proto: give the generated C++ Dispatcher a real error channel (P1, 1.4.0)
DAEMON's daemon/docs/proto-requests-m1.md P1: velox::proto::Dispatcher's
on_* methods returned Result<T> = expected<T, ParseError>, and dispatch()
mapped every handler error to -32603 InternalError. A handler had no way to
return -32010 (download.get not-found), -32011 (download.add invalid-path)
or -32013 (probe-failed) with their data payloads -- three error fixtures a
conformant server must satisfy were unreachable, blocking DAEMON's
"conformance as a server" M1 DoD.

Two error channels now, kept separate on purpose:
  - parse: Result<T> / ParseError -- dispatch() failing to turn the wire into
    typed params. Always -32602, always structural.
  - handler: HandlerResult<T> / HandlerError -- a handler deciding the request
    can't be fulfilled. Carries any ErrorCode + message + free-form data.

    struct HandlerError {
        ErrorCode code{ErrorCode::InternalError};  // bare {} is a valid -32603
        std::string message;
        nlohmann::json data = nullptr;             // straight into the error's data
    };
    template <class T> using HandlerResult = std::expected<T, HandlerError>;

dispatch()'s handler branch is now
  make_error(id, r.error().code, r.error().message, r.error().data)
instead of a hard-coded InternalError. -32001/-32002/-32003 stay the server
layer's to raise around dispatch(), as DAEMON already does.

Verified end to end against the real dispatch() path: a handler returning
TaskNotFound/InvalidPath/ProbeFailed produces -32010/-32011/-32013 with the
data object intact, and a bare HandlerError{} still yields a clean -32603
with no data field. The `= nullptr` on the member (not `{nullptr}`) matters:
brace-init of nlohmann::json from nullptr is the array [null], not JSON null.

FixtureDispatcher regenerated to HandlerResult; conformance_main.cpp only
inspects dispatch()'s JSON and needed no change. TS side is untouched beyond
the version string -- no server Dispatcher is generated there.

P2 also handled: session.hello.version-mismatch's data.expected was a stale
"1.0.0"; now $any, with a note that the error-fixture compare is on `code`
only so a server echoing kProtocolVersion there is fine.

Version: minor, 1.3.0 -> 1.4.0. Wire is byte-identical (no schema, fixture,
or OpenRPC change) but every Dispatcher implementer must swap Result ->
HandlerResult on regen, and the bump is how lanes are told to. Not an ADR:
one lane consumes this binding, it's the one that asked, and the shape is
the one they proposed. Answered in contracts/proto-answers-daemon-m1.md.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012fgjnqFCS5h5L7gZTZo3rV
2026-09-10 15:10:13 +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 — start an ISO now, 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.