From e52ddbe52e3058e24db6d09632b872aa3347db38 Mon Sep 17 00:00:00 2001 From: Amplify Logic AI Date: Wed, 19 Aug 2026 19:16:47 +0200 Subject: [PATCH 1/3] fix(bin): stop bridge glance hangs on a scrubbed PATH The snapshot child PATH omitted ~/.local/bin and nvm, so a missing herdr CLI triggered a 10s server-start poll per live task and /api/observation timed out. Fail that check immediately, resolve tool dirs at server start, replace eternal Loading with the unreachable-desk state, and keep today's photo count on 503. Co-authored-by: Cursor --- bin/backends/herdr.sh | 5 ++ bin/fm-bridge-view.py | 159 ++++++++++++++++++++++++++++----- bin/fm-crew-state.sh | 12 ++- docs/bridge-view.md | 9 +- tests/fm-backend-herdr.test.sh | 15 ++++ tests/fm-bridge-view.test.sh | 144 +++++++++++++++++++++++++++++ 6 files changed, 312 insertions(+), 32 deletions(-) diff --git a/bin/backends/herdr.sh b/bin/backends/herdr.sh index 327467c78aa..c19a51a8cf2 100644 --- a/bin/backends/herdr.sh +++ b/bin/backends/herdr.sh @@ -143,6 +143,7 @@ fm_backend_herdr_workspace_label() { fm_backend_herdr_cli() { # local session=$1 shift + command -v herdr >/dev/null 2>&1 || return 1 HERDR_SESSION="$session" herdr "$@" --session "$session" } @@ -212,6 +213,10 @@ fm_backend_herdr_session() { # call. Bounded poll for the server to report running. fm_backend_herdr_server_ensure() { # local session=$1 running out i + # Missing herdr must fail immediately. Backgrounding `herdr server` when the + # binary is not on PATH succeeds as a job launch, then this poll slept 10s + # per call (2026-08-19 bridge observation hang). + fm_backend_herdr_tool_check || return 1 running=$(fm_backend_herdr_cli "$session" status --json 2>/dev/null | jq -r '.server.running // false' 2>/dev/null) [ "$running" = "true" ] && return 0 ( fm_backend_herdr_cli "$session" server >/dev/null 2>&1 & ) || return 1 diff --git a/bin/fm-bridge-view.py b/bin/fm-bridge-view.py index 55eb49dd63f..a85f940c5f8 100755 --- a/bin/fm-bridge-view.py +++ b/bin/fm-bridge-view.py @@ -25,6 +25,7 @@ import re import secrets import selectors +import shutil import signal import socket import subprocess @@ -86,7 +87,89 @@ GITHUB_PR_RE = re.compile(r"^https://github\.com/[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+/pull/[0-9]+$") IPV4_RE = re.compile(r"^\d{1,3}(?:\.\d{1,3}){3}$") HOST_LABEL_RE = re.compile(r"^[A-Za-z0-9.-]+(?::\d+)?$") -CHILD_PATH = "/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin:/opt/homebrew/bin" +ISO_DAY_RE = re.compile(r"^(\d{4}-\d{2}-\d{2})") +FILENAME_DAY_RE = re.compile(r"^(\d{4})(\d{2})(\d{2})T") +BASE_CHILD_PATH = "/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin:/opt/homebrew/bin" +CHILD_PATH_TOOLS = ( + "jq", + "git", + "tmux", + "herdr", + "no-mistakes", + "tasks-axi", + "gh", + "node", + "timeout", + "gtimeout", + "perl", +) + + +def _append_unique_dir(dirs: List[str], seen: set[str], candidate: str) -> None: + if not candidate: + return + resolved = os.path.abspath(candidate) + if resolved in seen or not os.path.isdir(resolved): + return + seen.add(resolved) + dirs.append(resolved) + + +def _nvm_bin_from_home(home: str) -> str: + versions = os.path.join(home, ".nvm", "versions", "node") + named: List[str] = [] + with_axi = "" + if os.path.isdir(versions): + try: + children = os.listdir(versions) + except OSError: + children = [] + for name in sorted(children): + bin_dir = os.path.join(versions, name, "bin") + if not os.path.isdir(bin_dir): + continue + named.append(bin_dir) + if os.path.isfile(os.path.join(bin_dir, "tasks-axi")): + with_axi = bin_dir + if with_axi: + return with_axi + if named: + return named[-1] + env_bin = os.environ.get("NVM_BIN", "").strip() + if env_bin and os.path.isdir(env_bin): + return env_bin + return "" + + +def resolve_child_path(source_path: Optional[str] = None, home: Optional[str] = None) -> str: + """Build a scrubbed PATH that still locates snapshot tools. + + Keep the base system dirs, then add only the directories of tools the + snapshot actually needs, resolved from HOME and the parent PATH. Never + copy the rest of the parent environment. + """ + home_path = home if home is not None else os.environ.get("HOME", "") + user_dirs: List[str] = [] + seen: set[str] = set() + if home_path: + _append_unique_dir(user_dirs, seen, os.path.join(home_path, ".local", "bin")) + _append_unique_dir(user_dirs, seen, _nvm_bin_from_home(home_path)) + search = BASE_CHILD_PATH + if source_path is None: + source_path = os.environ.get("PATH", "") + if source_path: + search = source_path + ":" + BASE_CHILD_PATH + for tool in CHILD_PATH_TOOLS: + found = shutil.which(tool, path=search) + if found: + _append_unique_dir(user_dirs, seen, os.path.dirname(found)) + base_dirs: List[str] = [] + for part in BASE_CHILD_PATH.split(":"): + _append_unique_dir(base_dirs, seen, part) + return ":".join(user_dirs + base_dirs) + + +CHILD_PATH = resolve_child_path() TEST_MODE = os.environ.get("FM_BRIDGE_VIEW_TEST") == "1" @@ -301,6 +384,22 @@ def extract_upload(content_type: str, body: bytes) -> Optional[Tuple[bytes, str, return None +def sidecar_received_day(path: Path, name: str) -> str: + try: + data = json.loads(path.read_text(encoding="utf-8")) + except (OSError, UnicodeError, json.JSONDecodeError): + data = None + if isinstance(data, dict): + received = str(data.get("received_at") or "").strip() + match = ISO_DAY_RE.match(received) + if match: + return match.group(1) + match = FILENAME_DAY_RE.match(name) + if match: + return f"{match.group(1)}-{match.group(2)}-{match.group(3)}" + return "" + + def photos_received_today(home: Path) -> int: inbox = inbox_dir(home) if not inbox.is_dir() or inbox.is_symlink(): @@ -317,14 +416,7 @@ def photos_received_today(home: Path) -> int: path = inbox / name if path.is_symlink() or not path.is_file(): continue - try: - data = json.loads(path.read_text(encoding="utf-8")) - except (OSError, UnicodeError, json.JSONDecodeError): - continue - if not isinstance(data, dict): - continue - received = str(data.get("received_at") or "") - if received.startswith(today): + if sidecar_received_day(path, name) == today: count += 1 return count @@ -956,7 +1048,7 @@ def security_headers(nonce: str) -> List[Tuple[str, str]]: PAGE_JS = """ const STALE_MS = %d * 1000; const REFRESH_MS = %d * 1000; -let lastSuccess = Date.now(); +let lastSuccess = 0; function esc(value) { return String(value).replace(/[&<>"']/g, function(ch) { return ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[ch]); @@ -967,6 +1059,19 @@ def security_headers(nonce: str) -> List[Tuple[str, str]]: if (!el) return; el.classList.toggle('on', on); } +function setPhotoCount(n) { + const count = document.getElementById('photo-count'); + if (count) count.textContent = 'Photos received today: ' + n; +} +function markBucketsUnreachable() { + ['needs','underway','finished','waiting'].forEach(function(id) { + const root = document.getElementById(id); + if (!root) return; + if (root.textContent.indexOf('Loading') !== -1) { + root.innerHTML = '

Cannot reach the desk.

'; + } + }); +} function dotClass(name) { if (name === 'Needs you') return 'needs'; if (name === 'Under way') return 'under'; @@ -1012,8 +1117,7 @@ def security_headers(nonce: str) -> List[Tuple[str, str]]: renderBucket('underway', data.under_way, 'Nothing is under way.'); renderBucket('finished', data.just_finished, 'No recent completions.'); renderBucket('waiting', data.waiting, 'Nothing is waiting.'); - const count = document.getElementById('photo-count'); - if (count) count.textContent = 'Photos received today: ' + (data.photos_today || 0); + setPhotoCount(data.photos_today != null ? data.photos_today : 0); } async function sendPhoto(ev) { ev.preventDefault(); @@ -1045,10 +1149,7 @@ def security_headers(nonce: str) -> List[Tuple[str, str]]: status.className = 'ok'; status.textContent = 'Photo received.'; input.value = ''; - const count = document.getElementById('photo-count'); - if (count && payload.received_today != null) { - count.textContent = 'Photos received today: ' + payload.received_today; - } + if (payload.received_today != null) setPhotoCount(payload.received_today); } catch (err) { status.className = 'warn'; status.textContent = 'Send failed.'; @@ -1057,6 +1158,7 @@ def security_headers(nonce: str) -> List[Tuple[str, str]]: function tickObserved() { const age = document.getElementById('observed'); if (!age) return; + if (!lastSuccess) return; const seconds = Math.max(0, Math.round((Date.now() - lastSuccess) / 1000)); age.textContent = 'Observed ' + seconds + ' seconds ago'; if (Date.now() - lastSuccess > STALE_MS) setStale(true); @@ -1067,10 +1169,17 @@ def security_headers(nonce: str) -> List[Tuple[str, str]]: const timer = setTimeout(function() { ctl.abort(); }, 10000); const res = await fetch('/api/observation', { credentials: 'same-origin', cache: 'no-store', signal: ctl.signal }); clearTimeout(timer); + const payload = await res.json().catch(function() { return {}; }); + if (payload && payload.photos_today != null) setPhotoCount(payload.photos_today); if (!res.ok) throw new Error('status ' + res.status); - apply(await res.json()); + apply(payload); } catch (err) { - tickObserved(); + if (!lastSuccess) { + setStale(true); + markBucketsUnreachable(); + } else { + tickObserved(); + } } } document.addEventListener('DOMContentLoaded', function() { @@ -1132,8 +1241,8 @@ def glance_html(nonce: str) -> str:

Starship

-

Desk reachable · Mailbox…

-

Observed just now

+

Checking the desk · Mailbox…

+

Observing…

Summary only. Do not approve from this page.

Send a photo

@@ -1342,12 +1451,18 @@ def do_GET(self) -> None: # noqa: N802 if not self._authed(): self._send(401, b'{"error":"unauthorized"}\n', "application/json") return + photos_today = photos_received_today(STATE.home) try: payload = STATE.cache.get() except Exception as exc: - self._send(503, json.dumps({"error": "desk unreachable", "detail": str(exc)}).encode("utf-8"), "application/json") + body = json.dumps({ + "error": "desk unreachable", + "detail": str(exc), + "photos_today": photos_today, + }).encode("utf-8") + self._send(503, body, "application/json") return - payload["photos_today"] = photos_received_today(STATE.home) + payload["photos_today"] = photos_today body = json.dumps(payload).encode("utf-8") self._send(200, body, "application/json") return diff --git a/bin/fm-crew-state.sh b/bin/fm-crew-state.sh index c441b3edb52..887975c3104 100755 --- a/bin/fm-crew-state.sh +++ b/bin/fm-crew-state.sh @@ -142,17 +142,15 @@ LOG_VERB=$(status_line_verb "$LOG_LINE") # pane_readable is consulted ONLY in the no-run fallback below. The run-step path # stays authoritative regardless of pane liveness - judge by the run-step, not the # shell - so a finished crew whose endpoint has closed still reports its run-step -# state (e.g. done) instead of being masked as unknown. Backend-aware -# (fm_backend_of_meta defaults absent backend= to tmux, the P1 contract): a -# herdr task is read through fm_backend_capture instead of a bare tmux probe. +# state (e.g. done) instead of being masked as unknown. Use the shared +# read-only existence check (fm_backend_target_exists): it never starts a +# session-provider server. Capture would call herdr's server_ensure and, when +# herdr was missing from PATH, poll for 10s per task. TASK_BACKEND=$(fm_backend_of_meta "$META") BACKEND_TARGET=$(fm_backend_target_of_meta "$META") EXPECTED_LABEL="fm-$ID" pane_readable() { # - case "$TASK_BACKEND" in - tmux) tmux display-message -p -t "$1" '#{pane_id}' >/dev/null 2>&1 ;; - *) fm_backend_capture "$TASK_BACKEND" "$1" 1 "$EXPECTED_LABEL" >/dev/null 2>&1 ;; - esac + fm_backend_target_exists "$TASK_BACKEND" "$1" "$EXPECTED_LABEL" } # crew_pane_is_busy: the busy-signature fallback, backend-aware the same way - # fm_backend_busy_state's native semantic state (herdr's agent.get) when diff --git a/docs/bridge-view.md b/docs/bridge-view.md index 7d85e7a1ec6..e6510430c8d 100644 --- a/docs/bridge-view.md +++ b/docs/bridge-view.md @@ -75,18 +75,21 @@ launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.firstmate.bridge-vie The page calls `bin/fm-bearings-snapshot.sh --json --passive-view`. That named Bearings mode is allowed while away mode is on; ordinary `/bearings` chat still refuses until return catch-up finishes. The server caches one observation for about 30 seconds, runs one refresh at a time, and caps subprocess time and output size. +The snapshot child keeps a scrubbed environment (no parent secrets) but resolves tool directories at server start from HOME and the parent PATH, including `~/.local/bin` and the current nvm node bin, so CLIs such as herdr and tasks-axi are found without a version-specific hardcoded path. +A missing session-provider CLI must fail immediately rather than polling; the snapshot either returns glance data or a 503 with a clear error within seconds. It never takes the session lock, never drains wakes, and never writes backlog or state. Photo drops are the exception write path documented below; observation GETs still only read. +A 503 still includes today's photo count from inbox sidecars so the counter does not depend on glance data loading. The client refreshes every 30 seconds and also refreshes on `pageshow` and when a hidden tab becomes visible. The four buckets show at most 5 Needs you, 8 Under way, 6 Just finished, and 5 Waiting in the wings rows, with an honest `N more` count for omitted rows. +If the first observation request fails, the buckets replace "Loading…" with "Cannot reach the desk" immediately and the full-page overlay appears. +If refreshes stop for 90 seconds after a successful load, the already-open tab overlays "Cannot reach the desk" from the client clock. +Last-good on the server cannot save a tab that never hears back. The mailbox indicator is a local listen or launchd check. It must never call `GET /v1/announcements`, because that call marks announcements delivered. -If refreshes stop for 90 seconds, the already-open tab overlays "Cannot reach the desk" from the client clock. -Last-good on the server cannot save a tab that never hears back. - ## Writes Login and logout POSTs remain the authentication writes. diff --git a/tests/fm-backend-herdr.test.sh b/tests/fm-backend-herdr.test.sh index 884593e0d9e..6f7730644b2 100755 --- a/tests/fm-backend-herdr.test.sh +++ b/tests/fm-backend-herdr.test.sh @@ -245,6 +245,20 @@ test_version_check_refuses_missing_herdr() { pass "fm_backend_herdr_version_check: refuses loudly when herdr is not installed" } +test_server_ensure_fails_fast_when_herdr_missing() { + local dir out status started elapsed + dir="$TMP_ROOT/server-ensure-missing"; mkdir -p "$dir/empty-fakebin" + started=$(python3 -c 'import time; print(time.monotonic())') + out=$( PATH="$dir/empty-fakebin:/usr/bin:/bin" \ + bash -c '. "$0/bin/backends/herdr.sh"; fm_backend_herdr_server_ensure missinglab' "$ROOT" 2>&1 ) + status=$? + elapsed=$(python3 -c "import time; print(time.monotonic() - $started)") + [ "$status" -ne 0 ] || fail "server_ensure should refuse when herdr is not installed: $out" + python3 -c "import sys; sys.exit(0 if float(sys.argv[1]) < 2 else 1)" "$elapsed" \ + || fail "server_ensure hung ${elapsed}s when herdr was missing: $out" + pass "fm_backend_herdr_server_ensure: fails immediately when herdr is not installed" +} + # --- workspace_label: per-firstmate-HOME resolution (P3, herdr-sm-spaces-k4) - test_workspace_label_primary_home_no_marker() { @@ -2287,6 +2301,7 @@ test_list_live_hidden_legacy_and_other_home_boundary() { test_version_check_accepts_current_protocol test_version_check_refuses_old_protocol test_version_check_refuses_missing_herdr +test_server_ensure_fails_fast_when_herdr_missing test_workspace_label_primary_home_no_marker test_workspace_label_secondmate_home_uses_marker_id test_workspace_label_secondmate_marker_trims_whitespace diff --git a/tests/fm-bridge-view.test.sh b/tests/fm-bridge-view.test.sh index d14222e0408..21eb805ed92 100755 --- a/tests/fm-bridge-view.test.sh +++ b/tests/fm-bridge-view.test.sh @@ -967,6 +967,146 @@ PY pass "failed inbox pair publication leaves no partial upload" } +test_photos_received_today_counts_utc_sidecars() { + local home output + home=$(make_home photo-count) + output=$(python3 - "$ROOT/bin/fm-bridge-view.py" "$home" <<'PY' +import importlib.util, json, pathlib, sys, time +spec = importlib.util.spec_from_file_location("fm_bridge_view", sys.argv[1]) +module = importlib.util.module_from_spec(spec) +spec.loader.exec_module(module) +home = pathlib.Path(sys.argv[2]) +inbox = module.ensure_inbox_dir(home) +today = time.strftime("%Y-%m-%d", time.gmtime()) +yesterday = "1999-01-01" +(inbox / "today.json").write_text(json.dumps({ + "received_at": today + "T16:39:18Z", + "original_name": "one.jpg", + "size": 4, + "content_type": "image/jpeg", +})) +stem = time.strftime("%Y%m%dT%H%M%SZ", time.gmtime()) +(inbox / (stem + "-deadbeef.json")).write_text("{}\n") +(inbox / "old.json").write_text(json.dumps({"received_at": yesterday + "T00:00:00Z"})) +count = module.photos_received_today(home) +if count != 2: + raise SystemExit("expected 2 of today's sidecars, got %s" % count) +PY + ) || fail "photos_received_today failed: $output" + pass "photos_received_today counts today's sidecars by received_at and filename date" +} + +test_child_path_resolves_tool_dirs_without_hardcoded_nvm() { + local home output + home=$(make_home child-path) + mkdir -p "$home/.local/bin" "$home/.nvm/versions/node/v99.0.0/bin" + printf '#!/bin/sh\nexit 0\n' > "$home/.local/bin/herdr" + printf '#!/bin/sh\nexit 0\n' > "$home/.nvm/versions/node/v99.0.0/bin/tasks-axi" + printf '#!/bin/sh\nexit 0\n' > "$home/.nvm/versions/node/v99.0.0/bin/node" + chmod +x "$home/.local/bin/herdr" "$home/.nvm/versions/node/v99.0.0/bin/tasks-axi" \ + "$home/.nvm/versions/node/v99.0.0/bin/node" + output=$(python3 - "$ROOT/bin/fm-bridge-view.py" "$home" <<'PY' +import importlib.util, pathlib, sys +spec = importlib.util.spec_from_file_location("fm_bridge_view", sys.argv[1]) +module = importlib.util.module_from_spec(spec) +spec.loader.exec_module(module) +home = sys.argv[2] +resolved = module.resolve_child_path(source_path="/usr/bin:/bin", home=home) +if "/v99.0.0/bin" not in resolved: + raise SystemExit("nvm bin missing from resolved PATH: %s" % resolved) +if ".local/bin" not in resolved: + raise SystemExit("local bin missing from resolved PATH: %s" % resolved) +if "v22.23.1" in resolved: + raise SystemExit("hardcoded nvm version leaked into PATH: %s" % resolved) +PY + ) || fail "resolve_child_path failed: $output" + pass "child PATH resolves HOME tool dirs without a hardcoded nvm version" +} + +test_observation_error_fails_fast_and_keeps_photo_count() { + local home fakebin fixture port cookie hdr body started elapsed + home=$(make_home obs-error) + fakebin=$(make_fakebin "$home") + fixture=$home/root + mkdir -p "$fixture/bin" + cat > "$fixture/bin/fm-bearings-snapshot.sh" <<'SH' +#!/usr/bin/env bash +echo "missing tool: herdr" >&2 +exit 1 +SH + chmod +x "$fixture/bin/fm-bearings-snapshot.sh" + init_passcode "$home" >/dev/null + python3 - "$ROOT/bin/fm-bridge-view.py" "$home" <<'PY' +import importlib.util, pathlib, sys +spec = importlib.util.spec_from_file_location("fm_bridge_view", sys.argv[1]) +module = importlib.util.module_from_spec(spec) +spec.loader.exec_module(module) +home = pathlib.Path(sys.argv[2]) +module.write_inbox_pair(home, b"\xff\xd8\xff\xd9", "one.jpg", "image/jpeg", "jpeg") +module.write_inbox_pair(home, b"\xff\xd8\xff\xd9", "two.jpg", "image/jpeg", "jpeg") +PY + log=$home/bridge-serve.log + : > "$log" + FM_BRIDGE_VIEW_TEST=1 FM_BRIDGE_VIEW_LAUNCHCTL="$fakebin/launchctl" \ + PATH="$fakebin:$PATH" FM_HOME="$home" FM_ROOT_OVERRIDE="$fixture" \ + "$BRIDGE" serve --host "$HOST_NAME" --port 0 >"$log" 2>&1 & + BRIDGE_PIDS+=("$!") + port=$(wait_listening "$log") + cookie=$(bridge_cookie "$home" "$port") + hdr=$home/page.hdr; body=$home/page.body + curl_bridge "$port" / "$hdr" "$body" --header "Cookie: $cookie" + assert_contains "$(cat "$body")" "markBucketsUnreachable" \ + "glance client must render an honest unreachable bucket state" + assert_contains "$(cat "$body")" "let lastSuccess = 0" \ + "glance client must not treat page load as a successful observation" + hdr=$home/obs.hdr; body=$home/obs.body + started=$(python3 -c 'import time; print(time.monotonic())') + curl_bridge "$port" /api/observation "$hdr" "$body" --header "Cookie: $cookie" + elapsed=$(python3 -c "import time; print(time.monotonic() - $started)") + assert_contains "$(head -n 1 "$hdr")" "503" "broken snapshot must return 503: $(cat "$body")" + printf '%s' "$(cat "$body")" | jq -e '.photos_today == 2 and .error' >/dev/null \ + || fail "503 must keep today's photo count: $(cat "$body")" + python3 -c "import sys; sys.exit(0 if float(sys.argv[1]) < 8 else 1)" "$elapsed" \ + || fail "observation 503 hung ${elapsed}s: $(cat "$body")" + pass "observation errors fail fast, keep photo count, and ship unreachable client JS" +} + +test_scrubbed_snapshot_with_herdr_meta_fails_fast() { + local home output + home=$(make_home scrubbed-herdr) + mkdir -p "$home/projects/ship-wt" + fm_write_meta "$home/state/herdr-task.meta" \ + "window=default:w1:p2" \ + "worktree=$home/projects/ship-wt" \ + "project=firstmate" \ + "harness=codex" \ + "kind=ship" \ + "backend=herdr" + output=$(python3 - "$ROOT/bin/fm-bridge-view.py" "$home" "$ROOT" <<'PY' +import importlib.util, pathlib, sys, time +spec = importlib.util.spec_from_file_location("fm_bridge_view", sys.argv[1]) +module = importlib.util.module_from_spec(spec) +spec.loader.exec_module(module) +module.CHILD_PATH = module.BASE_CHILD_PATH +home = pathlib.Path(sys.argv[2]) +root = pathlib.Path(sys.argv[3]) +started = time.monotonic() +outcome = "ok" +try: + module.run_snapshot(home, root) +except RuntimeError as exc: + outcome = str(exc) +elapsed = time.monotonic() - started +if elapsed >= 8: + raise SystemExit("scrubbed snapshot hung %.2fs: %s" % (elapsed, outcome)) +if "timed out" in outcome: + raise SystemExit("scrubbed snapshot timed out: %s" % outcome) +print("elapsed=%.2f outcome=%s" % (elapsed, outcome.splitlines()[0][:120])) +PY + ) || fail "scrubbed herdr snapshot did not fail fast: $output" + pass "scrubbed-env snapshot with a herdr task succeeds or fails fast" +} + test_bind_is_loopback_constant test_funnel_on_refuses_to_serve test_funnel_off_serve_starts @@ -986,3 +1126,7 @@ test_unauthenticated_upload_rejected test_upload_rejects_oversize_non_image_and_rates test_inbox_unique_names_never_overwrite test_inbox_pair_failure_leaves_no_partial_upload +test_photos_received_today_counts_utc_sidecars +test_child_path_resolves_tool_dirs_without_hardcoded_nvm +test_observation_error_fails_fast_and_keeps_photo_count +test_scrubbed_snapshot_with_herdr_meta_fails_fast From 2875a8301ef1d0dd9a8603bd7e167c39f5a6548f Mon Sep 17 00:00:00 2001 From: Amplify Logic AI Date: Wed, 19 Aug 2026 19:23:18 +0200 Subject: [PATCH 2/3] no-mistakes(review): Exercise client error rendering with executable DOM test --- tests/fm-bridge-view.test.sh | 67 +++++++++++++++++++++++++++++++++--- 1 file changed, 62 insertions(+), 5 deletions(-) diff --git a/tests/fm-bridge-view.test.sh b/tests/fm-bridge-view.test.sh index 21eb805ed92..083912f8a52 100755 --- a/tests/fm-bridge-view.test.sh +++ b/tests/fm-bridge-view.test.sh @@ -1055,10 +1055,67 @@ PY cookie=$(bridge_cookie "$home" "$port") hdr=$home/page.hdr; body=$home/page.body curl_bridge "$port" / "$hdr" "$body" --header "Cookie: $cookie" - assert_contains "$(cat "$body")" "markBucketsUnreachable" \ - "glance client must render an honest unreachable bucket state" - assert_contains "$(cat "$body")" "let lastSuccess = 0" \ - "glance client must not treat page load as a successful observation" + node - "$body" <<'JS' || fail "initial observation error did not render unreachable state" +const fs = require('fs'); +const vm = require('vm'); + +const page = fs.readFileSync(process.argv[2], 'utf8'); +const match = page.match(/