diff --git a/src/services/meetings.ts b/src/services/meetings.ts index 72eb46a..58e3359 100644 --- a/src/services/meetings.ts +++ b/src/services/meetings.ts @@ -379,11 +379,12 @@ export async function searchMeetings( const needle = params.query.toLowerCase().trim(); // Fetch candidate meetings scoped to project if specified + const candidateBatchSize = Math.max(params.pageSize ?? 50, 50); const meetingsResult = await listMeetings( { projectId: params.projectId, - offset: params.offset ?? 1, - pageSize: params.pageSize ?? 50, + offset: 1, + pageSize: candidateBatchSize, }, opClient ); @@ -458,12 +459,18 @@ export async function searchMeetings( (item): item is MeetingSearchResult => item !== null ); + const total = matchedElements.length; + const offset = params.offset ?? 1; + const requestedPageSize = params.pageSize ?? 20; + const startIndex = Math.max(0, offset - 1); + const pagedElements = matchedElements.slice(startIndex, startIndex + requestedPageSize); + return { - total: matchedElements.length, - count: matchedElements.length, - pageSize: params.pageSize ?? matchedElements.length, - offset: params.offset ?? 1, - elements: matchedElements, - items: matchedElements, + total, + count: pagedElements.length, + pageSize: requestedPageSize, + offset, + elements: pagedElements, + items: pagedElements, }; } diff --git a/tests/meetings.test.ts b/tests/meetings.test.ts index 96456cc..d5d4f49 100644 --- a/tests/meetings.test.ts +++ b/tests/meetings.test.ts @@ -37,8 +37,8 @@ describe("Meetings Service", () => { const res = await listMeetings({ projectId: 1 }, mockClient); expect(res.total).toBe(1); - expect(res.elements[0].title).toBe("Weekly Planning"); - expect(res.elements[0].project.name).toBe("Demo project"); + expect(res.elements[0]!.title).toBe("Weekly Planning"); + expect(res.elements[0]!.project.name).toBe("Demo project"); }); test("getMeeting fetches details and embeds agenda items", async () => { @@ -88,8 +88,8 @@ describe("Meetings Service", () => { const meeting = await getMeeting(2, { includeAgendaItems: true }, mockClient); expect(meeting.title).toBe("Weekly Planning"); expect(meeting.agendaItems).toHaveLength(1); - expect(meeting.agendaItems![0].title).toBe("Good news"); - expect(meeting.agendaItems![0].notes).toBe("What went well this week?"); + expect(meeting.agendaItems![0]!.title).toBe("Good news"); + expect(meeting.agendaItems![0]!.notes).toBe("What went well this week?"); }); test("searchMeetings finds matches in agenda notes", async () => { @@ -138,12 +138,11 @@ describe("Meetings Service", () => { const res = await searchMeetings({ query: "architecture" }, mockClient); expect(res.total).toBe(1); - expect(res.elements[0].matchType).toBe("agenda_item"); - expect(res.elements[0].matchedAgendaItems![0].snippet).toContain("architecture"); + expect(res.elements[0]!.matchType).toBe("agenda_item"); + expect(res.elements[0]!.matchedAgendaItems![0]!.snippet).toContain("architecture"); }); - test("searchMeetings matches by title and location without calling agenda items", async () => { - let agendaItemsCalled = false; + test("searchMeetings matches by title and location", async () => { const mockClient = { get: async (path: string) => { if (path.startsWith("/api/v3/meetings?")) { @@ -181,7 +180,6 @@ describe("Meetings Service", () => { }; } if (path.includes("/agenda_items")) { - agendaItemsCalled = true; return { _type: "Collection", total: 0, _embedded: { elements: [] } }; } throw new Error(`Unexpected path: ${path}`); @@ -190,13 +188,68 @@ describe("Meetings Service", () => { const titleRes = await searchMeetings({ query: "Retrospective" }, mockClient); expect(titleRes.total).toBe(1); - expect(titleRes.elements[0].matchType).toBe("title"); - expect(titleRes.elements[0].meeting.id).toBe(20); + expect(titleRes.elements[0]!.matchType).toBe("title"); + expect(titleRes.elements[0]!.meeting.id).toBe(20); const locRes = await searchMeetings({ query: "Virtual" }, mockClient); expect(locRes.total).toBe(1); - expect(locRes.elements[0].matchType).toBe("location"); - expect(locRes.elements[0].meeting.id).toBe(21); + expect(locRes.elements[0]!.matchType).toBe("location"); + expect(locRes.elements[0]!.meeting.id).toBe(21); + }); + + test("searchMeetings slices matched elements according to offset and pageSize", async () => { + const mockClient = { + get: async (path: string) => { + if (path.startsWith("/api/v3/meetings?")) { + return { + _type: "Collection", + total: 3, + _embedded: { + elements: [ + { + _type: "Meeting", + id: 1, + title: "Arch Review 1", + state: "open", + startTime: "2026-09-01T10:00:00Z", + endTime: "2026-09-01T11:00:00Z", + _links: { project: { href: "/api/v3/projects/1", title: "Demo" } } + }, + { + _type: "Meeting", + id: 2, + title: "Arch Review 2", + state: "open", + startTime: "2026-09-02T10:00:00Z", + endTime: "2026-09-02T11:00:00Z", + _links: { project: { href: "/api/v3/projects/1", title: "Demo" } } + }, + { + _type: "Meeting", + id: 3, + title: "Arch Review 3", + state: "open", + startTime: "2026-09-03T10:00:00Z", + endTime: "2026-09-03T11:00:00Z", + _links: { project: { href: "/api/v3/projects/1", title: "Demo" } } + } + ] + } + }; + } + if (path.includes("/agenda_items")) { + return { _type: "Collection", total: 0, _embedded: { elements: [] } }; + } + throw new Error(`Unexpected path: ${path}`); + } + } as unknown as OpenProjectClient; + + const res = await searchMeetings({ query: "Arch", offset: 2, pageSize: 1 }, mockClient); + expect(res.total).toBe(3); + expect(res.count).toBe(1); + expect(res.pageSize).toBe(1); + expect(res.offset).toBe(2); + expect(res.elements[0]!.meeting.id).toBe(2); }); test("listMeetings supports time filter (upcoming and past)", async () => { diff --git a/tests/work-package-activities.test.ts b/tests/work-package-activities.test.ts index 7e01331..df38bd7 100644 --- a/tests/work-package-activities.test.ts +++ b/tests/work-package-activities.test.ts @@ -45,14 +45,14 @@ describe("Work Package Activities Service", () => { const all = await listWorkPackageActivities({ workPackageId: 38 }, mockClient); expect(all).toHaveLength(2); - expect(all[0].isComment).toBe(false); - expect(all[1].isComment).toBe(true); - expect(all[1].comment).toBe("Fixed in commit abc1234"); - expect(all[1].user?.name).toBe("Dev User"); + expect(all[0]!.isComment).toBe(false); + expect(all[1]!.isComment).toBe(true); + expect(all[1]!.comment).toBe("Fixed in commit abc1234"); + expect(all[1]!.user?.name).toBe("Dev User"); const commentsOnly = await listWorkPackageActivities({ workPackageId: 38, onlyComments: true }, mockClient); expect(commentsOnly).toHaveLength(1); - expect(commentsOnly[0].id).toBe(61); + expect(commentsOnly[0]!.id).toBe(61); }); test("handles empty activities collection and fallback user formatting", async () => { @@ -78,11 +78,11 @@ describe("Work Package Activities Service", () => { const res = await listWorkPackageActivities({ workPackageId: 10 }, mockClient); expect(res).toHaveLength(1); - expect(res[0].id).toBe(99); - expect(res[0].user).toEqual({ id: 42, name: "User #42" }); - expect(res[0].comment).toBeUndefined(); - expect(res[0].isComment).toBe(false); - expect(res[0].details).toEqual([]); + expect(res[0]!.id).toBe(99); + expect(res[0]!.user).toEqual({ id: 42, name: "User #42" }); + expect(res[0]!.comment).toBeUndefined(); + expect(res[0]!.isComment).toBe(false); + expect(res[0]!.details).toEqual([]); }); test("handles missing _embedded or empty elements gracefully", async () => {