Files
vdm/gui/docs/proto-requests-m1.md
T
samiandClaude Sonnet 5 755d85964e gui: M1 DoD harness — scroll-60fps, rss-flat, unhappy-path
gui/docs/pkg-qa-requests-m1.md R3, filed by the previous session: three GUI
M1 DoD items (10k rows at 60 fps, flat RSS over 10 minutes,
--slow/--flaky/--drop-connection recovery) had nowhere to run in CI. This is
the harness — `gui/tests/dod/run.sh <gate> [--json <path>]`, exactly the
path/invocation contract tests/integration/README.md already specified —
plus `gui/tests/dod/dod_harness.cpp`, the Qt/RpcClient-driven binary that
actually runs each gate against a real mockd run.sh starts and tears down
itself.

- scroll-60fps: an eased scripted scroll over the whole loaded table,
  timing each step's synchronous repaint; p99 against a 16.6 ms budget
  (auto-scaled 4x under a sanitized build — ASan/UBSan overhead, not a
  loosened bar, see the harness's isSanitizedBuild()).
- rss-flat: samples this process's own VmRSS at 1 Hz across the run,
  discards a warm-up window, checks post-warm-up growth against a stated
  20 MiB slack.
- unhappy-path: three phases (slow/flaky/drop-connection), each its own
  mockd instance; passes when the client reaches and holds Connected with
  no crash or hang. A watchdog (the harness's own QTimer, backstopped by
  run.sh's external `timeout`) turns a genuine hang into a bounded non-zero
  exit rather than needing the CI caller to timeout(1) around it.

Every gate honours the exit-code and --json contract PKG/QA's pre-drafted
CI job expects unchanged (one addition needed: the build step must also
build the `gui-dod-harness` target, noted in the R3 update). No leaked mockd
processes on any exit path (`trap cleanup EXIT INT TERM`); no writes outside
a tempdir except the caller's own --json path.

Verified live end-to-end (not just unit-level): all three gates run against
a real mockd under the exact `ASAN_OPTIONS=detect_leaks=1:halt_on_error=1`
`.github/workflows/ci.yml`'s sanitizers job already sets, all pass, and
scroll-60fps was forced red once on purpose
(VELOX_DOD_FRAME_BUDGET_MS=1) to prove the fail path and exit code actually
work. Building this is also what surfaced the two RpcClient bugs fixed in
the previous commit, and one real gap in mockd itself — --drop-connection
never worked over the Unix socket transport (only WebSocket) — filed as
gui/docs/proto-requests-m1.md since tools/mockd is PROTO's file.

gui/docs/pkg-qa-requests-m1.md R3 and R4 (an unrelated, non-blocking Qt6::DBus
CMake hygiene note filed while wiring the clipboard global-shortcut path)
are updated with the concrete findings above.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NSCdCWFXBTSBBK3MzWtJiC
2026-09-12 21:28:36 +04:00

4.2 KiB

GUI → PROTO requests (M1)

Filed by lane GUI while building gui/tests/dod/ (gui/docs/pkg-qa-requests-m1.md R3's harness). Touches tools/mockd/ — PROTO-owned (CLAUDE.md §1) — so GUI is not making the edit. Apply-ready below.


mockd --drop-connection is a no-op over the Unix socket transport

--drop-connection <s> is documented as "terminate every connection every N seconds, to exercise reconnect logic" and is exactly what gui/tests/dod/run.sh unhappy-path needs for its drop-connection phase. It works — but only over WebSocket.

Repro: tools/mockd/src/index.ts's startUds() call passes args.slow and stops there:

startUds(args.uds, dispatcher, connections, log, args.slow);

startWs(), two lines below, gets the full options object including dropEverySec. startUds()'s own signature (tools/mockd/src/transport/uds.ts) has no dropEverySec parameter at all, and nothing in it ever calls socket.destroy() — the periodic-drop setInterval that startWs has (its last ~6 lines) simply does not exist on the UDS side.

Verified live, not inferred from reading: ran mockd --no-ws --drop-connection 5, connected gui/tests/dod/dod_harness unhappy-path --phase drop-connection against it (UDS, the GUI's only transport) with a 45 s observation window, and stateChanged never fired — the connection sat in Connected the entire time. Same command with --flaky 0.3 correctly leaves the connection state alone (that flag only fails individual call replies, which is right), so this is specific to --drop-connection and the UDS transport, not a harness-side detection problem.

Effect: every GUI/CLI/nmhost consumer of mockd — the only transport they actually use — cannot be tested against a dropped connection at all today. gui/tests/dod/run.sh ships its unhappy-path drop-connection phase anyway (log intentionally records sawDisruption in its JSON so this is visible, not silently green), but it is currently only proving the client survives 45 quiet seconds, not a real drop.

Fix — mirror ws.ts's existing pattern onto uds.ts

tools/mockd/src/transport/uds.ts:

 export function startUds(
   path: string,
   dispatcher: Dispatcher,
   connections: Set<Connection>,
   log: (msg: string) => void,
   delayMs: number,
+  dropEverySec: number = 0,
 ): Server {
   mkdirSync(dirname(path), { recursive: true });
   rmSync(path, { force: true });

+  const sockets = new Set<Socket>();
   const server = createServer((socket: Socket) => {
+    sockets.add(socket);
     const session: Session = { transport: 'uds', paired: true, subscribed: new Set(), sessionId: randomUUID() };
     const conn: Connection = {
       session,
       send: (frame) => {
         if (!socket.destroyed) socket.write(JSON.stringify(frame) + '\n');
       },
     };
     connections.add(conn);
     log(`uds: client connected (${connections.size} open)`);
     ...
     socket.on('error', (err) => log(`uds: socket error: ${err.message}`));
     socket.on('close', () => {
       connections.delete(conn);
+      sockets.delete(socket);
       log(`uds: client disconnected (${connections.size} open)`);
     });
   });

   server.listen(path, () => log(`uds: listening on ${path}`));
+
+  if (dropEverySec > 0) {
+    setInterval(() => {
+      log(`uds: dropping ${sockets.size} connection(s) (--drop-connection)`);
+      for (const s of sockets) s.destroy();
+    }, dropEverySec * 1000).unref();
+  }
+
   return server;
 }

tools/mockd/src/index.ts (~line 205):

-    startUds(args.uds, dispatcher, connections, log, args.slow);
+    startUds(args.uds, dispatcher, connections, log, args.slow, args.dropEverySec);

Both use .unref()/existing shutdown handling already in index.ts, so no change needed there. socket.destroy() (vs. .end()) matches ws.ts's .terminate() — an abrupt drop, which is the point of the flag.

Not urgent for M0/M1 GUI work — gui/tests/dod/run.sh's other two unhappy-path phases (--slow, --flaky) both work correctly over UDS today, and the drop-connection phase still exercises 45 s of otherwise-idle connection handling. But the flag's whole purpose is unmet on the transport every real consumer uses, and the fix is a direct port of code that already exists two files over.