Files
amethyst/cli/tests/cordn/stack.sh
T
Claude 1741077c4d test(cordn): put our MLS and theirs in one group
Tier B runs amy against amy through the reference coordinator. Both MLS
endpoints are ours, so the ratchet tree, the Welcome and the Commit only ever
agree with themselves — it proves the transport and the coordinator client and
nothing about RFC 9420 interop. `interop-client.sh` closes that: `@cordn/cli`
(ts-mls) on one end, amy (quartz) on the other, one group, live wire. That
half is MIT and comes from npm; only the coordinator underneath it carries the
licensing problem, and `stack.sh` now holds that warning in one place for both
harnesses.

Three directions, and the third is why it was worth building.

1. **Their group, our joiner.** Our engine opens a ts-mls Welcome and reads
   their GroupContext extensions, metadata and credentials out of it.
2. **Our group, their joiner.** Their engine opens OUR Welcome — the direction
   no fixture can test, because a fixture we wrote accepts what we emit by
   construction.
3. **Our later Commit.** Until here their epoch came from a Welcome, which
   carries the group state ready-made. This is the first time they must apply
   one of our handshake messages, and ours are public-framed (wireformat 2)
   where theirs are private-framed. `CordnGroupManager.invite` has asserted in
   its KDoc since it was written that their `processMessageBase64` admits
   both — a claim read off their source and never executed. It holds.

All of it passes, and the harness bites: sealing `result.commitBytes` instead
of `result.framedCommitBytes` fails direction 3 and the third-member join
while **leaving direction 2 green**, because a peer that joined by Welcome
never parses that Commit and only stalls once it has to. That is exactly why
direction 3 is its own case rather than a variation of 2, and it is now
demonstrated instead of argued.

`amy cordn invite` gained a `kp_ref` field on the way: the harness needs to
tell their client which Welcome to accept, and reporting it is right anyway —
a KeyPackage is one-time, so the invite names something the invitee can no
longer be invited with by anyone else.

One asymmetry found and deliberately left open: the reference client sends
kind 25910 **in the clear** where we pin `EncryptionMode.REQUIRED` and always
gift-wrap (§8.6). Both work, so nothing is broken — but the two clients
exercise different halves of CEP-4 against the same server, and our encrypted
path is the one with no second implementation behind it. That is a Tier D
vector exchange, not something this harness can settle.

`tier-b.sh` is refactored onto `stack.sh` rather than keeping a second copy of
the boot; re-run after the refactor and still green.

Note on the suite: `Nip46ConsentInfoBuilderTest` failed once mid-session and
has not reproduced — not in isolation, not in two full `./gradlew test` runs,
not in a `--rerun-tasks` rebuild of that module. Its inputs are constants and
its collaborator is injected, so there is no nondeterminism in the test
itself; the likeliest cause is a stale incremental artifact, the same failure
mode that hit `:commons:jvmTest` earlier today. Recording it rather than
calling it a flake, because the report was overwritten before I could read it
and I cannot prove which it was.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012BfD4txdnsaPRXmNXbup9n
2026-09-22 13:53:56 +00:00

101 lines
4.1 KiB
Bash

# shellcheck shell=bash
#
# stack.sh — boot the cordn test stack: a geode relay plus the REFERENCE
# coordinator in Docker. Sourced by tier-b.sh and interop-client.sh.
#
# ─────────────────────────────────────────────────────────────────────────────
# The reference coordinator (`ghcr.io/cordn-msg/cordn`, and the
# `packages/coordinator` / `packages/server` sources it is built from) ships
# with NO LICENSE — default copyright, all rights reserved. See §7 of
# quartz/plans/2026-09-17-cordn-interop.md.
#
# Nothing here is wired into a build: no Gradle task, no CI job, and nothing
# pulls the image for you. You pull it by hand having decided that is
# something you want to do. Do not add these scripts to a build file.
# ─────────────────────────────────────────────────────────────────────────────
#
# The caller sets WORK (a scratch directory) and may set PORT. After
# `stack_up`, these are exported:
#
# RELAY ws://127.0.0.1:$PORT
# COORD the coordinator's pubkey, read from its own startup log
#
# `stack_down` is registered by the caller's EXIT trap; KEEP=1 skips it.
ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../../.." && pwd)"
AMY="$ROOT/cli/build/install/amy/bin/amy"
GEODE="$ROOT/geode/build/install/geode/bin/geode"
IMAGE="ghcr.io/cordn-msg/cordn:latest"
PORT="${PORT:-7447}"
RELAY="ws://127.0.0.1:$PORT"
CONTAINER="${CONTAINER:-cordn-test}"
GEODE_PID=""
# Prereqs, each with its own message. A stopped daemon and an unpulled image
# both fail `docker image inspect`, and telling someone to pull an image they
# cannot pull sends them the wrong way.
stack_require() {
for f in "$AMY" "$GEODE"; do
[ -x "$f" ] || {
echo "missing $f — run ./gradlew :cli:installDist :geode:installDist"
exit 2
}
done
docker info >/dev/null 2>&1 || {
echo "the docker daemon is not reachable — start it (e.g. 'sudo dockerd &' or 'systemctl start docker') and retry"
exit 2
}
docker image inspect "$IMAGE" >/dev/null 2>&1 || {
echo "missing $IMAGE — pull it by hand, and read the licence note at the top of this file first"
exit 2
}
}
stack_up() {
"$GEODE" --port "$PORT" >"$WORK/geode.log" 2>&1 &
GEODE_PID=$!
for _ in $(seq 30); do
curl -sS --noproxy '*' -H 'Accept: application/nostr+json' "http://127.0.0.1:$PORT/" >/dev/null 2>&1 && break
sleep 1
done
# --network host so the container reaches a relay on the host's loopback.
# A stable key so the coordinator pubkey survives a re-run against the
# same WORK directory.
[ -f "$WORK/coordinator.key" ] || openssl rand -hex 32 >"$WORK/coordinator.key"
docker rm -f "$CONTAINER" >/dev/null 2>&1
docker run -d --name "$CONTAINER" --network host \
-e CORDN_STORAGE_BACKEND=memory \
-e CORDN_ANNOUNCED=false \
-e CORDN_RELAY_URLS="$RELAY" \
-e CORDN_SERVER_PRIVATE_KEY="$(cat "$WORK/coordinator.key")" \
-e CORDN_SERVER_NAME="cordn-test" \
"$IMAGE" >/dev/null || { echo "could not start $CONTAINER"; exit 1; }
# Read the pubkey out of its own startup log rather than deriving it: the
# coordinator is the authority on its identity, and a key we derived
# wrongly would fail later as an unreachable coordinator.
COORD=""
for _ in $(seq 60); do
COORD=$(docker logs "$CONTAINER" 2>&1 | grep -oE 'serverPubkey":"[0-9a-f]{64}' | head -1 | cut -d'"' -f3)
[ -n "$COORD" ] && break
sleep 1
done
[ -n "$COORD" ] || {
echo "coordinator never announced its pubkey"
docker logs "$CONTAINER" | tail -20
exit 1
}
}
stack_down() {
if [ "${KEEP:-0}" != "1" ]; then
docker rm -f "$CONTAINER" >/dev/null 2>&1
[ -n "$GEODE_PID" ] && kill "$GEODE_PID" 2>/dev/null
else
echo
echo "KEEP=1: relay on $RELAY, coordinator $CONTAINER ($COORD), state in $WORK"
fi
}