From 497e437bf4931c47af58012bd2fe247b699a836d Mon Sep 17 00:00:00 2001 From: Brad Harris Date: Thu, 16 Jul 2026 20:25:15 -0600 Subject: [PATCH] Add boundary-merge tests for working-time-by-project Four integration tests for the previously smoke-tested boundary-merge path in handleWorkingTimeByProject: range-start clamping, DISTINCT ON picking the latest pre-range event, per-agent merge of interleaved events, and project_dir precedence over agent cwd. Co-Authored-By: Claude Fable 5 --- apps/server/test/activity-routes.test.ts | 93 +++++++++++++++++++++++- 1 file changed, 89 insertions(+), 4 deletions(-) diff --git a/apps/server/test/activity-routes.test.ts b/apps/server/test/activity-routes.test.ts index ca2390e3..f4f5133f 100644 --- a/apps/server/test/activity-routes.test.ts +++ b/apps/server/test/activity-routes.test.ts @@ -75,12 +75,13 @@ async function seedEvent( agentId: string, eventType: string, createdAt: string, - message = "test" + message = "test", + projectDir: string | null = null ): Promise { await ctx.pool.query( - `INSERT INTO agent_events (agent_id, event_type, message, created_at) - VALUES ($1, $2, $3, $4)`, - [agentId, eventType, message, createdAt] + `INSERT INTO agent_events (agent_id, event_type, message, created_at, project_dir) + VALUES ($1, $2, $3, $4, $5)`, + [agentId, eventType, message, createdAt, projectDir] ); } @@ -350,6 +351,90 @@ describe("GET /api/v1/activity/working-time-by-project", () => { const { projects } = res.json(); expect(projects.length).toBeGreaterThanOrEqual(1); }); + + const RANGE = "start=2026-01-10T00:00:00Z&end=2026-01-11T00:00:00Z&tz=UTC"; + + it("clamps a working session that began before the range start", async () => { + const agentId = await createAgent({ cwd: "/home/user/proj-carry" }); + await seedEvent(agentId, "working", "2026-01-09T22:00:00Z"); + await seedEvent(agentId, "done", "2026-01-10T00:30:00Z"); + + const res = await authedInject( + "GET", + `/api/v1/activity/working-time-by-project?${RANGE}` + ); + expect(res.statusCode).toBe(200); + // Only the portion inside the range counts: 00:00 → 00:30, not the + // two hours before the range start. + expect(res.json().projects).toEqual([ + { project_dir: "/home/user/proj-carry", working_time_ms: 30 * 60_000 }, + ]); + }); + + it("carries in the latest pre-range event, not the earliest", async () => { + const agentId = await createAgent({ cwd: "/home/user/proj-latest" }); + await seedEvent(agentId, "working", "2026-01-09T20:00:00Z"); + await seedEvent(agentId, "done", "2026-01-09T21:00:00Z"); + await seedEvent(agentId, "working", "2026-01-10T00:10:00Z"); + await seedEvent(agentId, "done", "2026-01-10T00:20:00Z"); + + const res = await authedInject( + "GET", + `/api/v1/activity/working-time-by-project?${RANGE}` + ); + expect(res.statusCode).toBe(200); + // The boundary carry-in must be the latest pre-range event ("done"), so + // only the 10-minute in-range session counts. If the boundary query + // picked the earlier "working" event, this would report 20 minutes. + expect(res.json().projects).toEqual([ + { project_dir: "/home/user/proj-latest", working_time_ms: 10 * 60_000 }, + ]); + }); + + it("merges boundary events per agent independently", async () => { + const agentA = await createAgent({ cwd: "/home/user/proj-a" }); + const agentB = await createAgent({ cwd: "/home/user/proj-b" }); + await seedEvent(agentA, "working", "2026-01-09T23:00:00Z"); + await seedEvent(agentB, "working", "2026-01-09T23:30:00Z"); + await seedEvent(agentB, "done", "2026-01-10T00:05:00Z"); + await seedEvent(agentA, "done", "2026-01-10T00:10:00Z"); + + const res = await authedInject( + "GET", + `/api/v1/activity/working-time-by-project?${RANGE}` + ); + expect(res.statusCode).toBe(200); + // Each agent's carry-in pairs with its own in-range event even though + // the events interleave in time, and results sort by duration desc. + expect(res.json().projects).toEqual([ + { project_dir: "/home/user/proj-a", working_time_ms: 10 * 60_000 }, + { project_dir: "/home/user/proj-b", working_time_ms: 5 * 60_000 }, + ]); + }); + + it("prefers the event project_dir over the agent cwd for boundary events", async () => { + const agentId = await createAgent({ cwd: "/home/user/proj-cwd" }); + await seedEvent( + agentId, + "working", + "2026-01-09T23:00:00Z", + "test", + "/home/user/proj-explicit" + ); + await seedEvent(agentId, "done", "2026-01-10T00:15:00Z"); + + const res = await authedInject( + "GET", + `/api/v1/activity/working-time-by-project?${RANGE}` + ); + expect(res.statusCode).toBe(200); + expect(res.json().projects).toEqual([ + { + project_dir: "/home/user/proj-explicit", + working_time_ms: 15 * 60_000, + }, + ]); + }); }); // ---------------------------------------------------------------------------