Compare commits

..
2 Commits
Author SHA1 Message Date
Garry TanandClaude Fable 5 4deee227be v0.45.18.0 fix(serve-http,pglite): UTC-instant spend day boundary + snapshot timezone parity (#4131)
* fix(serve-http,pglite): UTC-instant spend day boundary + snapshot timezone parity

The admin spend query compared created_at against a NAIVE date_trunc result,
reinterpreted in each session's timezone — any non-UTC session shifted the day
boundary by its offset and underreported today's spend every evening. The
boundary is now a timestamptz instant (double AT TIME ZONE), pinned by a
session-timezone-adversarial regression test (Etc/GMT+12 / Etc/GMT-12 / UTC)
that is red on the old query at any wall-clock hour.

Root cause of the local-red/CI-green suite: dumpDataDir bakes the BUILD
process's TimeZone into the snapshot tar, so snapshot-restored engines ran
sessions in the build machine's zone while cold-init engines follow the
runtime (bun test pins TZ=UTC). Restored engines now re-pin the session to
the runtime zone (heals existing tarballs with no rebuild), the builder pins
TZ=UTC before any PGLite work, and a serial parity test asserts cold and
snapshot engines agree on their session UTC offset.

* chore: bump version and changelog (v0.45.18.0)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 21:59:24 -07:00
Garry TanandClaude Fable 5 52140808fd v0.45.17.0 fix(test): refuse test runs with ambient database URLs — four-layer #3485 guard (re-land of #4126) (#4128)
* fix(test): name-floor guard for every destructive-SQL test + repo-wide coverage gate (#3485)

Moves assertSafeE2eDatabaseUrl to leaf module test/helpers/db-guard.ts
(re-exported from test/e2e/helpers.ts for existing call sites) and calls it
before connect() in all ten files that run destructive SQL against the
ambient URL — the eight from #3485, one newer offender
(bootstrap-keyed-postgres.serial), and the raw-postgres()-client OAuth suite
the original audit could not see.

test/db-guard-coverage.test.ts is the static gate that keeps the class
closed: walks every test file bun collects repo-wide (all naming patterns,
fixtures included), detects ambient-URL reads at the assignment site (any
binding name, both env vars, bracket notation), recognizes four connect
idioms, treats env-var deletes as scrubs not reads, refuses comment-only
guard mentions, and pins its own classifiers with positive controls so it
can never pass vacuously.

Patch for the ten files adopted from #3485 by @cheRoma (fork access blocked
a PR) — thank you.

* feat(test): refuse to start a test run while a database URL is ambient (#3485)

A bunfig [test] preload (registered first) hard-fails any bun test invocation
while DATABASE_URL or GBRAIN_DATABASE_URL is set, unless
GBRAIN_TEST_ALLOW_DATABASE_URL=1 — refusing with instructions, never silently
unsetting (a silent unset would turn DB-gated e2e tests into green skips).

Boundaries: run-e2e.sh and the e2e/heavy workflows opt in at their own
subprocess boundary (run-e2e.sh also keeps the opt-in vars past its hermetic
GBRAIN_* scrub and drops GBRAIN_DATABASE_URL, which has no name floor on
spawned-CLI paths); the unit/slow wrappers strip both vars instead — unit
tests need no database — which keeps `bun run test:full` with a DB URL
exported reaching its e2e leg. The phantom-redirect parity file rides the
e2e lane and CI's jsonb-parity job so its Postgres arm stays reachable.

Six subprocess tests spawn real bun test children against the actual
bunfig registration: refuses each var, refuses both, strict override value,
override allows, empty-string treated as unset, clean run.

* fix(tests-heavy): shared database name floor for the heavy shell lane (#3485)

The heavy lane runs schema drops, source-registry rewrites, migration
replays, and parallel syncs against whatever the environment names — outside
bun, where the preload guard cannot fire. tests/heavy/_db_floor.sh mirrors
test/helpers/db-guard.ts: sourced by run-heavy.sh and by every script
documented for direct invocation, it floors BOTH DATABASE_URL and
GBRAIN_DATABASE_URL (the CLI these scripts shell out to prefers the latter)
and strips query strings before extracting the name, so a
?host=/tmp/test-sockets parameter cannot smuggle a test-shaped segment past
the check.

* chore: bump version and changelog (v0.45.15.0)

TESTING.md documents the four guard layers and the cwd caveat; TODOS.md
files the disclosure-policy follow-up (P2).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: update project documentation for v0.45.15.0

Cross-reference pass after the #3485 test-safety wave (Wave -1):

- docs/TESTING.md: complete the guard layers (heavy shell floor
  tests/heavy/_db_floor.sh, schema-drift's accepted inline floor), note
  the phantom-redirect Postgres arm riding the e2e lane in the file
  taxonomy + E2E inventory.
- docs/architecture/KEY_FILES.md: scripts/run-e2e.sh entry updated to
  current behavior (no-args list carries phantom-redirect parity; #3485
  opt-in boundary, GBRAIN_DATABASE_URL drop, GBRAIN_E2E_ALLOW_DB
  preserved through the env scrub).
- CONTRIBUTING.md: heads-up that bare `bun test` refuses to start with a
  database URL ambient + the name floor for own-Postgres/Supabase e2e.
- tests/heavy/README.md: database name floor section (which scripts
  source it, PGLite scripts unset instead, new-script rule).
- .env.testing.example: Supabase's default "postgres" database name
  fails the floor — dedicated test DB or one-shot GBRAIN_E2E_ALLOW_DB.
- CHANGELOG.md v0.45.15.0: three accuracy-of-wording touches (headline
  "silently", lane boundary phrasing, note the one accepted inline
  floor) — no entries removed or regenerated.

Codex cross-model doc review ran; concrete gaps applied above. llms
bundles regenerated (no byte changes — touched docs are link-only).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(test): keep the coverage gate's own scrub-pattern out of the R1 isolation lint's sight

The gate detects 'delete process.env.X' as a scrub-not-read; the R1 lint greps
the same token textually and flagged the gate's comment and classifier fixture
as env mutations. Comment reworded; fixture built by concatenation so the
classifier still receives the contiguous statement.

* chore: re-slot as v0.45.17.0 (re-land of reverted #4126)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 21:44:07 -07:00
46 changed files with 896 additions and 43 deletions
+5
View File
@@ -6,6 +6,11 @@
DATABASE_URL=postgresql://postgres:postgres@localhost:5433/gbrain_test
# Option B: Real Supabase instance (tests the actual production path)
# DATABASE_URL=postgresql://postgres.[project-ref]:[password]@aws-0-us-east-1.pooler.supabase.com:6543/postgres
# NOTE (#3485): destructive tests enforce a database-name floor — the name must
# carry "test" as a word segment (gbrain_test passes; Supabase's default
# "postgres" does not). Use a dedicated test project/database, or opt the exact
# name in one-shot: GBRAIN_E2E_ALLOW_DB=postgres bun run test:e2e
# (never a shell-profile export — that would permanently disarm the floor).
# Tier 2 (required for skill tests, optional for mechanical tests)
OPENAI_API_KEY=sk-...
+10 -1
View File
@@ -61,10 +61,15 @@ jobs:
- name: Run JSONB double-encode parity tests on real Postgres
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/gbrain_test
# #3485 preload guard: this job intentionally tests against a DB.
GBRAIN_TEST_ALLOW_DATABASE_URL: '1'
# --timeout also raises bun's 5s default hook budget (beforeAll/afterAll
# do NOT inherit a test's third-arg timeout; verified on bun 1.3.x).
# Every runner script in scripts/ passes it; bare invocations must too.
run: bun test --timeout=60000 test/e2e/op-checkpoint-jsonb-parity.test.ts test/e2e/jsonb-roundtrip.test.ts
# phantom-redirect rides this job: its Postgres arm is the other
# engine-parity backstop and no other CI lane carries DATABASE_URL to it
# (the unit wrappers strip the URL per #3485).
run: bun test --timeout=60000 test/e2e/op-checkpoint-jsonb-parity.test.ts test/e2e/jsonb-roundtrip.test.ts test/phantom-redirect-engine-parity.test.ts
tier1:
name: Tier 1 (Mechanical)
@@ -94,6 +99,8 @@ jobs:
run: bun test --timeout=60000 test/e2e/mechanical.test.ts test/e2e/mcp.test.ts
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/gbrain_test
# #3485 preload guard: this job intentionally tests against a DB.
GBRAIN_TEST_ALLOW_DATABASE_URL: '1'
tier2:
name: Tier 2 (LLM Skills)
@@ -161,6 +168,8 @@ jobs:
run: bun test --timeout=60000 test/e2e/skills.test.ts test/e2e/zeroentropy-live.test.ts
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/gbrain_test
# #3485 preload guard: this job intentionally tests against a DB.
GBRAIN_TEST_ALLOW_DATABASE_URL: '1'
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
# v0.33.3.0: ZE live API tests skip gracefully when this is unset,
+2
View File
@@ -64,6 +64,8 @@ jobs:
- name: Run heavy tests
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/gbrain_test
# #3485 preload guard: this job intentionally tests against a DB.
GBRAIN_TEST_ALLOW_DATABASE_URL: '1'
run: bun run test:heavy
# Agent-bootstrap offline Docker e2e [A7] — placeholder registration.
+1 -1
View File
@@ -1,4 +1,4 @@
<!-- gbrain-runbook-stamp: 0.45.16.0 -->
<!-- gbrain-runbook-stamp: 0.45.18.0 -->
<!-- This stamp must equal the VERSION file at every release; CI enforces it
(scripts/check-bootstrap-tag.sh). `gbrain bootstrap status` compares it to
the installed binary and warns on skew. -->
+73
View File
@@ -2,6 +2,79 @@
All notable changes to GBrain will be documented in this file.
## [0.45.18.0] - 2026-08-15
**Today's agent spend now reads correctly at every hour, in every timezone.** The admin spend endpoint computed "today" against a naive timestamp that each database session reinterpreted in its own timezone — on any non-UTC session (a PGLite brain following the host clock, a timezone-configured Postgres role), the day boundary shifted by the offset and every evening's spend silently underreported as 0. The boundary is now a UTC instant, independent of session timezone, pinned by a regression test that exercises sessions 12 hours either side of UTC at any wall-clock hour.
The same class also made the new test-suite snapshot fixture time-of-day flaky: the snapshot bakes the build machine's timezone into the restored cluster, so snapshot-restored engines ran sessions in the builder's zone while cold-init engines followed the running process. Restored engines now re-pin their session to the runtime zone (existing tarballs heal without a rebuild), the snapshot builder pins UTC so tarballs are deterministic across hosts, and a parity test asserts cold and snapshot engines agree on their UTC offset.
### Fixed
- `/admin/api/agents/spend`: `spent_cents_today` no longer underreports on non-UTC sessions (UTC-instant day boundary).
- Snapshot-restored PGLite engines behave identically to cold-init engines regardless of the machine that built the tarball.
## [0.45.17.0] - 2026-08-15
**A test run can no longer silently touch a real brain.** `gbrain init` writes your
database URL into `~/.gbrain/.env`; anyone who had that sourced and ran a bare
`bun test` in the repo was one destructive fixture away from their own data
(#3485 — it has happened). Four independent layers now stand in the way, and
each one fails loudly instead of silently skipping:
- **The run refuses to start.** A test preload (registered first in
`bunfig.toml`) hard-fails any `bun test` invocation while `DATABASE_URL` or
`GBRAIN_DATABASE_URL` is ambient, with instructions — it never silently
unsets, because a silent unset would turn database-gated e2e tests into
green skips. The e2e and heavy lanes opt in at their own boundary;
the unit and slow lanes strip the variables at theirs, so
`bun run test:full` with a database URL exported still reaches its e2e leg.
- **Destructive tests check the database name.** Every test that runs
destructive SQL against the ambient URL now calls a shared name floor
(moved to a leaf module so unit-directory tests can use it too): the
database name must carry "test" as a word segment, or be opted in
explicitly, one-shot. (One suite keeps its own equivalent inline floor,
pinned by the coverage gate.) This adopts the patch contributed in #3485 by
@cheRoma — thank you — extended to two newer files the original audit
predates and one raw-client suite it couldn't see.
- **Shell lanes get the same floor.** The heavy-test scripts (schema drops,
parallel syncs, migration replays) share a floor that checks BOTH database
URL variables and strips query strings before extracting the name, so a
`?host=/tmp/test-sockets` parameter can't smuggle a test-shaped segment
past it.
- **A repo-wide static gate keeps it that way.** A scanner walks every test
file bun would collect (all naming patterns, fixtures included), flags any
file that reads an ambient database URL, opens a connection, and runs
destructive SQL without a guard — and its own classifiers are pinned by
positive controls so the gate can never rot into passing vacuously.
### Added
- Test-run guard preload (`test/helpers/database-url-guard-preload.ts`) with
subprocess tests covering every branch: both variables, both-set, override,
strict override value, empty-string, and clean runs.
- Shared destructive-SQL name floor `test/helpers/db-guard.ts` (re-exported
from `test/e2e/helpers.ts` for existing call sites) and shell twin
`tests/heavy/_db_floor.sh`.
- Repo-wide destructive-SQL coverage gate `test/db-guard-coverage.test.ts`
with classifier self-tests and positive controls.
### Fixed
- Ten destructive test files now verify the database name before connecting
(#3485; patch by @cheRoma, extended).
- The heavy lane's fixture builder, sync-lock, upgrade-matrix, and wallclock
scripts refuse non-test-shaped database names instead of operating on
whatever the environment points at.
- The phantom-redirect engine-parity test's Postgres arm is now carried by
the e2e lane and CI's parity job — previously no lane could reach it.
### To take advantage of v0.45.17.0
Nothing to configure. If a bare `bun test` now refuses to start, the message
tells you exactly why and what to do — usually just unset the database URL
(unit tests need no database) or use `bun run test:e2e`, which opts in at its
own boundary. If your e2e database has a non-test-shaped name, opt in one-shot
with `GBRAIN_E2E_ALLOW_DB=<name>` rather than exporting it in your shell
profile — a permanent export would disarm the guard for exactly the database
it protects.
## [0.45.16.0] - 2026-08-14
**Fix wave W0: the verified-bug hotfix pass of the code-smell series.** A 10-auditor sweep of the codebase produced 122 findings; the top claims were adversarially verified, and this release fixes every verified live bug — the ones that survived the skeptic pass. Long-running brains get the biggest wins: background cycles can no longer silently run twice, dead background jobs no longer strand their parents, and image search no longer silently degrades after re-embedding. Developers get a test suite that runs 10x faster.
+14
View File
@@ -115,6 +115,20 @@ DATABASE_URL=postgresql://postgres:postgres@localhost:5434/gbrain_test bun run t
DATABASE_URL=postgresql://... bun run test:e2e
```
Heads-up: a bare `bun test` refuses to start while `DATABASE_URL` or
`GBRAIN_DATABASE_URL` is set in your environment — some tests run destructive
SQL against whatever those URLs point at. Unset the variable for unit runs
(they need no database) or use the wrappers: the unit/slow runners strip the
variables at their boundary, and `bun run test:e2e` opts in at its own. The
refusal message walks you through it; details in
[`docs/TESTING.md`](docs/TESTING.md) ("Database-URL run guard"). If you point
`bun run test:e2e` at your own Postgres or Supabase, a second floor applies:
the database name must carry "test" as a word segment (like `gbrain_test`
above) or destructive tests refuse to run — opt a differently-named database
in one-shot with `GBRAIN_E2E_ALLOW_DB=<name>`.
Use `bun run verify` before pushing. It runs 19+ guard checks in parallel
Use `bun run verify` before pushing. It runs 40+ guard checks in parallel
(`scripts/run-verify-parallel.sh`), including: banned fork-name leaks
(`scripts/check-privacy.sh`), `JSON.stringify(x)::jsonb` interpolation
+12
View File
@@ -1,5 +1,17 @@
# TODOS
## Security-process follow-ups (filed with Wave 1 of the fix-wave campaign, 2026-08-14)
- [ ] **P2 — Vulnerability disclosure policy.** **What:** a written disclosure
process: advisory ownership, severity ladder, reporter acknowledgment SLA,
embargo windows, private patch review, supported-version/backport policy,
release timing, post-release rotation guidance. **Why:** private vulnerability
reporting is now enabled (#579) and a reporter has a channel, but a channel
without a process leaves triage decisions ad-hoc; a public PR diff can still
broadcast attack surface mid-embargo. **Context:** filed from the fix-wave
campaign's Codex review (CX-11); the campaign deliberately shipped only the
toggle + reporter acknowledgment. Start from the responsible-disclosure rules
already in CLAUDE.md and docs/RELEASING.md. **Effort:** M. **Priority:** P2.
## Code-smell fix-wave deferrals (filed at W0; plan: ~/.claude/plans/system-instruction-you-are-working-encapsulated-eclipse.md)
Each was individually decided as a deferral in the CEO/eng reviews of the
+1 -1
View File
@@ -1 +1 @@
0.45.16.0
0.45.18.0
+5 -1
View File
@@ -23,4 +23,8 @@ timeout = 60_000
# were appending rows into the operator's real ~/.gbrain/sync-failures.jsonl,
# which `gbrain doctor` reads and warns on. See
# test/helpers/sync-failures-preload.ts.
preload = ["./test/helpers/legacy-embedding-preload.ts", "./test/helpers/audit-dir-preload.ts", "./test/helpers/sync-failures-preload.ts"]
# #3485: database-url-guard-preload runs FIRST — it refuses to start the run
# while DATABASE_URL/GBRAIN_DATABASE_URL is ambient without the explicit
# GBRAIN_TEST_ALLOW_DATABASE_URL=1 opt-in that the e2e wrappers set at their
# own subprocess boundary. See test/helpers/database-url-guard-preload.ts.
preload = ["./test/helpers/database-url-guard-preload.ts", "./test/helpers/legacy-embedding-preload.ts", "./test/helpers/audit-dir-preload.ts", "./test/helpers/sync-failures-preload.ts"]
+31 -2
View File
@@ -132,7 +132,7 @@ Triage rule: a `warn-pass` EXIT-HANG line in `.context/test-summary.txt` is NOT
- `*.test.ts` → fast loop (parallel up-to-4-shard fan-out, memory-adaptive).
- `*.slow.test.ts` → run via `bun run test:slow` only (intentional cold-path tests; would dominate the fast loop's wallclock).
- `*.serial.test.ts` → run via `bun run test:serial` after the parallel pass completes; one bun process per file (`--max-concurrency=1` within a shared process is not enough — the module registry still leaks `mock.module`). Quarantine for tests that share file-wide state and race when run alongside other files in the same `bun test` process. Several dozen files, discovered by the `*.serial.test.ts` glob — no list to maintain. Typical residents: `mock.module(...)` users (top-level mocks leak across files in a shard process, e.g. `test/embed.serial.test.ts`), env-coupled files (e.g. `test/brain-registry.serial.test.ts`), and process-lifecycle suites that assert on `process.exitCode` (e.g. `test/pglite-engine-disconnect.serial.test.ts`). **Do not put the parallelism back on a serial file unless you've fixed the contention root cause** (it just re-introduces the flake).
- `test/e2e/*.test.ts` → real-Postgres E2E. Skipped when `DATABASE_URL` is unset.
- `test/e2e/*.test.ts` → real-Postgres E2E. Skipped when `DATABASE_URL` is unset. One out-of-directory file rides this lane: `test/phantom-redirect-engine-parity.test.ts` (lives in `test/` for its PGLite arm, but its Postgres arm is only reachable through a DATABASE_URL-bearing lane — the unit wrappers strip the URL per #3485, so `run-e2e.sh`'s no-args list and CI's parity job carry it).
- `tests/heavy/*.sh` → ops-shape shell scripts. Cost minutes per run; NOT in default `bun test`. Run via `bun run test:heavy` or scheduled nightly via `.github/workflows/heavy-tests.yml`. Examples: pg_upgrade matrix (boot legacy brain → walk to head), RSS budget gate (measure peak worker RSS vs committed baseline), read-latency-under-sync (p50/p95/p99 under concurrent writer load), sync lock regression (N concurrent syncs assert 1 winner + N-1 lock-busy + zero leaked `gbrain_cycle_locks` rows). See `tests/heavy/README.md` for when to add a script here vs `*.slow.test.ts`. Files prefixed with `_` (e.g. `tests/heavy/_build_legacy_fixtures.sh`) are helpers/libs invoked by sibling tests — the runner skips them.
- `test/fuzz/*.test.ts` → property-based fuzz harness. Pure-validator targets in `pure-validators.test.ts` are guarded by `scripts/check-fuzz-purity.sh` (in `bun run verify`), which `bun build --target=bun` bundles each target and greps the resulting bundle for banned transitive imports (`node:fs`, `node:child_process`, engine modules). Anything that fails the guard moves to `mixed-validators.test.ts` (still property-tested, but no purity guarantee) or `filesystem-validators.test.ts` (fs-backed, uses temp dirs). Fuzz tests run in the default `bun test` loop because they're fast (~3s for ~12 properties × 1000 runs each).
@@ -220,6 +220,35 @@ The quarantine has grown to dozens of files — treat it as debt: every addition
`bun test` runs all tests without a database. E2E tests skip gracefully when `DATABASE_URL` is not set.
**Database-URL run guard (#3485).** A `bun test` invocation REFUSES to start while
`DATABASE_URL` or `GBRAIN_DATABASE_URL` is ambient in the environment, because some
tests run destructive SQL against whatever those URLs point at (a bare `bun test`
with `~/.gbrain/.env` sourced has wiped a real brain). The guard is a bunfig
`[test]` preload (`test/helpers/database-url-guard-preload.ts`); it hard-fails with
instructions rather than silently unsetting (a silent unset would turn
DATABASE_URL-gated e2e tests into green skips). The e2e wrappers
(`scripts/run-e2e.sh`, the e2e/heavy workflows) opt in at their own boundary via
`GBRAIN_TEST_ALLOW_DATABASE_URL=1`; the unit/slow wrappers instead strip both
URL vars at their boundary (unit tests need no database), which keeps
`bun run test:full` working with DATABASE_URL exported. Caveat: bun loads
`bunfig.toml` from the invocation cwd, so the preload layer only applies to
runs started at the repo root — the per-file name floor below is the layer
that doesn't care about cwd. Two more layers apply after the opt-in: every
test that runs destructive SQL on the ambient URL must call
`assertSafeE2eDatabaseUrl()` (`test/helpers/db-guard.ts` — name floor: the database
name must contain "test" as a segment, or be opted in via `GBRAIN_E2E_ALLOW_DB`)
or carry an inline name floor the coverage gate recognizes
(`test/e2e/schema-drift.test.ts` keeps its own `looksLikeTestDb`, deliberately
different because it also accepts `*_e2e`), and `test/db-guard-coverage.test.ts`
statically scans the suite and fails when a file connects to `DATABASE_URL` and
runs destructive SQL unguarded. The heavy shell lane gets the same floor outside
bun: `tests/heavy/_db_floor.sh` (sourced by `scripts/run-heavy.sh` for the whole
lane, and by each database-touching heavy script itself, since scripts are
documented for direct invocation — the PGLite-based heavy scripts unset the URL
instead) checks BOTH URL variables and strips query strings before extracting
the database name, so a `?host=/tmp/test-sockets` parameter can't smuggle a
test-shaped segment past it.
Unit tests and what they cover:
- `test/markdown.test.ts` — frontmatter parsing; `splitBody` sentinel precedence, horizontal-rule preservation, `inferType` wiki subtypes.
@@ -335,7 +364,7 @@ Unit tests and what they cover:
### E2E test inventory
E2E tests live in `test/e2e/` and run against real Postgres+pgvector (require `DATABASE_URL`), except where noted as PGLite in-memory (no `DATABASE_URL` needed).
E2E tests live in `test/e2e/` and run against real Postgres+pgvector (require `DATABASE_URL`), except where noted as PGLite in-memory (no `DATABASE_URL` needed). One file outside the directory also rides the e2e lane: `test/phantom-redirect-engine-parity.test.ts` (Postgres arm; see the file taxonomy above).
- `bun run test:e2e` runs Tier 1 (mechanical, all operations, no API keys). Includes dedicated cases for the postgres-engine `addLinksBatch` / `addTimelineEntriesBatch` bind path — postgres-js's JSONB bind (`jsonb_to_recordset(($1::jsonb)->'rows')`) differs from PGLite's and gets its own coverage.
- `test/e2e/search-quality.test.ts` — search quality against PGLite (no API keys, in-memory).
+1 -1
View File
@@ -339,7 +339,7 @@ per-release `**vX.Y.Z:**` narration — CI enforces this
- `scripts/build-pglite-snapshot.ts``bun run build:pglite-snapshot`: bakes a post-`initSchema()` PGLite data dir into `test/fixtures/pglite-snapshot.tar` + a version file (schema hash line, then `dims=`/`model=` lines recording the embedding shape it was baked with). Idempotent (hash short-circuit ~40ms when fresh; rebuilds stale) and concurrency-safe (atomic `mkdir` lock at `test/fixtures/.pglite-snapshot.lock` with staleness-verified takeover — a live lock is never stolen; tar written first, version file last, so a crash can't leave a fresh-looking torn fixture; waiter bounded by `GBRAIN_SNAPSHOT_LOCK_TIMEOUT_MS`, default 120000; an exhausted waiter facing a still-live lock proceeds unlocked as a last resort — the loader's hash/shape gate validates the version file, not the tar bytes). Called by `scripts/run-unit-parallel.sh` (default-on for `bun run test`; opt out `GBRAIN_NO_SNAPSHOT=1`) and `scripts/ci-local.sh`, both of which export `GBRAIN_PGLITE_SNAPSHOT`. The loader side is `tryLoadSnapshot` + `computeSnapshotSchemaHash` (exported from `src/core/pglite-engine.ts`): the hash folds `PGLITE_SCHEMA_SQL`, every migration's `sql` + `sqlFor.pglite`, AND each migration `handler`'s function source (handler-only migrations are invisible to a sql-only hash); any hash or embedding-shape mismatch warns once and falls through to normal cold init — the snapshot is an optimization, never authoritative. Pinned by `test/snapshot-shape-guard.test.ts`.
- `docker-compose.ci.yml` + `scripts/ci-local.sh` — Local CI gate. `bun run ci:local` spins up four `pgvector/pgvector:pg16` services (postgres-1..4) + `oven/bun:1` with named volumes (`gbrain-ci-pg-data-{1..4}`, `gbrain-ci-node-modules`, `gbrain-ci-bun-cache`), runs gitleaks on host, smoke-tests `scripts/run-e2e.sh` argv handling, runs guards + typecheck, then the Tier 1 default: 4-shard parallel unit + E2E (`xargs -P4`, one Postgres per shard; unit phase keeps `DATABASE_URL` unset). `--no-shard` falls back to the legacy unsharded sequential flow (debug aid); `--diff` runs the diff-aware selector unsharded. Also runs a `pgbouncer` service (`edoburu/pgbouncer`, `POOL_MODE: transaction`, `AUTH_TYPE: plain` — pg16 stores SCRAM verifiers, so the userlist must hold the plaintext password; `IGNORE_STARTUP_PARAMETERS` whitelists gbrain's `statement_timeout`/`idle_in_transaction_session_timeout` startup params the way the Supabase pooler does) fronting postgres-1 on host port `GBRAIN_CI_PGBOUNCER_PORT` (default 6543); every E2E invocation exports `GBRAIN_PGBOUNCER_URL` (pooled; dedicated `gbrain_pgbouncer` database so it never races the `gbrain_test` TRUNCATE fixtures) + `GBRAIN_PGBOUNCER_DIRECT_URL`, consumed by `test/e2e/pgbouncer-teardown.test.ts` — the transaction-mode teardown bug class (#1972/#2015/#2084) reproduced in the local gate instead of only in production. `--no-pull` skips upstream pulls; `--clean` nukes named volumes. Postgres host port defaults to 5434; override with `GBRAIN_CI_PG_PORT=NNNN`. Stronger gate than PR CI's 2-file Tier 1 set.
- `scripts/select-e2e.ts` + `scripts/e2e-test-map.ts` — Diff-aware E2E test selector. Reads three git sources (committed `origin/master...HEAD`, working-tree `HEAD`, and `git ls-files --others --exclude-standard` for untracked NOT-gitignored files), classifies as EMPTY / DOC_ONLY / SRC. Fail-closed: EMPTY → all files; DOC_ONLY (every path matches the README/CLAUDE/AGENTS/CHANGELOG/TODOS allowlist) → empty stdout; SRC → escape-hatch paths (schema, package.json, skills/) trigger all, else the hand-tuned `E2E_TEST_MAP` glob narrows, and an unmapped src/ change still emits ALL files (never silently nothing). Pure-function exports `selectTests`, `classify`, `matchGlob`. `bun run ci:select-e2e` prints the current selection on stdout. `test/select-e2e.test.ts` covers all 4 branches plus 3 regression guards (skills/, untracked files, unmapped src/) — 24 cases.
- `scripts/run-e2e.sh` — Sequential E2E runner. Accepts an optional argv-driven file list (used by `ci:local:diff`) and a `--dry-run-list` flag that prints the resolved file list and exits (used by `ci-local.sh`'s startup smoke-test). Falls back to `test/e2e/*.test.ts` when invoked with no args.
- `scripts/run-e2e.sh` — Sequential E2E runner. Accepts an optional argv-driven file list (used by `ci:local:diff`) and a `--dry-run-list` flag that prints the resolved file list and exits (used by `ci-local.sh`'s startup smoke-test). Falls back to `test/e2e/*.test.ts` plus `test/phantom-redirect-engine-parity.test.ts` when invoked with no args (the phantom-redirect Postgres arm is only reachable through a DATABASE_URL-bearing lane; the unit wrappers strip the URL per #3485, so this lane must carry it). This wrapper is the database-URL opt-in boundary (#3485): it exports `GBRAIN_TEST_ALLOW_DATABASE_URL=1` so the bunfig preload guard (`test/helpers/database-url-guard-preload.ts`) lets the run start, unsets `GBRAIN_DATABASE_URL` (the e2e suite runs on `DATABASE_URL` only — an ambient `GBRAIN_DATABASE_URL` would pass the opt-in yet reach CLI-subprocess paths with no name floor), and its GBRAIN_* env scrub preserves `GBRAIN_E2E_ALLOW_DB` so the name-floor escape hatch the guard's own error message names stays usable.
- `scripts/llms-config.ts` + `scripts/build-llms.ts` — Generator for `llms.txt` (llmstxt.org-spec web index) + `llms-full.txt` (inlined single-fetch bundle). Curated config drives both. Run `bun run build:llms` after adding a new doc. `LLMS_REPO_BASE` env lets forks regenerate with their own URL base. `FULL_SIZE_BUDGET` (600KB) caps the inline bundle; generator WARNs if exceeded. Committed output has no runtime consumer; committed for GitHub browsing and fork-safe fetching.
- `AGENTS.md` — Local-clone entry point for non-Claude agents (Codex, Cursor, OpenClaw, Aider). Mirrors `CLAUDE.md` intent via relative links. Claude Code keeps using `CLAUDE.md`.
- `docs/UPGRADING_DOWNSTREAM_AGENTS.md` — Patches for downstream agent skill forks to apply when upgrading. Each release appends a new section; includes diffs for brain-ops, meeting-ingestion, signal-detector, enrich.
+1 -1
View File
@@ -1,7 +1,7 @@
{
"id": "gbrain-context-engine",
"name": "gbrain",
"version": "0.45.16.0",
"version": "0.45.18.0",
"description": "Personal knowledge brain with Postgres + pgvector hybrid search",
"family": "bundle-plugin",
"configSchema": {
+1 -1
View File
@@ -156,7 +156,7 @@
"bun": ">=1.3.10"
},
"license": "MIT",
"version": "0.45.16.0",
"version": "0.45.18.0",
"overrides": {
"@hono/node-server": "^2.0.5",
"fast-uri": "^3.1.5",
+7
View File
@@ -1,6 +1,13 @@
#!/usr/bin/env bun
// scripts/build-pglite-snapshot.ts
//
// TZ pinned to UTC BEFORE any PGLite work: dumpDataDir bakes this process's
// TimeZone into the tar's cluster defaults. Building under the host zone made
// restored engines run sessions in the build machine's zone (the engine also
// re-pins at restore — this is the belt to that suspender, and it keeps any
// OTHER zone-derived state baked into the tar deterministic across hosts).
process.env.TZ = 'UTC';
//
// Tier 3 fast-restore: boot a fresh PGLite, run the full initSchema (forward
// bootstrap + PGLITE_SCHEMA_SQL + every migration), dump the post-init state
// to a tar fixture. Test files that read GBRAIN_PGLITE_SNAPSHOT can skip the
+18 -1
View File
@@ -36,6 +36,16 @@ set -euo pipefail
cd "$(dirname "$0")/.."
# #3485: this wrapper IS the e2e boundary — opt in to running with a database
# URL present. The bunfig test preload (database-url-guard-preload.ts) refuses
# bare `bun test` runs while DATABASE_URL/GBRAIN_DATABASE_URL is ambient; the
# per-file name floor (test/helpers/db-guard.ts) still applies after this.
export GBRAIN_TEST_ALLOW_DATABASE_URL=1
# The e2e suite runs on DATABASE_URL only; an ambient GBRAIN_DATABASE_URL
# would pass the opt-in yet reach CLI-subprocess paths with no name floor —
# drop it here so only the floored variable crosses the boundary.
unset GBRAIN_DATABASE_URL
# --- HOME isolation: snapshot real user config before switching ---
# Tolerate unset HOME (minimal containers, exotic CI shells) without tripping set -u.
REAL_HOME="${HOME:-/tmp}"
@@ -83,6 +93,10 @@ mkdir -p "$E2E_TMP_HOME/.gbrain"
for _e2e_var in $(env | grep -oE '^(CONDUCTOR_|MCP_|OPENCLAW_|HERMES_|GBRAIN_)[A-Za-z0-9_]*' | sort -u); do
case "$_e2e_var" in
GBRAIN_HOME) ;; # required for HOME isolation (set above) — keep
GBRAIN_TEST_ALLOW_DATABASE_URL) ;; # #3485 preload opt-in (set above) — keep
GBRAIN_E2E_ALLOW_DB) ;; # #3485 name-floor opt-in — the guard's own error
# message tells operators to set it; stripping it
# here would make that escape hatch a dead end
*) unset "$_e2e_var" || true ;;
esac
done
@@ -99,7 +113,10 @@ fi
if [ "$#" -gt 0 ]; then
files=("$@")
else
files=(test/e2e/*.test.ts)
# phantom-redirect lives in test/ (its PGLite arm runs in the unit suite) but
# its Postgres arm is only reachable through a DATABASE_URL-bearing lane —
# the unit wrappers strip the URL (#3485), so this lane must carry it.
files=(test/e2e/*.test.ts test/phantom-redirect-engine-parity.test.ts)
fi
# SHARD env (e.g. SHARD=1/4) keeps every M-th file starting at index N (1-indexed).
+4
View File
@@ -16,6 +16,10 @@
set -euo pipefail
cd "$(dirname "$0")/.."
# #3485: the heavy lane runs destructive shell scripts against DATABASE_URL —
# apply the shared name floor once here for every script it dispatches.
source tests/heavy/_db_floor.sh
PATTERN="${1:-}"
heavy_files=()
+5
View File
@@ -4,6 +4,11 @@
# CI runs both; bun run ci:local skips slow tests via run-unit-shard.sh.
set -euo pipefail
# #3485: unit/slow tests need no database — strip ambient DB URLs at this
# wrapper boundary so the bunfig preload guard passes and nothing can reach a
# real brain. The e2e wrapper (run-e2e.sh) is the only lane that keeps them.
unset DATABASE_URL GBRAIN_DATABASE_URL
cd "$(dirname "$0")/.."
slow_files=()
+5
View File
@@ -44,6 +44,11 @@
set -uo pipefail
# #3485: unit tests need no database — strip ambient DB URLs at this wrapper
# boundary so the bunfig preload guard passes and nothing can reach a real
# brain. The e2e wrapper (run-e2e.sh) is the only lane that keeps them.
unset DATABASE_URL GBRAIN_DATABASE_URL
cd "$(dirname "$0")/.."
# ──────────────────────────────────────────────────────────────────────────
+5
View File
@@ -14,6 +14,11 @@
set -euo pipefail
# #3485: unit/slow tests need no database — strip ambient DB URLs at this
# wrapper boundary so the bunfig preload guard passes and nothing can reach a
# real brain. The e2e wrapper (run-e2e.sh) is the only lane that keeps them.
unset DATABASE_URL GBRAIN_DATABASE_URL
cd "$(dirname "$0")/.."
# --max-concurrency=N is forwarded to `bun test`. v0.26.4: invoked by
+7 -1
View File
@@ -551,7 +551,13 @@ export async function queryAgentClientSpend(engine: BrainEngine): Promise<AgentC
SELECT SUM(spend_cents)::text
FROM mcp_spend_log
WHERE client_id = c.client_id
AND created_at >= date_trunc('day', now() AT TIME ZONE 'UTC')
-- Double AT TIME ZONE: the inner one yields NAIVE UTC-midnight;
-- the outer one converts it back to a timestamptz INSTANT. Without
-- it, the naive value is reinterpreted in the SESSION timezone, so
-- any non-UTC session (host-tz PGLite, a tz-configured Postgres
-- role) shifts the day boundary by the offset and today's spend
-- underreports every evening.
AND created_at >= date_trunc('day', now() AT TIME ZONE 'UTC') AT TIME ZONE 'UTC'
), '0') AS spent_cents_today,
COALESCE((
SELECT SUM(estimated_cents)::text
+12
View File
@@ -616,6 +616,18 @@ export class PGLiteEngine implements BrainEngine {
...embedded,
}),
);
// Snapshot-timezone parity: dumpDataDir bakes the BUILD process's
// TimeZone into the restored cluster's defaults, so a snapshot-loaded
// engine would run sessions in the build machine's zone while a
// cold-init engine follows this process (bun test pins TZ=UTC; bun run
// follows the host). That divergence shifted every naive-timestamp
// day-boundary comparison by the offset — date-dependent tests failed
// only in the evening, only under the snapshot. Pin the session to the
// RUNTIME zone so restored engines behave exactly like cold ones.
if (this._snapshotLoaded && this._db) {
const runtimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
await this._db.query(`SELECT set_config('TimeZone', $1, false)`, [runtimeZone]);
}
// Healthy open: close any repair episode left open by a prior failed
// attempt (red-team: episodes otherwise stayed open forever — doctor
// kept reporting corruption-likely and a weeks-stale episode backup
+1 -1
View File
@@ -1,6 +1,6 @@
# gbrain agent workspace — template
<!-- gbrain-template-stamp: 0.45.16.0 -->
<!-- gbrain-template-stamp: 0.45.18.0 -->
This repository is the **"Use this template"** distribution artifact for a
[gbrain](https://github.com/garrytan/gbrain) personal-agent workspace — the same
+35
View File
@@ -150,6 +150,41 @@ describe('queryAgentClientSpend (v0.38 Slice 4 — /admin/api/agents/spend SQL)'
expect(rows[0].spent_cents_today).toBe(50);
});
it('day boundary is a UTC INSTANT, independent of the session timezone', async () => {
// Regression pin for the snapshot-timezone incident: the old predicate
// compared created_at against a NAIVE date_trunc result, which the
// session timezone reinterpreted — a non-UTC session (host-tz PGLite,
// tz-configured Postgres role, snapshot-restored engine pre-parity-fix)
// shifted the day boundary by its offset and underreported evening spend.
//
// Deterministic at ANY wall-clock hour: rows exactly AT UTC midnight and
// 1s BEFORE it must classify identically under sessions ±12h from UTC.
// Under the old predicate, Etc/GMT+12 excluded the midnight row and
// Etc/GMT-12 included the pre-midnight row — one of the two always broke.
await seedClient({ id: 'tz-edge', scope: 'read agent' });
await engine.executeRaw(
`INSERT INTO mcp_spend_log (client_id, operation, spend_cents, created_at)
VALUES
('tz-edge', 'subagent_loop', 7,
date_trunc('day', now() AT TIME ZONE 'UTC') AT TIME ZONE 'UTC'),
('tz-edge', 'subagent_loop', 999,
(date_trunc('day', now() AT TIME ZONE 'UTC') AT TIME ZONE 'UTC') - interval '1 second')`,
);
const original = (await engine.executeRaw<{ TimeZone: string }>(`SHOW timezone`))[0].TimeZone;
for (const zone of ['Etc/GMT+12', 'Etc/GMT-12', 'UTC']) {
await engine.executeRaw(`SELECT set_config('TimeZone', '${zone}', false)`);
try {
const rows = await queryAgentClientSpend(engine);
const edge = rows.find(r => r.client_id === 'tz-edge')!;
// Only the exactly-at-midnight row (7¢) counts as today — never the
// 1s-before row (999¢) — regardless of session zone.
expect(`${zone}:${edge.spent_cents_today}`).toBe(`${zone}:7`);
} finally {
await engine.executeRaw(`SELECT set_config('TimeZone', $1, false)`, [original]);
}
}
});
it('isolates spend by client_id (no cross-client leakage)', async () => {
await seedClient({ id: 'alice', scope: 'read agent' });
await seedClient({ id: 'bob', scope: 'read agent' });
+103
View File
@@ -0,0 +1,103 @@
/**
* #3485 invocation-level guard: a bare `bun test` must refuse to start while
* DATABASE_URL or GBRAIN_DATABASE_URL is ambient, unless the invoker opted in
* with GBRAIN_TEST_ALLOW_DATABASE_URL=1 (the e2e wrappers do, at their own
* subprocess boundary).
*
* Each case spawns a real `bun test` child on a trivial probe fixture with a
* controlled environment, so what's asserted is the actual preload behavior,
* not a re-implementation of it. The child inherits this repo's bunfig.toml
* (cwd = repo root), which registers the guard preload first.
*/
import { describe, test, expect } from 'bun:test';
import { resolve } from 'path';
const REPO_ROOT = resolve(import.meta.dir, '..');
const PROBE = 'test/fixtures/preload-guard/guard-probe.test.ts';
const GUARD_MARKER = 'TEST-RUN GUARD: refusing to start';
/** Base child env: inherit, then strip every var the guard reads. */
function baseEnv(): Record<string, string> {
const env: Record<string, string> = {};
for (const [k, v] of Object.entries(process.env)) {
if (v === undefined) continue;
if (k === 'DATABASE_URL' || k === 'GBRAIN_DATABASE_URL' || k === 'GBRAIN_TEST_ALLOW_DATABASE_URL') continue;
env[k] = v;
}
return env;
}
function runProbe(extra: Record<string, string>): { exitCode: number; stderr: string } {
const proc = Bun.spawnSync(['bun', 'test', '--timeout=15000', PROBE], {
cwd: REPO_ROOT,
env: { ...baseEnv(), ...extra },
stdout: 'pipe',
stderr: 'pipe',
// A hung child would otherwise block the sync call past bun's own
// per-test timeout (which cannot preempt a native sync call).
timeout: 20_000,
killSignal: 'SIGKILL',
});
return { exitCode: proc.exitCode ?? -1, stderr: proc.stderr.toString() };
}
describe('database-url-guard-preload (#3485)', () => {
test('refuses when DATABASE_URL is set', () => {
const r = runProbe({ DATABASE_URL: 'postgresql://localhost:5434/gbrain' });
expect(r.exitCode).not.toBe(0);
expect(r.stderr).toContain(GUARD_MARKER);
expect(r.stderr).toContain('DATABASE_URL');
}, 30_000);
test('refuses when GBRAIN_DATABASE_URL is set', () => {
const r = runProbe({ GBRAIN_DATABASE_URL: 'postgresql://localhost:5434/gbrain' });
expect(r.exitCode).not.toBe(0);
expect(r.stderr).toContain(GUARD_MARKER);
expect(r.stderr).toContain('GBRAIN_DATABASE_URL');
}, 30_000);
test('refuses and names both when both are set', () => {
const r = runProbe({
DATABASE_URL: 'postgresql://localhost:5434/gbrain',
GBRAIN_DATABASE_URL: 'postgresql://localhost:5434/gbrain',
});
expect(r.exitCode).not.toBe(0);
expect(r.stderr).toContain(GUARD_MARKER);
expect(r.stderr).toContain('DATABASE_URL and GBRAIN_DATABASE_URL are set');
}, 30_000);
test('allows with GBRAIN_TEST_ALLOW_DATABASE_URL=1', () => {
const r = runProbe({
DATABASE_URL: 'postgresql://localhost:5434/gbrain',
GBRAIN_TEST_ALLOW_DATABASE_URL: '1',
});
expect(r.stderr).not.toContain(GUARD_MARKER);
expect(r.exitCode).toBe(0);
}, 30_000);
test('runs clean with no database URL ambient', () => {
const r = runProbe({});
expect(r.stderr).not.toContain(GUARD_MARKER);
expect(r.exitCode).toBe(0);
}, 30_000);
test('refuses truthy-but-wrong override values (strict === "1")', () => {
// A future loosening to Boolean(process.env...) must fail this test:
// only the exact string '1' opts in.
const r = runProbe({
DATABASE_URL: 'postgresql://localhost:5434/gbrain',
GBRAIN_TEST_ALLOW_DATABASE_URL: 'true',
});
expect(r.exitCode).not.toBe(0);
expect(r.stderr).toContain(GUARD_MARKER);
}, 30_000);
test('treats empty-string values as unset (v !== "" branch)', () => {
// `export DATABASE_URL=` (empty) is a plausible dev state after a manual
// unset attempt; the guard deliberately does not refuse on it — an empty
// string cannot name a database to destroy.
const r = runProbe({ DATABASE_URL: '', GBRAIN_DATABASE_URL: '' });
expect(r.stderr).not.toContain(GUARD_MARKER);
expect(r.exitCode).toBe(0);
}, 30_000);
});
+249
View File
@@ -0,0 +1,249 @@
/**
* Coverage gate for the destructive-SQL guard.
*
* Any test file that connects a PostgresEngine to DATABASE_URL can TRUNCATE or
* DELETE from whatever brain that URL points at. setupDB() calls
* assertSafeE2eDatabaseUrl() before it does so; files that connect directly
* bypass that check unless they call the guard themselves.
*
* This test scans the suite and fails when a file connects to DATABASE_URL
* without either going through setupDB() or calling the guard. Static scan
* only: no database connection, no DATABASE_URL required.
*
* KNOWN BLIND SPOTS (documented, not silent): a test that only passes the
* ambient URL into a SPAWNED subprocess (gbrain CLI) has no in-file connect
* idiom and is invisible here — the preload guard and the spawned CLI's own
* config path are the layers that cover it. Destructive helpers living in
* non-test files (other than setupDB) are likewise unscanned.
*/
import { describe, test, expect } from 'bun:test';
import { readdirSync, readFileSync } from 'fs';
import { join, resolve, relative } from 'path';
// Walk from the REPO root, not test/: a bare `bun test` at the repo root
// discovers *.test.ts repo-wide (evals/, examples/, …), so a test/-only scan
// would be a permanent blind spot for destructive tests outside test/.
const REPO_ROOT = resolve(import.meta.dir, '..');
function walk(dir: string, out: string[] = []): string[] {
// fixtures/ IS scanned: `bun test` discovers *.test.ts there too, so an
// exemption would be a blind spot. Only node_modules (never collected by
// bun) and .git are skipped.
for (const entry of readdirSync(dir, { withFileTypes: true })) {
if (entry.name === 'node_modules' || entry.name === '.git') continue;
const full = join(dir, entry.name);
if (entry.isDirectory()) walk(full, out);
// Match every pattern bun test collects (.test/.spec/.bench and _test/_spec,
// all JS/TS extensions) — a destructive foo.spec.ts must not evade the scan.
// Skip this file itself: the classifier positive-control fixtures below
// are string literals that would otherwise scan as real test code.
else if (
/(\.(test|spec|bench)|_(test|spec))\.(ts|tsx|js|jsx|mjs|cjs)$/.test(entry.name) &&
full !== import.meta.path
) out.push(full);
}
return out;
}
/**
* Reads an ambient database URL AND constructs a connection path. Detection is
* at the ASSIGNMENT site (any binding name), not a connect-site identifier
* allowlist — `const url = process.env.DATABASE_URL!; connect({ database_url:
* url })` and GBRAIN_DATABASE_URL spellings must not evade the scan
* (fail-closed: prefer flagging a guarded file over missing an unguarded one).
*/
/**
* Yields only non-comment lines: skips `//` lines, tracks multi-line block
* comments statefully (a block comment written WITHOUT leading `*` per line
* must still be excluded — the fail-open direction), and JSDoc `*` lines.
*/
function* codeLines(src: string): Generator<string> {
let inBlock = false;
for (const line of src.split('\n')) {
if (inBlock) {
if (/\*\//.test(line)) inBlock = false;
continue;
}
if (/^\s*\/\*/.test(line)) {
if (!/\*\//.test(line)) inBlock = true;
continue;
}
if (/^\s*(\/\/|\*)/.test(line)) continue;
yield line;
}
}
function readsAmbientDatabaseUrl(src: string): boolean {
for (const line of codeLines(src)) {
// A `delete` of the env var is a scrub, not a read — files that explicitly
// REMOVE the ambient URL before building fixture engines must not be
// classified as connecting to it. (The regex is split from its match
// target here so the R1 isolation lint doesn't read this scanner's own
// pattern as a mutation.)
if (/delete\s+process\.env/.test(line)) continue;
if (/process\.env\.(GBRAIN_)?DATABASE_URL\b/.test(line)) return true;
// bracket notation: process.env['DATABASE_URL'] / ["GBRAIN_DATABASE_URL"]
if (/process\.env\[\s*['"`](GBRAIN_)?DATABASE_URL['"`]\s*\]/.test(line)) return true;
}
return false;
}
function connectsToAmbientDatabaseUrl(src: string): boolean {
if (!readsAmbientDatabaseUrl(src)) return false;
return /new\s+PostgresEngine\s*\(/.test(src)
|| /\bdb\.connect\s*\(/.test(src)
// engine-factory idiom (env-inferred postgres) and raw postgres.js clients
|| /\bcreateEngine\s*\(/.test(src)
|| /\bpostgres\s*\(\s*[A-Za-z_$'"`]/.test(src);
}
/** Runs SQL that destroys data or schema. initSchema() counts: it migrates. */
function runsDestructiveSql(src: string): boolean {
return /\b(TRUNCATE|DELETE\s+FROM|DROP\s+(TABLE|SCHEMA|INDEX|TRIGGER|FUNCTION|DATABASE|EXTENSION|OWNED)|ALTER\s+TABLE\s+\w+\s+DROP)\b/i.test(src)
|| /\.initSchema\s*\(/.test(src);
}
/**
* Guarded either by the shared helper, by setupDB() (which calls it), or by an
* inline db-name floor. schema-drift.test.ts uses the last form: its pattern is
* deliberately different from the shared one (it also accepts *_e2e), so it is
* recognized rather than rewritten.
*
* Comment-hardened: a guard token on a comment line (`// unlike setupDB() ...`)
* must NOT count — this classifier is the exempting (fail-open) direction, so
* it only accepts tokens on non-comment lines. Residual limitation: a trailing
* comment after real code on the same line still counts that line.
*/
function isGuarded(src: string): boolean {
for (const line of codeLines(src)) {
if (/\b(assertSafeE2eDatabaseUrl|setupDB)\s*\(/.test(line)) return true;
if (/looksLikeTestDb/.test(line)) return true;
}
return false;
}
function scan(): { unguarded: string[]; guarded: string[] } {
const unguarded: string[] = [];
const guarded: string[] = [];
for (const file of walk(REPO_ROOT)) {
const src = readFileSync(file, 'utf-8');
if (!connectsToAmbientDatabaseUrl(src)) continue;
if (!runsDestructiveSql(src)) continue;
(isGuarded(src) ? guarded : unguarded).push(relative(REPO_ROOT, file));
}
return { unguarded, guarded };
}
describe('destructive-SQL guard coverage', () => {
test('every test that runs destructive SQL on DATABASE_URL is guarded', () => {
expect(scan().unguarded).toEqual([]);
});
test('positive control: the scan actually detects known destructive files', () => {
// Guards against regex rot: if connectsToAmbientDatabaseUrl or
// runsDestructiveSql stopped matching anything, the gate above would pass
// vacuously forever. These two files connect to the ambient URL, run
// destructive SQL, and call the guard — the scan must classify them so.
const { guarded } = scan();
expect(guarded).toContain('test/e2e/multimodal-postgres.test.ts');
expect(guarded).toContain('test/phantom-redirect-engine-parity.test.ts');
});
});
describe('scan classifiers (the gate must be able to fire)', () => {
test('connectsToAmbientDatabaseUrl matches every ambient spelling in the suite', () => {
const spellings = [
`engine = new PostgresEngine();\nawait engine.connect({ database_url: process.env.DATABASE_URL });`,
`const DATABASE_URL = process.env.DATABASE_URL;\nconst engine = new PostgresEngine();\nawait engine.connect({ database_url: DATABASE_URL! });`,
`const dbUrl = process.env.DATABASE_URL;\npg = new PostgresEngine();\nawait pg.connect({ database_url: dbUrl } as never);`,
// arbitrary binding name — the exact idiom the old allowlist missed
`const url = process.env.DATABASE_URL!;\nengine = new PostgresEngine();\nawait engine.connect({ database_url: url });`,
// GBRAIN_DATABASE_URL is honored by the runtime and must not evade the scan
`const u = process.env.GBRAIN_DATABASE_URL;\nconst engine = new PostgresEngine();\nawait engine.connect({ database_url: u });`,
// db.connect() path (no engine construction in the test file itself)
`const url = process.env.DATABASE_URL;\nawait db.connect({ database_url: url });`,
];
for (const src of spellings) expect(connectsToAmbientDatabaseUrl(src)).toBe(true);
});
test('connectsToAmbientDatabaseUrl ignores fixture-URL connections and non-connecting files', () => {
expect(
connectsToAmbientDatabaseUrl(
`const engine = new PostgresEngine();\nawait engine.connect({ database_url: fixtureUrl });`,
),
).toBe(false);
expect(
connectsToAmbientDatabaseUrl(`const url = process.env.DATABASE_URL; // read, no engine/connect`),
).toBe(false);
});
test('runsDestructiveSql matches each destructive arm and not plain reads', () => {
for (const src of [
'await engine.executeRaw(`TRUNCATE pages`);',
"await engine.executeRaw('DELETE FROM pages');",
'await engine.executeRaw(`DROP TABLE takes`);',
'await engine.executeRaw(`ALTER TABLE pages DROP COLUMN x`);',
'await engine.initSchema();',
]) {
expect(runsDestructiveSql(src)).toBe(true);
}
expect(runsDestructiveSql('await engine.executeRaw(`SELECT 1`);')).toBe(false);
});
test('isGuarded recognizes each accepted guard form and nothing else', () => {
expect(isGuarded('assertSafeE2eDatabaseUrl(url);')).toBe(true);
expect(isGuarded('await setupDB();')).toBe(true);
expect(isGuarded('if (!looksLikeTestDb(name)) return;')).toBe(true);
expect(isGuarded('// totally unguarded')).toBe(false);
});
test('isGuarded rejects comment-only guard mentions (fail-open hardening)', () => {
expect(isGuarded('// unlike setupDB() we connect directly')).toBe(false);
expect(isGuarded('/* assertSafeE2eDatabaseUrl( would go here */')).toBe(false);
expect(isGuarded(' * setupDB() runs SCHEMA_SQL — JSDoc mention')).toBe(false);
// multi-line block comment WITHOUT leading * per line — must not count
expect(isGuarded('/*\nsetupDB() would be wrong here\n*/\nconnect();')).toBe(false);
// real call on a code line still counts even with comments elsewhere
expect(isGuarded('// intro comment\nawait setupDB();')).toBe(true);
expect(isGuarded('/* block */\nawait setupDB();')).toBe(true);
});
test('readsAmbientDatabaseUrl catches bracket notation and skips scrubs', () => {
expect(
connectsToAmbientDatabaseUrl(
`const u = process.env['DATABASE_URL'];\nconst engine = new PostgresEngine();`,
),
).toBe(true);
// Fixture built by concatenation: the classifier must see the contiguous
// scrub statement, but the R1 isolation lint must not read this test's
// own fixture as a real env mutation.
const scrub = 'delete ' + 'process.env.';
expect(
connectsToAmbientDatabaseUrl(
`${scrub}DATABASE_URL;\n${scrub}GBRAIN_DATABASE_URL;\nconst e = await createEngine(cfg);`,
),
).toBe(false);
});
test('runsDestructiveSql catches the non-TABLE DROP arms', () => {
for (const src of [
'await sql.unsafe(`DROP TRIGGER IF EXISTS t ON oauth_tokens`);',
'await sql.unsafe(`DROP FUNCTION IF EXISTS f()`);',
"psqlish('DROP DATABASE gbrain');",
]) {
expect(runsDestructiveSql(src)).toBe(true);
}
});
test('an unguarded destructive source would be flagged end-to-end', () => {
const bad = [
`const engine = new PostgresEngine();`,
`await engine.connect({ database_url: process.env.DATABASE_URL });`,
`await engine.executeRaw('TRUNCATE pages');`,
].join('\n');
expect(connectsToAmbientDatabaseUrl(bad)).toBe(true);
expect(runsDestructiveSql(bad)).toBe(true);
expect(isGuarded(bad)).toBe(false);
});
});
@@ -25,6 +25,7 @@ import { loadCorpusPages } from '../helpers/bootstrap-corpus.ts';
import { runEmbedCore } from '../../src/commands/embed.ts';
import { hybridSearch } from '../../src/core/search/hybrid.ts';
import { runSchemaTransition } from '../../src/core/retrieval-upgrade-planner.ts';
import { assertSafeE2eDatabaseUrl } from '../helpers/db-guard.ts';
import { extractTakesFromPages } from '../../src/core/extract-takes-from-pages.ts';
import { configureGateway, resetGateway } from '../../src/core/ai/gateway.ts';
import {
@@ -282,6 +283,7 @@ describe.skipIf(!DATABASE_URL)('Postgres bootstrap verify (real Postgres)', () =
process.env.GBRAIN_HOME = root;
engine = new PostgresEngine();
assertSafeE2eDatabaseUrl(DATABASE_URL!);
await engine.connect({ database_url: DATABASE_URL! });
await engine.initSchema();
await addSource(engine, { id: 'workspace', localPath: join(ws, 'brain'), force: true });
@@ -20,6 +20,7 @@ import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
import { PostgresEngine } from '../../src/core/postgres-engine.ts';
import { quoteIdentifier } from '../../src/core/search/embedding-column.ts';
import type { ResolvedColumn } from '../../src/core/types.ts';
import { assertSafeE2eDatabaseUrl } from '../helpers/db-guard.ts';
const dbUrl = process.env.DATABASE_URL;
if (!dbUrl) {
@@ -33,6 +34,7 @@ if (!dbUrl) {
beforeAll(async () => {
engine = new PostgresEngine();
assertSafeE2eDatabaseUrl(dbUrl!);
await engine.connect({ database_url: dbUrl } as never);
await engine.initSchema();
+2
View File
@@ -15,6 +15,7 @@
import { afterAll, beforeAll, beforeEach, describe, expect, test } from 'bun:test';
import { PostgresEngine } from '../../src/core/postgres-engine.ts';
import type { EvalCandidateInput } from '../../src/core/types.ts';
import { assertSafeE2eDatabaseUrl } from '../helpers/db-guard.ts';
const DATABASE_URL = process.env.DATABASE_URL;
@@ -26,6 +27,7 @@ beforeAll(async () => {
return;
}
engine = new PostgresEngine();
assertSafeE2eDatabaseUrl(DATABASE_URL!);
await engine.connect({ database_url: DATABASE_URL });
await engine.initSchema();
});
@@ -24,6 +24,7 @@ import { writeRunRow, loadTrend } from '../../src/core/eval-contradictions/trend
import { JudgeCache, buildCacheKey } from '../../src/core/eval-contradictions/cache.ts';
import type { ProbeReport } from '../../src/core/eval-contradictions/types.ts';
import { operationsByName, type OperationContext } from '../../src/core/operations.ts';
import { assertSafeE2eDatabaseUrl } from '../helpers/db-guard.ts';
const DATABASE_URL = process.env.DATABASE_URL;
@@ -35,6 +36,7 @@ beforeAll(async () => {
return;
}
engine = new PostgresEngine();
assertSafeE2eDatabaseUrl(DATABASE_URL!);
await engine.connect({ database_url: DATABASE_URL });
await engine.initSchema();
});
@@ -2,6 +2,7 @@ import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
import { PostgresEngine } from '../../src/core/postgres-engine.ts';
import { runExtractFacts } from '../../src/core/cycle/extract-facts.ts';
import { parseFactsFence, renderFactsTable, type ParsedFact } from '../../src/core/facts-fence.ts';
import { assertSafeE2eDatabaseUrl } from '../helpers/db-guard.ts';
const databaseUrl = process.env.DATABASE_URL;
const skip = !databaseUrl;
@@ -14,6 +15,7 @@ describe.skipIf(skip)('facts-fence escaped-pipe reconciliation on Postgres', ()
beforeAll(async () => {
engine = new PostgresEngine();
assertSafeE2eDatabaseUrl(databaseUrl!);
await engine.connect({ database_url: databaseUrl! });
await engine.initSchema();
});
+5 -31
View File
@@ -13,6 +13,7 @@ import { PostgresEngine } from '../../src/core/postgres-engine.ts';
import * as db from '../../src/core/db.ts';
import { importFromContent } from '../../src/core/import-file.ts';
import { parseMarkdown } from '../../src/core/markdown.ts';
import { assertSafeE2eDatabaseUrl } from '../helpers/db-guard.ts';
// Load .env.testing if present
const envPath = resolve(import.meta.dir, '../../.env.testing');
@@ -67,38 +68,11 @@ export function hasDatabase(): boolean {
}
/**
* Production guard: setupDB() TRUNCATEs every data table on whatever
* DATABASE_URL points at, and run-e2e.sh deliberately preserves an exported
* DATABASE_URL — so a developer with a production URL in their environment
* would wipe their real brain by running the suite. Refuse unless the
* database name identifies itself as a test database ("test" as a word
* segment, e.g. gbrain_test — the CI/.env.testing.example convention), or
* the operator explicitly opts the exact name in via GBRAIN_E2E_ALLOW_DB.
*
* Exported for unit testing; pure — no connection is made.
* Production guard, moved to test/helpers/db-guard.ts so test files outside
* test/e2e/ can import it without loading this module. Re-exported here for
* existing call sites (setupDB below, test/e2e/db-guard.test.ts).
*/
export function assertSafeE2eDatabaseUrl(
url: string,
env: Record<string, string | undefined> = process.env,
): void {
let dbName: string;
try {
dbName = decodeURIComponent(new URL(url).pathname.replace(/^\//, ''));
} catch {
throw new Error(`E2E guard: DATABASE_URL is not a parseable URL; refusing to run destructive setup.`);
}
if (!dbName) {
throw new Error(`E2E guard: DATABASE_URL has no database name; refusing to run destructive setup.`);
}
if (/(^|[_-])test([_-]|$)/i.test(dbName)) return;
if (env.GBRAIN_E2E_ALLOW_DB && env.GBRAIN_E2E_ALLOW_DB === dbName) return;
throw new Error(
`E2E guard: database "${dbName}" does not look like a test database ` +
`(expected "test" as a name segment, e.g. gbrain_test). setupDB() would ` +
`TRUNCATE every data table in it. If this is intentional, set ` +
`GBRAIN_E2E_ALLOW_DB=${dbName} to opt in explicitly.`,
);
}
export { assertSafeE2eDatabaseUrl };
/**
* Connect to DB, run schema init, truncate all tables.
@@ -14,6 +14,7 @@ import { afterAll, afterEach, beforeAll, describe, expect, test } from 'bun:test
import { randomUUIDv7 } from 'bun';
import { PostgresEngine } from 'gbrain';
import { BudgetExceededError, reserve } from '../../src/core/minions/budget-meter.ts';
import { assertSafeE2eDatabaseUrl } from '../helpers/db-guard.ts';
const databaseUrl = process.env.DATABASE_URL;
const describePostgres = databaseUrl ? describe : describe.skip;
@@ -24,6 +25,7 @@ describePostgres('MCP spend reservation — Postgres concurrency', () => {
beforeAll(async () => {
engine = new PostgresEngine();
assertSafeE2eDatabaseUrl(databaseUrl!);
await engine.connect({ database_url: databaseUrl!, poolSize: 16 });
await engine.initSchema();
});
+2
View File
@@ -15,6 +15,7 @@
import { afterAll, beforeAll, beforeEach, describe, expect, test } from 'bun:test';
import { PostgresEngine } from '../../src/core/postgres-engine.ts';
import { assertSafeE2eDatabaseUrl } from '../helpers/db-guard.ts';
const DATABASE_URL = process.env.DATABASE_URL;
const skip = !DATABASE_URL;
@@ -28,6 +29,7 @@ describe.skipIf(skip)('multimodal v0.27.1 against real Postgres', () => {
beforeAll(async () => {
pg = new PostgresEngine();
assertSafeE2eDatabaseUrl(DATABASE_URL!);
await pg.connect({ database_url: DATABASE_URL! });
await pg.initSchema();
}, 60_000);
+2
View File
@@ -25,6 +25,7 @@
import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
import { PostgresEngine } from '../../src/core/postgres-engine.ts';
import { LATEST_VERSION } from '../../src/core/migrate.ts';
import { assertSafeE2eDatabaseUrl } from '../helpers/db-guard.ts';
const DATABASE_URL = process.env.DATABASE_URL;
const skip = !DATABASE_URL;
@@ -34,6 +35,7 @@ describe.skipIf(skip)('PostgresEngine forward-reference bootstrap (E2E)', () =>
beforeAll(async () => {
engine = new PostgresEngine();
assertSafeE2eDatabaseUrl(DATABASE_URL!);
await engine.connect({ database_url: DATABASE_URL! });
}, 30_000);
+7
View File
@@ -15,8 +15,15 @@
import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
import { createHash } from 'crypto';
import { hasDatabase } from './helpers.ts';
import { assertSafeE2eDatabaseUrl } from '../helpers/db-guard.ts';
const skip = !hasDatabase();
// #3485 name floor: this suite opens raw postgres() clients on the ambient URL
// and runs DROP TRIGGER/FUNCTION + DELETE cleanups — refuse non-test-shaped
// database names before any connection is made.
if (!skip) {
assertSafeE2eDatabaseUrl(process.env.GBRAIN_DATABASE_URL || process.env.DATABASE_URL || '');
}
const describeE2E = skip ? describe.skip : describe;
if (skip) {
+13
View File
@@ -0,0 +1,13 @@
/**
* Probe fixture for test/database-url-guard-preload.test.ts.
*
* Deliberately trivial: the parent test spawns `bun test` on this file with
* controlled env and asserts on the RUN's exit code — the preload guard fires
* (or doesn't) before this file executes. If the guard refuses, this test
* never runs; if the guard allows, this passes and the run exits 0.
*/
import { test, expect } from 'bun:test';
test('probe: run started, preload guard allowed it', () => {
expect(true).toBe(true);
});
@@ -0,0 +1,59 @@
/**
* Invocation-level guard: refuse to start a test run while a database URL is
* ambient in the environment, unless the invoker explicitly opted in.
*
* Why this exists (#3485): `gbrain init` writes DATABASE_URL into
* ~/.gbrain/.env pointing at the developer's real brain. Several test files
* connect to whatever DATABASE_URL names and run TRUNCATE/DELETE/DROP — a
* bare `bun test` with that env sourced has destroyed a real brain before.
* The per-file name floor (test/helpers/db-guard.ts) is the second layer;
* this preload is the first: no test file loads at all while an unexpected
* database URL is present.
*
* Both DATABASE_URL and GBRAIN_DATABASE_URL are checked — the runtime honors
* both (src/core/config.ts), so guarding only one leaves a live path.
*
* HARD-FAIL, never silently unset: silently clearing the var would turn
* DATABASE_URL-gated e2e tests into green skips, hiding the exact class of
* bug (#2339) those tests exist to catch. The e2e wrappers (scripts/
* run-e2e.sh, the e2e/heavy workflows) set GBRAIN_TEST_ALLOW_DATABASE_URL=1
* at their own subprocess boundary; nothing sets it globally.
*
* Registered FIRST in bunfig.toml's [test].preload so it runs before any
* other preload does work.
*/
const ALLOW = process.env.GBRAIN_TEST_ALLOW_DATABASE_URL === '1';
if (!ALLOW) {
const offending = (['DATABASE_URL', 'GBRAIN_DATABASE_URL'] as const).filter(
(name) => {
const v = process.env[name];
return v !== undefined && v !== '';
},
);
if (offending.length > 0) {
console.error(
[
`TEST-RUN GUARD: refusing to start — ${offending.join(' and ')} ${offending.length === 1 ? 'is' : 'are'} set.`,
``,
`Some tests run destructive SQL (TRUNCATE/DELETE/DROP) against whatever`,
`these URLs point at. A bare \`bun test\` with a real brain's URL in the`,
`environment has wiped that brain before (#3485).`,
``,
`Pick one:`,
` - unset the variable${offending.length > 1 ? 's' : ''} and re-run (unit tests need no database).`,
` If it keeps coming back, check for a .env file in this directory or a`,
` sourced ~/.gbrain/.env — bun auto-loads cwd .env into the test run.`,
` - run the e2e suite through its wrapper: \`bun run test:e2e\``,
` (the wrapper opts in at its own boundary), or`,
` - if you really mean to run tests against this database, opt in`,
` ONE-SHOT: \`GBRAIN_TEST_ALLOW_DATABASE_URL=1 bun test ...\` — avoid a`,
` shell-profile export, which would permanently disarm this guard. The`,
` per-file guard (test/helpers/db-guard.ts) still requires a test-shaped`,
` database name after that.`,
].join('\n'),
);
process.exit(1);
}
}
+41
View File
@@ -0,0 +1,41 @@
/**
* Production guard for tests that run destructive SQL against DATABASE_URL.
*
* setupDB() TRUNCATEs every data table on whatever DATABASE_URL points at, and
* run-e2e.sh deliberately preserves an exported DATABASE_URL, so a developer
* with a production URL in their environment would wipe their real brain by
* running the suite. Test files that connect a PostgresEngine directly, without
* going through setupDB(), carry the same risk and must call this themselves.
*
* Refuse unless the database name identifies itself as a test database ("test"
* as a word segment, e.g. gbrain_test, the CI/.env.testing.example convention),
* or the operator explicitly opts the exact name in via GBRAIN_E2E_ALLOW_DB.
*
* Lives in its own leaf module so unit-directory tests can import it without
* pulling in test/e2e/helpers.ts, which loads .env.testing and the engines at
* import time. test/e2e/helpers.ts re-exports it for existing call sites.
*
* Pure: no connection is made.
*/
export function assertSafeE2eDatabaseUrl(
url: string,
env: Record<string, string | undefined> = process.env,
): void {
let dbName: string;
try {
dbName = decodeURIComponent(new URL(url).pathname.replace(/^\//, ''));
} catch {
throw new Error(`E2E guard: DATABASE_URL is not a parseable URL; refusing to run destructive setup.`);
}
if (!dbName) {
throw new Error(`E2E guard: DATABASE_URL has no database name; refusing to run destructive setup.`);
}
if (/(^|[_-])test([_-]|$)/i.test(dbName)) return;
if (env.GBRAIN_E2E_ALLOW_DB && env.GBRAIN_E2E_ALLOW_DB === dbName) return;
throw new Error(
`E2E guard: database "${dbName}" does not look like a test database ` +
`(expected "test" as a name segment, e.g. gbrain_test). This test runs ` +
`destructive SQL against it. If this is intentional, set ` +
`GBRAIN_E2E_ALLOW_DB=${dbName} to opt in explicitly.`,
);
}
@@ -0,0 +1,65 @@
/**
* Snapshot-timezone parity pin.
*
* dumpDataDir bakes the BUILD process's TimeZone into the snapshot tar's
* cluster defaults. Un-pinned, a snapshot-restored engine ran sessions in the
* build machine's zone while cold-init engines follow the runtime process
* (bun test pins TZ=UTC) — so every naive-timestamp day-boundary comparison
* shifted by the offset, and date-dependent tests failed only in the evening,
* only under GBRAIN_PGLITE_SNAPSHOT. Two fixes hold the line: the build
* script pins TZ=UTC before dumping, and the engine re-pins the session to
* the runtime zone on snapshot restore.
*
* This test is the deterministic pin: at ANY wall-clock hour, a cold engine
* and a snapshot engine created by the same process must report the same
* session TimeZone. Serial file: it mutates GBRAIN_PGLITE_SNAPSHOT (R1).
*/
import { describe, test, expect, afterAll } from 'bun:test';
import { existsSync } from 'fs';
import { PGLiteEngine } from '../src/core/pglite-engine.ts';
const SNAPSHOT = 'test/fixtures/pglite-snapshot.tar';
async function sessionUtcOffsetSeconds(env: Record<string, string | undefined>): Promise<number> {
const prev = process.env.GBRAIN_PGLITE_SNAPSHOT;
if (env.GBRAIN_PGLITE_SNAPSHOT === undefined) delete process.env.GBRAIN_PGLITE_SNAPSHOT;
else process.env.GBRAIN_PGLITE_SNAPSHOT = env.GBRAIN_PGLITE_SNAPSHOT;
const engine = new PGLiteEngine();
try {
await engine.connect({} as never);
await engine.initSchema();
// Compare the effective UTC OFFSET, not the zone label: cold init spells
// the runtime zone one way (Etc/GMT0), the restore re-pin another (UTC) —
// the invariant is identical instant arithmetic, not identical strings.
const rows = await engine.executeRaw<{ off: string }>(
`SELECT extract(timezone FROM now())::text AS off`,
);
return Number(rows[0].off);
} finally {
await engine.disconnect?.();
if (prev === undefined) delete process.env.GBRAIN_PGLITE_SNAPSHOT;
else process.env.GBRAIN_PGLITE_SNAPSHOT = prev;
}
}
describe('PGLite snapshot timezone parity', () => {
// Build the fixture in-test (idempotent hash short-circuit makes re-runs
// cheap) so this pin cannot silently skip in lanes without a prebuilt tar.
test('snapshot fixture builds', async () => {
const proc = Bun.spawnSync(['bun', 'run', 'build:pglite-snapshot'], {
cwd: `${import.meta.dir}/..`,
stdout: 'pipe',
stderr: 'pipe',
timeout: 300_000,
killSignal: 'SIGKILL',
});
expect(proc.exitCode).toBe(0);
expect(existsSync(`${import.meta.dir}/../${SNAPSHOT}`)).toBe(true);
}, 320_000);
test('snapshot-restored session UTC offset equals cold-init session UTC offset', async () => {
const cold = await sessionUtcOffsetSeconds({ GBRAIN_PGLITE_SNAPSHOT: undefined });
const snap = await sessionUtcOffsetSeconds({ GBRAIN_PGLITE_SNAPSHOT: SNAPSHOT });
expect(snap).toBe(cold);
}, 120_000);
});
@@ -14,6 +14,7 @@ import { PGLiteEngine } from '../src/core/pglite-engine.ts';
import { PostgresEngine } from '../src/core/postgres-engine.ts';
import { resetPgliteState } from './helpers/reset-pglite.ts';
import type { BrainEngine } from '../src/core/engine.ts';
import { assertSafeE2eDatabaseUrl } from './helpers/db-guard.ts';
let pglite: PGLiteEngine;
let pg: PostgresEngine | null = null;
@@ -25,6 +26,7 @@ beforeAll(async () => {
if (process.env.DATABASE_URL) {
pg = new PostgresEngine();
assertSafeE2eDatabaseUrl(process.env.DATABASE_URL!);
await pg.connect({ database_url: process.env.DATABASE_URL });
await pg.initSchema();
}
+15
View File
@@ -38,6 +38,21 @@ The runner is `scripts/run-heavy.sh`. It discovers every `tests/heavy/*.sh`
file at this directory's top level (NOT recursive), runs them in lexical
order, fails on the first non-zero exit.
## Database name floor (#3485)
Heavy scripts run destructive operations (schema drops, migration replays,
parallel syncs) against whatever database URL the environment names. The
runner sources `tests/heavy/_db_floor.sh` once for the whole lane, and every
database-touching script sources it itself: it refuses (exit 2) unless the
database name in `DATABASE_URL` / `GBRAIN_DATABASE_URL` carries "test" as a
word segment (e.g. `gbrain_test`), or the exact name is opted in one-shot via
`GBRAIN_E2E_ALLOW_DB=<name>`. The PGLite-based scripts (`measure_rss.sh`,
`read_latency_under_sync.sh`, `sync_timeout_rescue.sh`) unset the URL instead.
When adding a script that touches the database, add
`source "$(dirname "$0")/_db_floor.sh"` before it does — scripts are
documented for direct invocation, so the runner-level floor alone is not
enough.
## Naming convention
- `tests/heavy/<name>.sh` — top-level test script, picked up by the runner.
+4
View File
@@ -40,6 +40,10 @@ if [ -z "${DATABASE_URL:-}" ]; then
exit 2
fi
# #3485 name floor (shared): this script DROPs the public schema of whatever
# DATABASE_URL names — refuse non-test-shaped names without an explicit opt-in.
source "$(dirname "$0")/_db_floor.sh"
SQL_FILE="tests/heavy/fixtures/down-mutate-${SHAPE}.sql"
if [ ! -f "$SQL_FILE" ]; then
echo "[build_legacy_fixtures] no fixture for shape '$SHAPE' at $SQL_FILE" >&2
+49
View File
@@ -0,0 +1,49 @@
#!/usr/bin/env bash
# #3485 shared shell name floor, mirrored from test/helpers/db-guard.ts.
#
# Heavy-lane scripts run destructive operations (DROP SCHEMA, source-registry
# UPSERTs, initSchema migration replay, parallel syncs) against whatever
# database URL the resolved config names. The bunfig preload guard is
# bun-test-only — shell lanes need their own floor. Source this file AFTER
# confirming DATABASE_URL is set:
#
# source "$(dirname "$0")/_db_floor.sh" # exits 2 unless every set db-url
# # var is test-shaped or allowed
#
# BOTH variables are floored: heavy scripts shell out to the real CLI
# (gbrain doctor / gbrain sync), and src/core/config.ts resolves
# GBRAIN_DATABASE_URL ABOVE DATABASE_URL — flooring only one would let the
# other reach a real brain. Query strings are stripped BEFORE the name is
# extracted: `?host=/tmp/test-sockets` must not smuggle a test-shaped segment
# past the floor (the last path segment of the pre-query URL is the db name).
#
# Refuses unless the database name carries "test" as a word segment
# (gbrain_test) or the operator opts the exact name in via GBRAIN_E2E_ALLOW_DB
# (prefer one-shot inline usage — GBRAIN_E2E_ALLOW_DB=name ./script — over a
# shell-profile export, which would permanently disarm the floor).
_db_floor_check() {
_db_floor_url="$1"
_db_floor_var="$2"
[ -z "$_db_floor_url" ] && return 0
_db_floor_prequery="${_db_floor_url%%\?*}"
_db_floor_name="${_db_floor_prequery##*/}"
if [ -z "$_db_floor_name" ]; then
echo "[db-floor] REFUSING: $_db_floor_var has no database name" >&2
exit 2
fi
if ! printf '%s' "$_db_floor_name" | grep -qiE '(^|[_-])test([_-]|$)'; then
if [ "${GBRAIN_E2E_ALLOW_DB:-}" != "$_db_floor_name" ]; then
echo "[db-floor] REFUSING: $_db_floor_var database \"$_db_floor_name\" does not look like a test database" >&2
echo " (expected \"test\" as a name segment, e.g. gbrain_test). Heavy-lane scripts run" >&2
echo " destructive operations against it. If intentional, opt in one-shot:" >&2
echo " GBRAIN_E2E_ALLOW_DB=$_db_floor_name <command>" >&2
exit 2
fi
fi
}
_db_floor_check "${DATABASE_URL:-}" DATABASE_URL
_db_floor_check "${GBRAIN_DATABASE_URL:-}" GBRAIN_DATABASE_URL
unset -f _db_floor_check 2>/dev/null || true
unset _db_floor_url _db_floor_var _db_floor_prequery _db_floor_name 2>/dev/null || true
@@ -30,6 +30,10 @@
set -euo pipefail
# #3485: shared name floor — these scripts mutate whatever DATABASE_URL names
# and are documented for direct invocation, so each sources the floor itself.
source "$(dirname "$0")/_db_floor.sh"
cd "$(dirname "$0")/../.."
WALLCLOCK_BUDGET_S="${WALLCLOCK_BUDGET_S:-15}"
+4
View File
@@ -33,6 +33,10 @@
set -euo pipefail
# #3485: shared name floor — these scripts mutate whatever DATABASE_URL names
# and are documented for direct invocation, so each sources the floor itself.
source "$(dirname "$0")/_db_floor.sh"
cd "$(dirname "$0")/../.."
if [ -z "${DATABASE_URL:-}" ]; then
+4
View File
@@ -17,6 +17,10 @@
set -euo pipefail
# #3485: shared name floor — these scripts mutate whatever DATABASE_URL names
# and are documented for direct invocation, so each sources the floor itself.
source "$(dirname "$0")/_db_floor.sh"
cd "$(dirname "$0")/../.."
if [ -z "${DATABASE_URL:-}" ]; then