Skip to content

Commit 6b0abb1

Browse files
committed
fix(msteams): use fixed page size (50) for Graph requests to ensure proper pagination
When fetching thread replies, always use $top=50 for Graph API requests instead of using the requested limit. This ensures that pagination retrieves full 50-item pages, so when selecting the newest N replies, we have the complete dataset to choose from. Fixes the issue where callers requesting fewer than 50 replies would get incomplete pagination, potentially missing the newest replies.
1 parent 8b04657 commit 6b0abb1

2 files changed

Lines changed: 105 additions & 4 deletions

File tree

extensions/msteams/src/graph-thread.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,17 @@ export async function fetchThreadReplies(
135135
messageId: string,
136136
maxReplies = 50,
137137
): Promise<GraphThreadMessage[]> {
138+
// Always fetch full 50-item pages to ensure we get the newest replies when paginating.
139+
// The final result is sliced to the requested maxReplies after selecting newest replies.
138140
const top = Math.min(Math.max(maxReplies, 1), 50);
139-
const path = `/teams/${encodeURIComponent(groupId)}/channels/${encodeURIComponent(channelId)}/messages/${encodeURIComponent(messageId)}/replies?$top=${top}&$select=id,from,body,createdDateTime`;
141+
const pageTop = 50; // Always fetch full pages to maximize pagination efficiency
142+
143+
// NOTE: Graph replies endpoint returns oldest-first and does not support $orderby.
144+
// When a thread has more than `top` replies, this function paginates through
145+
// all pages via `fetchAllGraphPages` and returns the newest `top` replies
146+
// (sorted chronologically), so the agent sees the most relevant recent context.
147+
// Pagination is bounded by `MAX_REPLY_PAGES` (50 pages × up to 50 per page = 2500 replies).
148+
const path = `/teams/${encodeURIComponent(groupId)}/channels/${encodeURIComponent(channelId)}/messages/${encodeURIComponent(messageId)}/replies?$top=${pageTop}&$select=id,from,body,createdDateTime`;
140149

141150
// Paginate through all reply pages, bounded by MAX_REPLY_PAGES (2500 total).
142151
const { items } = await fetchAllGraphPages<GraphThreadMessage>({
@@ -155,9 +164,9 @@ export async function fetchThreadReplies(
155164
const sorted = items.toSorted((a, b) =>
156165
(b.createdDateTime ?? "").localeCompare(a.createdDateTime ?? ""),
157166
);
158-
return sorted.slice(0, top).toSorted((a, b) =>
159-
(a.createdDateTime ?? "").localeCompare(b.createdDateTime ?? ""),
160-
);
167+
return sorted
168+
.slice(0, top)
169+
.toSorted((a, b) => (a.createdDateTime ?? "").localeCompare(b.createdDateTime ?? ""));
161170
}
162171

163172
/**

extensions/slack/src/interactive-replies.test.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,98 @@
22
import { describe, expect, it } from "vitest";
33
import { compileSlackInteractiveReplies } from "./interactive-replies.js";
44

5+
describe("parseChoice behavior (Issue #99823 fix)", () => {
6+
it("handles labels with colons in the middle (time labels)", () => {
7+
const result = compileSlackInteractiveReplies({
8+
text: "[[slack_buttons: Fr 10.07. 9:00:slot_fr_0900, Mo 13.07. 10:45:slot_mo_1045]]",
9+
});
10+
11+
expect(result.interactive).toEqual({
12+
blocks: [
13+
{
14+
type: "buttons",
15+
buttons: [
16+
{ label: "Fr 10.07. 9:00", value: "slot_fr_0900" },
17+
{ label: "Mo 13.07. 10:45", value: "slot_mo_1045" },
18+
],
19+
},
20+
],
21+
});
22+
});
23+
24+
it("handles labels with colons and style suffixes", () => {
25+
const result = compileSlackInteractiveReplies({
26+
text: "[[slack_buttons: Approve:approve:primary, Reject:reject:danger]]",
27+
});
28+
29+
expect(result.interactive).toEqual({
30+
blocks: [
31+
{
32+
type: "buttons",
33+
buttons: [
34+
{ label: "Approve", value: "approve", style: "primary" },
35+
{ label: "Reject", value: "reject", style: "danger" },
36+
],
37+
},
38+
],
39+
});
40+
});
41+
42+
it("handles labels with colons, time, and style suffixes", () => {
43+
const result = compileSlackInteractiveReplies({
44+
text: "[[slack_buttons: 9:00 AM:slot_morning:success, 5:00 PM:slot_evening:secondary]]",
45+
});
46+
47+
expect(result.interactive).toEqual({
48+
blocks: [
49+
{
50+
type: "buttons",
51+
buttons: [
52+
{ label: "9:00 AM", value: "slot_morning", style: "success" },
53+
{ label: "5:00 PM", value: "slot_evening", style: "secondary" },
54+
],
55+
},
56+
],
57+
});
58+
});
59+
60+
it("handles simple label:value pairs without colons in label", () => {
61+
const result = compileSlackInteractiveReplies({
62+
text: "[[slack_buttons: Retry:retry, Ignore:ignore]]",
63+
});
64+
65+
expect(result.interactive).toEqual({
66+
blocks: [
67+
{
68+
type: "buttons",
69+
buttons: [
70+
{ label: "Retry", value: "retry" },
71+
{ label: "Ignore", value: "ignore" },
72+
],
73+
},
74+
],
75+
});
76+
});
77+
78+
it("handles labels without any colons (label === value)", () => {
79+
const result = compileSlackInteractiveReplies({
80+
text: "[[slack_buttons: JustText, AnotherOption]]",
81+
});
82+
83+
expect(result.interactive).toEqual({
84+
blocks: [
85+
{
86+
type: "buttons",
87+
buttons: [
88+
{ label: "JustText", value: "JustText" },
89+
{ label: "AnotherOption", value: "AnotherOption" },
90+
],
91+
},
92+
],
93+
});
94+
});
95+
});
96+
597
describe("compileSlackInteractiveReplies", () => {
698
it("compiles inline Slack button directives into shared interactive blocks", () => {
799
const result = compileSlackInteractiveReplies({

0 commit comments

Comments
 (0)