From 5826dd955604bccfc368fc593a2619878d19b1f3 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 20 Aug 2026 14:23:56 -0700 Subject: [PATCH] fix(connectors): index Google Chat spaces with no messages in the window Review round 1. - orderBy takes a full ordering expression, not a bare direction. The reference documents the default as `createTime ASC`, so send `createTime DESC`; a bare `DESC` either 400s every hydration or is ignored, which would make the cap keep the oldest traffic and the later reverse render the transcript backwards. - getDocument no longer returns null when the message window is empty. A space with no messages is still a live space, and null is the "document is gone" signal the engine treats as last-known-good: returning it dropped spaces whose only prose is their description or guidelines, and left a stale transcript indexed after a space was cleared or lookbackDays was tightened past every message. - The transcript header is omitted when no message contributed text. --- .../google-chat/google-chat.test.ts | 52 +++++++++++++++++-- .../sim/connectors/google-chat/google-chat.ts | 35 ++++++++----- 2 files changed, 72 insertions(+), 15 deletions(-) diff --git a/apps/sim/connectors/google-chat/google-chat.test.ts b/apps/sim/connectors/google-chat/google-chat.test.ts index 080455b933..2b68fb36b3 100644 --- a/apps/sim/connectors/google-chat/google-chat.test.ts +++ b/apps/sim/connectors/google-chat/google-chat.test.ts @@ -44,20 +44,26 @@ const fetchMock = vi.fn<(input: string | URL | Request, init?: RequestInit) => P let listedSpaces: Record[] = [SPACE] /** `nextPageToken` returned by `spaces.list`; per-test overridable. */ let listNextPageToken: string | undefined +/** Messages returned by `spaces.messages.list`; per-test overridable. */ +let listedMessages: Record[] = MESSAGES +/** Space returned by `spaces.list` / `spaces.get`; per-test overridable. */ +let fetchedSpace: Record = SPACE beforeEach(() => { requestedUrls.length = 0 listedSpaces = [SPACE] listNextPageToken = undefined + listedMessages = MESSAGES + fetchedSpace = SPACE fetchMock.mockReset() fetchMock.mockImplementation(async (input) => { const url = String(input) requestedUrls.push(url) - if (url.includes('/messages?')) return jsonResponse({ messages: MESSAGES }) + if (url.includes('/messages?')) return jsonResponse({ messages: listedMessages }) if (url.includes('/spaces?')) { return jsonResponse({ spaces: listedSpaces, nextPageToken: listNextPageToken }) } - if (url.endsWith(`/${SPACE_NAME}`)) return jsonResponse(SPACE) + if (url.endsWith(`/${SPACE_NAME}`)) return jsonResponse(fetchedSpace) return jsonResponse({ error: { message: 'not found' } }, 404) }) vi.stubGlobal('fetch', fetchMock) @@ -204,10 +210,50 @@ describe('google-chat listing caps', () => { }) }) +describe('google-chat empty windows', () => { + it('indexes a space whose only prose is its description when no message has text', async () => { + fetchedSpace = { ...SPACE, spaceDetails: { description: 'Release coordination' } } + listedSpaces = [fetchedSpace] + listedMessages = [] + + const doc = await googleChatConnector.getDocument('token', {}, SPACE_NAME) + expect(doc).not.toBeNull() + expect(doc?.content).toContain('Release coordination') + }) + + it('indexes a space whose only prose is its guidelines when no message has text', async () => { + fetchedSpace = { ...SPACE, spaceDetails: { guidelines: 'Be excellent to each other' } } + listedSpaces = [fetchedSpace] + listedMessages = [] + + const doc = await googleChatConnector.getDocument('token', {}, SPACE_NAME) + expect(doc?.content).toContain('Be excellent to each other') + }) + + it('returns a document rather than null when the window is empty, so a cleared space does not keep a stale transcript', async () => { + listedMessages = [] + const doc = await googleChatConnector.getDocument('token', {}, SPACE_NAME) + expect(doc).not.toBeNull() + expect(doc?.content).not.toContain('Shipping today') + expect(doc?.metadata?.messageCount).toBe(0) + }) + + it('omits the transcript header entirely when no message contributed text', async () => { + listedMessages = [] + const doc = await googleChatConnector.getDocument('token', {}, SPACE_NAME) + expect(doc?.content).not.toContain('--- Messages ---') + }) + + it('still returns null when the space itself is gone', async () => { + const doc = await googleChatConnector.getDocument('token', {}, 'spaces/MISSING') + expect(doc).toBeNull() + }) +}) + describe('google-chat message window', () => { it('requests messages newest-first so the cap keeps the most recent conversation', async () => { await googleChatConnector.getDocument('token', {}, SPACE_NAME) - expect(messagesParams().get('orderBy')).toBe('DESC') + expect(messagesParams().get('orderBy')).toBe('createTime DESC') }) it('renders the newest-first page back into chronological order', async () => { diff --git a/apps/sim/connectors/google-chat/google-chat.ts b/apps/sim/connectors/google-chat/google-chat.ts index 70b5d5c3f1..1dcca49d7b 100644 --- a/apps/sim/connectors/google-chat/google-chat.ts +++ b/apps/sim/connectors/google-chat/google-chat.ts @@ -256,8 +256,12 @@ function rfc3339(date: Date): string { /** * Fetches the newest `maxMessages` messages of a space, optionally bounded by a - * lookback window. Messages are requested newest-first (`orderBy=DESC`) so the - * cap keeps the most recent conversation, then returned in chronological order. + * lookback window. Messages are requested newest-first so the cap keeps the most + * recent conversation, then returned in chronological order. + * + * `orderBy` takes a full ordering expression, not a bare direction: the reference + * documents the default as `createTime ASC` and lists ASC/DESC as the ordering + * *operations* usable within one. `createTime` is the only orderable field here. */ async function fetchSpaceMessages( accessToken: string, @@ -276,7 +280,7 @@ async function fetchSpaceMessages( while (collected.length < maxMessages) { const params = new URLSearchParams({ pageSize: String(Math.min(MESSAGES_PAGE_SIZE, maxMessages - collected.length)), - orderBy: 'DESC', + orderBy: 'createTime DESC', }) if (filter) params.set('filter', filter) if (pageToken) params.set('pageToken', pageToken) @@ -326,14 +330,18 @@ function formatSpaceContent(space: Space, messages: ChatMessage[]): string { const guidelines = space.spaceDetails?.guidelines?.trim() if (guidelines) parts.push(`Guidelines: ${guidelines}`) - parts.push('') - parts.push('--- Messages ---') - + const lines: string[] = [] for (const message of messages) { const text = message.text?.trim() || message.fallbackText?.trim() if (!text) continue const timestamp = message.createTime ?? '' - parts.push(`[${timestamp}] ${senderLabel(message.sender)}: ${text}`) + lines.push(`[${timestamp}] ${senderLabel(message.sender)}: ${text}`) + } + + if (lines.length > 0) { + parts.push('') + parts.push('--- Messages ---') + parts.push(...lines) } return parts.join('\n') @@ -460,12 +468,15 @@ export const googleChatConnector: ConnectorConfig = { const lookbackDays = resolveLookbackDays(sourceConfig.lookbackDays) const messages = await fetchSpaceMessages(accessToken, spaceName, maxMessages, lookbackDays) + /** + * A space with no messages in the window is still a live space, so it is + * indexed rather than skipped. `null` is this connector's "document is gone" + * signal, and the engine treats it as last-known-good: returning it here would + * both drop spaces whose only prose is their description or guidelines, and + * leave a previously indexed transcript in place after the space was cleared + * or `lookbackDays` was tightened past every message. + */ const messageCount = countIndexedMessages(messages) - if (messageCount === 0) { - logger.info('No indexable messages in Google Chat space', { externalId }) - return null - } - const stub = spaceToStub(space, maxMessages, lookbackDays, syncContext) return {