mirror of
https://github.com/garrytan/gbrain.git
synced 2026-08-14 00:48:18 +00:00
223 lines
9.4 KiB
TypeScript
223 lines
9.4 KiB
TypeScript
/**
|
|
* v0.30.3 codex-mandated test gate C6 — regression for #745.
|
|
*
|
|
* `collectChildPutPageSlugs` reads `input->>'slug'` from
|
|
* `subagent_tool_executions`. Pre-#745 this failed silently when the
|
|
* `input` column held a double-encoded JSONB string (jsonb_typeof='string'
|
|
* containing '"{...}"' instead of jsonb_typeof='object'). The orchestrator
|
|
* collected zero slugs, child jobs finished, queue looked healthy, and
|
|
* the brain wrote nothing — the worst possible on-call shape.
|
|
*
|
|
* #745 added a COALESCE that handles both the proper jsonb-object shape and
|
|
* the double-encoded jsonb-string shape:
|
|
*
|
|
* COALESCE(input->>'slug', (input #>> '{}')::jsonb->>'slug') AS slug
|
|
*
|
|
* This test seeds both shapes in `subagent_tool_executions` and asserts the
|
|
* function recovers slugs from both.
|
|
*/
|
|
|
|
import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
|
|
import { PGLiteEngine } from '../src/core/pglite-engine.ts';
|
|
import { __testing } from '../src/core/cycle/synthesize.ts';
|
|
|
|
const { collectChildPutPageSlugs, stampDreamProvenance } = __testing;
|
|
|
|
let engine: PGLiteEngine;
|
|
|
|
beforeAll(async () => {
|
|
engine = new PGLiteEngine();
|
|
await engine.connect({});
|
|
await engine.initSchema();
|
|
|
|
// Seed minion_jobs parent rows so subagent_tool_executions FK is satisfied.
|
|
// The function only reads tool_executions; minion_jobs just needs to exist.
|
|
const db = (engine as any).db;
|
|
await db.exec(`
|
|
INSERT INTO minion_jobs (id, queue, name, data, status)
|
|
VALUES
|
|
(1001, 'default', 'subagent', '{}'::jsonb, 'completed'),
|
|
(1002, 'default', 'subagent', '{}'::jsonb, 'completed'),
|
|
(1003, 'default', 'subagent', '{}'::jsonb, 'completed')
|
|
ON CONFLICT (id) DO NOTHING;
|
|
`);
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await engine.disconnect();
|
|
});
|
|
|
|
describe('C6: collectChildPutPageSlugs survives double-encoded jsonb (#745)', () => {
|
|
test('recovers slug from properly-stored jsonb object (post-fix)', async () => {
|
|
const db = (engine as any).db;
|
|
// Use raw SQL with jsonb literal to ensure object shape, not string shape.
|
|
await db.query(
|
|
`INSERT INTO subagent_tool_executions (job_id, message_idx, tool_use_id, tool_name, status, input)
|
|
VALUES (1001, 0, 'tool_a', 'brain_put_page', 'complete', $1::jsonb)`,
|
|
[JSON.stringify({ slug: 'wiki/agents/test/normal-shape', body: 'hi' })],
|
|
);
|
|
const refs = await collectChildPutPageSlugs(engine as any, [1001], new Map());
|
|
expect(refs.map((r: { slug: string }) => r.slug)).toContain('wiki/agents/test/normal-shape');
|
|
});
|
|
|
|
test('recovers slug from DOUBLE-ENCODED jsonb string (#745 fix)', async () => {
|
|
const db = (engine as any).db;
|
|
// Construct double-encoded shape: input column contains a jsonb STRING
|
|
// (jsonb_typeof='string') whose VALUE is the JSON-encoded object.
|
|
// This is the bug-shape pre-#745: writing JSON.stringify of the object
|
|
// into a jsonb column produced jsonb_typeof='string', not 'object'.
|
|
const doubleEncoded = JSON.stringify(
|
|
JSON.stringify({ slug: 'wiki/agents/test/double-encoded', body: 'hi' }),
|
|
);
|
|
await db.query(
|
|
`INSERT INTO subagent_tool_executions (job_id, message_idx, tool_use_id, tool_name, status, input)
|
|
VALUES (1002, 0, 'tool_b', 'brain_put_page', 'complete', $1::jsonb)`,
|
|
[doubleEncoded],
|
|
);
|
|
|
|
// Sanity check: confirm the row IS double-encoded (jsonb_typeof='string').
|
|
const probe = await db.query(
|
|
`SELECT jsonb_typeof(input) AS t FROM subagent_tool_executions WHERE job_id=1002`,
|
|
);
|
|
expect(probe.rows[0].t).toBe('string');
|
|
|
|
const refs = await collectChildPutPageSlugs(engine as any, [1002], new Map());
|
|
expect(refs.map((r: { slug: string }) => r.slug)).toContain('wiki/agents/test/double-encoded');
|
|
});
|
|
|
|
test('handles MIXED inputs: returns slugs from both shapes in one query', async () => {
|
|
const refs = await collectChildPutPageSlugs(engine as any, [1001, 1002], new Map());
|
|
const slugs = refs.map((r: { slug: string }) => r.slug);
|
|
expect(slugs).toContain('wiki/agents/test/normal-shape');
|
|
expect(slugs).toContain('wiki/agents/test/double-encoded');
|
|
});
|
|
|
|
test('skips rows without a slug field gracefully (no throw)', async () => {
|
|
const db = (engine as any).db;
|
|
await db.query(
|
|
`INSERT INTO subagent_tool_executions (job_id, message_idx, tool_use_id, tool_name, status, input)
|
|
VALUES (1003, 0, 'tool_c', 'brain_put_page', 'complete', $1::jsonb)`,
|
|
[JSON.stringify({ unrelated: 'no-slug' })],
|
|
);
|
|
const refs = await collectChildPutPageSlugs(engine as any, [1003], new Map());
|
|
// Function silently drops rows whose slug resolves to null/empty.
|
|
expect(refs.map((r: { slug: string }) => r.slug)).not.toContain('no-slug');
|
|
});
|
|
|
|
// #1586: refs are stamped with the cycle's resolved source, not a
|
|
// hardcoded 'default'.
|
|
test('stamps refs with the provided cycle sourceId (#1586)', async () => {
|
|
const refs = await collectChildPutPageSlugs(engine as any, [1001], new Map(), 'mybrain');
|
|
expect(refs.length).toBeGreaterThan(0);
|
|
for (const r of refs) expect(r.source_id).toBe('mybrain');
|
|
});
|
|
|
|
test('defaults to source_id=default when no sourceId is passed (legacy)', async () => {
|
|
const refs = await collectChildPutPageSlugs(engine as any, [1001], new Map());
|
|
expect(refs.length).toBeGreaterThan(0);
|
|
for (const r of refs) expect(r.source_id).toBe('default');
|
|
});
|
|
|
|
// #1978: refs carry the source transcript path when the orchestrator
|
|
// supplies a job_id → path map, so stampDreamProvenance can persist it.
|
|
test('stamps refs with raw_source from the jobRawSource map (#1978)', async () => {
|
|
const jobRawSource = new Map([[1001, '/transcripts/2026-07-01-standup.md']]);
|
|
const refs = await collectChildPutPageSlugs(engine as any, [1001], new Map(), 'default', jobRawSource);
|
|
const ref = refs.find((r: { slug: string }) => r.slug === 'wiki/agents/test/normal-shape');
|
|
expect(ref?.raw_source).toBe('/transcripts/2026-07-01-standup.md');
|
|
});
|
|
|
|
test('normalizes bigint job ids before number-keyed metadata lookups', async () => {
|
|
const bigintEngine = {
|
|
executeRaw: async () => [{
|
|
job_id: 1001n,
|
|
slug: 'wiki/agents/test/bigint-job-abc123',
|
|
}],
|
|
};
|
|
const chunkInfo = new Map([[1001, { idx: 2, hash6: 'abc123' }]]);
|
|
const jobRawSource = new Map([[1001, '/transcripts/bigint-source.md']]);
|
|
|
|
const refs = await collectChildPutPageSlugs(
|
|
bigintEngine as any,
|
|
[1001],
|
|
chunkInfo,
|
|
'default',
|
|
jobRawSource,
|
|
);
|
|
|
|
expect(refs).toEqual([{
|
|
slug: 'wiki/agents/test/bigint-job-abc123-c2',
|
|
source_id: 'default',
|
|
raw_source: '/transcripts/bigint-source.md',
|
|
}]);
|
|
});
|
|
|
|
test('omits raw_source when no map entry exists for the job (#1978)', async () => {
|
|
const refs = await collectChildPutPageSlugs(engine as any, [1001], new Map(), 'default', new Map());
|
|
const ref = refs.find((r: { slug: string }) => r.slug === 'wiki/agents/test/normal-shape');
|
|
expect(ref).toBeDefined();
|
|
expect('raw_source' in (ref as object)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('#2569: stampDreamProvenance persists the marker into DB frontmatter', () => {
|
|
test('merges dream_generated + dream_cycle_date into pages.frontmatter', async () => {
|
|
await engine.putPage('wiki/originals/ideas/2026-07-17-stamp-me-abc123', {
|
|
type: 'note',
|
|
title: 'Stamp me',
|
|
compiled_truth: 'body',
|
|
timeline: '',
|
|
frontmatter: { keep_me: 'yes' },
|
|
});
|
|
await stampDreamProvenance(
|
|
engine as any,
|
|
[{ slug: 'wiki/originals/ideas/2026-07-17-stamp-me-abc123', source_id: 'default' }],
|
|
'2026-07-17',
|
|
);
|
|
const rows = await engine.executeRaw<{ fm: Record<string, unknown> }>(
|
|
`SELECT frontmatter AS fm FROM pages WHERE slug = 'wiki/originals/ideas/2026-07-17-stamp-me-abc123'`,
|
|
);
|
|
expect(rows.length).toBe(1);
|
|
const fm = rows[0].fm as Record<string, unknown>;
|
|
// The stamp lands as real JSONB values (queryable via ->>), not a
|
|
// double-encoded string scalar.
|
|
expect(fm.dream_generated).toBe(true);
|
|
expect(fm.dream_cycle_date).toBe('2026-07-17');
|
|
// Merge, not replace: pre-existing frontmatter keys survive.
|
|
expect(fm.keep_me).toBe('yes');
|
|
});
|
|
|
|
test('is idempotent and never throws for a missing page', async () => {
|
|
const refs = [{ slug: 'wiki/originals/ideas/does-not-exist', source_id: 'default' }];
|
|
await stampDreamProvenance(engine as any, refs, '2026-07-17'); // no throw
|
|
await stampDreamProvenance(engine as any, refs, '2026-07-17'); // idempotent
|
|
});
|
|
|
|
// #1978: raw-source persistence — the stamp carries the transcript path
|
|
// the synthesis was derived from, when the ref supplies one.
|
|
test('persists raw_source into pages.frontmatter when the ref carries it (#1978)', async () => {
|
|
await engine.putPage('wiki/originals/ideas/2026-07-17-raw-src-def456', {
|
|
type: 'note',
|
|
title: 'Raw source stamp',
|
|
compiled_truth: 'body',
|
|
timeline: '',
|
|
frontmatter: {},
|
|
});
|
|
await stampDreamProvenance(
|
|
engine as any,
|
|
[{
|
|
slug: 'wiki/originals/ideas/2026-07-17-raw-src-def456',
|
|
source_id: 'default',
|
|
raw_source: '/transcripts/2026-07-17-standup.md',
|
|
}],
|
|
'2026-07-17',
|
|
);
|
|
const rows = await engine.executeRaw<{ fm: Record<string, unknown> }>(
|
|
`SELECT frontmatter AS fm FROM pages WHERE slug = 'wiki/originals/ideas/2026-07-17-raw-src-def456'`,
|
|
);
|
|
const fm = rows[0].fm as Record<string, unknown>;
|
|
expect(fm.dream_generated).toBe(true);
|
|
expect(fm.raw_source).toBe('/transcripts/2026-07-17-standup.md');
|
|
});
|
|
});
|