394 lines
23 KiB
Bash
Executable File
394 lines
23 KiB
Bash
Executable File
#!/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" <<WRAPPER
|
|
#!/usr/bin/env bash
|
|
for arg in "\$@"; do
|
|
if [ "\$arg" = init ]; then
|
|
touch "$TEST_AREA/git-ready"
|
|
while [ ! -e "$TEST_AREA/git-release" ]; do sleep 0.1; done
|
|
break
|
|
fi
|
|
done
|
|
exec /usr/bin/git "\$@"
|
|
WRAPPER
|
|
chmod 700 "$TEST_AREA/git-bin/git"
|
|
stack exec -- env "PATH=$TEST_AREA/git-bin:$PATH" "$REPO/scripts/mosaic" org project repo init alpha concurrent --local-only --accept-local-loss-risk >"$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'
|