mirror of
https://github.com/simstudioai/sim.git
synced 2026-10-30 19:32:46 +08:00
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:
@@ -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 () => {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user