mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-27 22:15:33 +00:00
feat: pgGraph-inspired CI scaffolding wave (v0.37.4.0) (#1228)
Schema-migration matrix + fuzz harness + RSS budget gate + read-latency
under sync + sync lock regression + tests/heavy convention + nightly CI
workflow + BFS frontier cap on traverseGraph.
CI infra (T1-T7):
- tests/heavy/ directory convention + scripts/run-heavy.sh + bun run test:heavy
- tests/heavy/pg_upgrade_matrix.sh: walk pre-v0.13 + pre-v0.18 brain shapes
forward to head via bootstrap → SCHEMA_SQL → migrations → verifySchema
- test/fuzz/{pure,mixed,filesystem}-validators.test.ts: 1000-run fast-check
property tests across 8 trust-boundary validators
- scripts/check-fuzz-purity.sh: bun-bundle + grep guard, wired into verify
- tests/heavy/measure_rss.sh: in-memory PGLite workload + peak RSS measurement
via /proc/self/status (Linux) or process.memoryUsage().rss fallback (macOS,
refuses to write baseline)
- tests/heavy/read_latency_under_sync.sh: phase A baseline + phase B under
parallel writer load, reports p50/p95/p99 + delta_pct
- tests/heavy/sync_lock_regression.sh: N concurrent gbrain sync against one
DB, asserts 1 winner + N-1 lock-busy + zero leaked gbrain_cycle_locks rows
- .github/workflows/heavy-tests.yml: cron '17 8 * * *' + heavy-tests label
trigger + Postgres service + artifact upload on failure
Engine (T8):
- BrainEngine.traverseGraph opts gain frontierCap?: number + onTruncation?:
(info: TruncationInfo) => void callback. Return shape preserved
(Promise<GraphNode[]>) for MCP wire stability.
- Postgres CTE: parenthesized LIMIT N ORDER BY (slug, id) inside recursive term.
- PGLite: same SQL with positional params.
- Per-call callback closure — not engine-instance state — so concurrent
traversals on the same engine don't cross-talk. 5 contracts pinned in
test/regressions/v0_36_frontier_cap.test.ts.
Three plan-review passes ran before any code: CEO scope review (Approach C),
Eng dual-voice review (Claude subagent + Codex), and Codex 2nd-pass against
the revised plan. The 2nd pass caught issues the first two missed (Bun ESM
vs require.cache; engine-instance metadata stomping under concurrency;
fixture-size inconsistency). All addressed.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
772253ef44
commit
9a3ef3cda7
Executable
+164
@@ -0,0 +1,164 @@
|
||||
#!/usr/bin/env bash
|
||||
# scripts/check-fuzz-purity.sh
|
||||
# CI guard: verify that every fuzz target in `test/fuzz/pure-validators.test.ts`
|
||||
# is genuinely PURE — no transitive imports of `node:fs`, `node:child_process`,
|
||||
# the engine layer, or `node:net` / `node:http` / `node:https`.
|
||||
#
|
||||
# Mechanism: bundle each target file with `bun build --target=bun` (which
|
||||
# resolves the full transitive import graph) then grep the bundle for forbidden
|
||||
# imports. Bun surfaces transitively-imported node builtins in the bundle output
|
||||
# even when the indirect-importing file is only reached through several layers,
|
||||
# so the grep catches what require.cache / source-grep approaches miss. This is
|
||||
# the "isolated Bun subprocess with import-trace probe" design from the T2
|
||||
# plan revision.
|
||||
#
|
||||
# Smoke-tested 2026-05-19: adding `import { lstatSync } from 'node:fs'` to a
|
||||
# target file makes this script fail loudly with the offending file + matched
|
||||
# pattern.
|
||||
#
|
||||
# Failure modes the guard catches:
|
||||
# - Direct import of a banned builtin in a target file
|
||||
# - Transitive import through a helper (the failure mode my v1 require.cache
|
||||
# proposal couldn't catch in Bun's ESM loader)
|
||||
# - Re-export from a barrel module
|
||||
#
|
||||
# What this DOESN'T catch:
|
||||
# - Runtime-only `require('fs')` constructed via string concatenation
|
||||
# (intentional escape hatch is rare and would surface in code review)
|
||||
# - Native addon dynamic imports
|
||||
#
|
||||
# Usage: scripts/check-fuzz-purity.sh
|
||||
# Exit: 0 = all targets pure, 1 = any target imports a banned module.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
# Targets — keep in sync with `test/fuzz/pure-validators.test.ts` imports.
|
||||
# Only files whose bundle is bundle-pure live here. The original T2 plan listed
|
||||
# more files; the bundle disproved their purity (validator-shaped functions
|
||||
# living in modules that transitively import fs). Those validators still get
|
||||
# property-tested in `test/fuzz/mixed-validators.test.ts` — same fuzz coverage,
|
||||
# no purity guarantee. Filesystem-touching validators live in
|
||||
# `test/fuzz/filesystem-validators.test.ts`.
|
||||
TARGET_FILES=(
|
||||
"src/core/cjk.ts" # escapeLikePattern
|
||||
"src/core/facts-fence.ts" # parseFactsFence
|
||||
)
|
||||
|
||||
# Banned imports. Bun's bundler emits a mix of forms in the output JS:
|
||||
# - `from "fs"` / `from "node:fs"` (named + namespace imports preserve this)
|
||||
# - `__require("fs")` / `require("fs")` (CJS-style or dynamic-import lowering)
|
||||
# - `from "fs/promises"` / `from "node:fs/promises"` (subpaths — promises API)
|
||||
# - `import("fs")` (raw dynamic import that may survive bundling)
|
||||
# /review codex pass caught the subpath + dynamic-import gaps; this list closes
|
||||
# them. Subpath patterns use trailing-slash matches so `fs/promises` and any
|
||||
# future `fs/<subpath>` all trip the guard.
|
||||
BANNED_PATTERNS=(
|
||||
'from "node:fs"'
|
||||
'from "fs"'
|
||||
'from "node:fs/'
|
||||
'from "fs/'
|
||||
'from "node:child_process"'
|
||||
'from "child_process"'
|
||||
'from "node:net"'
|
||||
'from "node:http"'
|
||||
'from "node:https"'
|
||||
'from "node:dns"'
|
||||
'from "node:cluster"'
|
||||
'require("node:fs")'
|
||||
'require("fs")'
|
||||
'require("node:fs/'
|
||||
'require("fs/'
|
||||
'require("node:child_process")'
|
||||
'require("child_process")'
|
||||
'__require("fs")'
|
||||
'__require("node:fs")'
|
||||
'__require("child_process")'
|
||||
'__require("node:child_process")'
|
||||
'import("fs")'
|
||||
'import("node:fs")'
|
||||
'import("child_process")'
|
||||
'import("node:child_process")'
|
||||
)
|
||||
|
||||
# Engine-layer imports — these are the OTHER thing fuzz targets must not touch.
|
||||
# A fuzz harness that pulls in `engine.ts` could trigger DB connections through
|
||||
# transitive imports of engine-factory.
|
||||
BANNED_PATH_PATTERNS=(
|
||||
'src/core/engine.ts'
|
||||
'src/core/postgres-engine.ts'
|
||||
'src/core/pglite-engine.ts'
|
||||
'src/core/db.ts'
|
||||
'src/core/engine-factory.ts'
|
||||
)
|
||||
|
||||
TMP_BUNDLE_DIR=$(mktemp -d -t gbrain-fuzz-purity-XXXXXX)
|
||||
trap 'rm -rf "$TMP_BUNDLE_DIR"' EXIT
|
||||
|
||||
violations=0
|
||||
|
||||
for target in "${TARGET_FILES[@]}"; do
|
||||
if [ ! -f "$target" ]; then
|
||||
echo "[check-fuzz-purity] WARN: target not found: $target" >&2
|
||||
continue
|
||||
fi
|
||||
|
||||
# Bundle the target into a fresh subdir. --outdir handles the case where a
|
||||
# target's bundle includes side-asset files (WASM, etc) — --outfile fails on
|
||||
# those with "cannot write multiple output files." A pure target should
|
||||
# produce exactly one .js file, but we route through --outdir for safety.
|
||||
SUB="$TMP_BUNDLE_DIR/$(basename "$target" .ts)"
|
||||
mkdir -p "$SUB"
|
||||
if ! bun build --target=bun "$target" --outdir="$SUB" >"$SUB/build.log" 2>&1; then
|
||||
echo "[check-fuzz-purity] FAIL: $target failed to bundle." >&2
|
||||
sed 's/^/ /' "$SUB/build.log" >&2
|
||||
violations=$((violations + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
# A bundle that emits asset files (.wasm, etc) is itself a smell — pure
|
||||
# targets shouldn't ship binary assets.
|
||||
assets=$(find "$SUB" -maxdepth 1 -type f ! -name '*.js' ! -name 'build.log' | wc -l | tr -d ' ')
|
||||
if [ "$assets" -gt 0 ]; then
|
||||
echo "[check-fuzz-purity] FAIL: $target bundle emitted $assets side-asset file(s); pure targets must be JS-only." >&2
|
||||
find "$SUB" -maxdepth 1 -type f ! -name '*.js' ! -name 'build.log' | head -5 >&2
|
||||
violations=$((violations + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
BUNDLE_JS="$SUB/$(basename "$target" .ts).js"
|
||||
if [ ! -f "$BUNDLE_JS" ]; then
|
||||
echo "[check-fuzz-purity] FAIL: $target bundle produced no .js output." >&2
|
||||
violations=$((violations + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
# Check each banned import pattern against the bundled output.
|
||||
for pattern in "${BANNED_PATTERNS[@]}"; do
|
||||
if grep -F -q -- "$pattern" "$BUNDLE_JS"; then
|
||||
echo "[check-fuzz-purity] FAIL: $target bundle contains banned import: $pattern" >&2
|
||||
grep -n -F -- "$pattern" "$BUNDLE_JS" | head -3 >&2
|
||||
violations=$((violations + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
# Check engine-path patterns (these appear as `// <path>` comments in Bun's
|
||||
# bundle output when a transitively-imported module is bundled in).
|
||||
for path_pat in "${BANNED_PATH_PATTERNS[@]}"; do
|
||||
if grep -F -q -- "$path_pat" "$BUNDLE_JS"; then
|
||||
echo "[check-fuzz-purity] FAIL: $target transitively pulls in: $path_pat" >&2
|
||||
violations=$((violations + 1))
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
if [ "$violations" -gt 0 ]; then
|
||||
echo "" >&2
|
||||
echo "[check-fuzz-purity] $violations violation(s). Move impure validators to" >&2
|
||||
echo " test/fuzz/filesystem-validators.test.ts (no purity guard there)." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[check-fuzz-purity] OK — ${#TARGET_FILES[@]} pure-fuzz target(s) verified clean."
|
||||
Executable
+74
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env bash
|
||||
# scripts/run-heavy.sh
|
||||
# Runs every shell script under tests/heavy/ sequentially.
|
||||
# Sister to scripts/run-slow-tests.sh and scripts/run-e2e.sh, but for
|
||||
# ops-shape tests that aren't bun:test targets.
|
||||
#
|
||||
# Usage:
|
||||
# bun run test:heavy # all scripts, sequential
|
||||
# bash scripts/run-heavy.sh # same
|
||||
# bash scripts/run-heavy.sh <pattern> # only scripts whose basename matches glob
|
||||
#
|
||||
# Exit codes:
|
||||
# 0 all scripts passed (or no scripts found — informational)
|
||||
# N exit code of the first failing script
|
||||
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
PATTERN="${1:-}"
|
||||
|
||||
heavy_files=()
|
||||
while IFS= read -r f; do
|
||||
# Skip README.md, non-shell files, and underscore-prefixed helpers
|
||||
# (convention: `_foo.sh` is a library/helper invoked by sibling tests).
|
||||
[[ "$f" == *.sh ]] || continue
|
||||
base=$(basename "$f")
|
||||
case "$base" in
|
||||
_*) continue ;;
|
||||
esac
|
||||
if [ -n "$PATTERN" ]; then
|
||||
case "$base" in
|
||||
$PATTERN) ;;
|
||||
*) continue ;;
|
||||
esac
|
||||
fi
|
||||
heavy_files+=("$f")
|
||||
done < <(find tests/heavy -maxdepth 1 -type f -name '*.sh' | sort)
|
||||
|
||||
if [ "${#heavy_files[@]}" -eq 0 ]; then
|
||||
if [ -n "$PATTERN" ]; then
|
||||
echo "[run-heavy] no scripts under tests/heavy/ matched '$PATTERN'" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "[run-heavy] no scripts under tests/heavy/; nothing to do."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "[run-heavy] running ${#heavy_files[@]} heavy script(s):"
|
||||
for f in "${heavy_files[@]}"; do echo " - $f"; done
|
||||
echo ""
|
||||
|
||||
failed=0
|
||||
for f in "${heavy_files[@]}"; do
|
||||
echo "[run-heavy] --- $f ---"
|
||||
start=$(date +%s)
|
||||
if bash "$f"; then
|
||||
elapsed=$(( $(date +%s) - start ))
|
||||
echo "[run-heavy] OK ($f, ${elapsed}s)"
|
||||
else
|
||||
rc=$?
|
||||
elapsed=$(( $(date +%s) - start ))
|
||||
echo "[run-heavy] FAIL ($f exited $rc after ${elapsed}s)" >&2
|
||||
failed=$rc
|
||||
break
|
||||
fi
|
||||
echo ""
|
||||
done
|
||||
|
||||
if [ "$failed" -ne 0 ]; then
|
||||
echo "[run-heavy] FAILED — first failing script aborted the run." >&2
|
||||
exit "$failed"
|
||||
fi
|
||||
|
||||
echo "[run-heavy] all ${#heavy_files[@]} script(s) passed."
|
||||
Reference in New Issue
Block a user