Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,15 @@ program
try {
const config = loadConfig();
const { runTimeline } = await import("./events.js");
await runTimeline(config, opts);
// `--type` is the user-facing flag name; TimelineOpts calls it `kind`.
// Pass it across explicitly — spreading `opts` leaves `kind` undefined
// and the filter silently matches everything.
await runTimeline(config, {
json: opts.json,
since: opts.since,
kind: opts.type,
limit: opts.limit,
});
} catch (err) {
console.error((err as Error).message);
process.exit(1);
Expand Down
60 changes: 56 additions & 4 deletions test/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,12 @@ function buildProgram(): Command {
.action(async (opts) => {
try {
const { runTimeline } = await import("../src/events.js");
await runTimeline(config, opts);
await runTimeline(config, {
json: opts.json,
since: opts.since,
kind: opts.type,
limit: opts.limit,
});
} catch (err) {
console.error((err as Error).message);
process.exit(1);
Expand Down Expand Up @@ -176,7 +181,12 @@ describe("mex timeline parsing", () => {
const program = buildProgram();
await program.parseAsync(["node", "mex", "timeline", "--limit", "5"]);

expect(runTimeline).toHaveBeenCalledWith(config, { limit: 5 });
expect(runTimeline).toHaveBeenCalledWith(config, {
json: undefined,
since: undefined,
kind: undefined,
limit: 5,
});
});

it("rejects invalid --limit values", async () => {
Expand All @@ -189,7 +199,7 @@ describe("mex timeline parsing", () => {
}
});

it("passes --json, --since, and --type through to the timeline handler", async () => {
it("maps --type onto the handler's `kind` option", async () => {
const program = buildProgram();
await program.parseAsync([
"node",
Expand All @@ -205,7 +215,8 @@ describe("mex timeline parsing", () => {
expect(runTimeline).toHaveBeenCalledWith(config, {
json: true,
since: "30d",
type: "risk",
kind: "risk",
limit: undefined,
});
});
});
Expand Down Expand Up @@ -272,6 +283,47 @@ describe("built CLI main-module guard", () => {
rmSync(fixture, { recursive: true, force: true });
}
});

it("filters the timeline by --type through the real binary", () => {
const fixture = mkdtempSync(join(tmpdir(), "mex-timeline-"));
const env = { ...process.env, NO_COLOR: "1" };
try {
const mexPath = join(fixture, ".mex");
mkdirSync(join(mexPath, "events"), { recursive: true });
writeFileSync(join(mexPath, "ROUTER.md"), "");
writeFileSync(
join(mexPath, "events", "decisions.jsonl"),
[
{ timestamp: "2026-05-14T00:00:00.000Z", kind: "decision", message: "picked sqlite", files: [] },
{ timestamp: "2026-05-15T00:00:00.000Z", kind: "risk", message: "wasm heap pressure", files: [] },
]
.map((e) => JSON.stringify(e))
.join("\n") + "\n",
);

const result = spawnSync(
process.execPath,
[cliPath, "timeline", "--type", "decision", "--json"],
{ cwd: fixture, encoding: "utf8", env },
);
expect(result.status).toBe(0);

const { events } = JSON.parse(result.stdout) as { events: { kind: string }[] };
expect(events).toHaveLength(1);
expect(events[0].kind).toBe("decision");

// An unknown kind must fail the way `mex log` does, not quietly list everything.
const unknown = spawnSync(
process.execPath,
[cliPath, "timeline", "--type", "session_start"],
{ cwd: fixture, encoding: "utf8", env },
);
expect(unknown.status).toBe(1);
expect(unknown.stderr).toContain('Unknown event type "session_start"');
} finally {
rmSync(fixture, { recursive: true, force: true });
}
}, 30_000);
});

describe("mex --version", () => {
Expand Down
20 changes: 20 additions & 0 deletions test/events.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,24 @@ describe("events", () => {
await runTimeline(config, { json: true });
expect(spy.mock.calls.at(-1)?.[0]).toContain('"events"');
});

it("timeline keeps only the requested kind", async () => {
appendEvent(config, "picked sqlite", { kind: "decision" });
appendEvent(config, "wasm heap pressure", { kind: "risk" });
appendEvent(config, "plain note", { kind: "note" });

const spy = vi.spyOn(console, "log").mockImplementation(() => {});
await runTimeline(config, { json: true, kind: "decision" });

const { events } = JSON.parse(spy.mock.calls.at(-1)?.[0] as string);
expect(events).toHaveLength(1);
expect(events[0]).toMatchObject({ kind: "decision", message: "picked sqlite" });
});

it("timeline rejects an unknown kind instead of returning everything", async () => {
appendEvent(config, "plain note", { kind: "note" });
await expect(runTimeline(config, { kind: "session_start" })).rejects.toThrow(
/Unknown event type "session_start"/,
);
});
});