fix(timeline): expose date window filters (#2694)

This commit is contained in:
Ziyang Guo
2026-07-16 20:47:44 -07:00
committed by GitHub
parent 9315fd0746
commit e0ca74200a
2 changed files with 92 additions and 2 deletions
+15 -2
View File
@@ -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'] },
+77
View File
@@ -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',
});
});
});