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.
This commit is contained in:
Waleed Latif
2026-08-20 14:23:56 -07:00
parent d06ef0d666
commit 5826dd9556
2 changed files with 72 additions and 15 deletions
@@ -44,20 +44,26 @@ const fetchMock = vi.fn<(input: string | URL | Request, init?: RequestInit) => P
let listedSpaces: Record<string, unknown>[] = [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<string, unknown>[] = MESSAGES
/** Space returned by `spaces.list` / `spaces.get`; per-test overridable. */
let fetchedSpace: Record<string, unknown> = 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 () => {
+23 -12
View File
@@ -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 {