From 1da75e986e645843489a889305907c46b5a05557 Mon Sep 17 00:00:00 2001 From: John Coffey Date: Sat, 19 Sep 2026 20:21:05 -0700 Subject: [PATCH] Compat: run them from a copy, and keep our listeners out of the way Rehearsed the run against a real RocksDB store, and it died at startup before checking anything: the harness inserts listeners of its own, the registry keys them by name, and a real server already has a "jmap" and an "imap". The message was "Primary key conflict on property name with existing object NetworkListener", which says nothing about what to do. Under NO_INSERT the harness now calls its listeners compat-jmap and so on, and the same run gets through to the test's own checks. run-compat.sh copies the store for each test and removes the copy after, because several of these write to what they open: monitoring_compat purges the history it reads and undelete_compat restores what it finds. The source stays untouched, which matters when it is the only copy of a production store anyone took that day. INBUXA runs RocksDB, so a copy is a directory copy. The SQL backends would need more than this: the harness builds its own container and connects to fixed local credentials, so it cannot open a dump in place. --- docs/spec/compat-tests.md | 29 ++++++++++++++ tests/src/utils/server.rs | 14 ++++++- tools/fork/README.md | 10 +++++ tools/fork/run-compat.sh | 83 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 135 insertions(+), 1 deletion(-) create mode 100755 tools/fork/run-compat.sh diff --git a/docs/spec/compat-tests.md b/docs/spec/compat-tests.md index bf853f4..66c7a0f 100644 --- a/docs/spec/compat-tests.md +++ b/docs/spec/compat-tests.md @@ -78,6 +78,35 @@ It was exercised on 2026-09-19 against the fork's own test server, which answers the same JMAP: it recorded 3 tenants with their members, 8 masked addresses and 3 archived items, in the shapes above. +## Running them + +`tools/fork/run-compat.sh` copies the store for each test and runs it: + +``` +tools/fork/run-compat.sh --store /srv/inbuxa-copy/rocks.db \ + --admin 'admin@example.org:PASSWORD' --recordings ~/compat +``` + +It takes `--only ` for one, and `--keep` to leave each copy +behind for a post-mortem. A test whose recording is missing is skipped by +name rather than run against nothing. + +**The source copy has to be pristine.** Several of these write to the store +they open — `monitoring_compat` purges the history it reads, +`undelete_compat` restores what it finds — and the harness adds its own +listeners on the way in, so a copy that a run has already touched is not a +copy of INBUXA's data any more. The script copies from the source for each +test and removes the copy afterwards, so the source stays clean; take it +from a stopped server or a snapshot, never from under a running one. + +**Why `compat-` listeners appear in the copy.** The harness needs listeners +on its own ports, and the registry keys listeners by name. A real server +has its own, and a production listener called `jmap` or `imap` collided +with the harness's, killing every compat test at startup with `Primary key +conflict on property "name"` before it checked anything. Under `NO_INSERT` +the harness now names its listeners `compat-jmap` and so on +(`tests/src/utils/server.rs`), out of the way of whatever the copy holds. + ## Running one ``` diff --git a/tests/src/utils/server.rs b/tests/src/utils/server.rs index ec843d5..c575c6f 100644 --- a/tests/src/utils/server.rs +++ b/tests/src/utils/server.rs @@ -200,11 +200,23 @@ impl TestServerBuilder { if protocol == NetworkListenerProtocol::Http { self.http_listener_port = port; } + // inbuxa: with NO_INSERT the store is a copy of a real server's, and + // that server has listeners of its own. A production one called + // "jmap" or "imap" would collide with ours on the name, which is the + // registry's primary key, and every compat test would die here + // before it checked anything. Namespace ours out of its way. + // The copy must still be pristine: a second run over a copy the + // first one wrote to collides with `compat-` instead. + let name = if std::env::var("NO_INSERT").is_ok() { + format!("compat-{name}") + } else { + name.to_string() + }; self.insert_object(NetworkListener { bind: Map::new(vec![ SocketAddr::from_str(&format!("0.0.0.0:{port}")).unwrap(), ]), - name: name.to_string(), + name, protocol, use_tls: true, tls_implicit, diff --git a/tools/fork/README.md b/tools/fork/README.md index 81b2118..ee2c770 100644 --- a/tools/fork/README.md +++ b/tools/fork/README.md @@ -32,3 +32,13 @@ tools/fork/record-compat.py --server https://mail.example.org \ --admin 'admin@example.org:PASSWORD' --out ./compat \ --tenant-admin 'tenant-admin@example.org:PASSWORD' ``` + +## run-compat.sh + +Runs the `*_compat` tests against a copy of INBUXA's RocksDB store, making +a fresh copy for each one. See `docs/spec/compat-tests.md`. + +```bash +tools/fork/run-compat.sh --store /srv/inbuxa-copy/rocks.db \ + --admin 'admin@example.org:PASSWORD' --recordings ~/compat +``` diff --git a/tools/fork/run-compat.sh b/tools/fork/run-compat.sh new file mode 100755 index 0000000..5c3df47 --- /dev/null +++ b/tools/fork/run-compat.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: 2026 Coffey Labs +# SPDX-License-Identifier: AGPL-3.0-only +# +# Run the compat tests against a copy of INBUXA's RocksDB store. +# +# tools/fork/run-compat.sh --store /srv/inbuxa-copy/rocks.db \ +# --admin 'admin@example.org:PASSWORD' --recordings ~/compat +# +# Each test opens $TMPDIR//rocks.db and several of them write: +# monitoring_compat purges the history it reads, undelete_compat restores +# what it finds. So every test gets its own copy of the store, made fresh +# here and removed after, and the source is only ever read. +# +# The source must be a copy already: take it from a stopped server or a +# snapshot, never from under a running one, and never point this at the +# live store (docs/spec/compat-tests.md). +set -u + +REPO=$(cd "$(dirname "$0")/../.." && pwd) +STORE= ADMIN= RECORDINGS= ONLY= KEEP=no +while [ $# -gt 0 ]; do + case $1 in + --store) STORE=$2; shift 2 ;; + --admin) ADMIN=$2; shift 2 ;; + --recordings) RECORDINGS=$2; shift 2 ;; # where record-compat.py wrote its files + --only) ONLY=$2; shift 2 ;; # one test name, e.g. tenant_compat + --keep) KEEP=yes; shift ;; # leave each copy behind for a post-mortem + *) echo "run-compat: unknown argument $1" >&2; exit 2 ;; + esac +done +[ -n "$STORE" ] && [ -n "$ADMIN" ] || { + echo "run-compat: --store and --admin are required" >&2; exit 2; } +[ -d "$STORE" ] || { echo "run-compat: $STORE is not a directory" >&2; exit 2; } + +# name | full path | what it needs beyond the store +TESTS=( + "tenant_compat|system::tenant::tenant_compat|INBUXA_COMPAT_EXPECTED=expected.json" + "branding_compat|system::branding::branding_compat|" + "ai_compat|system::ai::ai_compat|" + "monitoring_compat|system::monitoring::monitoring_compat|" + "scim_compat|scim::scim_compat|" + "per_domain_directory_compat|directory::per_domain::per_domain_directory_compat|" + "masked_email_compat|system::masked_email::masked_email_compat|INBUXA_COMPAT_MASKS=masks.json" + "undelete_compat|system::undelete::undelete_compat|INBUXA_COMPAT_ARCHIVED=archived.json" +) + +BASE="$REPO/target/tmp/compat" +mkdir -p "$BASE" +failed=0 ran=0 + +for entry in "${TESTS[@]}"; do + IFS='|' read -r name path needs <<< "$entry" + [ -n "$ONLY" ] && [ "$ONLY" != "$name" ] && continue + + env_extra=() + if [ -n "$needs" ]; then + variable=${needs%%=*}; file=${needs#*=} + if [ -z "$RECORDINGS" ] || [ ! -f "$RECORDINGS/$file" ]; then + echo "SKIP $name: needs $variable ($file from record-compat.py)" + continue + fi + env_extra=("$variable=$RECORDINGS/$file") + fi + + dir="$BASE/$name" + rm -rf "$dir"; mkdir -p "$dir" + cp -a "$STORE" "$dir/rocks.db" || { echo "run-compat: copying $STORE failed" >&2; exit 2; } + + echo "=== $name ($(date +%H:%M:%S)) ===" + ( cd "$REPO" && env CARGO_TARGET_DIR=target TMPDIR="$BASE" RUST_MIN_STACK=8388608 \ + STORE=RocksDb NO_INSERT=1 INBUXA_COMPAT_ADMIN="$ADMIN" "${env_extra[@]}" \ + cargo test -p tests "$path" -- --exact --ignored --nocapture ) + rc=$? + ran=$((ran + 1)) + [ $rc -ne 0 ] && { failed=$((failed + 1)); echo "FAILED $name (exit $rc)"; } + [ "$KEEP" = no ] && rm -rf "$dir" +done + +echo +echo "ran $ran, failed $failed" +[ $failed -eq 0 ] || echo "A failure here is a cutover blocker unless compat-tests.md says otherwise." +exit $((failed > 0))