#!/usr/bin/env bash # The organizations CLI E2E (business-design r4 section 7), Bootstrap to # Console, on a fresh instance `orge2e` in a scratch HOME. A real worker turn # runs through the real launcher and sandboxed runner against the # deterministic fixture model on loopback. A separate fake-proxy phase checks # shared keys and fencing; all endpoints are loopback and credentials are synthetic. # # Usage: scripts/e2e-orgs-cli.sh # # Bus, Console and human verbs run through the Lead-owned devstack launcher. # Scratch HOME and test artifacts are retained for inspection. The trap stops # only this run's processes and checks that its devstack units are gone. # A /proc sampler checks worker sockets, with the fixture as a positive control. set -euo pipefail REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" NAME=orge2e H="$(mktemp -d /mnt/storage/scratch/tmp/XXXXXX)" TEST_AREA="$H/area" mkdir -m 700 "$TEST_AREA" CONFIG="$H/.config/mosaic-$NAME/config.json" WATCH_PIDS=() SETUP_PID='' FIXTURE_UP=0 PROXY_PID='' DEVSTACK=/mnt/storage/scratch/mosaic-lead/uidev/org-release/devstack.sh export HOME="$H" MOSAIC_CONFIG="$CONFIG" TMPDIR="$H" RUN_ENV=(env -i "PATH=$REPO/scripts:$PATH" "HOME=$H" "MOSAIC_CONFIG=$CONFIG" "TMPDIR=$H" "LANG=C.UTF-8") stack() { "$DEVSTACK" "$@"; } mosaic() { stack exec -- "$REPO/scripts/mosaic" "$@"; } fixture() { "${RUN_ENV[@]}" "$REPO/scripts/mosaic" dev fixture "$@"; } cleanup() { local rc=$? trap - EXIT set +e if [ -n "$SETUP_PID" ]; then touch "$TEST_AREA/git-release"; kill -TERM "$SETUP_PID" 2>/dev/null; wait "$SETUP_PID" 2>/dev/null; fi for pid in "${WATCH_PIDS[@]}"; do kill -TERM "$pid" 2>/dev/null; wait "$pid" 2>/dev/null; done for name in alpha beta gamma; do mosaic bus stop "$name" >/dev/null 2>&1; done mosaic console stop >/dev/null 2>&1 stack stop >/dev/null 2>&1 if [ -n "$PROXY_PID" ]; then kill -TERM "$PROXY_PID" 2>/dev/null || rc=1; wait "$PROXY_PID" 2>/dev/null || rc=1; fi if [ "$FIXTURE_UP" = 1 ]; then fixture stop --json >"$TEST_AREA/fixture-stop.json" 2>&1 || rc=1; fi stack list >"$TEST_AREA/devstack-final.txt" 2>&1 || rc=1 if [ -s "$TEST_AREA/devstack-final.txt" ]; then cat "$TEST_AREA/devstack-final.txt"; rc=1; fi echo "scratch HOME kept: $H" if [ "$rc" -eq 0 ]; then echo "E2E PASS"; else echo "E2E FAIL (exit $rc)"; fi exit "$rc" } trap cleanup EXIT trap 'exit 130' INT TERM STEP="" step() { STEP="$1"; printf '\n== %s\n' "$1"; } fail() { echo "FAIL [$STEP]: $*" >&2; exit 1; } # check FILE JQ-FILTER DESCRIPTION: the filter must yield true. check() { jq -e "$2" "$1" >/dev/null || fail "$3 ($2 on $1)"; echo "ok: $3"; } # show NAME CMD...: run, keep stdout in $TEST_AREA/NAME, print it. show() { local out="$TEST_AREA/$1"; shift; "$@" >"$out" || fail "exit $? from: $*"; cat "$out"; } # netwatch ROOT OUT: until ROOT exits, every 5 ms read /proc for ROOT and its # descendants, map their socket fds to /proc/net/{tcp,tcp6,udp,udp6} by inode, # and write each connected socket once, as "pid comm proto local -> peer", to # OUT.loopback (peer on loopback) or OUT.remote (any other peer). OUT.polls # counts samples, OUT.pids lists every pid seen in the tree, OUT.sockets every # socket inode seen, OUT.fderr fd directories that could not be read (ENOENT # is a process that exited mid-sample; EACCES would be a blind spot). Listening and # unconnected sockets are skipped. A connection opened and closed between two # samples is missed: this is a check with a positive control (the worker's # socket to the fixture port must show up), not a proof. NETWATCH_JS=' import fs from "node:fs"; const [root, out] = process.argv.slice(1); const seen = new Set(), pids = new Set(), fderr = new Map(), socks = new Set(); let polls = 0; const v4 = (h) => [3, 2, 1, 0].map((i) => parseInt(h.slice(i * 2, i * 2 + 2), 16)).join("."); const addr = (hex) => { const [h, port] = hex.split(":"); const p = parseInt(port, 16); if (h.length === 8) return { ip: v4(h), p }; const words = [0, 1, 2, 3].map((w) => h.slice(w * 8, w * 8 + 8)); if (words[0] === "00000000" && words[1] === "00000000" && words[2] === "FFFF0000") return { ip: v4(words[3]), p }; if (h === "00000000000000000000000001000000") return { ip: "::1", p }; if (/^0+$/.test(h)) return { ip: "::", p }; return { ip: "[" + words.map((w) => w.match(/../g).reverse().join("")).join("") + "]", p }; }; const loop = (ip) => ip.startsWith("127.") || ip === "::1"; const unset = (ip) => ip === "0.0.0.0" || ip === "::"; const read = (f) => { try { return fs.readFileSync(f, "utf8"); } catch { return ""; } }; const tick = () => { polls++; // Walk down from ROOT through the children list of every thread: cheaper than // reading the stat of every process on the host each sample. const tree = [], comm = new Map(), queue = [String(root)]; while (queue.length) { const pid = queue.shift(); let tasks = []; try { tasks = fs.readdirSync(`/proc/${pid}/task`); } catch { continue; } tree.push(pid); const st = read(`/proc/${pid}/stat`); comm.set(pid, st.slice(st.indexOf("(") + 1, st.lastIndexOf(")"))); for (const t of tasks) for (const c of read(`/proc/${pid}/task/${t}/children`).split(" ")) if (c.trim()) queue.push(c.trim()); } const owner = new Map(); for (const pid of tree) { pids.add(pid); let fds = []; try { fds = fs.readdirSync(`/proc/${pid}/fd`); } catch (e) { fderr.set(`${pid} ${comm.get(pid)}`, e.code); continue; } for (const fd of fds) { let l = ""; try { l = fs.readlinkSync(`/proc/${pid}/fd/${fd}`); } catch { continue; } const m = /^socket:\[(\d+)\]$/.exec(l); if (m) { owner.set(m[1], pid); socks.add(`${pid} ${comm.get(pid)} ${m[1]}`); } } } if (owner.size === 0) return; for (const proto of ["tcp", "tcp6", "udp", "udp6"]) { for (const line of read(`/proc/net/${proto}`).split("\n").slice(1)) { const f = line.trim().split(/\s+/); if (f.length < 10 || !owner.has(f[9])) continue; const l = addr(f[1]), r = addr(f[2]); if (unset(r.ip) && r.p === 0) continue; const pid = owner.get(f[9]); const text = `${pid} ${comm.get(pid)} ${proto} ${l.ip}:${l.p} -> ${r.ip}:${r.p}`; if (seen.has(text)) continue; seen.add(text); fs.appendFileSync(`${out}.${loop(r.ip) ? "loopback" : "remote"}`, text + "\n"); } } }; fs.writeFileSync(`${out}.loopback`, ""); fs.writeFileSync(`${out}.remote`, ""); const done = () => { fs.writeFileSync(`${out}.polls`, String(polls)); fs.writeFileSync(`${out}.pids`, [...pids].join("\n") + "\n"); fs.writeFileSync(`${out}.fderr`, [...fderr].map(([k, v]) => `${k} ${v}`).join("\n") + (fderr.size ? "\n" : "")); fs.writeFileSync(`${out}.sockets`, [...socks].join("\n") + (socks.size ? "\n" : "")); }; process.on("SIGTERM", () => { done(); process.exit(0); }); while (fs.existsSync(`/proc/${root}`)) { tick(); await new Promise(resolve => setTimeout(resolve, 5)); } done(); ' netwatch() { exec "${RUN_ENV[@]}" node --input-type=module -e "$NETWATCH_JS" "$1" "$2"; } echo "scratch HOME: $H" echo "instance: $NAME" echo "env passed to every command: ${RUN_ENV[*]:2}" | sed -E 's/(PATH=)[^ ]*/\1.../' held="$(env | cut -d= -f1 | /usr/bin/grep -E '^(ANTHROPIC_|OPENAI_|OPENROUTER_|ZAI_|GEMINI_|GOOGLE_API|CLAUDE_CODE_OAUTH)' | tr '\n' ' ' || true)" [ -n "$held" ] && echo "caller provider variables not passed (names only): $held" step "Bootstrap" [ -e "$H/.config/mosaic-$NAME" ] && fail "instance $NAME already exists" "${RUN_ENV[@]}" "$REPO/scripts/bootstrap.sh" --instance "$NAME" | tee "$TEST_AREA/bootstrap.out" [ -f "$CONFIG" ] || fail "no config at $CONFIG" DATA_ROOT="$(jq -r .dataRoot "$CONFIG")" echo "config: $CONFIG, dataRoot: $DATA_ROOT" [ "$DATA_ROOT" = "$H/.mosaic-$NAME" ] || fail "data root $DATA_ROOT is not $H/.mosaic-$NAME" check "$DATA_ROOT/instance.json" ".instance == \"$NAME\" or .name == \"$NAME\"" "marker names instance $NAME" echo "ok: instance identity read from config and marker" step "Fixture" show fixture-init.json fixture init --instance "$NAME" --model deterministic --json FIXTURE_UP=1 check "$TEST_AREA/fixture-init.json" '.receipt == "dev-fixture-init" and .loopback == true and .external == false and .upstream == "none"' "fixture receipt: loopback, no external upstream" check "$TEST_AREA/fixture-init.json" '.endpoint | test("^http://127\\.0\\.0\\.1:[0-9]+$")' "endpoint on 127.0.0.1, OS-assigned port" check "$TEST_AREA/fixture-init.json" '.principal == "local-dev:local-dev-owner"' "test principal is local-dev" show fixture-status.json fixture status --json check "$TEST_AREA/fixture-status.json" '.ready == true and .upstream == "none" and .external == false and .health.upstream == "none"' "status ready, upstream none" org_setup() { local id="$1" doc="$TEST_AREA/$1.json" step "$id document" mosaic org template render "$id" --fixture local --output "$doc" | tee "$TEST_AREA/$id-render.out" if [ "$id" = alpha ]; then jq '.projects.concurrent = (.projects.work | .root = "/__mosaic_org__/alpha/projects/concurrent")' "$doc" >"$doc.new" mv "$doc.new" "$doc" fi check "$doc" ".id == \"$id\" and (.projects.work | type) == \"object\"" "$id document names project work" if /usr/bin/grep -Eiq '(api[_-]?key|token|secret|password)"[[:space:]]*:' "$doc"; then fail "$doc carries a secret-looking key"; fi echo "ok: no secret-looking keys in $doc" mosaic org validate "$doc" | tee "$TEST_AREA/$id-validate.out" step "$id create" mosaic org create "$id" --from "$doc" | tee "$TEST_AREA/$id-create.out" show "$id-show.json" mosaic org show "$id" --json check "$TEST_AREA/$id-show.json" ".id == \"$id\"" "org show reads back $id" check "$TEST_AREA/$id-create.out" '.notifications == "none"' "$id receipt records notifications: none" check "$(jq -r .root "$TEST_AREA/$id-show.json")/notify/$id/notify.json" '.notifyVersion == 1 and .binding == null' "$id notifier records explicit null" step "$id Git" mosaic org queue init "$id" --local-only --accept-local-loss-risk | tee "$TEST_AREA/$id-queue-init.out" mosaic org project repo init "$id" work --local-only --accept-local-loss-risk | tee "$TEST_AREA/$id-repo-init.out" show "$id-queue.json" mosaic org queue show "$id" --json show "$id-repo.json" mosaic org project repo show "$id" work --json local root; root="$(jq -r .root "$TEST_AREA/$id-show.json")" check "$TEST_AREA/$id-queue.json" ".org == \"$id\" and .mode == \"local-only\" and .riskAccepted == true and .root == \"$root/checkout\"" "$id queue: local-only, risk accepted, root under the org root" check "$TEST_AREA/$id-repo.json" ".org == \"$id\" and .project == \"work\" and .mode == \"local-only\" and .riskAccepted == true and .root == \"$root/projects/work\"" "$id work repo: local-only, risk accepted, root derived from $id and work" local kind f dir for kind in queue repo; do f="$TEST_AREA/$id-$kind.json"; dir="$(jq -r .root "$f")" [ "$(git -C "$dir" rev-parse HEAD)" = "$(jq -r .commit "$f")" ] || fail "$dir HEAD is not the recorded commit" [ -z "$(git -C "$dir" remote)" ] || fail "$dir has a remote" echo "ok: $dir is a Git checkout at the recorded commit with no remote" done } # A unit owns each detached child until its matching CLI stop completes. cat >"$TEST_AREA/unit.sh" <<'UNIT' #!/usr/bin/env bash set -euo pipefail kind=$1; name=$2; out=$3 if [ "$kind" = bus ]; then "$REPO_ROOT/scripts/mosaic" bus start "$name" --dev-local-owner --detach >"$out" else "$REPO_ROOT/scripts/mosaic" console start --dev-local-owner --detach --port auto >"$out" fi pid=$(jq -r .pid "$out") while kill -0 "$pid" 2>/dev/null; do sleep 0.2; done UNIT chmod 700 "$TEST_AREA/unit.sh" # devstack passes its own small environment, so put the checkout path in the # unit script itself rather than depending on a caller's environment. sed -i "s|\$REPO_ROOT|$REPO|g" "$TEST_AREA/unit.sh" start_unit() { local name=$1 kind=$2 org=$3 out="$TEST_AREA/$1-start.json" rm -f "$out" stack run "$name" -- "$TEST_AREA/unit.sh" "$kind" "$org" "$out" for _ in $(seq 1 150); do [ -s "$out" ] && jq -e '.pid > 1' "$out" >/dev/null 2>&1 && return; sleep 0.2; done stack logs "$name"; fail "$name did not start" } org_setup alpha org_setup beta step "distinct organization roots" check "$TEST_AREA/beta-show.json" ".root != \"$(jq -r .root "$TEST_AREA/alpha-show.json")\"" "beta root differs from alpha" check "$TEST_AREA/beta-repo.json" ".root != \"$(jq -r .root "$TEST_AREA/alpha-repo.json")\"" "beta project root differs from alpha" start_unit alpha bus alpha start_unit beta bus beta for id in alpha beta; do show "$id-status.json" mosaic bus status "$id" --json check "$TEST_AREA/$id-status.json" ".org == \"$id\" and .instance == \"$NAME\" and .healthy == true and .pid > 1" "$id bus identity and health" pid=$(jq -r .pid "$TEST_AREA/$id-status.json") netwatch "$pid" "$TEST_AREA/netwatch-$id" & WATCH_PIDS+=("$!") done check "$TEST_AREA/beta-status.json" ".root != \"$(jq -r .root "$TEST_AREA/alpha-status.json")\" and .socket != \"$(jq -r .socket "$TEST_AREA/alpha-status.json")\"" "both buses have distinct roots and sockets" start_unit console console console check "$TEST_AREA/console-start.json" '.pid > 1 and (.url | test("^http://127\\.0\\.0\\.1:[0-9]+"))' "Console started on loopback" CONSOLE_URL=$(jq -r .url "$TEST_AREA/console-start.json") curl --noproxy '*' -fsS "${CONSOLE_URL%/}/api/orgs" >"$TEST_AREA/console-orgs.json" check "$TEST_AREA/console-orgs.json" '([.[] | select(.id == "alpha" and .busHealthy == true)] | length) == 1 and ([.[] | select(.id == "beta" and .busHealthy == true)] | length) == 1' 'Console lists both healthy organizations' for id in alpha beta; do curl --noproxy '*' -fsS "${CONSOLE_URL%/}/api/orgs/$id/business" >"$TEST_AREA/console-$id-business.json" check "$TEST_AREA/console-$id-business.json" ".business.id == \"$id\"" "Console scoped business read returns $id" done step 'broker document write remains visible to the organization service and Console' mkdir -m 700 "$TEST_AREA/git-bin" cat >"$TEST_AREA/git-bin/git" <"$TEST_AREA/concurrent-setup.out" 2>&1 & SETUP_PID=$! for _ in $(seq 1 150); do [ -e "$TEST_AREA/git-ready" ] && break; kill -0 "$SETUP_PID" 2>/dev/null || fail 'concurrent setup ended before git init'; sleep 0.1; done [ -e "$TEST_AREA/git-ready" ] || fail 'concurrent setup did not reach git init' show alpha-plugins-before.json mosaic plugins --org alpha --json jq '{revision, plugins: {channel: ["cli"]}}' "$TEST_AREA/alpha-plugins-before.json" >"$TEST_AREA/alpha-plugins-patch.json" show alpha-plugins-set.json mosaic plugins set --file "$TEST_AREA/alpha-plugins-patch.json" --org alpha touch "$TEST_AREA/git-release" if wait "$SETUP_PID"; then fail 'concurrent setup published a stale document'; fi SETUP_PID='' /usr/bin/grep -q 'revision-conflict' "$TEST_AREA/concurrent-setup.out" || fail 'concurrent setup did not report a revision conflict' echo 'ok: setup refused its stale document after the broker write' show alpha-after-write.json mosaic org show alpha --json check "$TEST_AREA/alpha-after-write.json" '.doc.plugins.channel == ["cli"]' 'organization service reads broker change' curl --noproxy '*' -fsS "${CONSOLE_URL%/}/api/orgs/alpha/business" >"$TEST_AREA/console-alpha-after-write.json" check "$TEST_AREA/console-alpha-after-write.json" '.business.id == "alpha"' 'Console resolves alpha after broker change' worker() { local id=$1 step "$id worker" show "$id-project.out" mosaic project create work --repo work --org "$id" show "$id-thread.out" mosaic thread create work trial --org "$id" show "$id-talk.out" mosaic talk --thread work/trial 'Reply with fixture word' --wait 60 --org "$id" show "$id-thread-read.json" mosaic thread read work/trial --org "$id" --json /usr/bin/grep -q PINEAPPLE "$TEST_AREA/$id-thread-read.json" || fail "$id thread has no fixture reply" show "$id-evidence.json" mosaic run evidence "$id" --thread work/trial --verify --json check "$TEST_AREA/$id-evidence.json" ".verified == true and .org.id == \"$id\" and (.runs | length) >= 1" "$id evidence verified" check "$TEST_AREA/$id-evidence.json" '.runs[0].launch.model == "fixture-deterministic" and .runs[0].launch.harness == "pi" and ([.runs[0].snapshot.files[].ok] | all)' "$id fixture launch and bundle hashes" } worker alpha worker beta show fixture-status-after.json fixture status --json check "$TEST_AREA/fixture-status-after.json" '.health.counts.messages >= 2' "fixture answered both workers" step "clean stop and restart alpha" show alpha-stop.json mosaic bus stop alpha check "$TEST_AREA/alpha-stop.json" '.stopped == true' "alpha stopped cleanly" wait "${WATCH_PIDS[0]}" || true for _ in $(seq 1 150); do if ! stack list | /usr/bin/grep -q 'orgdev-orge2e-alpha\.service'; then break; fi sleep 0.2 done if stack list | /usr/bin/grep -q 'orgdev-orge2e-alpha\.service'; then fail 'alpha devstack unit did not clear after stop'; fi start_unit alpha bus alpha show alpha-restarted.json mosaic bus status alpha --json check "$TEST_AREA/alpha-restarted.json" ".healthy == true and .pid != $(jq -r .pid "$TEST_AREA/alpha-status.json")" "alpha restarted under a new host" show beta-still.json mosaic bus status beta --json check "$TEST_AREA/beta-still.json" ".healthy == true and .pid == $(jq -r .pid "$TEST_AREA/beta-status.json")" "beta stayed healthy" for id in alpha beta; do [ "$id" = beta ] && { kill -TERM "${WATCH_PIDS[1]}"; wait "${WATCH_PIDS[1]}" || true; } watch="$TEST_AREA/netwatch-$id" [ -f "$watch.fderr" ] || fail "$id sampler did not finish" /usr/bin/grep -v ' ENOENT$' "$watch.fderr" && fail "$id sampler could not read process fds" [ ! -s "$watch.remote" ] || fail "$id sampler saw non-loopback peer" port=$(jq -r .port "$TEST_AREA/fixture-init.json") /usr/bin/grep -Eq -- "-> 127\\.0\\.0\\.1:$port\$" "$watch.loopback" || fail "$id sampler missed fixture positive control" echo "ok: $id socket sampler saw fixture and no remote peers" done WATCH_PIDS=() step "shared fake proxy" # The model fixture and shared proxy are intentionally separate phases. for id in alpha beta; do mosaic bus stop "$id" >/dev/null; done for id in alpha beta; do thread_id=$(jq -r .thread.id "$TEST_AREA/$id-evidence.json") show "$id-evidence-stopped.json" mosaic run evidence "$id" --thread "$thread_id" --verify --json check "$TEST_AREA/$id-evidence-stopped.json" ".verified == true and .org.id == \"$id\" and (.runs | length) >= 1" "$id evidence remains verified after bus stop" done mosaic console stop >/dev/null stack stop fixture stop --json >"$TEST_AREA/fixture-stop.json" FIXTURE_UP=0 start_proxy() { rm -f "$TEST_AREA/proxy-ready.json" "${RUN_ENV[@]}" node "$REPO/scripts/e2e-orgs-proxy.mjs" "$TEST_AREA" "${1:-0}" >"$TEST_AREA/proxy.log" 2>&1 & PROXY_PID=$! for _ in $(seq 1 100); do [ -s "$TEST_AREA/proxy-ready.json" ] && return; kill -0 "$PROXY_PID" 2>/dev/null || { cat "$TEST_AREA/proxy.log"; fail "fake proxy stopped"; }; sleep 0.1; done fail "fake proxy did not start" } start_proxy PROXY_PORT=$(jq -r .port "$TEST_AREA/proxy-ready.json") mkdir -m 700 "$DATA_ROOT/proxy" mkdir -p -m 700 "$(dirname "$CONFIG")/secrets" printf '%s' fixture-management >"$(dirname "$CONFIG")/secrets/proxy-management.key" chmod 600 "$(dirname "$CONFIG")/secrets/proxy-management.key" PROXY_VERSION=$(jq -r .version "$REPO/packages/model-proxy/PIN") jq -n --argjson port "$PROXY_PORT" --arg binary "$DATA_ROOT/opt/cliproxyapi/$PROXY_VERSION/cli-proxy-api" '{port:$port,binary:$binary}' >"$(dirname "$CONFIG")/proxy.json" chmod 600 "$(dirname "$CONFIG")/proxy.json" start_unit alpha bus alpha show alpha-proxy-status.json mosaic bus status alpha --json check "$TEST_AREA/alpha-proxy-status.json" '.healthy == true' 'alpha bus with shared proxy is healthy' show alpha-proxy-thread.out mosaic thread create work proxytrial --org alpha show alpha-proxy-talk.out mosaic talk --thread work/proxytrial 'Reply with fixture word' --wait 60 --org alpha show alpha-proxy-read.json mosaic thread read work/proxytrial --org alpha --json /usr/bin/grep -q PINEAPPLE "$TEST_AREA/alpha-proxy-read.json" || fail "alpha proxy worker did not answer" curl --noproxy "*" -fsS "http://127.0.0.1:$PROXY_PORT/test/alpha-key" >"$TEST_AREA/key-before.json" check "$TEST_AREA/key-before.json" '.accepted == true and .alphaKeys == 1' 'alpha current proxy key is accepted' start_unit beta bus beta show beta-proxy-status.json mosaic bus status beta --json check "$TEST_AREA/beta-proxy-status.json" '.healthy == true' 'beta bus with shared proxy is healthy' curl --noproxy "*" -fsS "http://127.0.0.1:$PROXY_PORT/test/alpha-key" >"$TEST_AREA/key-after.json" check "$TEST_AREA/key-after.json" ".accepted == true and .betaSentinels == 1 and .keyId == \"$(jq -r .keyId "$TEST_AREA/key-before.json")\"" 'alpha current key survives beta registration' org_setup gamma : >"$TEST_AREA/delay-put" start_unit gamma bus gamma show fence-set.json mosaic proxy fence status --json check "$TEST_AREA/fence-set.json" '.uncertain.org == "gamma" and .uncertain.op == "sentinel.register"' 'timed-out PUT leaves durable fence' show alpha-fenced-thread.out mosaic thread create work fenced --org alpha show alpha-fenced-talk.out mosaic talk --thread work/fenced 'This launch must be fenced' --wait 0 --org alpha alpha_log="$(jq -r .root "$TEST_AREA/alpha-show.json")/launches/alpha.jsonl" for _ in $(seq 1 100); do /usr/bin/grep -q '"code":"proxy-uncertain"' "$alpha_log" && break; sleep 0.1; done /usr/bin/grep -q '"code":"proxy-uncertain"' "$alpha_log" || fail 'alpha launch did not refuse the proxy fence' echo 'ok: alpha proxy-dependent launch refused while fenced' if mosaic proxy fence clear >"$TEST_AREA/fence-refusal.out" 2>&1; then fail 'fence cleared without restart statement'; fi /usr/bin/grep -q 'proxy-restart-required' "$TEST_AREA/fence-refusal.out" || fail 'expected restart-required refusal' kill -TERM "$PROXY_PID"; wait "$PROXY_PID"; PROXY_PID='' rm "$TEST_AREA/delay-put" start_proxy "$PROXY_PORT" show fence-clear.json mosaic proxy fence clear --proxy-restarted check "$TEST_AREA/fence-clear.json" '.cleared == true' 'fence cleared after proxy restart' show fence-final.json mosaic proxy fence status --json check "$TEST_AREA/fence-final.json" '.uncertain == null' 'fence status clear' mosaic bus stop gamma >/dev/null start_unit gamma bus gamma curl --noproxy "*" -fsS "http://127.0.0.1:$PROXY_PORT/test/alpha-key" >"$TEST_AREA/key-after-recovery.json" check "$TEST_AREA/key-after-recovery.json" ".accepted == true and .betaSentinels == 1 and .gammaSentinels == 1 and .keyId == \"$(jq -r .keyId "$TEST_AREA/key-before.json")\"" 'recovery registers gamma and preserves alpha key and beta sentinel'