Files
gbrain/test/doctor-source-config-shape.test.ts
16782aee7f fix(sources): recover corrupted config shapes (#3420)
* 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>
2026-07-27 14:16:05 -07:00

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');
});
});