Skip to content

Commit b948e2d

Browse files
committed
fix(cleanup): scope a chat's file row by the row's own workspace first
collectChatFiles placed a linked file row in the chat's organization whenever the chat had one, ignoring the workspace the row itself belongs to, so the reference check would look at the wrong scope's chats. A chat holds a workspace or an organization, never both, but a file row's own workspace is its real owner, so it now comes first. Autopsy: the reference-check tests seeded only organization chats with organization keys, so no test placed a workspace-owned row attached in an organization chat.
1 parent 972e235 commit b948e2d

2 files changed

Lines changed: 46 additions & 3 deletions

File tree

‎apps/sim/lib/cleanup/chat-cleanup.integration.ts‎

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
/** Exercises chat cleanup's storage-key collection against real PostgreSQL message rows. */
22
import { db, dbFor } from '@sim/db'
3-
import { copilotChats, copilotMessages, organization, user } from '@sim/db/schema'
3+
import {
4+
copilotChats,
5+
copilotMessages,
6+
organization,
7+
user,
8+
workspace,
9+
workspaceFiles,
10+
} from '@sim/db/schema'
11+
import { deleteWorkspaceFixture, insertWorkspaceFixture } from '@sim/db/testing/workspace-fixtures'
412
import { generateId } from '@sim/utils/id'
513
import { compareStrings } from '@sim/utils/string'
614
import { eq } from 'drizzle-orm'
@@ -169,6 +177,40 @@ describe('deletable chat files against PostgreSQL message rows', () => {
169177
)
170178
})
171179

180+
it("scopes a chat's file row by the row's own workspace, not the chat's organization", async () => {
181+
const workspaceId = generateId()
182+
await insertWorkspaceFixture(db, {
183+
id: workspaceId,
184+
name: 'Chat cleanup fixture',
185+
ownerId: userId,
186+
billedAccountUserId: userId,
187+
})
188+
try {
189+
const chatId = await seedChat([{ role: 'user', content: 'look' }], [], organizationId)
190+
const key = `workspace/${workspaceId}/${generateId()}/linked.png`
191+
await db.insert(workspaceFiles).values({
192+
id: generateId(),
193+
key,
194+
userId,
195+
workspaceId,
196+
context: 'mothership',
197+
chatId,
198+
originalName: 'linked.png',
199+
contentType: 'image/png',
200+
sizeBytes: 1,
201+
})
202+
203+
const files = await collectChatFiles([chatId])
204+
205+
expect(files.find((file) => file.key === key)?.scope).toEqual({
206+
kind: 'workspace',
207+
id: workspaceId,
208+
})
209+
} finally {
210+
await deleteWorkspaceFixture(db, eq(workspace.id, workspaceId))
211+
}
212+
})
213+
172214
it('places a legacy ref by its key, keeping one it cannot place', async () => {
173215
const referenced = `assistant/${organizationId}/${userId}/u1/kept.png`
174216
const unreferenced = `assistant/${organizationId}/${userId}/u2/gone.png`

‎apps/sim/lib/cleanup/chat-files.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -187,10 +187,11 @@ export async function collectChatFiles(
187187
chatId: string,
188188
fileWorkspaceId?: string | null
189189
): ChatFileReferenceScope | null => {
190+
// A file row's own workspace owns it, whatever the chat it was attached in belongs to.
191+
if (fileWorkspaceId) return { kind: 'workspace', id: fileWorkspaceId }
190192
const chat = chatScopes.get(chatId)
191193
if (chat?.organizationId) return { kind: 'organization', id: chat.organizationId }
192-
const workspaceId = fileWorkspaceId ?? chat?.workspaceId
193-
return workspaceId ? { kind: 'workspace', id: workspaceId } : null
194+
return chat?.workspaceId ? { kind: 'workspace', id: chat.workspaceId } : null
194195
}
195196

196197
for (const f of linkedFiles) {

0 commit comments

Comments
 (0)