Skip to content

Commit 1de1cfb

Browse files
committed
test(project-files): pin upload cancellation during provenance and new-chat tab order
1 parent 8a3fb00 commit 1de1cfb

2 files changed

Lines changed: 77 additions & 1 deletion

File tree

‎apps/sim/lib/mothership/chat/project-file-context.integration.ts‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { createEvent } from '@/lib/mothership/request/session/event'
2424
import { encodeSSEEnvelope } from '@/lib/mothership/request/session/sse'
2525
import { persistResourceEffect } from '@/lib/mothership/resources/persist-effect'
2626
import { persistChatResources } from '@/lib/mothership/resources/persistence'
27+
import { createSearchResource } from '@/lib/mothership/resources/search'
2728
import { type MothershipResource, sanitizeChatResources } from '@/lib/mothership/resources/types'
2829
import { executeTool, registerHandler } from '@/lib/mothership/tool-executor/executor'
2930
import { createServerToolHandler } from '@/lib/mothership/tools/registry/server-tool-adapter'
@@ -750,6 +751,40 @@ describe('open tabs that no longer resolve', () => {
750751
expect(stored?.resources).toEqual([])
751752
}
752753
)
754+
755+
identityCheck(
756+
'persists new-chat tabs in the order the user has them, not the order they resolve',
757+
async () => {
758+
const f = await fixture()
759+
const fileId = await insertWorkspaceFile(f, 'First.md')
760+
/** The search tab resolves synchronously, so it finishes before the file tab's lookups. */
761+
const search = createSearchResource({
762+
query: 'notes',
763+
scope: { kind: 'workspace', workspaceId: f.workspaceId },
764+
})
765+
try {
766+
await resolveResourceAttachmentContexts({
767+
resourceAttachments: [{ type: 'file', id: fileId, title: 'First.md' }, search],
768+
userId: f.userId,
769+
workspaceId: f.workspaceId,
770+
chatId: f.chatId,
771+
requestMode: 'agent',
772+
persistResources: true,
773+
requestId: generateId(),
774+
})
775+
const [stored] = await db
776+
.select({ resources: copilotChats.resources })
777+
.from(copilotChats)
778+
.where(eq(copilotChats.id, f.chatId))
779+
expect((stored?.resources as MothershipResource[]).map((resource) => resource.id)).toEqual([
780+
fileId,
781+
search.id,
782+
])
783+
} finally {
784+
await db.delete(workspaceFiles).where(eq(workspaceFiles.id, fileId))
785+
}
786+
}
787+
)
753788
})
754789

755790
describe('legacy browser file address compatibility', () => {

‎apps/sim/lib/projects/files/__integration__/uploads.integration.ts‎

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -669,9 +669,11 @@ function workbench(f: Awaited<ReturnType<typeof fixture>>, bytes: Buffer) {
669669

670670
async function privateUploadTransport(
671671
f: Awaited<ReturnType<typeof fixture>>,
672-
uploadProvenance: () => WorkspaceFileSecretProvenance
672+
uploadProvenance: () => WorkspaceFileSecretProvenance,
673+
signal?: AbortSignal
673674
) {
674675
const context = {
676+
signal,
675677
userId: f.editorId,
676678
workspaceId: f.workspaceId,
677679
toolCallId: generateId(),
@@ -885,6 +887,45 @@ describe.skipIf(!redisUrl)('private Project upload transport', () => {
885887
expect(current.metadata.projectFileSecretProvenance).toMatchObject({ pending: true })
886888
}
887889
)
890+
891+
for (const signalOwner of ['request', 'invocation'] as const) {
892+
check(
893+
`private Project upload never registers a file when the ${signalOwner} is cancelled while its provenance resolves`,
894+
async () => {
895+
const f = await fixture()
896+
const bytes = Buffer.from('cancelled upload')
897+
const controller = new AbortController()
898+
/** Resolves cleanly after the abort, so only a recheck before completion can stop it. */
899+
const cancelling = () => {
900+
controller.abort(new Error('cancelled during upload provenance'))
901+
return { status: 'exact' as const, entries: [] }
902+
}
903+
const transport = await privateUploadTransport(
904+
f,
905+
cancelling,
906+
signalOwner === 'invocation' ? controller.signal : undefined
907+
)
908+
const { session } = await createPrivateUpload(transport, f.projectId, bytes)
909+
await put(session, bytes)
910+
await expect(
911+
transport(
912+
`http://localhost:3000/api/v2/projects/${f.projectId}/files/uploads/${session.id}/complete`,
913+
{
914+
method: 'POST',
915+
headers: { 'upload-token': session.uploadToken },
916+
signal: signalOwner === 'request' ? controller.signal : undefined,
917+
}
918+
)
919+
).rejects.toThrow('cancelled during upload provenance')
920+
const current = await sessions.verifyUploadSessionToken(session.uploadToken)
921+
expect(current.completedFileId).toBeNull()
922+
expect(
923+
await db.select().from(workspaceFiles).where(eq(workspaceFiles.projectId, f.projectId))
924+
).toEqual([])
925+
expect(await usage(f.organizationId)).toBe(0)
926+
}
927+
)
928+
}
888929
})
889930

890931
afterAll(async () => {

0 commit comments

Comments
 (0)