diff --git a/src/slack/search-messages.ts b/src/slack/search-messages.ts index cc8d9ef..1c05af9 100644 --- a/src/slack/search-messages.ts +++ b/src/slack/search-messages.ts @@ -174,7 +174,7 @@ export async function searchMessagesInChannelsFallback( const results: SearchCompactMessage[] = []; - for (const channelId of channelIds) { + channelLoop: for (const channelId of channelIds) { let cursorLatest: string | undefined; for (;;) { const resp = await client.api("conversations.history", { @@ -187,6 +187,7 @@ export async function searchMessagesInChannelsFallback( break; } + let reachedAfterBoundary = false; for (const m of messages) { const summary = messageSummaryFromApiMessage(channelId, m); @@ -196,7 +197,7 @@ export async function searchMessagesInChannelsFallback( continue; } if (afterSec !== null && tsNum < afterSec) { - cursorLatest = undefined; + reachedAfterBoundary = true; break; } } @@ -230,31 +231,20 @@ export async function searchMessagesInChannelsFallback( matchedSummaries.push(summary); results.push(toSearchCompactMessage(compact)); if (results.length >= input.limit) { - const referencedUserIds = collectReferencedUserIds(matchedSummaries, { - includeReactions: false, - }); - const usersById = await resolveUsersById({ - client, - workspaceUrl: input.workspace_url ?? "", - userIds: referencedUserIds, - forceRefresh: Boolean(input.refreshUsers), - }); - return { - messages: results, - referenced_users: toReferencedUsers(referencedUserIds, usersById), - }; + break channelLoop; } } - if (!cursorLatest) { + if (reachedAfterBoundary || resp.has_more === false) { break; } const last = messages.at(-1); - cursorLatest = last ? getString(last.ts) : undefined; - if (!cursorLatest) { + const nextLatest = last ? getString(last.ts) : undefined; + if (!nextLatest || nextLatest === cursorLatest) { break; } + cursorLatest = nextLatest; } } diff --git a/test/search-command.test.ts b/test/search-command.test.ts index f918ce9..3db4bf7 100644 --- a/test/search-command.test.ts +++ b/test/search-command.test.ts @@ -49,6 +49,7 @@ function createClient(calls: ApiCall[]) { if (method === "conversations.history") { return { + has_more: false, messages: [ { ts: "1.000001", @@ -154,6 +155,78 @@ describe("search referenced users", () => { expect(result.referenced_users).not.toHaveProperty("U44444444"); }); + test("searchSlack scans older channel history pages", async () => { + const calls: ApiCall[] = []; + const client = { + api: async (method: string, params: Record) => { + calls.push({ method, params }); + if (method !== "conversations.history") { + throw new Error(`Unexpected API method: ${method}`); + } + if (params.latest === undefined) { + return { + has_more: true, + messages: [{ ts: "2.000002", text: "not a match", user: "U11111111" }], + }; + } + if (params.latest === "2.000002") { + return { + has_more: false, + messages: [{ ts: "1.000001", text: "needle", user: "U22222222" }], + }; + } + throw new Error(`Unexpected latest timestamp: ${String(params.latest)}`); + }, + }; + + const result = await searchSlack({ + client: client as never, + auth: { auth_type: "standard", token: "x" }, + options: { + workspace_url: "https://workspace.slack.com", + query: "needle", + kind: "messages", + channels: ["C12345678"], + limit: 20, + max_content_chars: 4000, + content_type: "any", + download: false, + }, + }); + + expect(result.messages).toHaveLength(1); + expect(result.messages?.[0]?.content).toBe("needle"); + expect( + calls + .filter((call) => call.method === "conversations.history") + .map((call) => call.params.latest), + ).toEqual([undefined, "2.000002"]); + }); + + test("searchSlack does not resolve users at the result limit without opt-in", async () => { + const calls: ApiCall[] = []; + const client = createClient(calls) as never; + + const result = await searchSlack({ + client, + auth: { auth_type: "standard", token: "x" }, + options: { + workspace_url: "https://workspace.slack.com", + query: "hello", + kind: "messages", + channels: ["C12345678"], + limit: 1, + max_content_chars: 4000, + content_type: "any", + download: false, + }, + }); + + expect(result.messages).toHaveLength(1); + expect(result.referenced_users).toBeUndefined(); + expect(calls.filter((call) => call.method === "users.info")).toHaveLength(0); + }); + test("search command forwards --refresh-users and bypasses cached user lookups", async () => { const calls: ApiCall[] = []; const ctx = createContext(calls);