mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-27 22:15:33 +00:00
* feat: structured error code summary for sync --skip-failed (#500) When sync encounters per-file failures, the blocked/skip-failed messages now include a breakdown by error code (SLUG_MISMATCH, YAML_PARSE, etc.) instead of just a raw count. This makes it immediately obvious *why* files failed without requiring manual investigation. Changes: - Add classifyErrorCode() — maps error messages to ParseValidationCode - Add summarizeFailuresByCode() — groups failures into sorted code summary - SyncFailure now carries a 'code' field (backfilled on acknowledge) - acknowledgeSyncFailures() returns AcknowledgeResult {count, summary} - sync blocked + skip-failed messages show code breakdown - doctor sync_failures check shows code breakdown for both unacked and historical - 12 new tests for classifyErrorCode, summarizeFailuresByCode, and structured returns Before: Sync blocked: 2688 file(s) failed to parse. After: Sync blocked: 2688 file(s) failed to parse: SLUG_MISMATCH: 2685 YAML_DUPLICATE_KEY: 3 Closes #500 * fix: eng-review fixes for sync error-code classification - Reorder classifyErrorCode() so DB-layer errors (DB_DUPLICATE_KEY, STATEMENT_TIMEOUT) check BEFORE YAML patterns. Postgres "duplicate key value violates unique constraint" no longer mislabels as YAML_DUPLICATE_KEY. - Rewrite MISSING_OPEN/MISSING_CLOSE/EMPTY_FRONTMATTER/NULL_BYTES/NESTED_QUOTES regexes to match the canonical messages emitted by collectValidationErrors() in src/core/markdown.ts. Previous patterns (e.g. /missing.*open/i) never fired because the upstream throw site emits prose ("File is empty...", "No closing --- delimiter found"), not the code name. - Extract formatCodeBreakdown() helper that accepts either raw failures or pre-summarized {code, count}[] input. Replaces 3 duplicate inline builders in src/commands/sync.ts. - 15 new tests (37/37 pass on test/sync-failures.test.ts): - DB vs YAML duplicate-key disambiguation (3 cases) - Canonical-message coverage for the 5 frontmatter codes (7 cases) - acknowledgeSyncFailures() legacy-entry backfill branch (2 cases) - formatCodeBreakdown() dual-input shape (3 cases) - TODOS.md: file 3 follow-ups (P2 plumb structured ParseValidationCode; P0-at-ship CHANGELOG migration note for AcknowledgeResult; P3 concurrent- safe ack of sync-failures.jsonl). Eng-review plan: ~/.claude/plans/then-codex-synchronous-toucan.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: bump version and changelog (v0.22.9) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ci: 16-core runner + 4-way matrix shard for test job The unit test suite ran 22m17s on ubuntu-latest (2-core/7GB) because: - 187 test files run with bun test parallelism bounded by core count - 23 of those files spin up a fresh PGLiteEngine + initSchema in beforeEach, paying ~22s WASM cold-start per test on the small runner This commit fixes the runner side: - runs-on: ubuntu-latest-16-cores (16 vCPU / 64 GB RAM) - strategy.matrix.shard splits 4 parallel jobs, each running ~40 of 158 unit test files. Single-file wall-time floor is ~3 min after the test refactor, so 4 shards × 16 cores hits the floor quickly without wasting cores past it. - pre-test gates (typecheck, check-jsonb, check-progress, check-wasm) only run on shard 1 — they're not test files and don't benefit from sharding. scripts/test-shard.sh partitions test files by stable FNV-1a hash mod N. Same file always lands in the same shard, so retries are reproducible. Pure shell, portable to bash 3.2 (macOS) and bash 5.x (CI). Excludes test/e2e/ which runs via bun run test:e2e separately and needs DATABASE_URL. Also: ignore .claude/ harness state files (scheduled_tasks.lock etc) instead of just .claude/skills/. Cost: ~$0.19/run vs $0 (public repo, default runner is free). At 50 PRs/month that's ~$10/month for ~5x faster CI. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test: refactor top-3 PGLite-heavy files to share one engine per file Three test files were spinning up a fresh PGLiteEngine + connect + initSchema in beforeEach. PGLite WASM cold-start is ~22s on the small CI runner; doing this per test multiplied wall-time across the suite. The 3 files alone accounted for ~6.5 min of the 22m CI run (177s + 132s + 87s). Refactor: move PGLite setup to beforeAll (one engine per file), wipe data in beforeEach via the new test/helpers/reset-pglite.ts helper. The reset helper: - TRUNCATEs every public table CASCADE, including sources (so tests that register their own sources don't leak rows into the next test). - Re-seeds the default source row that pages.source_id's DEFAULT FKs against. Without this, the next page insert would fail FK validation. - Preserves schema_version so migration helpers don't think the brain is on v0. Files refactored: - test/extract-incremental.test.ts (8 tests, was 177s on CI) - test/brain-writer.test.ts (16 tests; only the scanBrainSources block uses PGLite, was 132s on CI) - test/sync.test.ts (37 tests; only the performSync dry-run block uses PGLite, was 87s on CI) All 61 tests still pass locally. The remaining 20 PGLite-heavy files use the same beforeEach anti-pattern; this commit only refactors the proven worst offenders. Sweep the rest in a follow-up if CI numbers indicate it's worth it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ci: fall back to ubuntu-latest for matrix shard The ubuntu-latest-16-cores label requires a provisioned larger-runner pool in repo/org settings. Without that setup, jobs queue indefinitely waiting for a runner that doesn't exist (verified: 4 shards stuck in 'queued' status with empty runner_name for 5+ min). Drop back to the default 2-core ubuntu-latest. The 4-way matrix shard still delivers ~5-6x speedup via parallelism alone — 4 jobs running in parallel, each handling ~40 of 158 unit test files. Cost stays $0 (default runner is free for public repos). If we ever provision a larger-runner pool, flip this label back to ubuntu-latest-16-cores. The matrix + sharder will use the bigger boxes unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Wintermute <wintermute@garrytan.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
77 lines
2.2 KiB
Bash
Executable File
77 lines
2.2 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
|
|
|
|
if [ "$#" -ne 2 ]; then
|
|
echo "usage: scripts/test-shard.sh <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/.
|
|
# 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 -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
|
|
|
|
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[@]}"
|