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
204 lines
5.9 KiB
TypeScript
204 lines
5.9 KiB
TypeScript
/**
|
|
* @vitest-environment node
|
|
*/
|
|
import { dbChainMock, dbChainMockFns, resetDbChainMock, schemaMock } from '@sim/testing'
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
vi.mock('@sim/audit', () => ({
|
|
AuditAction: {
|
|
MCP_SERVER_UPDATED: 'mcp_server_updated',
|
|
MCP_TOOL_UPDATED: 'mcp_tool_updated',
|
|
},
|
|
AuditResourceType: {
|
|
MCP_SERVER: 'mcp_server',
|
|
MCP_TOOL: 'mcp_tool',
|
|
},
|
|
recordAudit: vi.fn(),
|
|
}))
|
|
vi.mock('@sim/db', () => ({
|
|
...dbChainMock,
|
|
workflow: schemaMock.workflow,
|
|
workflowMcpServer: schemaMock.workflowMcpServer,
|
|
workflowMcpTool: schemaMock.workflowMcpTool,
|
|
}))
|
|
vi.mock('@sim/db/schema', () => schemaMock)
|
|
vi.mock('drizzle-orm', () => ({
|
|
and: vi.fn(),
|
|
asc: vi.fn(),
|
|
eq: vi.fn(),
|
|
inArray: vi.fn(),
|
|
isNull: vi.fn(),
|
|
ne: vi.fn(),
|
|
sql: Object.assign(vi.fn(), { raw: vi.fn((value: string) => value) }),
|
|
}))
|
|
vi.mock('@/lib/mcp/pubsub', () => ({ mcpPubSub: undefined }))
|
|
vi.mock('@/lib/workflows/triggers/trigger-utils.server', () => ({
|
|
hasValidStartBlock: vi.fn(),
|
|
}))
|
|
vi.mock('@/lib/mcp/workflow-mcp-sync', () => ({
|
|
generateParameterSchemaForWorkflow: vi.fn().mockResolvedValue({ type: 'object', properties: {} }),
|
|
}))
|
|
|
|
import { MAX_MCP_PARAMETER_SCHEMA_BYTES, MAX_MCP_TOOLS_PER_SERVER } from '@/lib/mcp/constants'
|
|
import {
|
|
performCreateWorkflowMcpServer,
|
|
performUpdateWorkflowMcpTool,
|
|
} from '@/lib/mcp/orchestration/workflow-mcp-lifecycle'
|
|
import { hasValidStartBlock } from '@/lib/workflows/triggers/trigger-utils.server'
|
|
|
|
describe('workflow MCP lifecycle orchestration', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
resetDbChainMock()
|
|
})
|
|
|
|
it('rejects over-limit workflow server creation before inserting a server row', async () => {
|
|
const result = await performCreateWorkflowMcpServer({
|
|
workspaceId: 'workspace-1',
|
|
userId: 'user-1',
|
|
name: 'Too Many Tools',
|
|
workflowIds: Array.from(
|
|
{ length: MAX_MCP_TOOLS_PER_SERVER + 1 },
|
|
(_, index) => `wf-${index}`
|
|
),
|
|
})
|
|
|
|
expect(result).toMatchObject({
|
|
success: false,
|
|
errorCode: 'validation',
|
|
})
|
|
expect(dbChainMockFns.insert).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('rejects duplicate workflow IDs before inserting a server row', async () => {
|
|
const result = await performCreateWorkflowMcpServer({
|
|
workspaceId: 'workspace-1',
|
|
userId: 'user-1',
|
|
name: 'Duplicate Tools',
|
|
workflowIds: ['wf-1', 'wf-1'],
|
|
})
|
|
|
|
expect(result).toMatchObject({
|
|
success: false,
|
|
errorCode: 'validation',
|
|
})
|
|
expect(dbChainMockFns.insert).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('rechecks deployed workflow state inside the create transaction', async () => {
|
|
dbChainMockFns.where.mockResolvedValueOnce([
|
|
{
|
|
id: 'wf-1',
|
|
name: 'Workflow',
|
|
description: null,
|
|
isDeployed: true,
|
|
workspaceId: 'workspace-1',
|
|
deployedAt: new Date('2026-01-01T00:00:00Z'),
|
|
updatedAt: new Date('2026-01-01T00:00:00Z'),
|
|
},
|
|
])
|
|
vi.mocked(hasValidStartBlock).mockResolvedValueOnce(true)
|
|
dbChainMockFns.for.mockResolvedValueOnce([
|
|
{
|
|
id: 'wf-1',
|
|
name: 'Workflow',
|
|
description: null,
|
|
isDeployed: false,
|
|
workspaceId: 'workspace-1',
|
|
deployedAt: new Date('2026-01-01T00:00:00Z'),
|
|
updatedAt: new Date('2026-01-01T00:00:00Z'),
|
|
},
|
|
])
|
|
|
|
const result = await performCreateWorkflowMcpServer({
|
|
workspaceId: 'workspace-1',
|
|
userId: 'user-1',
|
|
name: 'Server',
|
|
workflowIds: ['wf-1'],
|
|
})
|
|
|
|
expect(result).toMatchObject({
|
|
success: false,
|
|
errorCode: 'validation',
|
|
})
|
|
expect(dbChainMockFns.transaction).toHaveBeenCalled()
|
|
expect(dbChainMockFns.for).toHaveBeenCalledTimes(1)
|
|
expect(dbChainMockFns.insert).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('rejects workflow MCP server fan-out above the per-workflow limit', async () => {
|
|
vi.mocked(hasValidStartBlock).mockResolvedValueOnce(true)
|
|
dbChainMockFns.where.mockResolvedValueOnce([
|
|
{
|
|
id: 'wf-1',
|
|
name: 'Workflow',
|
|
description: null,
|
|
isDeployed: true,
|
|
workspaceId: 'workspace-1',
|
|
deployedAt: new Date('2026-01-01T00:00:00Z'),
|
|
updatedAt: new Date('2026-01-01T00:00:00Z'),
|
|
},
|
|
])
|
|
dbChainMockFns.for.mockResolvedValueOnce([
|
|
{
|
|
id: 'wf-1',
|
|
name: 'Workflow',
|
|
description: null,
|
|
isDeployed: true,
|
|
workspaceId: 'workspace-1',
|
|
deployedAt: new Date('2026-01-01T00:00:00Z'),
|
|
updatedAt: new Date('2026-01-01T00:00:00Z'),
|
|
},
|
|
])
|
|
dbChainMockFns.groupBy.mockResolvedValueOnce([{ workflowId: 'wf-1', serverCount: 100 }])
|
|
|
|
const result = await performCreateWorkflowMcpServer({
|
|
workspaceId: 'workspace-1',
|
|
userId: 'user-1',
|
|
name: 'Server',
|
|
workflowIds: ['wf-1'],
|
|
})
|
|
|
|
expect(result).toMatchObject({
|
|
success: false,
|
|
errorCode: 'validation',
|
|
})
|
|
expect(dbChainMockFns.insert).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('allows updating tool metadata when an unchanged stored schema exceeds the new cap', async () => {
|
|
dbChainMockFns.limit.mockResolvedValueOnce([{ id: 'server-1' }]).mockResolvedValueOnce([
|
|
{
|
|
id: 'tool-1',
|
|
toolName: 'tool_a',
|
|
toolDescription: null,
|
|
parameterSchemaBytes: MAX_MCP_PARAMETER_SCHEMA_BYTES + 1,
|
|
},
|
|
])
|
|
dbChainMockFns.returning.mockResolvedValueOnce([
|
|
{
|
|
id: 'tool-1',
|
|
serverId: 'server-1',
|
|
toolName: 'tool_a',
|
|
toolDescription: 'Updated description',
|
|
},
|
|
])
|
|
|
|
const result = await performUpdateWorkflowMcpTool({
|
|
workspaceId: 'workspace-1',
|
|
userId: 'user-1',
|
|
serverId: 'server-1',
|
|
toolId: 'tool-1',
|
|
toolDescription: 'Updated description',
|
|
})
|
|
|
|
expect(result).toMatchObject({
|
|
success: true,
|
|
tool: {
|
|
toolDescription: 'Updated description',
|
|
},
|
|
})
|
|
expect(dbChainMockFns.update).toHaveBeenCalled()
|
|
})
|
|
})
|