| D-M4-17-0 |
2026-10-17 |
Base int/m1-wave35 at 4773519, branch feat/M4-17-fake-server in its own worktree; the branch is not merged locally and not pushed, the PR lane lands it. The brief relies on no core rule or ruling that main has and the base lacks (D-W-100 and D-W-114 are on the base), so nothing is ported; the API documents of the base hold every operation of Design note 3. The brief's 15-client-architecture.md and 04-sync-protocol.md are 15-client-architektur.md and 04-sync-protokoll.md on the base; the lines this ticket adds there are English. Differences to the brief's names, following the merged code: the local store is LocalDatabase; the REST client is AuthSession.api, which the session already builds with its interceptors, so SyncEngine.create does not build a second one; create also needs client (GatewayClientInfo for identify) and takes ids, draftEditing, keepConnectedInBackground, allowPlainHttpForTests and logHook; focus is an EngineFocus over the FocusManager; the engine exposes state and states. Fault names: delay(duration, operationId:), revoke(deviceId:), plus holdDispatches and releaseDispatches; a lost network is FakeServer.disconnect. |
Instructions of the launcher, which override the brief's landing step; merged names win over the brief (D-W-100, D-W-114). |
| D-M4-17-1 |
2026-10-17 |
One composition root: SyncEngine.create builds the parts of M4-13 to M4-16 and connects every hook (echo to Outbox.confirmByEcho with message_id and version; the catch-up's live signal to Outbox.start; every disconnect to Outbox.pause; run.updated to RunStore.onRunUpdated; run.state_changed to an end state to RunStore.onRunEnded; focused runs into the gateway focus through EngineFocus.focusRun). The run end is read by a listener that wraps the catch-up listener after it applied the frame, so the dispatcher of M4-14 stays unchanged. |
Brief Design note 2: the desktop and the mobile app cannot wire the hooks differently. |
| D-M4-17-2 |
2026-10-17 |
The fake's REST side routes by the paths of openapi.yaml (a literal segment before a parameter) and keeps a handler registry: FakeServer.handle(operationId, handler); this ticket's handlers register from one file per group under lib/src/testing/rest/; an operation without a handler answers 501 and is recorded. |
Later tickets add operations without editing this file (Design note 3). |
| D-M4-17-3 |
2026-10-17 |
Faults are deterministic: FaultPlan is a queue of REST rules matched in order and used up per request (applied after the token and proof checks, so a nonce retry does not eat a fault), and gateway faults act at once or after a fixed delay; IDs of the world are counters. |
A failing case must repeat on every run (Design note 4, 15 20 rationale). |
| D-M4-17-4 |
2026-10-17 |
Contract checks in the fake with an own validator (ContractSchemas) for the subset the documents use, plus allOf and the bounds minimum, maximum, minLength, maxLength, minItems, maxItems and pattern; no JSON Schema package is in the lock file, so no dependency was added. Invalid request bodies answer 400 validation_failed with the pointers and are recorded; a response the fake would send against the contract becomes a recorded 500; a frame of the fake against the contract throws; an invalid client frame closes with 4002. Every scenario fails on a recorded violation. |
The fake can never drift from the contract silently (Design note 5). |
| D-M4-17-5 |
2026-10-17 |
The scenarios of 04 16 are functions over the interface ScenarioClient (send, offline and online, local messages, outbox, channels, notices, cursor, focus, run document, run state) and a running FakeServer; they throw ScenarioFailure instead of using package:test, so the library has no test framework as a runtime dependency. test/scenarios/ implements the interface over SyncEngine; M4-18 implements it over the app. |
One set of flows for the headless and the app tests (Design note 7, D-W-114). |
| D-M4-17-6 |
2026-10-17 |
SyncEngine accepts only an https base URL (gateway wss); allowPlainHttpForTests: true admits http (gateway ws) for the fake and the app's test entry point. tools/arch-check gains rule 10 as a general rule: a library named testing.dart of any member and its lib/src/testing/ folder are imported or exported only from test/ and integration_test/ folders, also relatively inside the own member; 15 3.1 lists it. |
DPoP and the device binding assume a protected transport (15 8); test support never reaches a production build (Design note 8). |
| D-M4-17-7 |
2026-10-17 |
The gateway's hello carries the same nonce as the REST DPoP-Nonce and the gateway accepts the current and the previous one, as REST does. |
08 7.3 makes nonces stateless for every process; the client keeps one nonce store, so a gateway-only nonce made the next REST call fail with use_dpop_nonce (found by the first scenario run). |
| D-M4-17-8 |
2026-10-17 |
crypto and pointycastle move from dev_dependencies to dependencies of packages/sync_engine, and yaml (already in the lock file) is added there: lib/testing.dart lives under lib/ and needs them; the production library does not import them. |
Brief Scope (pubspec.yaml: the dependencies the fake needs); their licenses (BSD-3-Clause, MIT) are on the allowed list. |
| D-M4-17-9 |
2026-10-17 |
FakeWorld.append takes resource.version from the event catalogue of domain: a type the catalogue marks without it never carries one (reactions do not raise the message version, read_state.updated and settings.updated carry the version only in the payload), and a type that needs it fails the write; the contract test checks every logged event against the catalogue. |
04 9.3; found by the code review: a foreign version made the fake answer 412 to an edit after a reaction. |
| D-M4-17-10 |
2026-10-17 |
Fixes after the red team: the outbox's onConfirmed stores the Message a confirmed createMessage or updateMessage returned through the new CatchUpListener.storeConfirmedMessage (version rule, removed targets skipped), so a sent message never disappears between its response and its echo; after Resynced the engine ends the documents of focused runs that the local run summaries list as ended or no longer hold; the fake computes unread_threads and the read state's mention_count (messages of others after the read position, not deleted). |
04 13.2 item 6, 04 7.3 with 15 7.4, 04 14; red team of M4-17. |