Files
simstudioai--sim/apps/sim/lib/copilot/tools/handlers/deployment/manage.test.ts
T
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:20:55 +08:00

374 lines
11 KiB
TypeScript

/**
* @vitest-environment node
*/
import { auditMock, workflowsOrchestrationMock, workflowsOrchestrationMockFns } from '@sim/testing'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { ExecutionContext } from '@/lib/copilot/request/types'
const { ensureWorkflowAccessMock, checkNeedsRedeploymentMock } = vi.hoisted(() => ({
ensureWorkflowAccessMock: vi.fn(),
checkNeedsRedeploymentMock: vi.fn(),
}))
const performRevertToVersionMock = workflowsOrchestrationMockFns.mockPerformRevertToVersion
const performActivateVersionMock = workflowsOrchestrationMockFns.mockPerformActivateVersion
const { resolveWorkflowStateRefMock, generateWorkflowDiffSummaryMock, listWorkflowVersionsMock } =
vi.hoisted(() => ({
resolveWorkflowStateRefMock: vi.fn(),
generateWorkflowDiffSummaryMock: vi.fn(),
listWorkflowVersionsMock: vi.fn(),
}))
vi.mock('@sim/db', () => ({
db: {
select: vi.fn(),
insert: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
},
chat: {},
workflow: {},
workflowDeploymentVersion: {},
workflowMcpServer: {},
workflowMcpTool: {},
}))
vi.mock('@sim/audit', () => auditMock)
vi.mock('@/lib/mcp/pubsub', () => ({
mcpPubSub: {
publishWorkflowToolsChanged: vi.fn(),
},
}))
vi.mock('@/lib/mcp/workflow-mcp-sync', () => ({
generateParameterSchemaForWorkflow: vi.fn(),
}))
vi.mock('@/lib/mcp/workflow-tool-schema', () => ({
sanitizeToolName: vi.fn((value: string) => value),
}))
vi.mock('@/lib/workflows/triggers/trigger-utils.server', () => ({
hasValidStartBlock: vi.fn(),
}))
vi.mock('../access', () => ({
ensureWorkflowAccess: ensureWorkflowAccessMock,
ensureWorkspaceAccess: vi.fn(),
}))
vi.mock('@/lib/workflows/orchestration', () => workflowsOrchestrationMock)
vi.mock('./state-refs', () => ({
resolveWorkflowStateRef: resolveWorkflowStateRefMock,
}))
vi.mock('@/lib/workflows/comparison', () => ({
generateWorkflowDiffSummary: generateWorkflowDiffSummaryMock,
}))
vi.mock('@/app/api/workflows/utils', () => ({
checkNeedsRedeployment: checkNeedsRedeploymentMock,
}))
vi.mock('@/lib/workflows/persistence/utils', () => ({
listWorkflowVersions: listWorkflowVersionsMock,
updateDeploymentVersionMetadata: vi.fn(),
}))
import { db } from '@sim/db'
import {
executeCheckDeploymentStatus,
executeDiffWorkflows,
executeGetDeploymentLog,
executeLoadDeployment,
executePromoteToLive,
} from './manage'
function selectChain(result: unknown[], resolveOnWhere = false) {
const chain = {
from: vi.fn(() => chain),
innerJoin: vi.fn(() => chain),
where: vi.fn(() => (resolveOnWhere ? Promise.resolve(result) : chain)),
orderBy: vi.fn(() => Promise.resolve(result)),
limit: vi.fn(() => Promise.resolve(result)),
}
return chain
}
describe('executeLoadDeployment', () => {
beforeEach(() => {
vi.clearAllMocks()
ensureWorkflowAccessMock.mockResolvedValue({
workflow: { id: 'wf-1', workspaceId: 'ws-1', name: 'Test Workflow' },
})
})
it('loads a version into the draft via performRevertToVersion', async () => {
performRevertToVersionMock.mockResolvedValue({ success: true, lastSaved: 12345 })
const result = await executeLoadDeployment({ workflowId: 'wf-1', version: 7 }, {
userId: 'user-1',
workflowId: 'wf-1',
} as ExecutionContext)
expect(ensureWorkflowAccessMock).toHaveBeenCalledWith('wf-1', 'user-1', 'admin')
expect(performRevertToVersionMock).toHaveBeenCalledWith({
workflowId: 'wf-1',
version: 7,
userId: 'user-1',
workflow: { id: 'wf-1', workspaceId: 'ws-1', name: 'Test Workflow' },
})
expect(result).toEqual({
success: true,
output: {
workflowId: 'wf-1',
message: 'Loaded version 7 into the workflow draft',
lastSaved: 12345,
},
})
})
it('maps "live" to the active version', async () => {
performRevertToVersionMock.mockResolvedValue({ success: true, lastSaved: 1 })
await executeLoadDeployment({ workflowId: 'wf-1', version: 'live' }, {
userId: 'user-1',
workflowId: 'wf-1',
} as ExecutionContext)
expect(performRevertToVersionMock).toHaveBeenCalledWith(
expect.objectContaining({ version: 'active' })
)
})
it('rejects "draft"', async () => {
const result = await executeLoadDeployment({ workflowId: 'wf-1', version: 'draft' }, {
userId: 'user-1',
workflowId: 'wf-1',
} as ExecutionContext)
expect(result.success).toBe(false)
expect(performRevertToVersionMock).not.toHaveBeenCalled()
})
it('returns shared helper failures directly', async () => {
performRevertToVersionMock.mockResolvedValue({
success: false,
error: 'Deployment version not found',
})
const result = await executeLoadDeployment({ workflowId: 'wf-1', version: 7 }, {
userId: 'user-1',
workflowId: 'wf-1',
} as ExecutionContext)
expect(result).toEqual({ success: false, error: 'Deployment version not found' })
})
})
describe('executePromoteToLive', () => {
beforeEach(() => {
vi.clearAllMocks()
ensureWorkflowAccessMock.mockResolvedValue({
workflow: { id: 'wf-1', workspaceId: 'ws-1', name: 'Test Workflow' },
})
})
it('promotes a version via performActivateVersion', async () => {
performActivateVersionMock.mockResolvedValue({
success: true,
deployedAt: new Date('2026-05-30T00:00:00.000Z'),
})
const result = await executePromoteToLive({ workflowId: 'wf-1', version: 3 }, {
userId: 'user-1',
workflowId: 'wf-1',
} as ExecutionContext)
expect(ensureWorkflowAccessMock).toHaveBeenCalledWith('wf-1', 'user-1', 'admin')
expect(performActivateVersionMock).toHaveBeenCalledWith({
workflowId: 'wf-1',
version: 3,
userId: 'user-1',
workflow: { id: 'wf-1', workspaceId: 'ws-1', name: 'Test Workflow' },
})
expect(result.success).toBe(true)
expect(result.output).toMatchObject({
workflowId: 'wf-1',
version: 3,
message: 'Promoted version 3 to live',
})
})
it('rejects a non-numeric version like "live"', async () => {
const result = await executePromoteToLive({ workflowId: 'wf-1', version: 'live' as never }, {
userId: 'user-1',
workflowId: 'wf-1',
} as ExecutionContext)
expect(result.success).toBe(false)
expect(performActivateVersionMock).not.toHaveBeenCalled()
})
})
describe('executeGetDeploymentLog', () => {
beforeEach(() => {
vi.clearAllMocks()
ensureWorkflowAccessMock.mockResolvedValue({
workflow: { id: 'wf-1', workspaceId: 'ws-1', name: 'Test Workflow' },
})
})
it('returns versions from the shared listWorkflowVersions helper', async () => {
listWorkflowVersionsMock.mockResolvedValue({
versions: [
{
id: 'v2',
version: 2,
name: null,
description: null,
isActive: true,
createdAt: new Date('2026-05-30T00:00:00.000Z'),
createdBy: 'user-1',
deployedByName: 'Waleed',
},
{
id: 'v1',
version: 1,
name: 'first',
description: 'initial',
isActive: false,
createdAt: new Date('2026-05-29T00:00:00.000Z'),
createdBy: null,
deployedByName: null,
},
],
})
const result = await executeGetDeploymentLog({ workflowId: 'wf-1' }, {
userId: 'user-1',
workflowId: 'wf-1',
} as ExecutionContext)
expect(listWorkflowVersionsMock).toHaveBeenCalledWith('wf-1')
expect(result.success).toBe(true)
expect(result.output).toMatchObject({
workflowId: 'wf-1',
count: 2,
versions: [
{ id: 'v2', version: 2, isActive: true },
{ id: 'v1', version: 1, name: 'first', description: 'initial', isActive: false },
],
})
})
})
describe('executeDiffWorkflows', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('diffs ref2 against ref1 and returns the structured summary', async () => {
resolveWorkflowStateRefMock
.mockResolvedValueOnce({ state: { base: true }, ref: '1', version: 1, isActive: false })
.mockResolvedValueOnce({ state: { target: true }, ref: 'live', version: 2, isActive: true })
const summary = {
addedBlocks: [],
removedBlocks: [],
modifiedBlocks: [],
edgeChanges: { added: 0, removed: 0, addedDetails: [], removedDetails: [] },
loopChanges: { added: 0, removed: 0, modified: 0 },
parallelChanges: { added: 0, removed: 0, modified: 0 },
variableChanges: {
added: 0,
removed: 0,
modified: 0,
addedNames: [],
removedNames: [],
modifiedNames: [],
},
hasChanges: false,
}
generateWorkflowDiffSummaryMock.mockReturnValue(summary)
const result = await executeDiffWorkflows({ workflowId: 'wf-1', ref1: 1, ref2: 'live' }, {
userId: 'user-1',
workflowId: 'wf-1',
} as ExecutionContext)
expect(resolveWorkflowStateRefMock).toHaveBeenCalledWith('wf-1', 1, 'user-1')
expect(resolveWorkflowStateRefMock).toHaveBeenCalledWith('wf-1', 'live', 'user-1')
// ref1 = base/previous, ref2 = target/current.
expect(generateWorkflowDiffSummaryMock).toHaveBeenCalledWith({ target: true }, { base: true })
expect(result.success).toBe(true)
expect(result.output).toMatchObject({
workflowId: 'wf-1',
ref1: { ref: '1', version: 1 },
ref2: { ref: 'live', version: 2, isActive: true },
diff: { hasChanges: false },
})
})
})
describe('executeCheckDeploymentStatus', () => {
beforeEach(() => {
vi.clearAllMocks()
ensureWorkflowAccessMock.mockResolvedValue({
workflow: { id: 'wf-1', workspaceId: 'ws-1', name: 'Test Workflow' },
})
checkNeedsRedeploymentMock.mockResolvedValue(false)
})
it('uses the shared redeployment freshness helper for deployed APIs', async () => {
vi.mocked(db.select)
.mockReturnValueOnce(
selectChain([{ isDeployed: true, deployedAt: new Date('2026-05-28') }]) as never
)
.mockReturnValueOnce(selectChain([]) as never)
.mockReturnValueOnce(selectChain([], true) as never)
checkNeedsRedeploymentMock.mockResolvedValueOnce(true)
const result = await executeCheckDeploymentStatus({ workflowId: 'wf-1' }, {
userId: 'user-1',
workflowId: 'wf-1',
} as ExecutionContext)
expect(checkNeedsRedeploymentMock).toHaveBeenCalledWith('wf-1')
expect(result.success).toBe(true)
expect(result.output).toMatchObject({
isDeployed: true,
api: {
isDeployed: true,
needsRedeployment: true,
},
})
})
it('does not check redeployment freshness for undeployed APIs', async () => {
vi.mocked(db.select)
.mockReturnValueOnce(selectChain([{ isDeployed: false, deployedAt: null }]) as never)
.mockReturnValueOnce(selectChain([]) as never)
.mockReturnValueOnce(selectChain([], true) as never)
const result = await executeCheckDeploymentStatus({ workflowId: 'wf-1' }, {
userId: 'user-1',
workflowId: 'wf-1',
} as ExecutionContext)
expect(checkNeedsRedeploymentMock).not.toHaveBeenCalled()
expect(result.success).toBe(true)
expect(result.output).toMatchObject({
isDeployed: false,
api: {
isDeployed: false,
needsRedeployment: false,
},
})
})
})