chore: import upstream snapshot with attribution
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
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
This commit is contained in:
@@ -0,0 +1,215 @@
|
||||
/**
|
||||
* @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',
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user