Files
gbrain/test/fix-wave-structural.test.ts
T
6f26d5e4df v0.41.37.0 fix: critical fix wave — reindex tag wipe, grandfather hang, Windows migration spawn, sync ReDoS (#1621 #1581 #1605 #1569) (#1665)
* fix(reindex): add-only tag reconciliation + DB-only re-chunk preserves frontmatter (#1621)

reindex --markdown and re-import no longer wipe DB-side enrichment tags.
Tag reconciliation is now ADD-ONLY (import-file.ts): re-import adds current
frontmatter tags and never deletes, so auto/dream/signal-detector tags survive.
The reindex DB-only fallback reconstructs full markdown via serializeMarkdown
so re-chunking a page with no on-disk source preserves frontmatter/title/timeline.

* fix(migrations): v0.13.1 grandfather chunked, source-safe, soft-delete-filtered (#1581)

phaseCGrandfather rewritten from a per-page getPage+putPage loop (which hung
70+ min on an 82K-page PGLite brain) to a chunked bulk SQL pass keyed on
pages.id (NOT slug — slug isn't globally unique), filtering deleted_at IS NULL,
with a batched rollback log carrying source identity.

* fix(migrations): run schema phases in-process to fix Windows getaddrinfo ENOTFOUND (#1605)

The 9 'gbrain init --migrate-only' execSync spawns died on Windows+bun+Supabase
(child DNS resolution). runMigrateOnlyCore (extracted from initMigrateOnly) runs
the schema bring-up in-process for all engines, unblocking schema_version
advancement. Includes async-call-site audit, a wall-clock guard, and a
runGbrainSubprocess stderr-capture wrapper for the remaining backfill spawns.

* fix(sync): ReDoS hardening + diagnostics for schema-pack regexes (#1569)

Input-length cap in runRegexBounded + route the unbounded link-inference path
through it (closes the only no-timeout ReDoS hole); star-height lint rule warns
on nested-quantifier patterns; --no-schema-pack sync escape hatch; GBRAIN_SYNC_TRACE
per-file begin heartbeat; PGLite serve/sync concurrency doc. Defensive hardening +
diagnostics — the deterministic ~3100-file wedge root cause remains open (no repro).

* docs(todos): file v0.41.37.0 fix-wave follow-ups (#1621/#1605/#1569)

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

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

* docs: sync README + CLAUDE.md for v0.41.37.0 critical fix wave

Add reindex add-only tag reconciliation (#1621), v0.13.1 grandfather +
Windows in-process migration (#1581/#1605), and schema-pack ReDoS
hardening + sync --no-schema-pack / GBRAIN_SYNC_TRACE triage (#1569)
to CLAUDE.md key-files annotations and README Troubleshooting.
Regenerated llms-full.txt.

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

* fix(ci): bump llms-full.txt budget 700KB→750KB (CLAUDE.md crossed 700KB after master merge)

The build-llms size-budget test failed: llms-full.txt is 703,244 bytes after the
v0.41.37.0 key-files annotations merged on top of master's v0.41.34/35/36 CLAUDE.md
additions. Matches the v0.41.9.0 precedent (600→700); the single-fetch bundle still
fits comfortably in modern long-context models.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 14:56:26 -07:00

196 lines
10 KiB
TypeScript

/**
* Structural assertions for fix-wave fixes whose behavior is best verified
* at source-shape level rather than via a runtime harness:
*
* - #1125 query drain cache writes — assert cli.ts awaits the drain after
* the query op completes.
* - #1090 admin embed — assert the two-tier resolution (cwd path + embedded
* manifest fallback) is in serve-http.ts and consumes ADMIN_ASSETS.
* - #1077 admin register-client PKCE — assert the admin endpoint honors
* grantTypes / redirectUris / tokenEndpointAuthMethod from the body.
* - #1100 PGLite phaseASchema — assert the v0.11.0 orchestrator routes
* in-process when the engine is pglite (not via execSync subprocess).
* - #1124 query no-expand — assert the parseOpArgs negation logic exists.
*
* Source-grep regression tests are the right tool when the rule is "this
* specific line shape must stay present"; a behavioral test would either
* duplicate what an E2E covers or require heavy mocking that hides the
* regression behind a test seam.
*/
import { describe, test, expect } from 'bun:test';
import { readFileSync } from 'fs';
describe('v0.36.1.x #1125 — query drain cache writes before CLI exit', () => {
test("cli.ts awaits awaitPendingSearchCacheWrites for the 'query' op", () => {
const src = readFileSync('src/cli.ts', 'utf8');
// Sequence: 'query' op match → import the drain → await
expect(src).toMatch(/op\.name\s*===\s*'query'[\s\S]{0,200}awaitPendingSearchCacheWrites/);
expect(src).toMatch(/await\s+awaitPendingSearchCacheWrites\(\)/);
});
test('hybrid.ts exports the drain helper + trackCacheWrite', () => {
const src = readFileSync('src/core/search/hybrid.ts', 'utf8');
expect(src).toMatch(/export async function awaitPendingSearchCacheWrites/);
expect(src).toMatch(/pendingCacheWrites\.add\(promise\)/);
expect(src).toMatch(/trackCacheWrite\(/);
});
});
describe('v0.36.1.x #1090 — admin embed two-tier resolution', () => {
test('serve-http.ts uses ADMIN_ASSETS manifest when admin/dist is not next to cwd', () => {
const src = readFileSync('src/commands/serve-http.ts', 'utf8');
expect(src).toMatch(/import\(['"]\.\.\/admin-embedded/);
expect(src).toMatch(/ADMIN_ASSETS/);
expect(src).toMatch(/ADMIN_INDEX_HTML/);
// Two-tier: dev path (cwd-relative admin/dist) AND embedded manifest fallback
expect(src).toMatch(/useDevPath/);
});
test('src/admin-embedded.ts is auto-generated with file: imports', () => {
const src = readFileSync('src/admin-embedded.ts', 'utf8');
expect(src).toMatch(/AUTO-GENERATED/);
expect(src).toMatch(/with \{ type: 'file' \}/);
expect(src).toMatch(/export const ADMIN_ASSETS/);
expect(src).toMatch(/export const ADMIN_INDEX_HTML/);
});
test('build script + CI guard exist', () => {
const buildSrc = readFileSync('scripts/build-admin-embedded.ts', 'utf8');
expect(buildSrc).toMatch(/walk\(DIST/);
expect(buildSrc).toMatch(/with \{ type: 'file' \}/);
const guard = readFileSync('scripts/check-admin-embedded.sh', 'utf8');
expect(guard).toMatch(/git diff --exit-code -- src\/admin-embedded\.ts/);
});
});
describe('v0.36.1.x #1077 — admin register-client supports PKCE public clients', () => {
test('admin endpoint reads grantTypes / redirectUris / tokenEndpointAuthMethod from request body', () => {
const src = readFileSync('src/commands/serve-http.ts', 'utf8');
// The destructure must surface name / tokenTtl / grantTypes /
// redirectUris / tokenEndpointAuthMethod from req.body. v0.39.3.0
// WARN-9 (PR #1308) moved `scopes` to a separate read line that
// accepts BOTH `scopes` (admin SPA) AND `scope` (OAuth wire singular)
// via `?? `, so this regex no longer requires `scopes` in the inline
// destructure — it's separately covered by the scope-source check
// below.
expect(src).toMatch(/const\s+\{\s*name,\s*(?:[^}]*?,\s*)?tokenTtl,\s*grantTypes,\s*redirectUris,\s*tokenEndpointAuthMethod\s*\}\s*=\s*req\.body/);
// v0.39.3.0 WARN-9: the route must still read a `scope`/`scopes` field
// (under either name) from req.body. Pin the fallback pattern so the
// PKCE-fix regression contract stays load-bearing.
expect(src).toMatch(/req\.body[^;]*scopes\s*\?\?\s*[^;]*scope\b/);
// v0.41.3 (T4 atomicity fix, codex F4): admin endpoint now validates
// tokenEndpointAuthMethod via the shared validator and passes it to
// registerClientManual as a positional arg. Pre-v0.41.3 the route did
// INSERT (confidential) → UPDATE (NULL out secret_hash) for the 'none'
// case, which left a confidential row stranded if the UPDATE failed.
// Atomic now: one INSERT writes the correct shape; no post-insert
// UPDATE block (the regex deliberately asserts the post-insert UPDATE
// is GONE).
expect(src).toMatch(/validateTokenEndpointAuthMethod\(tokenEndpointAuthMethod\)/);
expect(src).toMatch(/registerClientManual\([^)]*validatedAuthMethod[^)]*\)/);
// Regression guard: post-insert UPDATE flipping client_secret_hash to
// NULL based on a runtime check is exactly the non-atomic pattern T4
// killed. Re-introducing it brings back codex F4.
expect(src).not.toMatch(/UPDATE oauth_clients SET client_secret_hash = NULL, token_endpoint_auth_method = 'none'/);
});
});
describe('v0.41.37.0 #1605 — v0.11.0 phaseASchema routes in-process for ALL engines', () => {
test('phaseASchema calls runMigrateOnlyCore (in-process) + is awaited', () => {
// Supersedes #1100's PGLite-only in-process branch. v0.41.37.0 #1605 routes
// EVERY engine through runMigrateOnlyCore (no execSync subprocess at all),
// which is strictly stronger: PGLite still never subprocesses, AND the
// Windows+Postgres getaddrinfo-ENOTFOUND spawn bug is closed too.
// The eng.initSchema() call moved into src/commands/migrations/in-process.ts.
const src = readFileSync('src/commands/migrations/v0_11_0.ts', 'utf8');
expect(src).toContain('runMigrateOnlyCore()');
expect(src).not.toContain("execSync('gbrain init --migrate-only'");
expect(src).toMatch(/await\s+phaseASchema/);
});
test('apply-migrations skips pre-flight schema-version probe on PGLite', () => {
const src = readFileSync('src/commands/apply-migrations.ts', 'utf8');
expect(src).toMatch(/skipPreflight\s*=\s*cfg\.engine\s*===\s*'pglite'/);
});
});
describe('v0.36.1.x #1124 — query --no-expand actually negates expand', () => {
test("cli.ts parseOpArgs handles --no-<key> as boolean negation", () => {
const src = readFileSync('src/cli.ts', 'utf8');
expect(src).toMatch(/arg\.startsWith\(['"]--no-['"]\)/);
expect(src).toMatch(/positiveDef\?\.type\s*===\s*'boolean'/);
expect(src).toMatch(/params\[positiveKey\]\s*=\s*false/);
});
});
describe('v0.41.8.0 #1247/#1269/#1290 — drain last-retrieved before CLI disconnect', () => {
test('cli.ts imports awaitPendingLastRetrievedWrites', () => {
const src = readFileSync('src/cli.ts', 'utf8');
// Allow additional type-imports from the same module (e.g. `type DrainOutcome`)
expect(src).toMatch(/import\s+\{[^}]*\bawaitPendingLastRetrievedWrites\b[^}]*\}\s*from\s+['"]\.\/core\/last-retrieved\.ts['"]/);
});
test('last-retrieved.ts exports the drain + tracks promises in a module-scoped Set', () => {
const src = readFileSync('src/core/last-retrieved.ts', 'utf8');
expect(src).toMatch(/export async function awaitPendingLastRetrievedWrites/);
expect(src).toMatch(/pendingLastRetrievedWrites\s*=\s*new\s+Set/);
expect(src).toMatch(/pendingLastRetrievedWrites\.add\(promise\)/);
// Per D5+D8: snapshot pattern (Codex finding #3) + bounded timeout
expect(src).toMatch(/Promise\.race/);
expect(src).toMatch(/drain timed out/);
});
test('cli.ts behavioral positioning: drain appears BEFORE engine.disconnect in op-dispatch', () => {
const src = readFileSync('src/cli.ts', 'utf8');
// Per D5+D8: replaces the brittle literal-output regex from PR #1259
// with a behavioral-positioning assertion. The drain CALL must appear
// textually before the disconnect CALL in the local-engine path.
// Match `await fn(` not bare names — bare names also appear in
// comments and would false-match the comment ordering.
const localPath = src.match(/\/\/ Local engine path \(unchanged behavior[\s\S]+?^\}/m);
expect(localPath).not.toBeNull();
const block = localPath![0];
const drainCallRe = /await\s+awaitPendingLastRetrievedWrites\s*\(/;
const disconnectCallRe = /await\s+engine\.disconnect\s*\(/;
expect(block).toMatch(drainCallRe);
expect(block).toMatch(disconnectCallRe);
const drainMatch = block.match(drainCallRe);
const disconnectMatch = block.match(disconnectCallRe);
const drainIdx = block.indexOf(drainMatch![0]);
const disconnectIdx = block.indexOf(disconnectMatch![0]);
expect(drainIdx).toBeGreaterThan(-1);
expect(disconnectIdx).toBeGreaterThan(-1);
expect(drainIdx).toBeLessThan(disconnectIdx);
});
test('cli.ts uses shouldForceExitAfterMain only on the timeout path', () => {
const src = readFileSync('src/cli.ts', 'utf8');
expect(src).toMatch(/import\s+\{\s*shouldForceExitAfterMain\s*\}\s*from\s+['"]\.\/core\/cli-force-exit\.ts['"]/);
// The force-exit gate MUST be conditioned on drainResult.outcome ==='timeout'
expect(src).toMatch(/drainResult\.outcome\s*===\s*['"]timeout['"]/);
});
test('cli-force-exit.ts daemon guard excludes "serve"', () => {
const src = readFileSync('src/core/cli-force-exit.ts', 'utf8');
expect(src).toMatch(/export function shouldForceExitAfterMain/);
expect(src).toMatch(/DAEMON_COMMANDS[\s\S]*serve/);
});
});
describe('v0.41.8.0 #1340 — PGLite WASM init classifier', () => {
test('pglite-engine.ts exports classifyPgliteInitError + buildPgliteInitErrorMessage', () => {
const src = readFileSync('src/core/pglite-engine.ts', 'utf8');
expect(src).toMatch(/export function classifyPgliteInitError/);
expect(src).toMatch(/export function buildPgliteInitErrorMessage/);
// Per Codex finding #9: regex tightened to $$bunfs OR ENOENT+pglite.data
expect(src).toMatch(/\$\$bunfs/);
expect(src).toMatch(/ENOENT/);
});
test('pglite-engine.ts connect catch block routes through the classifier', () => {
const src = readFileSync('src/core/pglite-engine.ts', 'utf8');
expect(src).toMatch(/classifyPgliteInitError\(original\)/);
expect(src).toMatch(/buildPgliteInitErrorMessage\(verdict, original\)/);
});
});