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,373 @@
|
||||
/**
|
||||
* @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,
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user