d25d482dc2
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
216 lines
7.2 KiB
TypeScript
216 lines
7.2 KiB
TypeScript
/**
|
|
* @vitest-environment node
|
|
*/
|
|
import { dbChainMock, dbChainMockFns, resetDbChainMock } from '@sim/testing'
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
const {
|
|
mockSumForkCopyBytes,
|
|
mockAssertForkStorageHeadroom,
|
|
mockLoadSourceDeployedStates,
|
|
mockPlanForkFileCopies,
|
|
mockCopyForkResourceContainers,
|
|
mockStartBackgroundWork,
|
|
mockFinishBackgroundWork,
|
|
mockScheduleForkContentCopy,
|
|
mockSeedEdgeMappings,
|
|
} = vi.hoisted(() => ({
|
|
mockSumForkCopyBytes: vi.fn(),
|
|
mockAssertForkStorageHeadroom: vi.fn(),
|
|
mockLoadSourceDeployedStates: vi.fn(),
|
|
mockPlanForkFileCopies: vi.fn(),
|
|
mockCopyForkResourceContainers: vi.fn(),
|
|
mockStartBackgroundWork: vi.fn(),
|
|
mockFinishBackgroundWork: vi.fn(),
|
|
mockScheduleForkContentCopy: vi.fn(),
|
|
mockSeedEdgeMappings: vi.fn(),
|
|
}))
|
|
|
|
vi.mock('@sim/db', () => dbChainMock)
|
|
vi.mock('@/lib/workflows/defaults', () => ({
|
|
buildDefaultWorkflowArtifacts: vi.fn(() => ({ workflowState: {} })),
|
|
}))
|
|
vi.mock('@/lib/workflows/persistence/utils', () => ({
|
|
saveWorkflowToNormalizedTables: vi.fn(),
|
|
}))
|
|
vi.mock('@/ee/workspace-forking/lib/background-work/store', () => ({
|
|
startBackgroundWork: mockStartBackgroundWork,
|
|
finishBackgroundWork: mockFinishBackgroundWork,
|
|
}))
|
|
vi.mock('@/ee/workspace-forking/lib/copy/content-copy-runner', () => ({
|
|
hasForkContentToCopy: vi.fn(() => false),
|
|
scheduleForkContentCopy: mockScheduleForkContentCopy,
|
|
serializeContentRefMaps: vi.fn(() => ({})),
|
|
}))
|
|
vi.mock('@/ee/workspace-forking/lib/copy/copy-chats', () => ({
|
|
copyForkChatDeployments: vi.fn(async () => ({ created: 0 })),
|
|
}))
|
|
vi.mock('@/ee/workspace-forking/lib/copy/copy-files', () => ({
|
|
planForkFileCopies: mockPlanForkFileCopies,
|
|
}))
|
|
vi.mock('@/ee/workspace-forking/lib/copy/workflow-mcp-attachments', () => ({
|
|
copyForkWorkflowMcpAttachments: vi.fn(async () => ({ copied: 0 })),
|
|
}))
|
|
vi.mock('@/ee/workspace-forking/lib/copy/copy-resources', () => ({
|
|
copyForkResourceContainers: mockCopyForkResourceContainers,
|
|
}))
|
|
vi.mock('@/ee/workspace-forking/lib/copy/storage-quota', () => ({
|
|
sumForkCopyBytes: mockSumForkCopyBytes,
|
|
assertForkStorageHeadroom: mockAssertForkStorageHeadroom,
|
|
}))
|
|
vi.mock('@/ee/workspace-forking/lib/copy/copy-workflows', () => ({
|
|
copyWorkflowStateIntoTarget: vi.fn(),
|
|
loadWorkflowNameRegistry: vi.fn(async () => new Map()),
|
|
resolveForkFolderMapping: vi.fn(async () => new Map()),
|
|
}))
|
|
vi.mock('@/ee/workspace-forking/lib/copy/deploy-bridge', () => ({
|
|
loadSourceDeployedStates: mockLoadSourceDeployedStates,
|
|
}))
|
|
vi.mock('@/ee/workspace-forking/lib/lineage/lineage', () => ({
|
|
setForkLockTimeout: vi.fn(),
|
|
}))
|
|
vi.mock('@/ee/workspace-forking/lib/mapping/block-map-store', () => ({
|
|
reconcileForkBlockPairs: vi.fn(),
|
|
toForkBlockPairs: vi.fn(() => []),
|
|
}))
|
|
vi.mock('@/ee/workspace-forking/lib/mapping/mapping-store', () => ({
|
|
seedEdgeMappings: mockSeedEdgeMappings,
|
|
}))
|
|
vi.mock('@/ee/workspace-forking/lib/remap/fork-bootstrap', () => ({
|
|
createForkBootstrapTransform: vi.fn(() => (subBlocks: unknown) => subBlocks),
|
|
}))
|
|
vi.mock('@/ee/workspace-forking/lib/remap/reference-scan', () => ({
|
|
collectReferencedDocumentIds: vi.fn(() => new Set<string>()),
|
|
}))
|
|
vi.mock('@/lib/workspaces/policy', () => ({
|
|
WORKSPACE_MODE: {
|
|
PERSONAL: 'personal',
|
|
ORGANIZATION: 'organization',
|
|
GRANDFATHERED_SHARED: 'grandfathered_shared',
|
|
},
|
|
}))
|
|
|
|
import { createFork } from '@/ee/workspace-forking/lib/create-fork'
|
|
|
|
const SOURCE = { id: 'src-ws', name: 'Parent' } as never
|
|
const POLICY = {
|
|
organizationId: null,
|
|
workspaceMode: 'personal',
|
|
billedAccountUserId: null,
|
|
} as never
|
|
|
|
function forkParams(selection?: {
|
|
files?: string[]
|
|
knowledgeBases?: string[]
|
|
}): Parameters<typeof createFork>[0] {
|
|
return {
|
|
source: SOURCE,
|
|
policy: POLICY,
|
|
userId: 'user-1',
|
|
name: 'My Fork',
|
|
selection: {
|
|
files: selection?.files ?? [],
|
|
tables: [],
|
|
knowledgeBases: selection?.knowledgeBases ?? [],
|
|
customTools: [],
|
|
skills: [],
|
|
mcpServers: [],
|
|
workflowMcpServers: [],
|
|
},
|
|
requestId: 'test',
|
|
}
|
|
}
|
|
|
|
describe('createFork storage headroom gate', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
resetDbChainMock()
|
|
mockSumForkCopyBytes.mockResolvedValue(0)
|
|
mockAssertForkStorageHeadroom.mockResolvedValue(undefined)
|
|
mockLoadSourceDeployedStates.mockResolvedValue({
|
|
deployedWorkflows: [],
|
|
sourceStates: new Map(),
|
|
})
|
|
mockPlanForkFileCopies.mockResolvedValue({
|
|
keyMap: new Map(),
|
|
idMap: new Map(),
|
|
blobTasks: [],
|
|
})
|
|
mockCopyForkResourceContainers.mockResolvedValue({
|
|
idMap: new Map(),
|
|
mappingEntries: [],
|
|
contentPlan: {
|
|
sourceWorkspaceId: 'src-ws',
|
|
childWorkspaceId: 'child-ws',
|
|
userId: 'user-1',
|
|
tables: [],
|
|
knowledgeBases: [],
|
|
skills: [],
|
|
documents: [],
|
|
},
|
|
names: {
|
|
tables: [],
|
|
knowledgeBases: [],
|
|
customTools: [],
|
|
skills: [],
|
|
mcpServers: [],
|
|
workflowMcpServers: [],
|
|
},
|
|
})
|
|
mockStartBackgroundWork.mockResolvedValue('status-1')
|
|
mockFinishBackgroundWork.mockResolvedValue(undefined)
|
|
})
|
|
|
|
it('fails an over-quota fork BEFORE any read or write, with the storage error', async () => {
|
|
mockSumForkCopyBytes.mockResolvedValue(999_999)
|
|
mockAssertForkStorageHeadroom.mockRejectedValue(
|
|
new Error(
|
|
'Not enough storage to copy the selected resources. Storage limit exceeded. Used: 10.50GB, Limit: 10GB'
|
|
)
|
|
)
|
|
|
|
await expect(
|
|
createFork(forkParams({ files: ['wf-1'], knowledgeBases: ['kb-1'] }))
|
|
).rejects.toThrow('Not enough storage to copy the selected resources')
|
|
|
|
expect(mockAssertForkStorageHeadroom).toHaveBeenCalledWith({ userId: 'user-1', bytes: 999_999 })
|
|
// Nothing was read, created, or recorded: the fork failed before all of it.
|
|
expect(mockLoadSourceDeployedStates).not.toHaveBeenCalled()
|
|
expect(dbChainMockFns.transaction).not.toHaveBeenCalled()
|
|
expect(mockStartBackgroundWork).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('proceeds under quota, summing exactly the selected files + knowledge bases', async () => {
|
|
mockSumForkCopyBytes.mockResolvedValue(500)
|
|
|
|
const result = await createFork(forkParams({ files: ['wf-1'], knowledgeBases: ['kb-1'] }))
|
|
|
|
expect(result.workspace.name).toBe('My Fork')
|
|
expect(result.workflowsCopied).toBe(0)
|
|
expect(mockSumForkCopyBytes).toHaveBeenCalledWith(expect.anything(), 'src-ws', {
|
|
fileIds: ['wf-1'],
|
|
knowledgeBaseIds: ['kb-1'],
|
|
})
|
|
expect(mockAssertForkStorageHeadroom).toHaveBeenCalledWith({ userId: 'user-1', bytes: 500 })
|
|
expect(dbChainMockFns.transaction).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('seeds identity mappings for copied FILES by storage key (a later sync must not re-offer them)', async () => {
|
|
mockPlanForkFileCopies.mockResolvedValue({
|
|
keyMap: new Map([['workspace/src-ws/a.png', 'workspace/child/a.png']]),
|
|
idMap: new Map([['file-1', 'file-1-copy']]),
|
|
blobTasks: [],
|
|
})
|
|
|
|
await createFork(forkParams({ files: ['file-1'] }))
|
|
|
|
expect(mockSeedEdgeMappings).toHaveBeenCalledTimes(1)
|
|
const seeded = mockSeedEdgeMappings.mock.calls[0][3] as Array<Record<string, unknown>>
|
|
expect(seeded).toContainEqual({
|
|
resourceType: 'file',
|
|
parentResourceId: 'workspace/src-ws/a.png',
|
|
childResourceId: 'workspace/child/a.png',
|
|
})
|
|
})
|
|
})
|