Files
gbrain/scripts/test-shard.sh
T
943e7b9dec v0.31.4.1 chore: align VERSION + package.json with #795 + mandate 4-segment versions (#815)
* v0.31.4.1 chore: align VERSION/package.json with #795 + mandate MAJOR.MINOR.PATCH.MICRO

PR #795 (takes v2) landed on master with `v0.31.4` in its commit subject but
never bumped VERSION, package.json, or CHANGELOG.md. Master shipped at 0.31.3.

This corrective release:
- Bumps VERSION + package.json to 0.31.4.1 (the dot-suffix follow-up channel
  documented in CLAUDE.md, so the patch number doesn't churn to 0.31.5)
- Adds the v0.31.4.1 CHANGELOG entry covering takes v2 (lessons from a 100K-take
  production extraction), the auth-on-Postgres regression fix, and the new
  `gbrain eval takes-quality` CLI surface
- Updates CLAUDE.md to mandate `MAJOR.MINOR.PATCH.MICRO` for every new release.
  Historical 3-segment versions in git log + migration filenames stay valid;
  do not rewrite. Going forward only.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: regenerate llms-full.txt for v0.31.4.1 doc edits

The build-llms regen-drift guard caught that llms-full.txt was stale relative
to the CHANGELOG + CLAUDE.md edits in the prior commit. Per CLAUDE.md the
bundle is auto-derived: bump VERSION/CHANGELOG/CLAUDE.md, then run
`bun run build:llms`. Did the second part now.

llms.txt unchanged (it's just the curated index). Only llms-full.txt picks
up the v0.31.4.1 CHANGELOG entry and the new "Version format is mandatory"
section in CLAUDE.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(ci): exclude *.serial.test.ts from test-shard.sh hash buckets

Root cause of test (2) failing on the v0.31.4.1 PR (and on master since
#795 landed): CI's scripts/test-shard.sh hashed every test file into 4
shards via FNV-1a, INCLUDING *.serial.test.ts files. Serial files share
file-wide state (top-level mock.module, module singletons) that's
supposed to be quarantined by the .serial.test.ts naming + local
run-serial-tests.sh running them at --max-concurrency=1.

In CI the quarantine didn't apply. eval-takes-quality-runner.serial.test.ts
(new in #795) hashes into shard 2, where it calls:

  mock.module('../src/core/ai/gateway.ts', () => ({
    chat: async (opts) => { ... },
    configureGateway: () => undefined,
  }));

That replaces every export of gateway.ts at module-load time for the
WHOLE shard process. voyage-multimodal.test.ts also lives in shard 2
(both files happen to hash there), and it imports `embedMultimodal` from
gateway.ts. After the serial file loads, `embedMultimodal` is undefined
inside the shard process, and all 18 of voyage-multimodal's
embedMultimodal tests fail. Tests still passed locally because
run-unit-shard.sh excludes .serial files from its parallel pass.

Fix:
  - scripts/test-shard.sh: add `-not -name '*.serial.test.ts'` to the
    find expression so serial files no longer compete for shard buckets.
    Add --dry-run-list flag to mirror run-unit-shard.sh's interface so
    the regression test can introspect without spawning bun test.
  - .github/workflows/test.yml: add a `bun run test:serial` step that
    runs on shard 1 (which already runs `bun run verify`). Uses the
    existing scripts/run-serial-tests.sh which invokes bun test at
    --max-concurrency=1, matching local behavior.
  - test/scripts/test-shard.slow.test.ts: 4 regression cases that pin
    the contract (no serial files in any shard, no e2e files in any
    shard, plain files partitioned without overlap). .slow.test.ts
    because it shells out 4× with pure-bash FNV-1a hashing (~14s
    wallclock); excluded from the local fast loop, runs in CI via the
    same hash bucketing as other slow tests.
  - CLAUDE.md: update the CI vs local divergence section so this
    intentional asymmetry is documented going forward.

Build-llms drift in test (1) was fixed in the prior commit (c99a4af1).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: regenerate llms-full.txt for the CI-fix CLAUDE.md edits

The prior commit updated the "CI vs local: intentionally divergent file sets"
section in CLAUDE.md, which drifted llms-full.txt. Per CLAUDE.md the bundle
is auto-derived: edit CLAUDE.md, then run `bun run build:llms`. Did the
second part now.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 11:28:01 -07:00

98 lines
2.9 KiB
Bash
Executable File

#!/usr/bin/env bash
# Partition unit test files into N shards by stable hash and run one shard.
#
# Usage: scripts/test-shard.sh <shard-index> <total-shards>
# shard-index: 1-based (1..N)
# total-shards: positive integer
#
# E2E tests under test/e2e/ are excluded — they need DATABASE_URL and run via
# bun run test:e2e separately.
#
# Stable partitioning: a file's shard is `(hash(path) % N) + 1`. Same file
# lands in the same shard on every run, regardless of how many other files
# exist, so retries are reproducible. Hash is FNV-1a — pure shell, no jq.
set -euo pipefail
DRY_RUN_LIST=0
if [ "${1:-}" = "--dry-run-list" ]; then
DRY_RUN_LIST=1
shift
fi
if [ "$#" -ne 2 ]; then
echo "usage: scripts/test-shard.sh [--dry-run-list] <shard-index> <total-shards>" >&2
exit 1
fi
SHARD_INDEX="$1"
TOTAL_SHARDS="$2"
if ! [[ "$SHARD_INDEX" =~ ^[0-9]+$ ]] || ! [[ "$TOTAL_SHARDS" =~ ^[0-9]+$ ]]; then
echo "error: shard index and total must be positive integers" >&2
exit 1
fi
if [ "$SHARD_INDEX" -lt 1 ] || [ "$SHARD_INDEX" -gt "$TOTAL_SHARDS" ]; then
echo "error: shard index $SHARD_INDEX out of range 1..$TOTAL_SHARDS" >&2
exit 1
fi
cd "$(dirname "$0")/.."
# Find all unit test files, deterministic order. Excludes test/e2e/ and
# *.serial.test.ts. Serial files share file-wide state (top-level
# mock.module, module singletons) that leaks across files in the same
# `bun test` shard process — see scripts/check-test-isolation.sh R2.
# CI runs them via `bun run test:serial` (scripts/run-serial-tests.sh) at
# --max-concurrency=1 in a separate step on shard 1. Local `bun run test`
# already excludes them from the parallel pass and runs them after the
# same way. Portable: avoid `mapfile` (bash 4+) so this runs on macOS
# bash 3.2 too.
FILES=()
while IFS= read -r line; do
FILES+=("$line")
done < <(find test -name '*.test.ts' -not -name '*.serial.test.ts' -not -path 'test/e2e/*' | sort)
if [ "${#FILES[@]}" -eq 0 ]; then
echo "no test files found under test/" >&2
exit 1
fi
# FNV-1a 32-bit hash of a string — implemented in pure bash so we don't depend
# on python/openssl/etc on the runner. Output is decimal.
fnv1a() {
local str="$1"
local h=2166136261 # FNV offset basis
local i ord
for (( i=0; i<${#str}; i++ )); do
ord=$(printf '%d' "'${str:$i:1}")
h=$(( (h ^ ord) & 0xFFFFFFFF ))
h=$(( (h * 16777619) & 0xFFFFFFFF ))
done
echo "$h"
}
SHARD_FILES=()
for f in "${FILES[@]}"; do
hash=$(fnv1a "$f")
bucket=$(( hash % TOTAL_SHARDS + 1 ))
if [ "$bucket" -eq "$SHARD_INDEX" ]; then
SHARD_FILES+=("$f")
fi
done
if [ "$DRY_RUN_LIST" = "1" ]; then
if [ "${#SHARD_FILES[@]}" -eq 0 ]; then
exit 0
fi
printf '%s\n' "${SHARD_FILES[@]}"
exit 0
fi
echo "shard $SHARD_INDEX/$TOTAL_SHARDS: ${#SHARD_FILES[@]}/${#FILES[@]} files"
if [ "${#SHARD_FILES[@]}" -eq 0 ]; then
echo "warning: shard $SHARD_INDEX has no files (rehash or reduce shard count)" >&2
exit 0
fi
exec bun test --timeout=60000 "${SHARD_FILES[@]}"