carlos / platform Public

ci-speed

Squashed from ci-speed into main.
claude agent, operated by paul@keymail.dev d9149a01cc9d8bfdb3822ccf5dcb526b45cb41f1 parent 771ca01
6 files changed, +345 −14
  • .amadan/ci.d/10-gate +14 −9
  • .amadan/test-gate.sh +254 −0
  • .amadan/test-weights.sh +37 −0
  • .amadan/test-weights.tsv +28 −0
  • AGENTS.md +7 −1
  • Makefile +5 −4

CI passed — run details

CI log
=== gate ===
.amadan/test-gate.sh
=== launchctx (0 s)
ok  	amadan.net/carlos/platform.git/launchctx	0.016s
?   	amadan.net/carlos/platform.git/launchctx/controlname	[no test files]
?   	amadan.net/carlos/platform.git/launchctx/wireid	[no test files]
=== vet (1 s)
=== gofmt (1 s)
gofmt -l .: clean
=== rest (252 s)
?   	github.com/carlosframework/platform/cmd/changelog	[no test files]
ok  	github.com/carlosframework/platform/cmd/console	3.663s
ok  	github.com/carlosframework/platform/cmd/moveprobe	93.733s
ok  	github.com/carlosframework/platform/cmd/zddprobe	0.707s
ok  	github.com/carlosframework/platform/internal/activator	249.688s
ok  	github.com/carlosframework/platform/internal/agentsock	0.029s
ok  	github.com/carlosframework/platform/internal/alerting	0.005s
ok  	github.com/carlosframework/platform/internal/analytics	0.032s
ok  	github.com/carlosframework/platform/internal/appconfig	2.862s
ok  	github.com/carlosframework/platform/internal/apperr	0.143s
ok  	github.com/carlosframework/platform/internal/applog	0.235s
ok  	github.com/carlosframework/platform/internal/awsinv	0.004s
ok  	github.com/carlosframework/platform/internal/boxchoice	0.008s
ok  	github.com/carlosframework/platform/internal/boxid	0.681s
?   	github.com/carlosframework/platform/internal/boxid/boxidtest	[no test files]
?   	github.com/carlosframework/platform/internal/boxid/internal/signtest	[no test files]
ok  	github.com/carlosframework/platform/internal/boxvolumes	0.010s
ok  	github.com/carlosframework/platform/internal/caps	0.032s
ok  	github.com/carlosframework/platform/internal/certwarm	0.012s
ok  	github.com/carlosframework/platform/internal/cgroup	0.745s
ok  	github.com/carlosframework/platform/internal/changelog	0.478s
ok  	github.com/carlosframework/platform/internal/claimactivation	1.172s
ok  	github.com/carlosframework/platform/internal/clamd	0.066s
?   	github.com/carlosframework/platform/internal/cliflag	[no test files]
?   	github.com/carlosframework/platform/internal/controlname	[no test files]
?   	github.com/carlosframework/platform/internal/controlsock	[no test files]
ok  	github.com/carlosframework/platform/internal/dbdoor	5.775s
ok  	github.com/carlosframework/platform/internal/dbdoor/dbdoortest	0.014s
ok  	github.com/carlosframework/platform/internal/dbverify	0.004s
ok  	github.com/carlosframework/platform/internal/dnsrec	0.004s
ok  	github.com/carlosframework/platform/internal/docsite	0.006s
ok  	github.com/carlosframework/platform/internal/domains	2.833s
ok  	github.com/carlosframework/platform/internal/doorkey	0.006s
ok  	github.com/carlosframework/platform/internal/economics	0.004s
ok  	github.com/carlosframework/platform/internal/edge	45.245s
?   	github.com/carlosframework/platform/internal/edgeproof	[no test files]
ok  	github.com/carlosframework/platform/internal/edgesfile	0.012s
ok  	github.com/carlosframework/platform/internal/fleetapi	0.193s
ok  	github.com/carlosframework/platform/internal/gate	1.932s
ok  	github.com/carlosframework/platform/internal/genselect	0.015s
ok  	github.com/carlosframework/platform/internal/geo	0.038s
ok  	github.com/carlosframework/platform/internal/geoip	0.054s
ok  	github.com/carlosframework/platform/internal/historyreadback	0.277s
ok  	github.com/carlosframework/platform/internal/host	193.347s
ok  	github.com/carlosframework/platform/internal/hostactors	6.059s
ok  	github.com/carlosframework/platform/internal/hostactors/awsmembership	0.010s
ok  	github.com/carlosframework/platform/internal/hostbox	3.070s
ok  	github.com/carlosframework/platform/internal/hostclaim	0.788s
ok  	github.com/carlosframework/platform/internal/hostenv	0.007s
ok  	github.com/carlosframework/platform/internal/hostevidence	0.010s
ok  	github.com/carlosframework/platform/internal/hostexec	5.724s
ok  	github.com/carlosframework/platform/internal/hostfence	0.049s
ok  	github.com/carlosframework/platform/internal/hostfiles	10.324s
ok  	github.com/carlosframework/platform/internal/hostfloor	0.021s
ok  	github.com/carlosframework/platform/internal/hostguard	1.088s
ok  	github.com/carlosframework/platform/internal/hostops	2.103s
?   	github.com/carlosframework/platform/internal/hostops/fencerelease	[no test files]
ok  	github.com/carlosframework/platform/internal/hostops/internal/fencerelease	0.008s
ok  	github.com/carlosframework/platform/internal/hostquarantine	0.008s
ok  	github.com/carlosframework/platform/internal/hostrelocate	0.653s
ok  	github.com/carlosframework/platform/internal/hostresolve	0.304s
ok  	github.com/carlosframework/platform/internal/hostwrite	0.004s
ok  	github.com/carlosframework/platform/internal/infraview	0.004s
ok  	github.com/carlosframework/platform/internal/instancectl	5.252s
ok  	github.com/carlosframework/platform/internal/instanceexec	2.857s
ok  	github.com/carlosframework/platform/internal/instancefs	0.011s
ok  	github.com/carlosframework/platform/internal/instancekey	0.004s
ok  	github.com/carlosframework/platform/internal/instanceupdates	0.017s
ok  	github.com/carlosframework/platform/internal/layout	1.954s
ok  	github.com/carlosframework/platform/internal/lbhealth	0.004s
ok  	github.com/carlosframework/platform/internal/lease	10.108s
ok  	github.com/carlosframework/platform/internal/lease/leasetest	0.633s
ok  	github.com/carlosframework/platform/internal/ledger	0.035s
ok  	github.com/carlosframework/platform/internal/mail	0.238s
ok  	github.com/carlosframework/platform/internal/maintenance	0.089s
ok  	github.com/carlosframework/platform/internal/maintenanceexec	5.607s
ok  	github.com/carlosframework/platform/internal/maintenanceguard	0.095s
ok  	github.com/carlosframework/platform/internal/metrics	0.005s
ok  	github.com/carlosframework/platform/internal/moves	21.423s
ok  	github.com/carlosframework/platform/internal/moves/archive	1.097s
ok  	github.com/carlosframework/platform/internal/objstore	0.329s
ok  	github.com/carlosframework/platform/internal/ops	0.237s
ok  	github.com/carlosframework/platform/internal/park	0.303s
ok  	github.com/carlosframework/platform/internal/plans	0.006s
ok  	github.com/carlosframework/platform/internal/privatefile	0.006s
ok  	github.com/carlosframework/platform/internal/provision	19.362s
ok  	github.com/carlosframework/platform/internal/publicrelease	0.013s
?   	github.com/carlosframework/platform/internal/regionname	[no test files]
ok  	github.com/carlosframework/platform/internal/registry	5.666s
ok  	github.com/carlosframework/platform/internal/relay	7.245s
ok  	github.com/carlosframework/platform/internal/release	25.369s
ok  	github.com/carlosframework/platform/internal/rollout	3.872s
ok  	github.com/carlosframework/platform/internal/rolloutwords	0.006s
ok  	github.com/carlosframework/platform/internal/rootaws	0.008s
ok  	github.com/carlosframework/platform/internal/rootparticipation	0.060s
ok  	github.com/carlosframework/platform/internal/rootstate	0.027s
ok  	github.com/carlosframework/platform/internal/runs	0.569s
ok  	github.com/carlosframework/platform/internal/s3compat	0.005s
ok  	github.com/carlosframework/platform/internal/safefile	0.011s
ok  	github.com/carlosframework/platform/internal/schedule	2.724s
ok  	github.com/carlosframework/platform/internal/seeds	0.013s
ok  	github.com/carlosframework/platform/internal/sim/hostlife	11.116s
ok  	github.com/carlosframework/platform/internal/sim/litestreamfacts	3.771s
ok  	github.com/carlosframework/platform/internal/status	0.098s
ok  	github.com/carlosframework/platform/internal/steering	0.010s
ok  	github.com/carlosframework/platform/internal/stopevidence	0.166s
ok  	github.com/carlosframework/platform/internal/svcrole	0.002s
ok  	github.com/carlosframework/platform/internal/telemetry	0.008s
ok  	github.com/carlosframework/platform/internal/termfence	0.006s
ok  	github.com/carlosframework/platform/internal/tunnelclient	0.004s
ok  	github.com/carlosframework/platform/internal/unixdial	0.003s
ok  	github.com/carlosframework/platform/internal/updatewords	0.005s
ok  	github.com/carlosframework/platform/internal/usage	0.006s
ok  	github.com/carlosframework/platform/internal/version	0.003s
ok  	github.com/carlosframework/platform/internal/vet	1.622s
=== internal_console@0 (209 s)
ok  	github.com/carlosframework/platform/internal/console	206.813s
=== internal_console@1 (34 s)
ok  	github.com/carlosframework/platform/internal/console	32.469s
=== internal_console@2 (34 s)
ok  	github.com/carlosframework/platform/internal/console	31.959s
=== internal_console@3 (36 s)
ok  	github.com/carlosframework/platform/internal/console	33.924s
=== internal_console@4 (36 s)
ok  	github.com/carlosframework/platform/internal/console	34.264s
=== internal_console@5 (37 s)
ok  	github.com/carlosframework/platform/internal/console	34.460s
=== internal_console@6 (38 s)
ok  	github.com/carlosframework/platform/internal/console	35.924s
=== internal_console@7 (39 s)
ok  	github.com/carlosframework/platform/internal/console	36.602s
=== root@0 (33 s)
ok  	github.com/carlosframework/platform	30.527s
=== root@1 (44 s)
ok  	github.com/carlosframework/platform	42.333s
=== root@2 (44 s)
ok  	github.com/carlosframework/platform	42.242s
=== cross ===
--- shard 0 of 5 ---
cross-compiling darwin/arm64
--- shard 1 of 5 ---
cross-compiling darwin/amd64
--- shard 2 of 5 ---
cross-compiling linux/arm64
linux/arm64 carlos binary: 49873058 bytes (ceiling 52000000)
--- shard 3 of 5 ---
cross-compiling windows/amd64
--- shard 4 of 5 ---
cross-compiling windows/arm64
=== minio-contract ===
minio contract: arm64, minio RELEASE.2025-09-07T16-13-09Z, mc RELEASE.2025-08-13T08-35-41Z
  minio: cached, digest verified
  mc: cached, digest verified
  minio healthy on 127.0.0.1:20999
Added `local` successfully.
Bucket created successfully `local/contract-bucket`.
Bucket created successfully `local/denied-bucket`.
Created policy `contract-only` successfully.
Added user `scopeduser` successfully.
Attached Policies: [contract-only]
To User: scopeduser
  buckets and scoped user ready
=== RUN   TestDeliveredContractMinio
=== RUN   TestDeliveredContractMinio/put
=== RUN   TestDeliveredContractMinio/get_round-trips_the_body
=== RUN   TestDeliveredContractMinio/list_shows_the_key
=== RUN   TestDeliveredContractMinio/head_and_list_agree_on_the_version
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed
=== RUN   TestDeliveredContractMinio/delete_removes_it
=== RUN   TestDeliveredContractMinio/cross-bucket_denial
--- PASS: TestDeliveredContractMinio (0.02s)
    --- PASS: TestDeliveredContractMinio/put (0.00s)
    --- PASS: TestDeliveredContractMinio/get_round-trips_the_body (0.00s)
    --- PASS: TestDeliveredContractMinio/list_shows_the_key (0.00s)
    --- PASS: TestDeliveredContractMinio/head_and_list_agree_on_the_version (0.00s)
    --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload (0.00s)
    --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route (0.01s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed (0.00s)
    --- PASS: TestDeliveredContractMinio/delete_removes_it (0.00s)
    --- PASS: TestDeliveredContractMinio/cross-bucket_denial (0.00s)
PASS
ok  	github.com/carlosframework/platform/internal/objstore	0.024s
=== RUN   TestT4StorageProbeMinio
2026/10/21 14:01:57 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18E0904E278458D4, HostID: 7c420053e0fbcd7f41653fdd92ffa2802c903d05303b1aa15adc2e8bb65d183f, api error SignatureDoesNotMatch: The request signature we calculated does not match the signature you provided. Check your key and signing method.
--- PASS: TestT4StorageProbeMinio (0.01s)
PASS
ok  	github.com/carlosframework/platform/internal/host	0.023s
minio contract: PASS
diff --git a/.amadan/ci.d/10-gate b/.amadan/ci.d/10-gate
index 434170661..daf132329 100755
--- a/.amadan/ci.d/10-gate
+++ b/.amadan/ci.d/10-gate
@@ -36,16 +36,21 @@
# same target a maintainer runs on a laptop — so CI and the local gate
# cannot drift apart, and a check added to one is a check added to both.
# That is how the gofmt check gets here at all: `make test` runs vet, a
-# repo-wide `gofmt -l .`, then the tests, and this file no longer restates
-# any of it.
+# repo-wide `gofmt -l .` and the tests (side by side, through
+# .amadan/test-gate.sh), and this file no longer restates any of it.
#
-# `go build ./...` stays spelled out because `make test` does not do it —
-# the Makefile's test target starts at vet.
+# `go build ./...` stays spelled out because `make test` does not do it.
#
-# -count=1 and -timeout 30m moved to the Makefile's go test line along with
-# the reasoning for them; they are still in force here, just no longer
-# written down twice.
+# -count=1 and -timeout 30m live in .amadan/test-gate.sh, on each go test
+# line, with the Makefile's note on why; they are still in force here,
+# just not written down twice.
set -eu
-go build ./...
-exec make test
+# The build runs beside the tests rather than before them: neither needs
+# the other, and the gate's wall clock is whichever finishes last.
+go build ./... &
+build=$!
+status=0
+make test || status=$?
+wait "$build" || status=1
+exit "$status"
diff --git a/.amadan/test-gate.sh b/.amadan/test-gate.sh
new file mode 100755
index 000000000..f1f02c9cd
--- /dev/null
+++ b/.amadan/test-gate.sh
@@ -0,0 +1,254 @@
+#!/bin/sh
+# The test half of `make test`, and so of CI: `.amadan/ci.d/10-gate` runs
+# `make test`, which runs this. Still one definition.
+#
+# What it checks is what `make test` always checked: launchctx's vet and
+# tests, `go vet ./...`, a repo-wide `gofmt -l .`, and every test in every
+# package with -count=1. What changed is the shape of the run.
+#
+# The gate's wall clock was the slowest single package, because Go runs
+# packages side by side but a package's own tests one after another.
+# internal/console took about 440 s on CI's 32 cores while most of them
+# sat idle, and the root package was not far behind. So:
+#
+# - vet, gofmt, launchctx and the tests all start at once instead of
+# vet finishing before the first test starts;
+# - the packages named in TEST_SPLIT have their top-level tests dealt
+# across several `go test -run` processes running concurrently.
+#
+# The deal is by measured weight (.amadan/test-weights.tsv, longest first
+# onto the least-loaded shard), so one slow test gets a shard to itself
+# rather than dragging its neighbours along. A test missing from the
+# weights file still runs; it is only dealt as if it were short. Stale
+# weights cost balance, never coverage.
+#
+# THE FENCE. Each split package's test list comes from `go test -list`,
+# every name on it goes to exactly one shard, and a shard whose -run
+# pattern matches nothing fails the gate. A test can therefore be added,
+# renamed or removed without touching this file, and no edit to the
+# deal can drop one silently.
+#
+# Shards are separate processes, so tests that leaned on running in the
+# same process as some other test (package state one test leaves for the
+# next) can surface here. That is a bug in the test, not in the split.
+set -eu
+
+: "${TEST_SPLIT=./internal/console=8 .=3}"
+weights=$(pwd)/.amadan/test-weights.tsv
+
+# A shard count that is not a whole number of at least 1 would take a
+# package out of the ordinary run and then launch none of its shards: a
+# green gate that ran none of that package's tests. Refuse it up front.
+for spec in $TEST_SPLIT; do
+ count=${spec#*=}
+ case $spec:$count in
+ *=*:[0-9]*) ;;
+ *)
+ echo "TEST_SPLIT entry '$spec' is not dir=count" >&2
+ exit 2
+ ;;
+ esac
+ case $count in
+ *[!0-9]*)
+ echo "TEST_SPLIT entry '$spec' needs a whole number of shards" >&2
+ exit 2
+ ;;
+ esac
+ if [ "$count" -lt 1 ]; then
+ echo "TEST_SPLIT entry '$spec' needs at least one shard" >&2
+ exit 2
+ fi
+done
+
+# Stopping the gate stops its jobs: the go test processes and the test
+# binaries under them, not just the shells that started them. That covers
+# an interrupt, a TERM from the runner, and an early exit while jobs are
+# already running. Process groups (set -m) would be simpler, but dash
+# turns job control off without a terminal, which is how CI runs it, so
+# each job's tree is walked with pgrep -P instead, and the gate refuses
+# to start without it rather than leave orphans when it is stopped.
+if ! command -v pgrep >/dev/null 2>&1; then
+ echo "pgrep is required: without it a stopped gate cannot stop its jobs" >&2
+ exit 2
+fi
+out=$(mktemp -d "${TMPDIR:-/tmp}/carlos-gate-XXXXXX")
+# started holds name:pid for every job started; discovery is the one
+# foreground command in flight, if any. A job is only signalled while its
+# .secs file (the last thing it writes) is absent, so a finished job's
+# pid, which the system may since have handed to someone else's process
+# on this shared machine, is never touched.
+started=""
+discovery=""
+kill_tree() {
+ for child in $(pgrep -P "$1" 2>/dev/null); do
+ kill_tree "$child"
+ done
+ kill -TERM "$1" 2>/dev/null || true
+}
+stop_jobs() {
+ if [ -n "$discovery" ]; then
+ kill_tree "$discovery"
+ fi
+ for job in $started; do
+ if [ ! -e "$out/${job%:*}.secs" ]; then
+ kill_tree "${job##*:}"
+ fi
+ done
+ wait 2>/dev/null || true
+ rm -rf "$out"
+}
+trap stop_jobs EXIT
+trap 'trap - EXIT; stop_jobs; exit 130' INT TERM
+
+names=""
+# A failure found before any test runs (a package that will not list, a
+# deal that lost a test) is reported as a job of its own, so the jobs
+# already running still finish and print rather than being cut off.
+fail_job() {
+ names="$names $1"
+ printf '%s\n' "$2" >>"$out/$1.log"
+ echo 1 >"$out/$1.rc"
+ echo 0 >"$out/$1.secs"
+}
+start() {
+ start_name=$1
+ shift
+ names="$names $start_name"
+ (
+ set +e
+ t0=$(date +%s)
+ "$@" >"$out/$start_name.log" 2>&1
+ echo $? >"$out/$start_name.rc"
+ echo $(($(date +%s) - t0)) >"$out/$start_name.secs"
+ ) &
+ started="$started $start_name:$!"
+}
+
+# Runs one discovery command in the background and waits for it, so an
+# interrupt reaches the trap at once instead of after a compile.
+discover() {
+ "$@" &
+ discovery=$!
+ discover_rc=0
+ wait "$discovery" || discover_rc=$?
+ discovery=""
+ return "$discover_rc"
+}
+
+start launchctx sh -c 'cd launchctx && go vet ./... && go test ./... -count=1'
+start vet go vet ./...
+start gofmt sh -c 'fmt_out=$(gofmt -l .); if [ -n "$fmt_out" ]; then echo "gofmt needed:"; echo "$fmt_out"; exit 1; fi; echo "gofmt -l .: clean"'
+
+# Everything not split runs as one ordinary `go test`. Packages are named
+# by import path, so match TEST_SPLIT's directories through `go list`.
+split_paths=""
+for spec in $TEST_SPLIT; do
+ discover go list "${spec%=*}" >"$out/go-list"
+ split_paths="$split_paths $(cat "$out/go-list")"
+done
+# go list runs on its own, not in a pipeline: a pipeline's status is its
+# last command's, and a go list that failed halfway would hand on a short
+# package list with a zero status. Each go list goes through discover, and
+# set -e stops the gate on its status.
+discover go list ./... >"$out/go-list"
+all=$(cat "$out/go-list")
+if [ -z "$all" ]; then
+ echo "go list ./... found no packages" >&2
+ exit 1
+fi
+rest=""
+for p in $all; do
+ case " $split_paths " in
+ *" $p "*) ;;
+ *) rest="$rest $p" ;;
+ esac
+done
+# With no packages left, go test would test the current directory instead.
+if [ -n "$rest" ]; then
+ # shellcheck disable=SC2086 # one word per package, on purpose
+ start rest go test -count=1 -timeout 30m $rest
+fi
+
+for spec in $TEST_SPLIT; do
+ dir=${spec%=*}
+ shards=${spec#*=}
+ discover go list "$dir" >"$out/go-list"
+ pkg=$(cat "$out/go-list")
+ tag=$(echo "$dir" | sed 's|^\./||; s|^\.$|root|; s|/|_|g')
+
+ # -list compiles the test binary, which the shards below then reuse
+ # from the build cache. Benchmarks are listed but never run by a plain
+ # `go test`, so they are left out of the deal.
+ if ! discover go test -count=1 -list '.*' "$dir" >"$out/list-$tag.raw" 2>&1; then
+ cp "$out/list-$tag.raw" "$out/$tag.log"
+ fail_job "$tag" "could not list the tests in $pkg"
+ continue
+ fi
+ grep -E '^(Test|Example|Fuzz)' "$out/list-$tag.raw" | sort -u >"$out/list-$tag" || true
+ listed=$(wc -l <"$out/list-$tag")
+ if [ "$listed" -eq 0 ]; then
+ fail_job "$tag" "go test -list found no tests in $pkg, which is not believable"
+ continue
+ fi
+
+ # Longest-processing-time first: heaviest test onto the least-loaded
+ # shard. Unweighted tests count as 0.2 s.
+ awk -v pkg="$pkg" -v n="$shards" -F '\t' '
+ FILENAME == ARGV[1] { if ($1 == pkg) w[$2] = $3; next }
+ { print (($0 in w) ? w[$0] : 0.2) "\t" $0 }
+ ' "$weights" "$out/list-$tag" | sort -t "$(printf '\t')" -k1,1gr -k2,2 |
+ awk -v n="$shards" -F '\t' '
+ {
+ best = 0
+ for (i = 1; i < n; i++) if (load[i] < load[best]) best = i
+ load[best] += $1
+ print best "\t" $2
+ }
+ ' >"$out/deal-$tag"
+
+ dealt=$(wc -l <"$out/deal-$tag")
+ if [ "$dealt" -ne "$listed" ]; then
+ fail_job "$tag" "dealt $dealt of $listed tests in $pkg"
+ continue
+ fi
+
+ i=0
+ launched=0
+ while [ "$i" -lt "$shards" ]; do
+ pattern=$(awk -v i="$i" -F '\t' '$1 == i { printf "%s%s", sep, $2; sep = "|" }' "$out/deal-$tag")
+ if [ -n "$pattern" ]; then
+ start "$tag@$i" go test -count=1 -timeout 30m -run "^($pattern)\$" "$dir"
+ launched=$((launched + $(awk -v i="$i" -F '\t' '$1 == i' "$out/deal-$tag" | wc -l)))
+ fi
+ i=$((i + 1))
+ done
+ if [ "$launched" -ne "$listed" ]; then
+ fail_job "$tag-deal" "launched $launched of $listed tests in $pkg"
+ fi
+done
+
+wait
+started=""
+
+failed=""
+for name in $names; do
+ # A job with no status file was killed before it could write one.
+ rc=$(cat "$out/$name.rc" 2>/dev/null || echo 1)
+ echo "=== $name ($(cat "$out/$name.secs" 2>/dev/null || echo '?') s)"
+ cat "$out/$name.log" 2>/dev/null || true
+ case $name in
+ *@*)
+ if grep -q 'no tests to run' "$out/$name.log"; then
+ echo "--- $name matched no tests; its -run pattern has drifted from the list"
+ rc=1
+ fi
+ ;;
+ esac
+ [ "$rc" -eq 0 ] || failed="$failed $name"
+done
+
+if [ -n "$failed" ]; then
+ echo
+ echo "FAILED:$failed"
+ exit 1
+fi
diff --git a/.amadan/test-weights.sh b/.amadan/test-weights.sh
new file mode 100755
index 000000000..651e4a271
--- /dev/null
+++ b/.amadan/test-weights.sh
@@ -0,0 +1,37 @@
+#!/bin/sh
+# Rewrites .amadan/test-weights.tsv, the measured weights .amadan/test-gate.sh
+# deals the split packages' tests by.
+#
+# go test -count=1 -json ./internal/console/ >console.json
+# go test -count=1 -json . >root.json
+# .amadan/test-weights.sh console.json root.json
+#
+# Measure on a quiet machine with the temp root on tmpfs, as CI has it, or
+# the fsync-heavy tests weigh several times what they do there. Only
+# top-level tests of a second or more are kept; everything else is dealt
+# as short, which is what it is.
+set -eu
+
+[ $# -gt 0 ] || {
+ echo "usage: .amadan/test-weights.sh go-test-json-file..." >&2
+ exit 2
+}
+
+out=.amadan/test-weights.tsv
+{
+ printf '# package\ttest\tseconds (.amadan/test-weights.sh, %s)\n' "$(date -u +%Y-%m-%d)"
+ cat "$@" | awk '
+ /"Action":"(pass|fail)"/ && /"Test":"/ {
+ if (!match($0, /"Test":"[^"]*"/)) next
+ test = substr($0, RSTART + 8, RLENGTH - 9)
+ if (index(test, "/")) next
+ if (!match($0, /"Package":"[^"]*"/)) next
+ pkg = substr($0, RSTART + 11, RLENGTH - 12)
+ if (!match($0, /"Elapsed":[0-9.]+/)) next
+ secs = substr($0, RSTART + 10, RLENGTH - 10) + 0
+ if (secs >= 1) printf "%s\t%s\t%.1f\n", pkg, test, secs
+ }
+ ' | sort -t "$(printf '\t')" -k1,1 -k3,3gr
+} >"$out.tmp"
+mv "$out.tmp" "$out"
+echo "wrote $out: $(($(wc -l <"$out") - 1)) weighted tests"
diff --git a/.amadan/test-weights.tsv b/.amadan/test-weights.tsv
new file mode 100644
index 000000000..d994e6ff6
--- /dev/null
+++ b/.amadan/test-weights.tsv
@@ -0,0 +1,28 @@
+# package test seconds (.amadan/test-weights.sh, 2026-10-21)
+github.com/carlosframework/platform/internal/console TestClaimConsoleMutationInvariant 199.7
+github.com/carlosframework/platform/internal/console TestRetiringAnInstancePurgesItsMoveLogRows 9.3
+github.com/carlosframework/platform/internal/console TestDeclaredLostIsLoggedByTheConsole 7.2
+github.com/carlosframework/platform/internal/console TestClaimConsoleMutationInvariantWitnesses 6.6
+github.com/carlosframework/platform/internal/console TestClaimMailPushCountsOutboundEffects 6.4
+github.com/carlosframework/platform/internal/console TestEconomicsPassWritesCensusOnce 5.0
+github.com/carlosframework/platform/internal/console TestCallbackPostsToSelectedHost 3.8
+github.com/carlosframework/platform/internal/console TestClaimDeleteRetryWhileParkingIsNotDone 3.2
+github.com/carlosframework/platform/internal/console TestClaimProbeRunnerDetectsAHollowRequest 3.2
+github.com/carlosframework/platform/internal/console TestClaimPurgeMailRecordRefusesBeforeEffects 2.5
+github.com/carlosframework/platform/internal/console TestDestroyedLocalOnlyLogsNoMoveEvent 2.4
+github.com/carlosframework/platform/internal/console TestSystemOperatorRollsSignedReleases 2.3
+github.com/carlosframework/platform/internal/console TestSystemDoorsRefuseAPreLayoutMoveWhileABoxIsNested 2.2
+github.com/carlosframework/platform/internal/console TestSystemStatusAndRollback 2.2
+github.com/carlosframework/platform/internal/console TestTodaysFleetWithNoLayoutFieldKeepsRollbackAndUpdate 2.2
+github.com/carlosframework/platform/internal/console TestPublishSurvivesDatabasesThatHangOrPanicTheParser 2.1
+github.com/carlosframework/platform/internal/console TestSelectMoveBoxesComposesTheCreateDoorsInputs 1.7
+github.com/carlosframework/platform/internal/console TestClaimPurgeStoreInventoryRefusesBeforeEffects 1.6
+github.com/carlosframework/platform/internal/console TestJ4HostDoorRoutesOnlyMoveKinds 1.2
+github.com/carlosframework/platform/internal/console TestJ4MoveDoorInspectsThenDeclares 1.2
+github.com/carlosframework/platform/internal/console TestJ4MoveDoorStaleSubmission 1.2
+github.com/carlosframework/platform/internal/console TestAPIPromoteRetryResignsThePointer 1.1
+github.com/carlosframework/platform/internal/console TestMoveDoorRefusesAnIneligibleSource 1.1
+github.com/carlosframework/platform/internal/console TestPlan9RollbackCASConflictIsAConflictAndNotRetried 1.1
+github.com/carlosframework/platform/internal/console TestAnIncompletePlacementReadIsNotNoRoom 1.0
+github.com/carlosframework/platform/internal/console TestClaimLossPagesScanProgressSurvivesRestartAndWraps 1.0
+github.com/carlosframework/platform/internal/console TestCreateDoorsRefuseWithNoRoomBeforeRegistering 1.0
diff --git a/AGENTS.md b/AGENTS.md
index 8b81caa89..f3637c3d5 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -190,9 +190,15 @@ anything.
that used to run this gate was deleted on 2026-08-24, once the amadan
steps covered everything it did and more; `.amadan/README.md` records
what each step descends from, and `.amadan/ci.d/10-gate`'s header
- records why the switch was safe. `10-gate` is `go build ./...` then
+ records why the switch was safe. `10-gate` is `go build ./...` beside
`make test`, so CI and what you run locally are one definition rather
than two that can drift.
+- **`make test` splits the slowest packages' tests across processes**
+ (`.amadan/test-gate.sh`, `TEST_SPLIT`). Each shard is its own process,
+ so a test must not depend on another top-level test having run first
+ in the same process. Shards are dealt by `.amadan/test-weights.tsv`;
+ refresh it when the gate's shard times in the CI log drift far apart
+ (its header says how). A test missing from it still runs.
- **CI is triggered by a push to amadan, not by GitHub.** Nothing on
GitHub runs a build. A branch that exists only on GitHub has been gated
by exactly nothing. Read a branch's verdict with
diff --git a/Makefile b/Makefile
index 8228319ee..fd9a7f63f 100644
--- a/Makefile
+++ b/Makefile
@@ -11,11 +11,12 @@ GOARCH ?=
# -count=1 is CLAUDE.md's rule: never satisfy the gate from a cached pass.
# The 30m timeout is above Go's 10m default because the root package's e2e
# tests compile the carlos binary many times over.
+#
+# .amadan/test-gate.sh runs launchctx, vet, gofmt and the tests at once,
+# and splits the slowest packages' tests across processes. TEST_SPLIT
+# overrides which packages and how many ways (TEST_SPLIT= runs none split).
test:
- cd launchctx && go vet ./... && go test ./... -count=1
- go vet ./...
- @fmt_out=$$(gofmt -l .); if [ -n "$$fmt_out" ]; then echo "gofmt needed:"; echo "$$fmt_out"; exit 1; fi; echo "gofmt -l .: clean"
- go test ./... -count=1 -timeout 30m
+ .amadan/test-gate.sh
build:
CGO_ENABLED=0 GOOS=$(GOOS) GOARCH=$(GOARCH) go build -ldflags "$(LDFLAGS)" -o carlos .