mirror of
https://github.com/garrytan/gbrain.git
synced 2026-08-14 00:48:18 +00:00
getLastSeen had no upper date bound, so a future-dated chronicle event (a scheduled calendar-event, a planned milestone) became the entity's "last seen" date; finalizeLastSeen's Math.max(0, ...) then clamped the negative delta, reporting days_ago: 0 -- the entity reads as seen-today. Recording future events is intended (eligibility ELIGIBLE_TYPES includes calendar-event); the reader just needs to stop counting them as "seen". Bound the query to te.date <= COALESCE(asof, current_date) in both engines, mirroring getOnThisDay's existing te.date < target bound. asof now reaches the WHERE clause (previously it only reached finalizeLastSeen), so as-of time-travel is honored for the date filter too. Regression test added: an entity with past events plus a future event -> last seen returns the most recent PAST event, not the future one; and as-of after the future date lets it through. Fails before, passes after.
138 lines
7.7 KiB
TypeScript
138 lines
7.7 KiB
TypeScript
/**
|
||
* v0.42.x — Life Chronicle (#2390) timeline read methods (Phase A.2).
|
||
* Runs against PGLite in-memory. Covers getTimelineForDate (day + ISO week),
|
||
* getSince (+ kind filter), getLastSeen (own page + via event `who`),
|
||
* intra-day ordering by event effective_date, source isolation, read-time
|
||
* hiding of soft-deleted event projections, and the (event_page_id, date)
|
||
* dedup index.
|
||
*/
|
||
import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
|
||
import { PGLiteEngine } from '../src/core/pglite-engine.ts';
|
||
|
||
let engine: PGLiteEngine;
|
||
const ids: Record<string, number> = {};
|
||
|
||
async function insertPage(opts: {
|
||
slug: string; type: string; sourceId?: string;
|
||
effectiveDate?: string | null; frontmatter?: string;
|
||
}): Promise<number> {
|
||
const rows = await engine.executeRaw<{ id: number }>(
|
||
`INSERT INTO pages (source_id, slug, type, title, effective_date, frontmatter)
|
||
VALUES ($1, $2, $3, $4, $5::timestamptz, $6::text::jsonb)
|
||
RETURNING id`,
|
||
[opts.sourceId ?? 'default', opts.slug, opts.type, opts.slug,
|
||
opts.effectiveDate ?? null, opts.frontmatter ?? '{}'],
|
||
);
|
||
return rows[0].id;
|
||
}
|
||
|
||
async function insertProjection(depthId: number, eventId: number, date: string, summary: string): Promise<void> {
|
||
await engine.executeRaw(
|
||
`INSERT INTO timeline_entries (page_id, date, source, summary, detail, event_page_id)
|
||
VALUES ($1, $2::date, $3, $4, '', $5)`,
|
||
[depthId, date, `life-chronicle:event:${eventId}`, summary, eventId],
|
||
);
|
||
}
|
||
|
||
beforeAll(async () => {
|
||
engine = new PGLiteEngine();
|
||
await engine.connect({ database_url: '' });
|
||
await engine.initSchema();
|
||
|
||
ids.meeting = await insertPage({ slug: 'meetings/2026-06-18-sync', type: 'meeting' });
|
||
await insertPage({ slug: 'people/sarah-chen', type: 'person' });
|
||
// 06-18: event2 at 09:00 (Bob), event1 at 15:30 (Sarah, commitment).
|
||
ids.e1 = await insertPage({ slug: 'life/events/2026-06-18-001', type: 'event', effectiveDate: '2026-06-18T15:30:00Z', frontmatter: '{"event":{"who":["people/sarah-chen"],"kind":"commitment"}}' });
|
||
ids.e2 = await insertPage({ slug: 'life/events/2026-06-18-002', type: 'event', effectiveDate: '2026-06-18T09:00:00Z', frontmatter: '{"event":{"who":["people/bob"],"kind":"meeting"}}' });
|
||
// 06-20 (same ISO week as 06-18): event3 (Sarah, decision).
|
||
ids.e3 = await insertPage({ slug: 'life/events/2026-06-20-001', type: 'event', effectiveDate: '2026-06-20T10:00:00Z', frontmatter: '{"event":{"who":["people/sarah-chen"],"kind":"decision"}}' });
|
||
await insertProjection(ids.meeting, ids.e1, '2026-06-18', 'Sarah committed to Q3');
|
||
await insertProjection(ids.meeting, ids.e2, '2026-06-18', 'Bob standup');
|
||
await insertProjection(ids.meeting, ids.e3, '2026-06-20', 'Decision on launch');
|
||
|
||
// Other-source event (isolation fixture).
|
||
await engine.executeRaw(`INSERT INTO sources (id, name) VALUES ('other', 'Other') ON CONFLICT (id) DO NOTHING`);
|
||
const otherMeeting = await insertPage({ slug: 'meetings/o', type: 'meeting', sourceId: 'other' });
|
||
ids.eOther = await insertPage({ slug: 'life/events/2026-06-18-099', type: 'event', sourceId: 'other', effectiveDate: '2026-06-18T12:00:00Z', frontmatter: '{"event":{"who":["people/zed"],"kind":"meeting"}}' });
|
||
await insertProjection(otherMeeting, ids.eOther, '2026-06-18', 'Other-source event');
|
||
});
|
||
|
||
afterAll(async () => { await engine.disconnect(); });
|
||
|
||
describe('Life Chronicle timeline reads', () => {
|
||
test('getTimelineForDate orders intra-day by event effective_date', async () => {
|
||
const rows = await engine.getTimelineForDate('2026-06-18', { sourceId: 'default' });
|
||
expect(rows.map(r => r.event_slug)).toEqual([
|
||
'life/events/2026-06-18-002', // 09:00 first
|
||
'life/events/2026-06-18-001', // 15:30 second
|
||
]);
|
||
expect(rows[0].page_slug).toBe('meetings/2026-06-18-sync'); // backlink = depth page
|
||
expect(rows[1].kind).toBe('commitment');
|
||
});
|
||
|
||
test('week expansion pulls the whole ISO week', async () => {
|
||
const day = await engine.getTimelineForDate('2026-06-18', { sourceId: 'default' });
|
||
const week = await engine.getTimelineForDate('2026-06-18', { week: true, sourceId: 'default' });
|
||
expect(day.length).toBe(2);
|
||
expect(week.length).toBe(3); // adds 06-20 (same Mon–Sun week)
|
||
expect(week[week.length - 1].event_slug).toBe('life/events/2026-06-20-001');
|
||
});
|
||
|
||
test('getSince filters by lower-bound date and event kind', async () => {
|
||
const since19 = await engine.getSince('2026-06-19', { sourceId: 'default' });
|
||
expect(since19.map(r => r.event_slug)).toEqual(['life/events/2026-06-20-001']);
|
||
const commitments = await engine.getSince('2026-06-18', { kind: 'commitment', sourceId: 'default' });
|
||
expect(commitments.map(r => r.event_slug)).toEqual(['life/events/2026-06-18-001']);
|
||
});
|
||
|
||
test('getLastSeen finds the entity via event who, with days_ago', async () => {
|
||
const seen = await engine.getLastSeen('people/sarah-chen', { asof: '2026-06-25', sourceId: 'default' });
|
||
expect(seen.last_date).toBe('2026-06-20');
|
||
expect(seen.last_event_slug).toBe('life/events/2026-06-20-001');
|
||
expect(seen.days_ago).toBe(5);
|
||
const never = await engine.getLastSeen('people/nobody', { asof: '2026-06-25', sourceId: 'default' });
|
||
expect(never.last_date).toBeNull();
|
||
expect(never.days_ago).toBeNull();
|
||
});
|
||
|
||
test('getLastSeen ignores future-dated events (bounds to <= asof/today)', async () => {
|
||
// Chronicle legitimately stores future events (a scheduled calendar-event,
|
||
// a planned milestone). "Last seen" must not return one, or the entity
|
||
// reads as seen-today (days_ago clamped to 0). Regression for the missing
|
||
// upper date bound in getLastSeen.
|
||
const fut = await insertPage({ slug: 'life/events/2026-08-01-fut', type: 'event', effectiveDate: '2026-08-01T10:00:00Z', frontmatter: '{"event":{"who":["people/sarah-chen"],"kind":"event"}}' });
|
||
await insertProjection(ids.meeting, fut, '2026-08-01', 'Planned Q3 launch');
|
||
// asof BEFORE the future event: last-seen is the most recent PAST event.
|
||
const seen = await engine.getLastSeen('people/sarah-chen', { asof: '2026-06-25', sourceId: 'default' });
|
||
expect(seen.last_date).toBe('2026-06-20'); // NOT 2026-08-01
|
||
expect(seen.days_ago).toBe(5); // NOT 0
|
||
// asof AFTER it: now it legitimately counts.
|
||
const later = await engine.getLastSeen('people/sarah-chen', { asof: '2026-08-02', sourceId: 'default' });
|
||
expect(later.last_date).toBe('2026-08-01');
|
||
expect(later.days_ago).toBe(1);
|
||
// Clean up so this future event doesn't leak into later shared-fixture tests.
|
||
await engine.executeRaw('UPDATE pages SET deleted_at = now() WHERE id = $1', [fut]);
|
||
});
|
||
|
||
test('source isolation: default scope excludes other-source events', async () => {
|
||
const def = await engine.getTimelineForDate('2026-06-18', { sourceId: 'default' });
|
||
expect(def.some(r => r.event_slug === 'life/events/2026-06-18-099')).toBe(false);
|
||
const other = await engine.getTimelineForDate('2026-06-18', { sourceId: 'other' });
|
||
expect(other.map(r => r.event_slug)).toEqual(['life/events/2026-06-18-099']);
|
||
});
|
||
|
||
test('(event_page_id, date) dedup index rejects a duplicate projection', async () => {
|
||
await expect(
|
||
insertProjection(ids.meeting, ids.e1, '2026-06-18', 'dup'),
|
||
).rejects.toThrow();
|
||
});
|
||
|
||
test('soft-deleting an event page hides it from reads (read-time, not doctor)', async () => {
|
||
await engine.executeRaw('UPDATE pages SET deleted_at = now() WHERE id = $1', [ids.e3]);
|
||
const since19 = await engine.getSince('2026-06-19', { sourceId: 'default' });
|
||
expect(since19.length).toBe(0); // event3 hidden
|
||
const seen = await engine.getLastSeen('people/sarah-chen', { asof: '2026-06-25', sourceId: 'default' });
|
||
expect(seen.last_date).toBe('2026-06-18'); // falls back to event1
|
||
});
|
||
});
|