Skip to content

Commit ca1d616

Browse files
committed
test(forks): assert the sync-default audit fan-out against real audit_log rows
1 parent 7e11748 commit ca1d616

2 files changed

Lines changed: 65 additions & 49 deletions

File tree

Lines changed: 13 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
/**
22
* @vitest-environment node
33
*/
4-
import { workspace } from '@sim/db/schema'
54
import { createSessionPrincipal } from '@sim/testing/factories/principal.factory'
6-
import { auditMock, auditMockFns } from '@sim/testing/mocks/audit.mock'
7-
import { dbChainMockFns, queueTableRows, resetDbChainMock } from '@sim/testing/mocks/database.mock'
5+
import { auditMock } from '@sim/testing/mocks/audit.mock'
6+
import { resetDbChainMock } from '@sim/testing/mocks/database.mock'
87
import { permissionsMock, permissionsMockFns } from '@sim/testing/mocks/permissions.mock'
98
import { posthogServerMock } from '@sim/testing/mocks/posthog-server.mock'
109
import {
@@ -33,7 +32,6 @@ vi.mock('@/ee/workspace-forking/lib/lineage/lineage-root', () => workspaceForkin
3332
import { setForkSyncDefault } from '@/ee/workspace-forking/application/sync-default'
3433

3534
const principal = createSessionPrincipal({ userId: 'actor-1' })
36-
const LINEAGE = ['root-ws', 'fork-a', 'fork-b', 'grandchild']
3735

3836
beforeEach(() => {
3937
resetDbChainMock()
@@ -45,50 +43,21 @@ beforeEach(() => {
4543
})
4644
workspaceAuthorizationMockFns.mockAuthorizeWorkspaceOperation.mockResolvedValue(undefined)
4745
mockResolveForkLineageRootId.mockResolvedValue('root-ws')
48-
mockResolveForkLineageWorkspaceIds.mockResolvedValue(LINEAGE)
49-
// The live-member row lock, then the `UPDATE ... RETURNING` of the members that changed.
50-
queueTableRows(
51-
workspace,
52-
LINEAGE.map((id) => ({ id }))
53-
)
54-
dbChainMockFns.returning.mockResolvedValue(LINEAGE.map((id) => ({ id, name: `Name of ${id}` })))
5546
})
5647

57-
const run = (excludeNewWorkflows: boolean) =>
58-
setForkSyncDefault.execute({
59-
principal,
60-
input: { workspaceId: 'fork-a', excludeNewWorkflows },
61-
})
62-
48+
/**
49+
* The lineage-wide write and its audit fan-out are proven against real Postgres in
50+
* `fork-sync.integration.ts`. This covers the one branch that suite cannot stage: an unlink
51+
* committing between the root walk and the lineage lock.
52+
*/
6353
describe('setForkSyncDefault', () => {
64-
/**
65-
* One entry per member, because the default genuinely changed for all of them. A single
66-
* entry on the calling workspace would leave the other members' admins with no record.
67-
*/
68-
it("files one audit entry per updated member, in that member's own workspace and name", async () => {
69-
await run(true)
70-
const audited = auditMockFns.mockRecordAudit.mock.calls.map(([entry]) => entry)
71-
expect(audited).toHaveLength(LINEAGE.length)
72-
expect(audited.map((entry) => entry.resourceId).sort()).toEqual([...LINEAGE].sort())
73-
for (const entry of audited) {
74-
expect(entry.action).toBe('workspace.fork_sync_default_changed')
75-
// Filed in the workspace it describes. Without an explicit workspaceId the wrapper
76-
// defaults it to the caller's workspace, so every entry would pile into one log and
77-
// the other members' admins would see nothing.
78-
expect(entry.workspaceId).toBe(entry.resourceId)
79-
// The member's OWN name, not a raw id and not the caller's name.
80-
expect(entry.resourceName).toBe(`Name of ${entry.resourceId}`)
81-
expect(entry.metadata).toMatchObject({
82-
forkSyncNewWorkflowsExcluded: true,
83-
originWorkspaceId: 'fork-a',
84-
originWorkspaceName: 'Fork A',
85-
})
86-
}
87-
})
88-
89-
/** An unlink that moved the caller out of the locked root's lineage must not be written. */
9054
it('refuses when the locked root no longer reaches the calling workspace', async () => {
9155
mockResolveForkLineageWorkspaceIds.mockResolvedValue(['root-ws', 'fork-b'])
92-
await expect(run(true)).rejects.toMatchObject({ statusCode: 409 })
56+
await expect(
57+
setForkSyncDefault.execute({
58+
principal,
59+
input: { workspaceId: 'fork-a', excludeNewWorkflows: true },
60+
})
61+
).rejects.toMatchObject({ statusCode: 409 })
9362
})
9463
})

‎apps/sim/lib/workspaces/__integration__/fork-sync.integration.ts‎

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import { AuditAction } from '@sim/audit'
12
import { db } from '@sim/db'
23
import {
4+
auditLog,
35
folder,
46
outboxEvent,
57
permissions,
@@ -15,8 +17,8 @@ import {
1517
workspaceSandbox,
1618
} from '@sim/db/schema'
1719
import { generateId } from '@sim/utils/id'
18-
import { and, eq } from 'drizzle-orm'
19-
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
20+
import { and, eq, inArray } from 'drizzle-orm'
21+
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'
2022
import { withWorkspaceInvocationScope } from '@/lib/core/application/workspace-invocation-scope'
2123
import { processOutboxEventById } from '@/lib/core/outbox/service'
2224
import { createScopedCliTransport } from '@/lib/mothership/agent-cli/scoped-transport'
@@ -765,9 +767,10 @@ describe('authorized fork and sync against PostgreSQL', () => {
765767

766768
/**
767769
* The opt-in policy end to end: set from a fork, it reaches the parent and changes only
768-
* what differs; a genuinely new workflow (created, duplicated, or a fork's starter) and a
769-
* new fork take it; a forked copy stays synced; no existing workflow moves; and an archived
770-
* member is walked through for the lineage root but never written.
770+
* what differs, filing one audit entry in each changed workspace's own log; a genuinely
771+
* new workflow (created, duplicated, or a fork's starter) and a new fork take it; a forked
772+
* copy stays synced; no existing workflow moves; and an archived member is walked through
773+
* for the lineage root but never written.
771774
*/
772775
it('gives new workflows the lineage fork-sync default while copies stay synced', async () => {
773776
const childId = await createChild()
@@ -794,6 +797,50 @@ describe('authorized fork and sync against PostgreSQL', () => {
794797
)
795798
expect((await setDefault(childId, true)).changedWorkspaces).toEqual([])
796799
expect(await policyOf(sourceWorkspaceId)).toBe(true)
800+
801+
// Each changed member's admins see the change in their own log, under that workspace's name.
802+
const changed = new Map(
803+
(
804+
await db
805+
.select({ id: workspace.id, name: workspace.name })
806+
.from(workspace)
807+
.where(
808+
inArray(
809+
workspace.id,
810+
first.changedWorkspaces.map((member) => member.id)
811+
)
812+
)
813+
).map((member) => [member.id, member.name])
814+
)
815+
await vi.waitFor(
816+
async () => {
817+
const entries = await db
818+
.select({
819+
workspaceId: auditLog.workspaceId,
820+
resourceId: auditLog.resourceId,
821+
resourceName: auditLog.resourceName,
822+
metadata: auditLog.metadata,
823+
})
824+
.from(auditLog)
825+
.where(
826+
and(
827+
eq(auditLog.action, AuditAction.WORKSPACE_FORK_SYNC_DEFAULT_CHANGED),
828+
inArray(auditLog.resourceId, [...changed.keys()])
829+
)
830+
)
831+
expect(entries).toHaveLength(changed.size)
832+
for (const entry of entries) {
833+
expect(entry.workspaceId).toBe(entry.resourceId)
834+
expect(entry.resourceName).toBe(changed.get(entry.resourceId!))
835+
expect(entry.metadata).toMatchObject({
836+
forkSyncNewWorkflowsExcluded: true,
837+
originWorkspaceId: childId,
838+
originWorkspaceName: changed.get(childId),
839+
})
840+
}
841+
},
842+
{ timeout: 5000 }
843+
)
797844
expect(await excludedFor(sourceWorkflowId)).toBe(false)
798845

799846
const [copy] = await db

0 commit comments

Comments
 (0)