Skip to content

Commit ecffcbb

Browse files
committed
fix(files): resolve CI audits and test types after restacking on staging
- Keep prepareProjectsForAccountDeletion returning the Project IDs to delete. The object return added an unused storageCleanupEventIds field; the durable cleanup outbox already processes those events, so restore the staging contract and its stronger integration assertions. - Exempt lib/mothership/generated from the relative-import convention. contracts:sync copies those wire contracts verbatim and byte-checks them, so their sibling imports must stay relative; drop their baseline rows. - Re-record the files page module-count baseline for the Project browser. - Type the test fixtures that regressed, including a queue-recovery fixture that persisted the invalid completion status 'success'.
1 parent d66d69d commit ecffcbb

16 files changed

Lines changed: 105 additions & 70 deletions

File tree

‎.claude/rules/sim-imports.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ paths:
1010
## Absolute Imports
1111

1212
**Always use absolute imports.** Never use relative imports, except that a barrel `index.ts` re-exports its own siblings relatively (`export { X } from './x'`). `check:source-conventions` ratchets this per file.
13+
The wire contracts in `lib/mothership/generated/` are also exempt: the worker's `contracts:sync` copies them verbatim and byte-checks the copies, so never hand-edit their imports.
1314

1415
```typescript
1516
// ✓ Good

‎.cursor/rules/sim-imports.mdc‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ globs: ["apps/sim/**/*.ts","apps/sim/**/*.tsx"]
1010
## Absolute Imports
1111

1212
**Always use absolute imports.** Never use relative imports, except that a barrel `index.ts` re-exports its own siblings relatively (`export { X } from './x'`). `check:source-conventions` ratchets this per file.
13+
The wire contracts in `lib/mothership/generated/` are also exempt: the worker's `contracts:sync` copies them verbatim and byte-checks the copies, so never hand-edit their imports.
1314

1415
```typescript
1516
// ✓ Good

‎apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/editor-lifecycle.test.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -832,7 +832,7 @@ it('inserts an uploaded inline image using its Project content address', async (
832832
fileSource.owner = { entityType: 'project', entityId: 'project-a' }
833833
uploadProjectFile.mockResolvedValue({ file: { id: 'image-a' } })
834834
await render('Project document')
835-
await act(async () => getEditor().storage.slashCommand.insertImage(1))
835+
await act(async () => getEditor().storage.slashCommand.insertImage?.(1))
836836
const input = container.querySelector<HTMLInputElement>('input[type="file"]')
837837
if (!input) throw new Error('Editor image picker is unavailable')
838838
Object.defineProperty(input, 'files', {

‎apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/mothership-view.test.tsx‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { authClientMock } from '@sim/testing/mocks/auth-client.mock'
55
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
66
import { createRoot } from 'react-dom/client'
77
import { afterEach, expect, it, vi } from 'vitest'
8+
import type { ProjectFileRecord } from '@/lib/api/contracts/project-files'
89
import { getChatResourceSelectionId } from '@/lib/mothership/resources/types'
910
import { MothershipResourcesProvider } from '@/app/workspace/[workspaceId]/home/components/mothership-resources-context'
1011
import { MothershipView } from '@/app/workspace/[workspaceId]/home/components/mothership-view/mothership-view'
@@ -64,8 +65,21 @@ it.each([false, true])(
6465
owner: { entityType: 'project' as const, entityId: 'project' },
6566
}
6667
const options = getProjectFileQueryOptions('project', 'file')
68+
const file: ProjectFileRecord = {
69+
id: 'file',
70+
owner: resource.owner,
71+
name: 'notes.txt',
72+
key: 'project/project/notes.txt',
73+
path: '/notes.txt',
74+
size: 5,
75+
type: 'text/plain',
76+
uploadedBy: 'user',
77+
folderId: null,
78+
uploadedAt: new Date('2026-10-01T00:00:00.000Z'),
79+
updatedAt: new Date('2026-10-01T00:00:00.000Z'),
80+
}
6781
client.setQueryData(options.queryKey, {
68-
file: { id: 'file', name: 'notes.txt', type: 'text/plain' },
82+
file,
6983
capabilities: { canRead: true, canWrite: false },
7084
})
7185
const container = document.createElement('div')

‎apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.dom.test.tsx‎

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,10 @@ import {
9090
} from '@/lib/core/config/deployment-shape'
9191
import { MothershipHandoffStorage } from '@/lib/core/utils/browser-storage'
9292
import { MOTHERSHIP_STREAM_REPLAY_HEADER } from '@/lib/mothership/constants'
93-
import type { MothershipStreamV1EventEnvelope } from '@/lib/mothership/generated/mothership-stream-v1'
93+
import {
94+
MothershipStreamV1CompletionStatus,
95+
type MothershipStreamV1EventEnvelope,
96+
} from '@/lib/mothership/generated/mothership-stream-v1'
9497
import type { PersistedStreamEventEnvelope } from '@/lib/mothership/request/session/contract'
9598
import { getChatResourceSelectionId } from '@/lib/mothership/resources/types'
9699
import { ChatSurfaceProvider } from '@/app/workspace/[workspaceId]/home/components/chat-surface-context'
@@ -984,7 +987,14 @@ describe('useChat remount send recovery', () => {
984987
id: 'chat-strict-reconnect',
985988
title: 'Project architecture',
986989
activeStreamId: 'accepted-architecture',
987-
messages: [{ id: 'accepted-architecture', role: 'user', content: 'Read the architecture' }],
990+
messages: [
991+
{
992+
id: 'accepted-architecture',
993+
role: 'user',
994+
content: 'Read the architecture',
995+
timestamp: '',
996+
},
997+
],
988998
resources: [],
989999
}
9901000
const event: MothershipStreamV1EventEnvelope = {
@@ -1024,7 +1034,9 @@ describe('useChat remount send recovery', () => {
10241034
id: 'chat-reconnecting-search',
10251035
title: 'Search',
10261036
activeStreamId: 'accepted-search',
1027-
messages: [{ id: 'accepted-search', role: 'user', content: 'Find the policy' }],
1037+
messages: [
1038+
{ id: 'accepted-search', role: 'user', content: 'Find the policy', timestamp: '' },
1039+
],
10281040
resources: [
10291041
{
10301042
type: 'search',
@@ -4942,19 +4954,27 @@ describe('useChat remount send recovery', () => {
49424954
title: 'Trace inspection',
49434955
activeStreamId: status === 'active' ? 'accepted-request' : null,
49444956
messages: [
4945-
{ id: 'accepted-request', role: 'user', content: 'check the trace for this req' },
4957+
{
4958+
id: 'accepted-request',
4959+
role: 'user',
4960+
content: 'check the trace for this req',
4961+
timestamp: '',
4962+
},
49464963
...(status === 'active'
49474964
? []
49484965
: [
49494966
{
49504967
id: 'accepted-answer',
49514968
role: 'assistant' as const,
49524969
content: '',
4970+
timestamp: '',
49534971
contentBlocks: [
49544972
{
49554973
type: 'complete' as const,
49564974
status:
4957-
status === 'cancelled' ? ('cancelled' as const) : ('success' as const),
4975+
status === 'cancelled'
4976+
? MothershipStreamV1CompletionStatus.cancelled
4977+
: MothershipStreamV1CompletionStatus.complete,
49584978
},
49594979
],
49604980
},
@@ -4980,7 +5000,12 @@ describe('useChat remount send recovery', () => {
49805000
title: 'Trace inspection',
49815001
activeStreamId: null,
49825002
messages: [
4983-
{ id: 'different-request', role: 'user', content: 'check the trace for this req' },
5003+
{
5004+
id: 'different-request',
5005+
role: 'user',
5006+
content: 'check the trace for this req',
5007+
timestamp: '',
5008+
},
49845009
],
49855010
resources: [],
49865011
})

‎apps/sim/hooks/queries/workspace-files.test.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ describe('useReloadWorkspaceFileContent', () => {
179179
async (cached) => {
180180
const owner = { entityType: 'project', entityId: 'project-1' } as const
181181
const oldRecord = {
182-
file: { ...file, owner, key: 'project/project-1/old.md' },
182+
file: { ...file, folderId: null, owner, key: 'project/project-1/old.md' },
183183
capabilities: { canRead: true, canWrite: true },
184184
}
185185
const currentRecord = {

‎apps/sim/lib/mothership/application/resource-adapter.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ describe('Copilot resource authoring admission', () => {
141141
it('refuses a registered operation that does not admit Copilot resource delegation', () => {
142142
const refusedOperation = Object.freeze({
143143
...operation,
144-
delegatedServices: Object.freeze(['realtime']),
144+
delegatedServices: Object.freeze(['realtime'] as const),
145145
} as const)
146146
const execute = createCopilotApplicationAdapter({
147147
mode: 'resource',

‎apps/sim/lib/projects/__integration__/foundation.integration.ts‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1253,11 +1253,10 @@ describe('Project foundation at the database and application boundary', () => {
12531253
})
12541254
const blocker = await held.promise
12551255
const deletion = db.transaction(async (tx) => {
1256-
const { projectIdsToDelete } = await prepareProjectsForAccountDeletion(tx, f.ownerId, [
1257-
f.ids[1],
1258-
])
1256+
const projectIds = await prepareProjectsForAccountDeletion(tx, f.ownerId, [f.ids[1]])
1257+
expect(projectIds).toHaveLength(1)
12591258
await tx.delete(workspace).where(eq(workspace.id, f.ids[1]))
1260-
await tx.delete(project).where(inArray(project.id, projectIdsToDelete))
1259+
await tx.delete(project).where(inArray(project.id, projectIds))
12611260
})
12621261
try {
12631262
await waitUntilBlockedBy(blocker, "lock='project'")
@@ -1384,13 +1383,14 @@ describe('Project foundation at the database and application boundary', () => {
13841383
.where(eq(workspace.projectId, privateProject.projectId))
13851384
).toEqual([{ workspaceId: privateProject.ids[0] }])
13861385
await db.transaction(async (tx) => {
1387-
const { projectIdsToDelete } = await prepareProjectsForAccountDeletion(
1386+
const projectIds = await prepareProjectsForAccountDeletion(
13881387
tx,
13891388
privateProject.ownerId,
13901389
privateProject.ids
13911390
)
1391+
expect(projectIds).toEqual([privateProject.projectId])
13921392
await tx.delete(workspace).where(inArray(workspace.id, privateProject.ids))
1393-
await tx.delete(project).where(inArray(project.id, projectIdsToDelete))
1393+
await tx.delete(project).where(inArray(project.id, projectIds))
13941394
})
13951395
expect(
13961396
await db.select().from(project).where(eq(project.id, privateProject.projectId))

‎apps/sim/lib/projects/account-deletion.ts‎

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -148,13 +148,13 @@ export async function getProjectAccountDeletionBlockers(
148148
return blockers
149149
}
150150

151-
/** Prepares private Project cleanup and returns IDs to delete after their workspaces in this transaction. */
151+
/** Transfers surviving Projects and returns private Projects to delete after their workspaces. */
152152
export async function prepareProjectsForAccountDeletion(
153153
tx: DbTransaction,
154154
userId: string,
155155
doomedWorkspaceIds: string[],
156156
additionalWorkspaceIds: string[] = []
157-
): Promise<{ projectIdsToDelete: string[]; storageCleanupEventIds: string[] }> {
157+
): Promise<string[]> {
158158
const ownedEnvironments = await tx
159159
.select({ id: workspace.id })
160160
.from(workspace)
@@ -264,12 +264,8 @@ export async function prepareProjectsForAccountDeletion(
264264
(environments.get(record.id) ?? []).filter((row) => !doomed.has(row.id)).map((row) => row.id)
265265
)
266266
}
267-
const storageCleanupEventIds: string[] = []
268267
for (const { projectId: id, billableBytes } of removedProjects) {
269-
storageCleanupEventIds.push(...(await purgeProjectFilesInTx(tx, id, billableBytes)))
270-
}
271-
return {
272-
projectIdsToDelete: removedProjects.map(({ projectId }) => projectId),
273-
storageCleanupEventIds,
268+
await purgeProjectFilesInTx(tx, id, billableBytes)
274269
}
270+
return projectsToDelete
275271
}

‎apps/sim/lib/projects/files/__integration__/private-purge.integration.ts‎

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -235,8 +235,7 @@ async function erasePrivateProject(f: Awaited<ReturnType<typeof fixture>>) {
235235
return db.transaction(async (tx) => {
236236
const result = await prepareProjectsForAccountDeletion(tx, f.userId, [f.workspaceId])
237237
await tx.delete(workspace).where(eq(workspace.id, f.workspaceId))
238-
if (result.projectIdsToDelete.length)
239-
await tx.delete(project).where(inArray(project.id, result.projectIdsToDelete))
238+
await tx.delete(project).where(inArray(project.id, result))
240239
return result
241240
})
242241
}
@@ -291,10 +290,9 @@ describe('Private Project teardown and durable object cleanup', () => {
291290
const f = await fixture()
292291
await expect(
293292
db.transaction(async (tx) => {
294-
const result = await prepareProjectsForAccountDeletion(tx, f.userId, [f.workspaceId])
293+
const projectIds = await prepareProjectsForAccountDeletion(tx, f.userId, [f.workspaceId])
295294
await tx.delete(workspace).where(eq(workspace.id, f.workspaceId))
296-
if (result.projectIdsToDelete.length)
297-
await tx.delete(project).where(inArray(project.id, result.projectIdsToDelete))
295+
await tx.delete(project).where(inArray(project.id, projectIds))
298296
throw new Error('Later account teardown failed')
299297
})
300298
).rejects.toThrow('Later account teardown failed')
@@ -339,10 +337,9 @@ describe('Private Project teardown and durable object cleanup', () => {
339337
const deletion = db.transaction(async (tx) => {
340338
const [connection] = await tx.execute<{ pid: number }>(sql`SELECT pg_backend_pid() AS pid`)
341339
pid.resolve(connection.pid)
342-
const result = await prepareProjectsForAccountDeletion(tx, f.userId, [f.workspaceId])
340+
const projectIds = await prepareProjectsForAccountDeletion(tx, f.userId, [f.workspaceId])
343341
await tx.delete(workspace).where(eq(workspace.id, f.workspaceId))
344-
if (result.projectIdsToDelete.length)
345-
await tx.delete(project).where(inArray(project.id, result.projectIdsToDelete))
342+
await tx.delete(project).where(inArray(project.id, projectIds))
346343
})
347344
const observedDeletion = deletion.then(
348345
() => null,

0 commit comments

Comments
 (0)