From e0ca74200a31290bd322bee1482152120ab2cb0a Mon Sep 17 00:00:00 2001 From: Ziyang Guo <121015044+RerankerGuo@users.noreply.github.com> Date: Fri, 17 Jul 2026 11:47:44 +0800 Subject: [PATCH] fix(timeline): expose date window filters (#2694) --- src/core/operations.ts | 17 +++++++- test/get-timeline-op.test.ts | 77 ++++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 test/get-timeline-op.test.ts diff --git a/src/core/operations.ts b/src/core/operations.ts index 98e02d3c0..d95f045fd 100644 --- a/src/core/operations.ts +++ b/src/core/operations.ts @@ -2176,14 +2176,27 @@ const add_timeline_entry: Operation = { const get_timeline: Operation = { name: 'get_timeline', - description: 'Get timeline entries for a page', + description: 'Get timeline entries for a page, optionally filtered by date window', params: { slug: { type: 'string', required: true }, + after: { type: 'string', description: 'Return entries on or after this date (YYYY-MM-DD)' }, + before: { type: 'string', description: 'Return entries on or before this date (YYYY-MM-DD)' }, + since: { type: 'string', description: 'Alias for after; accepted for agent callers' }, + until: { type: 'string', description: 'Alias for before; accepted for agent callers' }, + limit: { type: 'number', description: 'Maximum number of timeline entries to return' }, }, handler: async (ctx, p) => { // #2200: route through sourceScopeOpts so a federated grant reaches the // engine via TimelineOpts.sourceIds; scalar/unset unchanged. - return ctx.engine.getTimeline(p.slug as string, sourceScopeOpts(ctx)); + const after = typeof p.after === 'string' ? p.after : typeof p.since === 'string' ? p.since : undefined; + const before = typeof p.before === 'string' ? p.before : typeof p.until === 'string' ? p.until : undefined; + const limit = typeof p.limit === 'number' ? p.limit : undefined; + return ctx.engine.getTimeline(p.slug as string, { + ...sourceScopeOpts(ctx), + ...(after ? { after } : {}), + ...(before ? { before } : {}), + ...(limit !== undefined ? { limit } : {}), + }); }, scope: 'read', cliHints: { name: 'timeline', positional: ['slug'] }, diff --git a/test/get-timeline-op.test.ts b/test/get-timeline-op.test.ts new file mode 100644 index 000000000..b969de0d0 --- /dev/null +++ b/test/get-timeline-op.test.ts @@ -0,0 +1,77 @@ +import { describe, expect, test } from 'bun:test'; +import { operationsByName, type OperationContext } from '../src/core/operations.ts'; +import type { TimelineOpts } from '../src/core/types.ts'; + +const getTimeline = operationsByName['get_timeline']; + +function makeCtx(): OperationContext { + const calls: Array<{ slug: string; opts?: TimelineOpts }> = []; + const engine = { + getTimeline: async (slug: string, opts?: TimelineOpts) => { + calls.push({ slug, opts }); + return []; + }, + }; + + return { + engine, + config: {}, + logger: { info() {}, warn() {}, error() {}, debug() {} }, + dryRun: false, + remote: true, + sourceId: 'default', + auth: { + token: 'test', + clientId: 'client', + scopes: ['read'], + sourceId: 'default', + allowedSources: ['alpha', 'beta'], + }, + __calls: calls, + } as unknown as OperationContext & { __calls: Array<{ slug: string; opts?: TimelineOpts }> }; +} + +describe('get_timeline op', () => { + test('declares date-window and limit params', () => { + expect(getTimeline.params.after.type).toBe('string'); + expect(getTimeline.params.before.type).toBe('string'); + expect(getTimeline.params.since.type).toBe('string'); + expect(getTimeline.params.until.type).toBe('string'); + expect(getTimeline.params.limit.type).toBe('number'); + }); + + test('threads after/before/limit with federated source scope', async () => { + const ctx = makeCtx(); + await getTimeline.handler(ctx, { + slug: 'people/alice-example', + after: '2026-01-01', + before: '2026-03-31', + limit: 7, + }); + + expect((ctx as typeof ctx & { __calls: Array<{ slug: string; opts?: TimelineOpts }> }).__calls).toEqual([{ + slug: 'people/alice-example', + opts: { + sourceIds: ['alpha', 'beta'], + after: '2026-01-01', + before: '2026-03-31', + limit: 7, + }, + }]); + }); + + test('accepts since/until as aliases for after/before', async () => { + const ctx = makeCtx(); + await getTimeline.handler(ctx, { + slug: 'people/alice-example', + since: '2026-04-01', + until: '2026-04-30', + }); + + expect((ctx as typeof ctx & { __calls: Array<{ slug: string; opts?: TimelineOpts }> }).__calls[0]?.opts).toMatchObject({ + sourceIds: ['alpha', 'beta'], + after: '2026-04-01', + before: '2026-04-30', + }); + }); +});