mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-29 19:01:39 +00:00
164 lines
6.4 KiB
TypeScript
164 lines
6.4 KiB
TypeScript
/**
|
|
* v0.41.16.0 — E2E test for the conversation parser cathedral against
|
|
* a real PGLite brain.
|
|
*
|
|
* For each built-in format: seed a page through
|
|
* `importFromContent`, run `parseConversation` against the body, assert
|
|
* the parser identifies the correct pattern AND produces at least one
|
|
* message AND the message timestamp lands in the expected date range.
|
|
*
|
|
* Per CLAUDE.md test-isolation R3+R4: uses the canonical PGLite block.
|
|
* Hermetic (no DATABASE_URL needed); the in-memory PGLite is created
|
|
* once per file and reset between tests.
|
|
*
|
|
* This is the integration test that proves the parser ↔ engine
|
|
* interaction is correct for the dream cycle's
|
|
* `conversation_facts_backfill` phase.
|
|
*/
|
|
|
|
import { describe, expect, test, beforeAll, afterAll, beforeEach } from 'bun:test';
|
|
import { PGLiteEngine } from '../../src/core/pglite-engine.ts';
|
|
import { resetPgliteState } from '../helpers/reset-pglite.ts';
|
|
import {
|
|
parseConversation,
|
|
} from '../../src/core/conversation-parser/parse.ts';
|
|
import { BUILTIN_PATTERNS } from '../../src/core/conversation-parser/builtins.ts';
|
|
import { importFromContent } from '../../src/core/import-file.ts';
|
|
import type { Page } from '../../src/core/types.ts';
|
|
|
|
let engine: PGLiteEngine;
|
|
|
|
beforeAll(async () => {
|
|
engine = new PGLiteEngine();
|
|
await engine.connect({});
|
|
await engine.initSchema();
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await engine.disconnect();
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
await resetPgliteState(engine);
|
|
});
|
|
|
|
/**
|
|
* Build a conversation page body using the first test_positive sample
|
|
* from the pattern's registry entry. Multi-line patterns get a body
|
|
* line appended.
|
|
*/
|
|
function buildSampleBody(patternId: string): string {
|
|
const pattern = BUILTIN_PATTERNS.find((p) => p.id === patternId);
|
|
if (!pattern) throw new Error(`pattern ${patternId} not found`);
|
|
// Repeat the positive sample 3 times with slight variations to
|
|
// simulate a multi-message conversation.
|
|
const samples = pattern.test_positive.slice(0, 2);
|
|
if (pattern.multi_line && pattern.captures.text_group === 0) {
|
|
// For patterns where text comes from the next line, append a body
|
|
// line after each header.
|
|
return samples.flatMap((s, i) => [s, `body line ${i + 1}`]).join('\n');
|
|
}
|
|
return samples.join('\n');
|
|
}
|
|
|
|
describe('E2E: parser ↔ engine integration for every built-in', () => {
|
|
for (const pattern of BUILTIN_PATTERNS) {
|
|
test(`pattern=${pattern.id}: page imports + parser identifies pattern`, async () => {
|
|
const slug = `conversations/test/${pattern.id}-sample`;
|
|
const body = buildSampleBody(pattern.id);
|
|
const frontmatter: Record<string, unknown> = {
|
|
type: 'conversation',
|
|
date: '2024-03-15',
|
|
};
|
|
if (pattern.timezone_policy === 'utc_assumed_with_warn') {
|
|
// Some patterns warn without a timezone; provide one to avoid
|
|
// stderr noise in the test output.
|
|
frontmatter.timezone = 'America/Los_Angeles';
|
|
}
|
|
|
|
// Seed the page through the canonical import path.
|
|
const fullBody = `---\n${Object.entries(frontmatter)
|
|
.map(([k, v]) => `${k}: ${typeof v === 'string' ? v : JSON.stringify(v)}`)
|
|
.join('\n')}\n---\n\n${body}`;
|
|
await importFromContent(engine, slug, fullBody, {
|
|
sourceId: 'default',
|
|
noEmbed: true,
|
|
});
|
|
|
|
// Read the page back and parse it through the orchestrator
|
|
// (mirrors what extract-conversation-facts.ts does in production).
|
|
const page = (await engine.getPage(slug)) as Page;
|
|
expect(page).not.toBeNull();
|
|
|
|
const bodyToParse = `${page.compiled_truth ?? ''}\n${page.timeline ?? ''}`.trim();
|
|
const result = parseConversation(bodyToParse, { page });
|
|
|
|
expect(result.phase).toBe('regex_match');
|
|
expect(result.matched_pattern_id).toBe(pattern.id);
|
|
expect(result.messages.length).toBeGreaterThanOrEqual(1);
|
|
|
|
// Every parsed message has a valid ISO timestamp.
|
|
for (const msg of result.messages) {
|
|
const ts = Date.parse(msg.timestamp);
|
|
expect(Number.isFinite(ts)).toBe(true);
|
|
// Pattern's timezone policy determines date range constraints.
|
|
// For inline-date patterns the date is in the line; for
|
|
// time-only patterns it's from frontmatter ('2024-03-15').
|
|
if (pattern.date_source === 'frontmatter') {
|
|
expect(msg.timestamp.slice(0, 10)).toBe('2024-03-15');
|
|
}
|
|
expect(msg.speaker.length).toBeGreaterThan(0);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
describe('E2E: parser handles unparseable bodies cleanly', () => {
|
|
test('non-chat content returns no_match without crashing', async () => {
|
|
const slug = 'conversations/test/not-a-chat';
|
|
const fullBody = `---\ntype: conversation\ndate: 2024-03-15\n---\n\nThis is just prose. It has no chat structure at all. Just words that flow.`;
|
|
await importFromContent(engine, slug, fullBody, {
|
|
sourceId: 'default',
|
|
noEmbed: true,
|
|
});
|
|
const page = (await engine.getPage(slug)) as Page;
|
|
const bodyToParse = `${page.compiled_truth ?? ''}\n${page.timeline ?? ''}`.trim();
|
|
const result = parseConversation(bodyToParse, { page });
|
|
expect(result.phase).toBe('no_match');
|
|
expect(result.messages).toEqual([]);
|
|
});
|
|
|
|
test('empty body returns no_match', async () => {
|
|
const slug = 'conversations/test/empty';
|
|
const fullBody = `---\ntype: conversation\ndate: 2024-03-15\n---\n\n`;
|
|
await importFromContent(engine, slug, fullBody, {
|
|
sourceId: 'default',
|
|
noEmbed: true,
|
|
});
|
|
const page = (await engine.getPage(slug)) as Page;
|
|
const result = parseConversation(`${page.compiled_truth ?? ''}\n${page.timeline ?? ''}`.trim(), { page });
|
|
expect(result.phase).toBe('no_match');
|
|
expect(result.messages).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('E2E: D8 date derivation chain through Page', () => {
|
|
test('frontmatter.date wins over epoch default', async () => {
|
|
const slug = 'conversations/test/date-derive';
|
|
const body = '**[18:37] 👤 Alice:** hi';
|
|
const fullBody = `---\ntype: conversation\ndate: 2026-05-24\ntimezone: America/Los_Angeles\n---\n\n${body}`;
|
|
await importFromContent(engine, slug, fullBody, {
|
|
sourceId: 'default',
|
|
noEmbed: true,
|
|
});
|
|
const page = (await engine.getPage(slug)) as Page;
|
|
const result = parseConversation(
|
|
`${page.compiled_truth ?? ''}\n${page.timeline ?? ''}`.trim(),
|
|
{ page },
|
|
);
|
|
expect(result.matched_pattern_id).toBe('telegram-bracket');
|
|
expect(result.messages).toHaveLength(1);
|
|
expect(result.messages[0].timestamp).toBe('2026-05-24T18:37:00Z');
|
|
});
|
|
});
|