Skip to content

feat(mock): live scenario re-binding across gateways + mock received write verification#85

Merged
ShortArrow merged 11 commits into
mainfrom
feat/live-rebind-all-gateways
Jul 21, 2026
Merged

feat(mock): live scenario re-binding across gateways + mock received write verification#85
ShortArrow merged 11 commits into
mainfrom
feat/live-rebind-all-gateways

Conversation

@ShortArrow

@ShortArrow ShortArrow commented Jul 21, 2026

Copy link
Copy Markdown
Owner

Two mock-testing capabilities that let a client-app integration test drive a scenario-backed mock and verify what it sent, without touching real hardware. Ready for review; a follow-up release PR (0.2.8) adds the version bump + CHANGELOG (covering this, the doctor rename #81, and the dual license #72).

What this does

1 · Live scenario re-binding on a running gateway. A serving gateway seeds its FakeBackend scenario bindings once at startup, so a separate-process mock scenario activate (which only writes the session) was invisible until restart. A new IScenarioBindingRefresher — invoked by the SOCKET, HiSLIP, and VXI-11 gateways before each SCPI dispatch — re-syncs the binding from the session. It re-applies only when the bound scenario name changes (in-flight scene state is preserved) and is no-throw.

2 · mock received — out-of-band write verification. A client-app integration test drives the mock through its own VISA stack, so it never sends raw SCPI and cannot assert what it wrote. ivicli mock received <device> [--match <substr> | --exact <scpi>] [--all] [--count] [--json] reads the IVICLI_CAPTURE NDJSON (shared-read, so it works while the gateway serves) and reports the writes the device received. --json is always an array; non-zero exit when nothing matched (except --count).

Container-smoke fix folded in. Env-var scenario activation (IVICLI_SCENARIO) now persists to the session, so the refresher does not tear down an env-activated scenario — which is exactly what the mock container relies on. Verified end-to-end.

Design notes

  • The refresher is an Application-layer port; the impl lives in the Fake backend and is DI-wired. Gateways take it as an optional dependency defaulting to a no-op, so a gateway composed without the Fake backend keeps its startup bindings.
  • mock received adds no new persistence path — it reuses the existing NDJSON traffic capture via INdjsonTrafficReader.
  • Docs updated in-place across the mock guide and README + PRD (EN/JP in lockstep).

Testing

  • TDD Red→Green for live re-binding on all three gateways, plus a DI-wiring guard and a gateway→capture seam test.
  • mock received: handler, CLI render, and real-adapter round-trip; exercised on the real binary.
  • Env-scenario fix reproduced and verified locally (repeated *IDN? over a SOCKET gateway keeps returning the scenario IDN).
  • Full suite green (12 projects, ~722 tests, Category!=Integration); csharpier clean.

Out of scope / follow-up

  • Version bump + CHANGELOG → separate 0.2.8 release PR.
  • mock received needs the gateway started with IVICLI_CAPTURE=<path>; use a fresh path per run (documented — no time filter by design).
  • Real-hardware smoke for the live re-binding path (live instrument) is delegated to a separate run.

A gateway seeds its FakeBackend scenario bindings once at startup; a
separate-process `mock scenario activate` wrote only the session store,
so a client on a long-lived connection kept seeing the startup scenario
until the gateway restarted. Introduce IScenarioBindingRefresher, invoked
by the SOCKET, HiSLIP, and VXI-11 gateways before each SCPI dispatch, to
re-sync the binding from the session. Re-application is scoped to a
changed scenario name so in-flight scene state is preserved; the port is
no-throw and defaults to a no-op NullScenarioBindingRefresher when the
Fake backend is not composed in.
The gateways take IScenarioBindingRefresher as an optional constructor
dependency, so the direct-construction live-rebind tests would still pass
even if the composition root failed to inject the real refresher — live
re-binding would then silently no-op in the shipped CLI. Resolve each
gateway from a production-like container and assert the injected refresher
is the real SessionScenarioBindingRefresher, not the no-op default.
…band

A client-app integration test drives the mock through its own VISA stack, so
it never sends raw SCPI and cannot assert what it wrote by echoing. Add
`ivicli mock writes <device> [--match <substr>] [--all] [--json]`, which reads
the NDJSON traffic capture a serving gateway produced (IVICLI_CAPTURE) via the
existing INdjsonTrafficReader and reports the SCPI writes the device received —
last match by default, all with --all, non-zero exit when nothing matched. The
reader opens the log with shared access, so a separate process can query it
while the gateway is still serving. No new persistence path: MockWritesQuery
sits in the Application layer over the ADR 0031 capture.
The mock writes handler is unit-tested against a fake capture, but the
deployment path — a client write reaching the gateway, wrapped for capture,
producing a Write event with the exact SCPI — was only covered by composed
units. Drive the real SocketGatewayServer wrapped in CapturingBackendFactory
over a loopback TCP client and assert the `:VOLT 24.000` write is captured as
a TrafficEvent for the device.
Round out the requirement-2 documentation: add the `mock writes` verb to the
PRD Phase-3 command roadmap (EN + JP) next to `mock scenario record`/`import`,
and point the README "Audit-friendly" bullet at it (EN + JP) so the
write-verification workflow is discoverable from the capture entry point.
`mock writes` read ambiguously (imperative verb vs. noun) and did not convey
the reception angle — the point is to verify which writes actually reached the
server/mock. Rename the verb to `received` and the types to
MockReceivedWrites* across command, handler, query, errors, tests, and docs
(README/PRD EN+JP, ADR 0031, guide). Behaviour and flags are unchanged.
…ck received`

Refine the received-writes query surface: `--exact <scpi>` full-string filter
(mutually exclusive with the substring `--match`, so `:VOLT` no longer also
matches `:VOLT:PROT 30`), and `--count` to print the match count (exit 0 even
at zero). `--json` now always emits an array — a single-element array by
default, `[]` when empty — so a parser never branches on --all. Documents the
run-isolation practice (fresh IVICLI_CAPTURE per run) in the guide and ADR 0031
instead of adding a time filter.
The env-var scenario activation (IVICLI_SCENARIO / IVICLI_SCENARIO_FOR)
activated the scenario on the FakeBackend but never wrote the session store.
The new live scenario-binding refresher treats the session as the source of
truth and deactivates any running binding the session does not name, so it
tore down an env-activated scenario on the first request — breaking the mock
container's *IDN? roundtrip (docker smoke). Write the binding into the session
at startup, mirroring `mock scenario activate`, so the refresher sees a
matching binding and leaves it alone.

Verified end-to-end locally: a SOCKET gateway started with IVICLI_SCENARIO now
returns the scenario IDN on repeated *IDN? queries instead of deactivating.
The PRD is a living document describing the current product; ADRs are the
frozen decision journal. A living doc must not depend on the journal (the
pointer belongs on the concrete side), so the ADR references scattered through
the PRD are dropped. The surrounding prose already states what each feature
does; readers wanting the rationale reach the ADRs via their own index, not a
back-link from the PRD. EN and JP updated in lockstep.
The README and the mock-instrument guide are living documents; a living
document must not back-reference the ADR journal (the pointer belongs on the
concrete side). Drop the inline ADR references and reword the few sentences
whose only content was such a pointer, so each reads self-contained. The
"Architecture Decision Records" index link is kept — a directory-level
navigation entry into the journal, not a specific back-reference. README EN
and JP updated in lockstep.
Code comments are living documentation. The mock-received query docstring
back-referenced the ADR journal (ADR 0031) and, together with three test
docstrings, pointed at a "requirement 2" label that lives only in an untracked
scratch file — neither resolvable by a repo reader. Reword them to describe the
behaviour directly so each comment stands on its own.
@ShortArrow
ShortArrow marked this pull request as ready for review July 21, 2026 23:37
@ShortArrow
ShortArrow merged commit 147c4be into main Jul 21, 2026
11 checks passed
@ShortArrow
ShortArrow deleted the feat/live-rebind-all-gateways branch July 21, 2026 23:40
@ShortArrow ShortArrow mentioned this pull request Jul 22, 2026
ShortArrow added a commit that referenced this pull request Jul 22, 2026
Version bump and CHANGELOG for 0.2.8: live mock-scenario re-binding across the
SOCKET / HiSLIP / VXI-11 gateways and `ivicli mock received` (#85), the
`diagnose` -> `doctor` rename with a deprecated alias (#81), and the
MIT OR Apache-2.0 dual license (#72). Internal project lock files refreshed to
0.2.8.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant