mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-27 10:59:32 +00:00
* fix(sync): op_checkpoints pin write double-encodes jsonb — every sync aborts (#2339) recordCompleted bound JSON.stringify(array) to a $3::jsonb param via postgres.js .unsafe(), double-encoding it into a jsonb string scalar that violates the v119 op_checkpoints_completed_keys_array CHECK — aborting every multi-source sync on real Postgres at the first checkpoint write. PGLite parses the string silently, which is why unit tests stayed green and it shipped. Cast through $3::text::jsonb so the text->jsonb cast parses a genuine array. Adds a DATABASE_URL-gated parity test + a dedicated Postgres CI job so the guard can never silently skip. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(db): sweep positional jsonb double-encode sites + AST CI guard (#2324) Every executeRaw/.unsafe site that bound JSON.stringify(x) to a bare positional jsonb cast double-encodes on real Postgres (same class as #2339). Sweep them all to the text::jsonb form across query-cache, sources-ops, llm-base, calibration-profile, impact-capture, subagent, receipt-write, traversal-cache, symbol-resolver, and the agent/sources commands. Adds scripts/check-jsonb-params.mjs (AST-lite scanner for the positional form the legacy template grep misses, incl. generic-typed calls), wired into check-jsonb-pattern.sh, with a self-test. PGLite's native db.query is not scanned — it parses text to jsonb natively, so the bug can't occur there. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(search,eval): alias-hop injected results carry page_id (contradiction-probe crash) applyAliasHop injected synthetic SearchResults without page_id (the `as SearchResult` cast hid the missing field), so listActiveTakesForPages bound undefined/NaN into ANY($1::int[]) and crashed the whole contradiction probe on real Postgres. Stamp page_id=page.id at the injection site and add a finite-id filter in generateIntraPagePairs as a defensive backstop (mirrors hybrid.ts:63). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(engines): positional jsonb binding rule (text::jsonb vs the double-encode trap) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * v0.42.53.0 fix(sync,db): #2339 op_checkpoints jsonb double-encode + bug-class sweep + CI guard Bumps VERSION + package.json to 0.42.53.0, adds the CHANGELOG entry, and regenerates llms-full.txt. Ships the #2339 sync-abort hotfix, the repo-wide positional jsonb double-encode sweep, the alias-hop contradiction-probe crash fix, and the new positional-form CI guard. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: post-ship sync — jsonb invariant now covers the positional form + new guard CLAUDE.md JSONB invariant + KEY_FILES (sql-query, check-jsonb-pattern, op-checkpoint) now describe the #2339 positional double-encode class, the $N::text::jsonb fix, and the new check-jsonb-params.mjs guard. Regenerates llms-full.txt. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
84 lines
3.1 KiB
TypeScript
84 lines
3.1 KiB
TypeScript
/**
|
|
* Self-test for scripts/check-jsonb-params.mjs — the positional jsonb
|
|
* double-encode guard (#2339 / #2324 class). Verifies it catches the bug shape
|
|
* (including generic-typed calls and the `jsonStr` variable case is acknowledged
|
|
* as out of scope) and does NOT false-positive on the sanctioned forms.
|
|
*
|
|
* Fixtures are written to a temp dir and the scanner is pointed at it via argv,
|
|
* so this never touches the real src/ tree.
|
|
*/
|
|
import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
|
|
import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { tmpdir } from 'node:os';
|
|
|
|
const SCRIPT = join(import.meta.dir, '..', 'scripts', 'check-jsonb-params.mjs');
|
|
|
|
let root: string;
|
|
let badDir: string;
|
|
let goodDir: string;
|
|
|
|
function runGuard(dir: string): { code: number; err: string } {
|
|
const res = Bun.spawnSync([process.execPath, SCRIPT, dir]);
|
|
return { code: res.exitCode, err: res.stderr.toString() + res.stdout.toString() };
|
|
}
|
|
|
|
beforeAll(() => {
|
|
root = mkdtempSync(join(tmpdir(), 'jsonb-guard-'));
|
|
badDir = join(root, 'bad');
|
|
goodDir = join(root, 'good');
|
|
mkdirSync(badDir, { recursive: true });
|
|
mkdirSync(goodDir, { recursive: true });
|
|
|
|
// BAD: positional $1::jsonb bound to a JSON.stringify'd value.
|
|
writeFileSync(
|
|
join(badDir, 'bad.ts'),
|
|
"await engine.executeRaw(`INSERT INTO t (a) VALUES ($1::jsonb)`, [JSON.stringify(x)]);\n",
|
|
);
|
|
// BAD: generic-typed executeRaw<T>(...) must still be caught.
|
|
writeFileSync(
|
|
join(badDir, 'bad_generic.ts'),
|
|
"await engine.executeRaw<{ id: string }>(`UPDATE t SET a = $2::jsonb WHERE id = $1`, [id, JSON.stringify(x)]);\n",
|
|
);
|
|
|
|
// GOOD: the fix — $1::text::jsonb.
|
|
writeFileSync(
|
|
join(goodDir, 'good_text.ts'),
|
|
"await engine.executeRaw(`INSERT INTO t (a) VALUES ($1::text::jsonb)`, [JSON.stringify(x)]);\n",
|
|
);
|
|
// GOOD: text[] array path (the appendCompleted unnest shape).
|
|
writeFileSync(
|
|
join(goodDir, 'good_array.ts'),
|
|
"await engine.executeRaw(`INSERT INTO t (a) SELECT unnest($1::text[])`, [JSON.stringify(arr)]);\n",
|
|
);
|
|
// GOOD: executeRawJsonb passes a raw object, not a string — excluded.
|
|
writeFileSync(
|
|
join(goodDir, 'good_helper.ts'),
|
|
"await executeRawJsonb(engine, `INSERT INTO t (a) VALUES ($1::jsonb)`, [], [JSON.stringify(x)]);\n",
|
|
);
|
|
// GOOD: explicit opt-out for a rare legitimate object-binding case.
|
|
writeFileSync(
|
|
join(goodDir, 'good_optout.ts'),
|
|
"await engine.executeRaw(`INSERT INTO t (a) VALUES ($1::jsonb)` /* jsonb-guard-ok */, [JSON.stringify(x)]);\n",
|
|
);
|
|
});
|
|
|
|
afterAll(() => {
|
|
rmSync(root, { recursive: true, force: true });
|
|
});
|
|
|
|
describe('check-jsonb-params guard', () => {
|
|
test('flags positional $N::jsonb + JSON.stringify (incl. generic-typed calls)', () => {
|
|
const { code, err } = runGuard(badDir);
|
|
expect(code).toBe(1);
|
|
expect(err).toContain('bad.ts');
|
|
expect(err).toContain('bad_generic.ts');
|
|
});
|
|
|
|
test('passes the sanctioned forms (::text::jsonb, ::text[], executeRawJsonb, opt-out)', () => {
|
|
const { code, err } = runGuard(goodDir);
|
|
expect(code).toBe(0);
|
|
expect(err).toContain('clean');
|
|
});
|
|
});
|