detached 4913180

test(acceptance): poll the broker publication for remote inventory

The harness waited on the daemon's remote-catalog cache file, which the
broker architecture no longer writes; it now waits until vev ls remote
(the broker's committed snapshot) lists the fixture session. Unaccepted
input_busy refusals are resent within a bound, and the README drops the
client-picker-navigate known gap.
commit
4913180a9b9a781c845fc63ac3c3bfc6f256fe21
author
brice · · 2026-09-23 02:57 UTC
parent
87a5a2a
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +49 -19
M scripts/remote-picker-harness/README.md +11 -6
@@ -70,9 +70,14 @@ exhaustive transport or geometry matrix. For a focused run, set
7070 above (for example `warm-reuse@quic warm-reuse@stdio`); the containers, keys
7171 and artifacts still come from the same setup.
7272
73-Known gap: `client-picker-navigate` fails independently of this matrix. After
74-entering the search and the exact target name, the client-owned picker commits
75-the current session instead of the searched target. It fails in isolation and
76-at any log level; it predates the named-direct refactor, which previously
77-aborted the full run before this scenario was reached. The named-direct, warm
78-reuse and hybrid scenarios above are green.
73+Remote scenarios wait for the fixture session to reach the broker's
74+committed publication (`vev ls remote` in the client container) before they
75+search the palette: the broker owns remote observation and re-probes each host
76+on its own cadence. A driver request refused with `input_busy` (not accepted)
77+is resent for up to three seconds; accepted actions are never retried.
78+
79+`client-picker-navigate` opens the client-owned picker over the live
80+attachment with `SSP`, searches the target session by name, and commits it.
81+Every step must settle as `processed`: the opener on its daemon receipt, the
82+search keys at the client's event-loop boundary, and the commit on the
83+destination's first committed publication.
@@ -70,9 +70,14 @@ exhaustive transport or geometry matrix. For a focused run, set
70 above (for example `warm-reuse@quic warm-reuse@stdio`); the containers, keys70 above (for example `warm-reuse@quic warm-reuse@stdio`); the containers, keys
71 and artifacts still come from the same setup.71 and artifacts still come from the same setup.
72 72
73-Known gap: `client-picker-navigate` fails independently of this matrix. After73+Remote scenarios wait for the fixture session to reach the broker's
74-entering the search and the exact target name, the client-owned picker commits74+committed publication (`vev ls remote` in the client container) before they
75-the current session instead of the searched target. It fails in isolation and75+search the palette: the broker owns remote observation and re-probes each host
76-at any log level; it predates the named-direct refactor, which previously76+on its own cadence. A driver request refused with `input_busy` (not accepted)
77-aborted the full run before this scenario was reached. The named-direct, warm77+is resent for up to three seconds; accepted actions are never retried.
78-reuse and hybrid scenarios above are green.78+
79+`client-picker-navigate` opens the client-owned picker over the live
80+attachment with `SSP`, searches the target session by name, and commits it.
81+Every step must settle as `processed`: the opener on its daemon receipt, the
82+search keys at the client's event-loop boundary, and the commit on the
83+destination's first committed publication.
M scripts/remote-picker-harness/acceptance.py +38 -13
@@ -38,8 +38,9 @@ DAEMON_LOG = "vev-daemon.log"
3838 STDIO_LOG = "vev-stdio.log"
3939 # The broker logs one broker_remote_dial per remote physical bootstrap.
4040 BROKER_LOG = "broker/log/vev-daemon.log"
41-REMOTE_CACHE_FILE = "remote-catalog-cache.json"
41+REMOTE_HOST = "remote"
4242 INVENTORY_TIMEOUT_S = 45
43+INPUT_BUSY_RETRY_S = 3
4344 ARTIFACTS_DIR = os.environ.get("VEV_ACCEPTANCE_ARTIFACTS_DIR", "").strip()
4445 # The warm-reuse scenario requires client debug events. Every driver gets
4546 # debug logging unless a scenario explicitly overrides it.
@@ -47,7 +48,9 @@ DRIVER_BASE_ENV = {"VEV_LOG": "debug"}
4748
4849
4950 class DriverError(Exception):
50- pass
51+ def __init__(self, message, code=None):
52+ super().__init__(message)
53+ self.code = code
5154
5255
5356 class Driver:
@@ -118,10 +121,30 @@ class Driver:
118121 except json.JSONDecodeError as exc:
119122 raise DriverError(f"invalid driver JSON while reading {what}: {exc}") from exc
120123 if "error" in envelope:
121- raise DriverError(f"driver error while reading {what}: {envelope['error']}")
124+ error = envelope["error"]
125+ raise DriverError(f"driver error while reading {what}: {error}",
126+ code=error.get("code") if isinstance(error, dict) else None)
122127 return envelope["result"]
123128
124129 def call(self, operation, **fields):
130+ """Send one request; retry only an input admission the owner refused.
131+
132+ ``input_busy`` is returned with ``accepted: false`` while the input
133+ owner is still delivering an earlier terminal read (for example a
134+ reply to a query the new attachment's first frame emitted). Nothing
135+ was admitted, so resending is the documented client behavior; every
136+ other error, and every accepted action, is reported unchanged.
137+ """
138+ deadline = time.monotonic() + INPUT_BUSY_RETRY_S
139+ while True:
140+ try:
141+ return self._call_once(operation, **fields)
142+ except DriverError as exc:
143+ if exc.code != "input_busy" or time.monotonic() >= deadline:
144+ raise
145+ time.sleep(0.05)
146+
147+ def _call_once(self, operation, **fields):
125148 self.request_id += 1
126149 request = dict(version=1, id=self.request_id, op=operation,
127150 attachment=self.attachment, **fields)
@@ -255,16 +278,18 @@ def write_artifact(name, payload):
255278
256279
257280 def await_remote_inventory(container, session, timeout_s=INVENTORY_TIMEOUT_S, interval_s=0.5):
258- """Wait until the local daemon publishes ``session`` for its remote host.
259-
260- The remote monitor observes each host on a ~15s healthy cadence, so a
261- freshly created remote session is invisible to the palette until the next
262- observation. Wait on the daemon's persisted catalog cache (rewritten right
263- after a successful observation) rather than guessing a timeout, so the
264- following navigation is deterministic and near-immediate in isolation.
281+ """Wait until the broker publishes ``session`` for its remote host.
282+
283+ The broker owns remote observation: it re-probes each configured host on
284+ its own healthy cadence, and every attached client derives its palette
285+ and picker rows from the broker's committed publication. A freshly created
286+ remote session is invisible until the next observation, so wait on that
287+ publication itself (``vev ls HOST`` reads the broker snapshot) rather than
288+ guessing a timeout. The following palette search then asserts the row
289+ independently.
265290 """
266- script = (f'f="{STATE_LOG_DIR}/{REMOTE_CACHE_FILE}"; '
267- f'grep -qF "{session}" "$f" 2>/dev/null')
291+ display = f"{session}@{REMOTE_HOST}"
292+ script = f'vev ls {REMOTE_HOST} 2>/dev/null | grep -qF "{display} "'
268293 deadline = time.monotonic() + timeout_s
269294 while True:
270295 try:
@@ -274,7 +299,7 @@ def await_remote_inventory(container, session, timeout_s=INVENTORY_TIMEOUT_S, in
274299 pass
275300 if time.monotonic() >= deadline:
276301 raise DriverError(
277- f"remote session {session} never reached the local inventory "
302+ f"remote session {session} never reached the broker publication "
278303 f"within {timeout_s}s")
279304 time.sleep(interval_s)
280305
@@ -38,8 +38,9 @@ DAEMON_LOG = "vev-daemon.log"
38 STDIO_LOG = "vev-stdio.log"38 STDIO_LOG = "vev-stdio.log"
39 # The broker logs one broker_remote_dial per remote physical bootstrap.39 # The broker logs one broker_remote_dial per remote physical bootstrap.
40 BROKER_LOG = "broker/log/vev-daemon.log"40 BROKER_LOG = "broker/log/vev-daemon.log"
41-REMOTE_CACHE_FILE = "remote-catalog-cache.json"41+REMOTE_HOST = "remote"
42 INVENTORY_TIMEOUT_S = 4542 INVENTORY_TIMEOUT_S = 45
43+INPUT_BUSY_RETRY_S = 3
43 ARTIFACTS_DIR = os.environ.get("VEV_ACCEPTANCE_ARTIFACTS_DIR", "").strip()44 ARTIFACTS_DIR = os.environ.get("VEV_ACCEPTANCE_ARTIFACTS_DIR", "").strip()
44 # The warm-reuse scenario requires client debug events. Every driver gets45 # The warm-reuse scenario requires client debug events. Every driver gets
45 # debug logging unless a scenario explicitly overrides it.46 # debug logging unless a scenario explicitly overrides it.
@@ -47,7 +48,9 @@ DRIVER_BASE_ENV = {"VEV_LOG": "debug"}
47 48
48 49
49 class DriverError(Exception):50 class DriverError(Exception):
50- pass51+ def __init__(self, message, code=None):
52+ super().__init__(message)
53+ self.code = code
51 54
52 55
53 class Driver:56 class Driver:
@@ -118,10 +121,30 @@ class Driver:
118 except json.JSONDecodeError as exc:121 except json.JSONDecodeError as exc:
119 raise DriverError(f"invalid driver JSON while reading {what}: {exc}") from exc122 raise DriverError(f"invalid driver JSON while reading {what}: {exc}") from exc
120 if "error" in envelope:123 if "error" in envelope:
121- raise DriverError(f"driver error while reading {what}: {envelope['error']}")124+ error = envelope["error"]
125+ raise DriverError(f"driver error while reading {what}: {error}",
126+ code=error.get("code") if isinstance(error, dict) else None)
122 return envelope["result"]127 return envelope["result"]
123 128
124 def call(self, operation, **fields):129 def call(self, operation, **fields):
130+ """Send one request; retry only an input admission the owner refused.
131+
132+ ``input_busy`` is returned with ``accepted: false`` while the input
133+ owner is still delivering an earlier terminal read (for example a
134+ reply to a query the new attachment's first frame emitted). Nothing
135+ was admitted, so resending is the documented client behavior; every
136+ other error, and every accepted action, is reported unchanged.
137+ """
138+ deadline = time.monotonic() + INPUT_BUSY_RETRY_S
139+ while True:
140+ try:
141+ return self._call_once(operation, **fields)
142+ except DriverError as exc:
143+ if exc.code != "input_busy" or time.monotonic() >= deadline:
144+ raise
145+ time.sleep(0.05)
146+
147+ def _call_once(self, operation, **fields):
125 self.request_id += 1148 self.request_id += 1
126 request = dict(version=1, id=self.request_id, op=operation,149 request = dict(version=1, id=self.request_id, op=operation,
127 attachment=self.attachment, **fields)150 attachment=self.attachment, **fields)
@@ -255,16 +278,18 @@ def write_artifact(name, payload):
255 278
256 279
257 def await_remote_inventory(container, session, timeout_s=INVENTORY_TIMEOUT_S, interval_s=0.5):280 def await_remote_inventory(container, session, timeout_s=INVENTORY_TIMEOUT_S, interval_s=0.5):
258- """Wait until the local daemon publishes ``session`` for its remote host.281+ """Wait until the broker publishes ``session`` for its remote host.
259-282+
260- The remote monitor observes each host on a ~15s healthy cadence, so a283+ The broker owns remote observation: it re-probes each configured host on
261- freshly created remote session is invisible to the palette until the next284+ its own healthy cadence, and every attached client derives its palette
262- observation. Wait on the daemon's persisted catalog cache (rewritten right285+ and picker rows from the broker's committed publication. A freshly created
263- after a successful observation) rather than guessing a timeout, so the286+ remote session is invisible until the next observation, so wait on that
264- following navigation is deterministic and near-immediate in isolation.287+ publication itself (``vev ls HOST`` reads the broker snapshot) rather than
288+ guessing a timeout. The following palette search then asserts the row
289+ independently.
265 """290 """
266- script = (f'f="{STATE_LOG_DIR}/{REMOTE_CACHE_FILE}"; '291+ display = f"{session}@{REMOTE_HOST}"
267- f'grep -qF "{session}" "$f" 2>/dev/null')292+ script = f'vev ls {REMOTE_HOST} 2>/dev/null | grep -qF "{display} "'
268 deadline = time.monotonic() + timeout_s293 deadline = time.monotonic() + timeout_s
269 while True:294 while True:
270 try:295 try:
@@ -274,7 +299,7 @@ def await_remote_inventory(container, session, timeout_s=INVENTORY_TIMEOUT_S, in
274 pass299 pass
275 if time.monotonic() >= deadline:300 if time.monotonic() >= deadline:
276 raise DriverError(301 raise DriverError(
277- f"remote session {session} never reached the local inventory "302+ f"remote session {session} never reached the broker publication "
278 f"within {timeout_s}s")303 f"within {timeout_s}s")
279 time.sleep(interval_s)304 time.sleep(interval_s)
280 305

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close