mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-29 19:01:39 +00:00
* fix(sources): recover corrupted config shapes (#3401) Use one canonical normalizer for nested string and array-shaped source configs across federation reads, config writes, archive/restore, and doctor remediation.\n\nFixes #3401\nFixes #3402\nFixes #3403 Signed-off-by: arisgysel-design <arisgysel-design@users.noreply.github.com> * fix(sources): bind restoreSource federated patch via ::text::jsonb (#2339 class) restoreSource bound a JS JSON string to a bare $1::jsonb placeholder; postgres.js double-encodes that into a jsonb string scalar, so on the Postgres engine the coerced object || string-scalar concat evaluates as array-concat and restore RE-CORRUPTS the exact config shape this PR repairs. PGLite masks the bug (its driver parses the bind natively). Fix: bind through $1::text::jsonb per the repo JSONB rule. Adds the DATABASE_URL-gated Postgres regression (test/e2e/restore-source-config-jsonb-postgres.test.ts): seeds a corrupted string-scalar config, runs archive -> restore, asserts jsonb_typeof(config) = 'object' with the federated flag applied and pre-existing keys preserved. Verified red on the bare ::jsonb bind (config became a jsonb array) and green on the fix against a real pgvector Postgres; skips cleanly without DATABASE_URL. Co-authored-by: arisgysel-design <arisgysel-design@users.noreply.github.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Signed-off-by: arisgysel-design <arisgysel-design@users.noreply.github.com> Co-authored-by: arisgysel-design <arisgysel-design@users.noreply.github.com> Co-authored-by: Garry Tan <garrytan@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
66 lines
2.5 KiB
TypeScript
66 lines
2.5 KiB
TypeScript
/**
|
|
* Test: `checkSourceConfigShape` (#2829 — source config string-scalar re-wrapping).
|
|
*
|
|
* Pure-helper surface — the check only consumes `engine.executeRaw`, so a
|
|
* structurally-typed mock satisfies the contract (same pattern as
|
|
* `doctor-child-orphans.test.ts`). No PGLite spin-up required.
|
|
*/
|
|
|
|
import { describe, test, expect } from 'bun:test';
|
|
import { checkSourceConfigShape } from '../src/commands/doctor.ts';
|
|
import type { BrainEngine } from '../src/core/engine.ts';
|
|
|
|
/** Build a structurally-typed BrainEngine whose executeRaw returns per-SQL results. */
|
|
function makeMockEngine(handler: (sql: string) => Promise<unknown[]>): BrainEngine {
|
|
return {
|
|
executeRaw: handler,
|
|
} as unknown as BrainEngine;
|
|
}
|
|
|
|
describe('checkSourceConfigShape (#2829)', () => {
|
|
test('all configs are objects → status:ok', async () => {
|
|
const engine = makeMockEngine(async () => []);
|
|
const result = await checkSourceConfigShape(engine);
|
|
expect(result.name).toBe('source_config_shape');
|
|
expect(result.status).toBe('ok');
|
|
expect(result.message).toContain('JSON objects');
|
|
});
|
|
|
|
test('non-object configs → warn naming affected sources + repair hint', async () => {
|
|
const engine = makeMockEngine(async () => [
|
|
{ id: 'default', typ: 'string' },
|
|
{ id: 'wiki', typ: 'string' },
|
|
]);
|
|
const result = await checkSourceConfigShape(engine);
|
|
expect(result.status).toBe('warn');
|
|
expect(result.message).toContain('2 source(s)');
|
|
expect(result.message).toContain('default (string)');
|
|
expect(result.message).toContain('wiki (string)');
|
|
expect(result.message).toContain('#2829');
|
|
// Paste-ready repair SQL is part of the hint.
|
|
expect(result.message).toContain('UPDATE sources SET config');
|
|
expect(result.message).toContain('jsonb_array_elements');
|
|
expect(result.message).toContain('IS JSON');
|
|
});
|
|
|
|
test('detection query targets the exact jsonb_typeof predicate', async () => {
|
|
let captured = '';
|
|
const engine = makeMockEngine(async (sql: string) => {
|
|
captured = sql;
|
|
return [];
|
|
});
|
|
await checkSourceConfigShape(engine);
|
|
expect(captured).toContain('jsonb_typeof(config) AS typ');
|
|
expect(captured).toContain("WHERE jsonb_typeof(config) <> 'object'");
|
|
});
|
|
|
|
test('engine error → warn, never a false ok', async () => {
|
|
const engine = makeMockEngine(async () => {
|
|
throw new Error('relation "sources" does not exist');
|
|
});
|
|
const result = await checkSourceConfigShape(engine);
|
|
expect(result.status).toBe('warn');
|
|
expect(result.message).toContain('Check failed');
|
|
});
|
|
});
|