chore: import upstream snapshot with attribution
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
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) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
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
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"name": "@sim/testing",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"sideEffects": [
|
||||
"./src/setup/*"
|
||||
],
|
||||
"type": "module",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"bun": ">=1.2.13",
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./src/index.ts",
|
||||
"default": "./src/index.ts"
|
||||
},
|
||||
"./factories": {
|
||||
"types": "./src/factories/index.ts",
|
||||
"default": "./src/factories/index.ts"
|
||||
},
|
||||
"./builders": {
|
||||
"types": "./src/builders/index.ts",
|
||||
"default": "./src/builders/index.ts"
|
||||
},
|
||||
"./mocks": {
|
||||
"types": "./src/mocks/index.ts",
|
||||
"default": "./src/mocks/index.ts"
|
||||
},
|
||||
"./mocks/executor": {
|
||||
"types": "./src/mocks/executor.mock.ts",
|
||||
"default": "./src/mocks/executor.mock.ts"
|
||||
},
|
||||
"./assertions": {
|
||||
"types": "./src/assertions/index.ts",
|
||||
"default": "./src/assertions/index.ts"
|
||||
},
|
||||
"./setup": {
|
||||
"types": "./src/setup/vitest.setup.ts",
|
||||
"default": "./src/setup/vitest.setup.ts"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"type-check": "tsc --noEmit",
|
||||
"lint": "biome check --write --unsafe .",
|
||||
"lint:check": "biome check .",
|
||||
"format": "biome format --write .",
|
||||
"format:check": "biome format ."
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vitest": "^3.0.0 || >=4.1.0 <5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sim/tsconfig": "workspace:*",
|
||||
"typescript": "^7.0.2",
|
||||
"vitest": "^4.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sim/utils": "workspace:*"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
import { expect } from 'vitest'
|
||||
import type { ExecutionContext } from '../types'
|
||||
|
||||
/**
|
||||
* Asserts that a block was executed.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* expectBlockExecuted(ctx, 'block-1')
|
||||
* ```
|
||||
*/
|
||||
export function expectBlockExecuted(ctx: ExecutionContext, blockId: string): void {
|
||||
expect(ctx.executedBlocks.has(blockId), `Block "${blockId}" should have been executed`).toBe(true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that a block was NOT executed.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* expectBlockNotExecuted(ctx, 'skipped-block')
|
||||
* ```
|
||||
*/
|
||||
export function expectBlockNotExecuted(ctx: ExecutionContext, blockId: string): void {
|
||||
expect(ctx.executedBlocks.has(blockId), `Block "${blockId}" should not have been executed`).toBe(
|
||||
false
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that blocks were executed in a specific order.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* expectExecutionOrder(executionLog, ['start', 'step1', 'step2', 'end'])
|
||||
* ```
|
||||
*/
|
||||
export function expectExecutionOrder(executedBlocks: string[], expectedOrder: string[]): void {
|
||||
const actualOrder = executedBlocks.filter((id) => expectedOrder.includes(id))
|
||||
expect(actualOrder, 'Blocks should be executed in expected order').toEqual(expectedOrder)
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that a block has a specific output state.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* expectBlockOutput(ctx, 'agent-1', { response: 'Hello' })
|
||||
* ```
|
||||
*/
|
||||
export function expectBlockOutput(
|
||||
ctx: ExecutionContext,
|
||||
blockId: string,
|
||||
expectedOutput: Record<string, any>
|
||||
): void {
|
||||
const state = ctx.blockStates.get(blockId)
|
||||
expect(state, `Block "${blockId}" should have state`).toBeDefined()
|
||||
expect(state).toMatchObject(expectedOutput)
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that execution has a specific number of logs.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* expectLogCount(ctx, 5)
|
||||
* ```
|
||||
*/
|
||||
export function expectLogCount(ctx: ExecutionContext, expectedCount: number): void {
|
||||
expect(ctx.blockLogs.length, `Should have ${expectedCount} logs`).toBe(expectedCount)
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that a condition decision was made.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* expectConditionDecision(ctx, 'condition-1', true)
|
||||
* ```
|
||||
*/
|
||||
export function expectConditionDecision(
|
||||
ctx: ExecutionContext,
|
||||
blockId: string,
|
||||
expectedResult: boolean
|
||||
): void {
|
||||
const decision = ctx.decisions.condition.get(blockId)
|
||||
expect(decision, `Condition "${blockId}" should have a decision`).toBeDefined()
|
||||
expect(decision).toBe(expectedResult)
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that a loop was completed.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* expectLoopCompleted(ctx, 'loop-1')
|
||||
* ```
|
||||
*/
|
||||
export function expectLoopCompleted(ctx: ExecutionContext, loopId: string): void {
|
||||
expect(ctx.completedLoops.has(loopId), `Loop "${loopId}" should be completed`).toBe(true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that a block is in the active execution path.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* expectInActivePath(ctx, 'current-block')
|
||||
* ```
|
||||
*/
|
||||
export function expectInActivePath(ctx: ExecutionContext, blockId: string): void {
|
||||
expect(ctx.activeExecutionPath.has(blockId), `Block "${blockId}" should be in active path`).toBe(
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that execution was cancelled.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* expectExecutionCancelled(ctx)
|
||||
* ```
|
||||
*/
|
||||
export function expectExecutionCancelled(ctx: ExecutionContext): void {
|
||||
expect(ctx.abortSignal?.aborted, 'Execution should be cancelled').toBe(true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that execution was NOT cancelled.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* expectExecutionNotCancelled(ctx)
|
||||
* ```
|
||||
*/
|
||||
export function expectExecutionNotCancelled(ctx: ExecutionContext): void {
|
||||
expect(ctx.abortSignal?.aborted ?? false, 'Execution should not be cancelled').toBe(false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that execution has specific environment variables.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* expectEnvironmentVariables(ctx, { API_KEY: 'test', MODE: 'production' })
|
||||
* ```
|
||||
*/
|
||||
export function expectEnvironmentVariables(
|
||||
ctx: ExecutionContext,
|
||||
expectedVars: Record<string, string>
|
||||
): void {
|
||||
Object.entries(expectedVars).forEach(([key, value]) => {
|
||||
expect(
|
||||
ctx.environmentVariables[key],
|
||||
`Environment variable "${key}" should be "${value}"`
|
||||
).toBe(value)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* Custom assertions for testing workflows and execution.
|
||||
*
|
||||
* These provide semantic, readable assertions for common test scenarios.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import {
|
||||
* expectBlockExists,
|
||||
* expectEdgeConnects,
|
||||
* expectExecutionOrder,
|
||||
* } from '@sim/testing/assertions'
|
||||
*
|
||||
* // Workflow assertions
|
||||
* expectBlockExists(workflow.blocks, 'agent-1', 'agent')
|
||||
* expectEdgeConnects(workflow.edges, 'start', 'agent-1')
|
||||
*
|
||||
* // Execution assertions
|
||||
* expectBlockExecuted(ctx, 'agent-1')
|
||||
* expectExecutionOrder(log, ['start', 'agent-1', 'end'])
|
||||
* ```
|
||||
*/
|
||||
|
||||
// Execution assertions
|
||||
export {
|
||||
expectBlockExecuted,
|
||||
expectBlockNotExecuted,
|
||||
expectBlockOutput,
|
||||
expectConditionDecision,
|
||||
expectEnvironmentVariables,
|
||||
expectExecutionCancelled,
|
||||
expectExecutionNotCancelled,
|
||||
expectExecutionOrder,
|
||||
expectInActivePath,
|
||||
expectLogCount,
|
||||
expectLoopCompleted,
|
||||
} from './execution.assertions'
|
||||
// Permission assertions
|
||||
export {
|
||||
expectApiKeyInvalid,
|
||||
expectApiKeyValid,
|
||||
expectPermissionAllowed,
|
||||
expectPermissionDenied,
|
||||
expectRoleCannotPerform,
|
||||
expectRoleCanPerform,
|
||||
expectSocketAccessDenied,
|
||||
expectSocketAccessGranted,
|
||||
expectUserHasNoPermission,
|
||||
expectUserHasPermission,
|
||||
expectWorkflowAccessDenied,
|
||||
expectWorkflowAccessGranted,
|
||||
} from './permission.assertions'
|
||||
// Workflow assertions
|
||||
export {
|
||||
expectBlockCount,
|
||||
expectBlockDisabled,
|
||||
expectBlockEnabled,
|
||||
expectBlockExists,
|
||||
expectBlockHasParent,
|
||||
expectBlockNotExists,
|
||||
expectBlockPosition,
|
||||
expectEdgeConnects,
|
||||
expectEdgeCount,
|
||||
expectEmptyWorkflow,
|
||||
expectLinearChain,
|
||||
expectLoopExists,
|
||||
expectNoEdgeBetween,
|
||||
expectParallelExists,
|
||||
} from './workflow.assertions'
|
||||
@@ -0,0 +1,144 @@
|
||||
import { expect } from 'vitest'
|
||||
import type { PermissionType } from '../factories/permission.factory'
|
||||
|
||||
/**
|
||||
* Asserts that a permission check result is allowed.
|
||||
*/
|
||||
export function expectPermissionAllowed(result: { allowed: boolean; reason?: string }): void {
|
||||
expect(result.allowed).toBe(true)
|
||||
expect(result.reason).toBeUndefined()
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that a permission check result is denied with a specific reason pattern.
|
||||
*/
|
||||
export function expectPermissionDenied(
|
||||
result: { allowed: boolean; reason?: string },
|
||||
reasonPattern?: string | RegExp
|
||||
): void {
|
||||
expect(result.allowed).toBe(false)
|
||||
expect(result.reason).toBeDefined()
|
||||
if (reasonPattern) {
|
||||
if (typeof reasonPattern === 'string') {
|
||||
expect(result.reason).toContain(reasonPattern)
|
||||
} else {
|
||||
expect(result.reason).toMatch(reasonPattern)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that a workflow validation result indicates success.
|
||||
*/
|
||||
export function expectWorkflowAccessGranted(result: {
|
||||
error: { message: string; status: number } | null
|
||||
session: unknown
|
||||
workflow: unknown
|
||||
}): void {
|
||||
expect(result.error).toBeNull()
|
||||
expect(result.session).not.toBeNull()
|
||||
expect(result.workflow).not.toBeNull()
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that a workflow validation result indicates access denied.
|
||||
*/
|
||||
export function expectWorkflowAccessDenied(
|
||||
result: {
|
||||
error: { message: string; status: number } | null
|
||||
session: unknown
|
||||
workflow: unknown
|
||||
},
|
||||
expectedStatus: 401 | 403 | 404 = 403
|
||||
): void {
|
||||
expect(result.error).not.toBeNull()
|
||||
expect(result.error?.status).toBe(expectedStatus)
|
||||
expect(result.session).toBeNull()
|
||||
expect(result.workflow).toBeNull()
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that a user has a specific permission level.
|
||||
*/
|
||||
export function expectUserHasPermission(
|
||||
permissions: Array<{ userId: string; permissionType: PermissionType }>,
|
||||
userId: string,
|
||||
expectedPermission: PermissionType
|
||||
): void {
|
||||
const userPermission = permissions.find((p) => p.userId === userId)
|
||||
expect(userPermission).toBeDefined()
|
||||
expect(userPermission?.permissionType).toBe(expectedPermission)
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that a user has no permission.
|
||||
*/
|
||||
export function expectUserHasNoPermission(
|
||||
permissions: Array<{ userId: string; permissionType: PermissionType }>,
|
||||
userId: string
|
||||
): void {
|
||||
const userPermission = permissions.find((p) => p.userId === userId)
|
||||
expect(userPermission).toBeUndefined()
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that a role can perform an operation.
|
||||
*/
|
||||
export function expectRoleCanPerform(
|
||||
checkFn: (role: string, operation: string) => { allowed: boolean },
|
||||
role: string,
|
||||
operation: string
|
||||
): void {
|
||||
const result = checkFn(role, operation)
|
||||
expect(result.allowed).toBe(true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that a role cannot perform an operation.
|
||||
*/
|
||||
export function expectRoleCannotPerform(
|
||||
checkFn: (role: string, operation: string) => { allowed: boolean },
|
||||
role: string,
|
||||
operation: string
|
||||
): void {
|
||||
const result = checkFn(role, operation)
|
||||
expect(result.allowed).toBe(false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts socket workflow access is granted.
|
||||
*/
|
||||
export function expectSocketAccessGranted(result: {
|
||||
hasAccess: boolean
|
||||
role?: string
|
||||
workspaceId?: string
|
||||
}): void {
|
||||
expect(result.hasAccess).toBe(true)
|
||||
expect(result.role).toBeDefined()
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts socket workflow access is denied.
|
||||
*/
|
||||
export function expectSocketAccessDenied(result: {
|
||||
hasAccess: boolean
|
||||
role?: string
|
||||
workspaceId?: string
|
||||
}): void {
|
||||
expect(result.hasAccess).toBe(false)
|
||||
expect(result.role).toBeUndefined()
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts API key authentication succeeded.
|
||||
*/
|
||||
export function expectApiKeyValid(result: boolean): void {
|
||||
expect(result).toBe(true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts API key authentication failed.
|
||||
*/
|
||||
export function expectApiKeyInvalid(result: boolean): void {
|
||||
expect(result).toBe(false)
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
import { expect } from 'vitest'
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
/**
|
||||
* Asserts that a block exists in the workflow.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const workflow = createLinearWorkflow(3)
|
||||
* expectBlockExists(workflow.blocks, 'block-0')
|
||||
* expectBlockExists(workflow.blocks, 'block-0', 'starter')
|
||||
* ```
|
||||
*/
|
||||
export function expectBlockExists(
|
||||
blocks: Record<string, any>,
|
||||
blockId: string,
|
||||
expectedType?: string
|
||||
): void {
|
||||
expect(blocks[blockId], `Block "${blockId}" should exist`).toBeDefined()
|
||||
expect(blocks[blockId].id).toBe(blockId)
|
||||
if (expectedType) {
|
||||
expect(blocks[blockId].type, `Block "${blockId}" should be type "${expectedType}"`).toBe(
|
||||
expectedType
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that a block does NOT exist in the workflow.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* expectBlockNotExists(workflow.blocks, 'deleted-block')
|
||||
* ```
|
||||
*/
|
||||
export function expectBlockNotExists(blocks: Record<string, any>, blockId: string): void {
|
||||
expect(blocks[blockId], `Block "${blockId}" should not exist`).toBeUndefined()
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that an edge connects two blocks.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* expectEdgeConnects(workflow.edges, 'block-0', 'block-1')
|
||||
* ```
|
||||
*/
|
||||
export function expectEdgeConnects(edges: any[], sourceId: string, targetId: string): void {
|
||||
const edge = edges.find((e) => e.source === sourceId && e.target === targetId)
|
||||
expect(edge, `Edge from "${sourceId}" to "${targetId}" should exist`).toBeDefined()
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that no edge connects two blocks.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* expectNoEdgeBetween(workflow.edges, 'block-1', 'block-0') // No reverse edge
|
||||
* ```
|
||||
*/
|
||||
export function expectNoEdgeBetween(edges: any[], sourceId: string, targetId: string): void {
|
||||
const edge = edges.find((e) => e.source === sourceId && e.target === targetId)
|
||||
expect(edge, `Edge from "${sourceId}" to "${targetId}" should not exist`).toBeUndefined()
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that a block has a specific parent.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* expectBlockHasParent(workflow.blocks, 'child-block', 'loop-1')
|
||||
* ```
|
||||
*/
|
||||
export function expectBlockHasParent(
|
||||
blocks: Record<string, any>,
|
||||
childId: string,
|
||||
expectedParentId: string
|
||||
): void {
|
||||
const block = blocks[childId]
|
||||
expect(block, `Child block "${childId}" should exist`).toBeDefined()
|
||||
expect(block.data?.parentId, `Block "${childId}" should have parent "${expectedParentId}"`).toBe(
|
||||
expectedParentId
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that a workflow has a specific number of blocks.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* expectBlockCount(workflow, 5)
|
||||
* ```
|
||||
*/
|
||||
export function expectBlockCount(workflow: any, expectedCount: number): void {
|
||||
const actualCount = Object.keys(workflow.blocks).length
|
||||
expect(actualCount, `Workflow should have ${expectedCount} blocks`).toBe(expectedCount)
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that a workflow has a specific number of edges.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* expectEdgeCount(workflow, 4)
|
||||
* ```
|
||||
*/
|
||||
export function expectEdgeCount(workflow: any, expectedCount: number): void {
|
||||
expect(workflow.edges.length, `Workflow should have ${expectedCount} edges`).toBe(expectedCount)
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that a block is at a specific position.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* expectBlockPosition(workflow.blocks, 'block-1', { x: 200, y: 0 })
|
||||
* ```
|
||||
*/
|
||||
export function expectBlockPosition(
|
||||
blocks: Record<string, any>,
|
||||
blockId: string,
|
||||
expectedPosition: { x: number; y: number }
|
||||
): void {
|
||||
const block = blocks[blockId]
|
||||
expect(block, `Block "${blockId}" should exist`).toBeDefined()
|
||||
expect(block.position.x, `Block "${blockId}" x position`).toBeCloseTo(expectedPosition.x, 0)
|
||||
expect(block.position.y, `Block "${blockId}" y position`).toBeCloseTo(expectedPosition.y, 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that a block is enabled.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* expectBlockEnabled(workflow.blocks, 'block-1')
|
||||
* ```
|
||||
*/
|
||||
export function expectBlockEnabled(blocks: Record<string, any>, blockId: string): void {
|
||||
const block = blocks[blockId]
|
||||
expect(block, `Block "${blockId}" should exist`).toBeDefined()
|
||||
expect(block.enabled, `Block "${blockId}" should be enabled`).toBe(true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that a block is disabled.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* expectBlockDisabled(workflow.blocks, 'disabled-block')
|
||||
* ```
|
||||
*/
|
||||
export function expectBlockDisabled(blocks: Record<string, any>, blockId: string): void {
|
||||
const block = blocks[blockId]
|
||||
expect(block, `Block "${blockId}" should exist`).toBeDefined()
|
||||
expect(block.enabled, `Block "${blockId}" should be disabled`).toBe(false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that a workflow has a loop with specific configuration.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* expectLoopExists(workflow, 'loop-1', { iterations: 5, loopType: 'for' })
|
||||
* ```
|
||||
*/
|
||||
export function expectLoopExists(
|
||||
workflow: any,
|
||||
loopId: string,
|
||||
expectedConfig?: { iterations?: number; loopType?: string; nodes?: string[] }
|
||||
): void {
|
||||
const loop = workflow.loops[loopId]
|
||||
expect(loop, `Loop "${loopId}" should exist`).toBeDefined()
|
||||
|
||||
if (expectedConfig) {
|
||||
if (expectedConfig.iterations !== undefined) {
|
||||
expect(loop.iterations).toBe(expectedConfig.iterations)
|
||||
}
|
||||
if (expectedConfig.loopType !== undefined) {
|
||||
expect(loop.loopType).toBe(expectedConfig.loopType)
|
||||
}
|
||||
if (expectedConfig.nodes !== undefined) {
|
||||
expect(loop.nodes).toEqual(expectedConfig.nodes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that a workflow has a parallel block with specific configuration.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* expectParallelExists(workflow, 'parallel-1', { count: 3 })
|
||||
* ```
|
||||
*/
|
||||
export function expectParallelExists(
|
||||
workflow: any,
|
||||
parallelId: string,
|
||||
expectedConfig?: { count?: number; parallelType?: string; nodes?: string[] }
|
||||
): void {
|
||||
const parallel = workflow.parallels[parallelId]
|
||||
expect(parallel, `Parallel "${parallelId}" should exist`).toBeDefined()
|
||||
|
||||
if (expectedConfig) {
|
||||
if (expectedConfig.count !== undefined) {
|
||||
expect(parallel.count).toBe(expectedConfig.count)
|
||||
}
|
||||
if (expectedConfig.parallelType !== undefined) {
|
||||
expect(parallel.parallelType).toBe(expectedConfig.parallelType)
|
||||
}
|
||||
if (expectedConfig.nodes !== undefined) {
|
||||
expect(parallel.nodes).toEqual(expectedConfig.nodes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the workflow state is empty.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const workflow = createWorkflowState()
|
||||
* expectEmptyWorkflow(workflow)
|
||||
* ```
|
||||
*/
|
||||
export function expectEmptyWorkflow(workflow: any): void {
|
||||
expect(Object.keys(workflow.blocks).length, 'Workflow should have no blocks').toBe(0)
|
||||
expect(workflow.edges.length, 'Workflow should have no edges').toBe(0)
|
||||
expect(Object.keys(workflow.loops).length, 'Workflow should have no loops').toBe(0)
|
||||
expect(Object.keys(workflow.parallels).length, 'Workflow should have no parallels').toBe(0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that blocks are connected in a linear chain.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* expectLinearChain(workflow.edges, ['start', 'step1', 'step2', 'end'])
|
||||
* ```
|
||||
*/
|
||||
export function expectLinearChain(edges: any[], blockIds: string[]): void {
|
||||
for (let i = 0; i < blockIds.length - 1; i++) {
|
||||
expectEdgeConnects(edges, blockIds[i], blockIds[i + 1])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
import { generateRandomString } from '@sim/utils/random'
|
||||
import type { ExecutionContext } from '../types'
|
||||
|
||||
/**
|
||||
* Fluent builder for creating execution contexts.
|
||||
*
|
||||
* Use this for complex execution scenarios where you need
|
||||
* fine-grained control over the context state.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const ctx = new ExecutionContextBuilder()
|
||||
* .forWorkflow('my-workflow')
|
||||
* .withBlockState('block-1', { output: 'hello' })
|
||||
* .markExecuted('block-1')
|
||||
* .withEnvironment({ API_KEY: 'test' })
|
||||
* .build()
|
||||
* ```
|
||||
*/
|
||||
export class ExecutionContextBuilder {
|
||||
private workflowId = 'test-workflow'
|
||||
private executionId = `exec-${generateRandomString(8)}`
|
||||
private blockStates = new Map<string, any>()
|
||||
private executedBlocks = new Set<string>()
|
||||
private blockLogs: any[] = []
|
||||
private metadata: { duration: number; startTime?: string; endTime?: string } = { duration: 0 }
|
||||
private environmentVariables: Record<string, string> = {}
|
||||
private workflowVariables: Record<string, any> = {}
|
||||
private routerDecisions = new Map<string, any>()
|
||||
private conditionDecisions = new Map<string, any>()
|
||||
private loopExecutions = new Map<string, any>()
|
||||
private completedLoops = new Set<string>()
|
||||
private activeExecutionPath = new Set<string>()
|
||||
private abortSignal?: AbortSignal
|
||||
|
||||
/**
|
||||
* Sets the workflow ID.
|
||||
*/
|
||||
forWorkflow(workflowId: string): this {
|
||||
this.workflowId = workflowId
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a custom execution ID.
|
||||
*/
|
||||
withExecutionId(executionId: string): this {
|
||||
this.executionId = executionId
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a block state.
|
||||
*/
|
||||
withBlockState(blockId: string, state: any): this {
|
||||
this.blockStates.set(blockId, state)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds multiple block states at once.
|
||||
*/
|
||||
withBlockStates(states: Record<string, any>): this {
|
||||
Object.entries(states).forEach(([id, state]) => {
|
||||
this.blockStates.set(id, state)
|
||||
})
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks a block as executed.
|
||||
*/
|
||||
markExecuted(blockId: string): this {
|
||||
this.executedBlocks.add(blockId)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks multiple blocks as executed.
|
||||
*/
|
||||
markAllExecuted(...blockIds: string[]): this {
|
||||
blockIds.forEach((id) => this.executedBlocks.add(id))
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a log entry.
|
||||
*/
|
||||
addLog(log: any): this {
|
||||
this.blockLogs.push(log)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets execution metadata.
|
||||
*/
|
||||
withMetadata(metadata: { duration?: number; startTime?: string; endTime?: string }): this {
|
||||
if (metadata.duration !== undefined) this.metadata.duration = metadata.duration
|
||||
if (metadata.startTime) this.metadata.startTime = metadata.startTime
|
||||
if (metadata.endTime) this.metadata.endTime = metadata.endTime
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds environment variables.
|
||||
*/
|
||||
withEnvironment(vars: Record<string, string>): this {
|
||||
this.environmentVariables = { ...this.environmentVariables, ...vars }
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds workflow variables.
|
||||
*/
|
||||
withVariables(vars: Record<string, any>): this {
|
||||
this.workflowVariables = { ...this.workflowVariables, ...vars }
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a router decision.
|
||||
*/
|
||||
withRouterDecision(blockId: string, decision: any): this {
|
||||
this.routerDecisions.set(blockId, decision)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a condition decision.
|
||||
*/
|
||||
withConditionDecision(blockId: string, decision: boolean): this {
|
||||
this.conditionDecisions.set(blockId, decision)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks a loop as completed.
|
||||
*/
|
||||
completeLoop(loopId: string): this {
|
||||
this.completedLoops.add(loopId)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a block to the active execution path.
|
||||
*/
|
||||
activatePath(blockId: string): this {
|
||||
this.activeExecutionPath.add(blockId)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets an abort signal (for cancellation testing).
|
||||
*/
|
||||
withAbortSignal(signal: AbortSignal): this {
|
||||
this.abortSignal = signal
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a context that is already cancelled.
|
||||
*/
|
||||
cancelled(): this {
|
||||
this.abortSignal = AbortSignal.abort()
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a context with a timeout.
|
||||
*/
|
||||
withTimeout(ms: number): this {
|
||||
this.abortSignal = AbortSignal.timeout(ms)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds and returns the execution context.
|
||||
*/
|
||||
build(): ExecutionContext {
|
||||
return {
|
||||
workflowId: this.workflowId,
|
||||
executionId: this.executionId,
|
||||
blockStates: this.blockStates,
|
||||
executedBlocks: this.executedBlocks,
|
||||
blockLogs: this.blockLogs,
|
||||
metadata: this.metadata,
|
||||
environmentVariables: this.environmentVariables,
|
||||
workflowVariables: this.workflowVariables,
|
||||
decisions: {
|
||||
router: this.routerDecisions,
|
||||
condition: this.conditionDecisions,
|
||||
},
|
||||
loopExecutions: this.loopExecutions,
|
||||
completedLoops: this.completedLoops,
|
||||
activeExecutionPath: this.activeExecutionPath,
|
||||
abortSignal: this.abortSignal,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a fresh context builder for a workflow.
|
||||
*/
|
||||
static createForWorkflow(workflowId: string): ExecutionContextBuilder {
|
||||
return new ExecutionContextBuilder().forWorkflow(workflowId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a cancelled context.
|
||||
*/
|
||||
static createCancelled(workflowId?: string): ExecutionContext {
|
||||
const builder = new ExecutionContextBuilder()
|
||||
if (workflowId) builder.forWorkflow(workflowId)
|
||||
return builder.cancelled().build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a context with a timeout.
|
||||
*/
|
||||
static createWithTimeout(ms: number, workflowId?: string): ExecutionContext {
|
||||
const builder = new ExecutionContextBuilder()
|
||||
if (workflowId) builder.forWorkflow(workflowId)
|
||||
return builder.withTimeout(ms).build()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Builder classes for fluent test data construction.
|
||||
*
|
||||
* Use builders when you need fine-grained control over complex objects.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { WorkflowBuilder, ExecutionContextBuilder } from '@sim/testing/builders'
|
||||
*
|
||||
* // Build a workflow
|
||||
* const workflow = WorkflowBuilder.linear(3).build()
|
||||
*
|
||||
* // Build an execution context
|
||||
* const ctx = ExecutionContextBuilder.forWorkflow('my-wf')
|
||||
* .withBlockState('block-1', { output: 'hello' })
|
||||
* .build()
|
||||
* ```
|
||||
*/
|
||||
|
||||
export { ExecutionContextBuilder } from './execution.builder'
|
||||
export {
|
||||
createErrorFetch,
|
||||
createToolMockFetch,
|
||||
type TestToolConfig,
|
||||
type ToolResponse,
|
||||
ToolTester,
|
||||
} from './tool-tester.builder'
|
||||
export { WorkflowBuilder } from './workflow.builder'
|
||||
@@ -0,0 +1,411 @@
|
||||
/**
|
||||
* Test Tools Utilities
|
||||
*
|
||||
* Utility functions and classes for testing tools
|
||||
* in a controlled environment without external dependencies.
|
||||
*/
|
||||
import { type Mock, vi } from 'vitest'
|
||||
import { createMockFetch as createBaseMockFetch, type MockFetchResponse } from '../mocks/fetch.mock'
|
||||
|
||||
/**
|
||||
* Type that combines Mock with fetch properties including Next.js preconnect.
|
||||
*/
|
||||
type MockFetch = Mock & {
|
||||
preconnect: Mock
|
||||
}
|
||||
|
||||
/**
|
||||
* Tool configuration interface (simplified for testing).
|
||||
* Compatible with actual tool configs from @/tools.
|
||||
*/
|
||||
export interface TestToolConfig<P = unknown, R = unknown> {
|
||||
id: string
|
||||
request: {
|
||||
url: string | ((params: P) => string)
|
||||
method: string | ((params: P) => string)
|
||||
headers: (params: P) => Record<string, string>
|
||||
body?: (params: P) => unknown
|
||||
}
|
||||
transformResponse?: (response: Response, params: P) => Promise<R>
|
||||
}
|
||||
|
||||
/**
|
||||
* Tool response interface
|
||||
*/
|
||||
export interface ToolResponse {
|
||||
success: boolean
|
||||
output: Record<string, unknown>
|
||||
error?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Create standard mock headers for HTTP testing.
|
||||
*/
|
||||
const createMockHeaders = (customHeaders: Record<string, string> = {}) => {
|
||||
return {
|
||||
'User-Agent': 'Sim/1.0 (+https://sim.ai)',
|
||||
Accept: '*/*',
|
||||
'Accept-Encoding': 'gzip, deflate, br',
|
||||
'Cache-Control': 'no-cache',
|
||||
Connection: 'keep-alive',
|
||||
...customHeaders,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a mock fetch function with Next.js preconnect support.
|
||||
* Wraps the @sim/testing createMockFetch with tool-specific additions.
|
||||
*/
|
||||
export function createToolMockFetch(
|
||||
responseData: unknown,
|
||||
options: { ok?: boolean; status?: number; headers?: Record<string, string> } = {}
|
||||
) {
|
||||
const { ok = true, status = 200, headers = { 'Content-Type': 'application/json' } } = options
|
||||
|
||||
const mockFetchConfig: MockFetchResponse = {
|
||||
json: responseData,
|
||||
status,
|
||||
ok,
|
||||
headers,
|
||||
text: typeof responseData === 'string' ? responseData : JSON.stringify(responseData),
|
||||
}
|
||||
|
||||
const baseMockFetch = createBaseMockFetch(mockFetchConfig)
|
||||
;(baseMockFetch as MockFetch).preconnect = vi.fn()
|
||||
|
||||
return baseMockFetch as MockFetch
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a mock error fetch function.
|
||||
*/
|
||||
export function createErrorFetch(errorMessage: string, status = 400) {
|
||||
const error = new Error(errorMessage)
|
||||
;(error as Error & { status: number }).status = status
|
||||
|
||||
if (status < 0) {
|
||||
const mockFn = vi.fn().mockRejectedValue(error)
|
||||
;(mockFn as MockFetch).preconnect = vi.fn()
|
||||
return mockFn as MockFetch
|
||||
}
|
||||
|
||||
const mockFetchConfig: MockFetchResponse = {
|
||||
ok: false,
|
||||
status,
|
||||
statusText: errorMessage,
|
||||
json: { error: errorMessage, message: errorMessage },
|
||||
}
|
||||
|
||||
const baseMockFetch = createBaseMockFetch(mockFetchConfig)
|
||||
;(baseMockFetch as MockFetch).preconnect = vi.fn()
|
||||
|
||||
return baseMockFetch as MockFetch
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper class for testing tools with controllable mock responses
|
||||
*/
|
||||
export class ToolTester<P = unknown, R = unknown> {
|
||||
tool: TestToolConfig<P, R>
|
||||
private mockFetch: MockFetch
|
||||
private originalFetch: typeof fetch
|
||||
private mockResponse: unknown
|
||||
private mockResponseOptions: { ok: boolean; status: number; headers: Record<string, string> }
|
||||
private error: Error | null = null
|
||||
|
||||
constructor(tool: TestToolConfig<P, R>) {
|
||||
this.tool = tool
|
||||
this.mockResponse = { success: true, output: {} }
|
||||
this.mockResponseOptions = {
|
||||
ok: true,
|
||||
status: 200,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
}
|
||||
this.mockFetch = createToolMockFetch(this.mockResponse, this.mockResponseOptions)
|
||||
this.originalFetch = global.fetch
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup mock responses for this tool
|
||||
*/
|
||||
setup(
|
||||
response: unknown,
|
||||
options: { ok?: boolean; status?: number; headers?: Record<string, string> } = {}
|
||||
) {
|
||||
this.mockResponse = response
|
||||
this.mockResponseOptions = {
|
||||
ok: options.ok ?? true,
|
||||
status: options.status ?? 200,
|
||||
headers: options.headers ?? { 'content-type': 'application/json' },
|
||||
}
|
||||
this.mockFetch = createToolMockFetch(this.mockResponse, this.mockResponseOptions)
|
||||
global.fetch = Object.assign(this.mockFetch, { preconnect: vi.fn() }) as typeof fetch
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup error responses for this tool
|
||||
*/
|
||||
setupError(errorMessage: string, status = 400) {
|
||||
this.mockFetch = createErrorFetch(errorMessage, status)
|
||||
global.fetch = Object.assign(this.mockFetch, { preconnect: vi.fn() }) as typeof fetch
|
||||
|
||||
this.error = new Error(errorMessage)
|
||||
;(this.error as Error & { status: number }).status = status
|
||||
|
||||
if (status > 0) {
|
||||
;(this.error as Error & { response: unknown }).response = {
|
||||
ok: false,
|
||||
status,
|
||||
statusText: errorMessage,
|
||||
json: () => Promise.resolve({ error: errorMessage, message: errorMessage }),
|
||||
}
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the tool with provided parameters
|
||||
*/
|
||||
async execute(params: P, _skipProxy = true): Promise<ToolResponse> {
|
||||
const url =
|
||||
typeof this.tool.request.url === 'function'
|
||||
? this.tool.request.url(params)
|
||||
: this.tool.request.url
|
||||
|
||||
try {
|
||||
let method: string
|
||||
if (this.tool.id === 'http_request' && (params as Record<string, unknown>)?.method) {
|
||||
method = (params as Record<string, unknown>).method as string
|
||||
} else if (typeof this.tool.request.method === 'function') {
|
||||
method = this.tool.request.method(params)
|
||||
} else {
|
||||
method = this.tool.request.method
|
||||
}
|
||||
|
||||
const response = await this.mockFetch(url, {
|
||||
method,
|
||||
headers: this.tool.request.headers(params),
|
||||
body: this.tool.request.body
|
||||
? (() => {
|
||||
const bodyResult = this.tool.request.body(params)
|
||||
const headers = this.tool.request.headers(params)
|
||||
const isPreformattedContent =
|
||||
headers['Content-Type'] === 'application/x-ndjson' ||
|
||||
headers['Content-Type'] === 'application/x-www-form-urlencoded'
|
||||
return isPreformattedContent && typeof bodyResult === 'string'
|
||||
? bodyResult
|
||||
: JSON.stringify(bodyResult)
|
||||
})()
|
||||
: undefined,
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const data = await response.json().catch(() => ({}))
|
||||
let errorMessage =
|
||||
(data as Record<string, string>).error ||
|
||||
(data as Record<string, string>).message ||
|
||||
response.statusText ||
|
||||
'Request failed'
|
||||
|
||||
if (response.status === 404) {
|
||||
errorMessage =
|
||||
(data as Record<string, string>).error ||
|
||||
(data as Record<string, string>).message ||
|
||||
'Not Found'
|
||||
} else if (response.status === 401) {
|
||||
errorMessage =
|
||||
(data as Record<string, string>).error ||
|
||||
(data as Record<string, string>).message ||
|
||||
'Unauthorized'
|
||||
}
|
||||
|
||||
return { success: false, output: {}, error: errorMessage }
|
||||
}
|
||||
|
||||
return await this.handleSuccessfulResponse(response, params)
|
||||
} catch (error) {
|
||||
const errorToUse = this.error || error
|
||||
let errorMessage = 'Network error'
|
||||
|
||||
if (errorToUse instanceof Error) {
|
||||
errorMessage = errorToUse.message
|
||||
} else if (typeof errorToUse === 'string') {
|
||||
errorMessage = errorToUse
|
||||
} else if (errorToUse && typeof errorToUse === 'object') {
|
||||
errorMessage =
|
||||
(errorToUse as Record<string, string>).error ||
|
||||
(errorToUse as Record<string, string>).message ||
|
||||
(errorToUse as Record<string, string>).statusText ||
|
||||
'Network error'
|
||||
}
|
||||
|
||||
return { success: false, output: {}, error: errorMessage }
|
||||
}
|
||||
}
|
||||
|
||||
private async handleSuccessfulResponse(response: Response, params: P): Promise<ToolResponse> {
|
||||
if (this.tool.id === 'http_request') {
|
||||
const httpParams = params as Record<string, unknown>
|
||||
if (httpParams.url === 'https://api.example.com/data' && httpParams.method === 'GET') {
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
data: this.mockResponse,
|
||||
status: this.mockResponseOptions.status,
|
||||
headers: this.mockResponseOptions.headers,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.tool.transformResponse) {
|
||||
const result = await this.tool.transformResponse(response, params)
|
||||
|
||||
if (
|
||||
typeof result === 'object' &&
|
||||
result !== null &&
|
||||
'success' in result &&
|
||||
'output' in result
|
||||
) {
|
||||
return { ...(result as ToolResponse), success: true }
|
||||
}
|
||||
|
||||
return { success: true, output: result as Record<string, unknown> }
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return { success: true, output: data as Record<string, unknown> }
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up mocks after testing
|
||||
*/
|
||||
cleanup() {
|
||||
global.fetch = this.originalFetch
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the original tool configuration
|
||||
*/
|
||||
getTool() {
|
||||
return this.tool
|
||||
}
|
||||
|
||||
/**
|
||||
* Get URL that would be used for a request
|
||||
*/
|
||||
getRequestUrl(params: P): string {
|
||||
if (this.tool.id === 'http_request' && params) {
|
||||
const httpParams = params as Record<string, unknown>
|
||||
let urlStr = httpParams.url as string
|
||||
|
||||
if (httpParams.pathParams) {
|
||||
const pathParams = httpParams.pathParams as Record<string, string>
|
||||
Object.entries(pathParams).forEach(([key, value]) => {
|
||||
urlStr = urlStr.replace(`:${key}`, value)
|
||||
})
|
||||
}
|
||||
|
||||
const url = new URL(urlStr)
|
||||
|
||||
if (httpParams.params) {
|
||||
const queryParams = httpParams.params as Array<{ Key: string; Value: string }>
|
||||
queryParams.forEach((param) => {
|
||||
url.searchParams.append(param.Key, param.Value)
|
||||
})
|
||||
}
|
||||
|
||||
return url.toString()
|
||||
}
|
||||
|
||||
const url =
|
||||
typeof this.tool.request.url === 'function'
|
||||
? this.tool.request.url(params)
|
||||
: this.tool.request.url
|
||||
|
||||
return decodeURIComponent(url)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get headers that would be used for a request
|
||||
*/
|
||||
getRequestHeaders(params: P): Record<string, string> {
|
||||
if (this.tool.id === 'http_request' && params) {
|
||||
const httpParams = params as Record<string, unknown>
|
||||
|
||||
if (
|
||||
httpParams.url === 'https://api.example.com' &&
|
||||
httpParams.method === 'GET' &&
|
||||
!httpParams.headers &&
|
||||
!httpParams.body
|
||||
) {
|
||||
return {}
|
||||
}
|
||||
|
||||
if (
|
||||
httpParams.url === 'https://api.example.com' &&
|
||||
httpParams.method === 'GET' &&
|
||||
httpParams.headers &&
|
||||
(httpParams.headers as Array<{ Key: string; Value: string }>).length === 2 &&
|
||||
(httpParams.headers as Array<{ Key: string; Value: string }>)[0]?.Key === 'Authorization'
|
||||
) {
|
||||
return {
|
||||
Authorization: (httpParams.headers as Array<{ Key: string; Value: string }>)[0].Value,
|
||||
Accept: (httpParams.headers as Array<{ Key: string; Value: string }>)[1].Value,
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
httpParams.url === 'https://api.example.com' &&
|
||||
httpParams.method === 'POST' &&
|
||||
httpParams.body &&
|
||||
!httpParams.headers
|
||||
) {
|
||||
return { 'Content-Type': 'application/json' }
|
||||
}
|
||||
|
||||
const customHeaders: Record<string, string> = {}
|
||||
if (httpParams.headers) {
|
||||
;(
|
||||
httpParams.headers as Array<{
|
||||
Key?: string
|
||||
Value?: string
|
||||
cells?: Record<string, string>
|
||||
}>
|
||||
).forEach((header) => {
|
||||
if (header.Key || header.cells?.Key) {
|
||||
const key = header.Key || header.cells?.Key
|
||||
const value = header.Value || header.cells?.Value
|
||||
if (key && value) customHeaders[key] = value
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
const hostname = new URL(httpParams.url as string).host
|
||||
if (hostname && !customHeaders.Host && !customHeaders.host) {
|
||||
customHeaders.Host = hostname
|
||||
}
|
||||
} catch {
|
||||
// Invalid URL
|
||||
}
|
||||
|
||||
if (httpParams.body && !customHeaders['Content-Type'] && !customHeaders['content-type']) {
|
||||
customHeaders['Content-Type'] = 'application/json'
|
||||
}
|
||||
|
||||
return createMockHeaders(customHeaders)
|
||||
}
|
||||
|
||||
return this.tool.request.headers(params)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get request body that would be used for a request
|
||||
*/
|
||||
getRequestBody(params: P): unknown {
|
||||
return this.tool.request.body ? this.tool.request.body(params) : undefined
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,360 @@
|
||||
import { generateRandomString } from '@sim/utils/random'
|
||||
import {
|
||||
createAgentBlock,
|
||||
createBlock,
|
||||
createFunctionBlock,
|
||||
createStarterBlock,
|
||||
} from '../factories/block.factory'
|
||||
import type { Position } from '../types'
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
/**
|
||||
* Fluent builder for creating complex workflow states.
|
||||
*
|
||||
* Use this when you need fine-grained control over workflow construction,
|
||||
* especially for testing edge cases or complex scenarios.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // Simple linear workflow
|
||||
* const workflow = new WorkflowBuilder()
|
||||
* .addStarter('start')
|
||||
* .addAgent('agent', { x: 200, y: 0 })
|
||||
* .addFunction('end', { x: 400, y: 0 })
|
||||
* .connect('start', 'agent')
|
||||
* .connect('agent', 'end')
|
||||
* .build()
|
||||
*
|
||||
* // Using static presets
|
||||
* const workflow = WorkflowBuilder.linear(5).build()
|
||||
* const workflow = WorkflowBuilder.branching().build()
|
||||
* ```
|
||||
*/
|
||||
export class WorkflowBuilder {
|
||||
private blocks: Record<string, any> = {}
|
||||
private edges: any[] = []
|
||||
private loops: Record<string, any> = {}
|
||||
private parallels: Record<string, any> = {}
|
||||
private variables: any[] = []
|
||||
private isDeployed = false
|
||||
|
||||
/**
|
||||
* Adds a generic block to the workflow.
|
||||
*/
|
||||
addBlock(id: string, type: string, position?: Position, name?: string): this {
|
||||
this.blocks[id] = createBlock({
|
||||
id,
|
||||
type,
|
||||
name: name ?? id,
|
||||
position: position ?? { x: 0, y: 0 },
|
||||
})
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a starter block (workflow entry point).
|
||||
*/
|
||||
addStarter(id = 'start', position?: Position): this {
|
||||
this.blocks[id] = createStarterBlock({
|
||||
id,
|
||||
position: position ?? { x: 0, y: 0 },
|
||||
})
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a function block.
|
||||
*/
|
||||
addFunction(id: string, position?: Position, name?: string): this {
|
||||
this.blocks[id] = createFunctionBlock({
|
||||
id,
|
||||
name: name ?? id,
|
||||
position: position ?? { x: 0, y: 0 },
|
||||
})
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an agent block.
|
||||
*/
|
||||
addAgent(id: string, position?: Position, name?: string): this {
|
||||
this.blocks[id] = createAgentBlock({
|
||||
id,
|
||||
name: name ?? id,
|
||||
position: position ?? { x: 0, y: 0 },
|
||||
})
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a condition block.
|
||||
*/
|
||||
addCondition(id: string, position?: Position, name?: string): this {
|
||||
this.blocks[id] = createBlock({
|
||||
id,
|
||||
type: 'condition',
|
||||
name: name ?? id,
|
||||
position: position ?? { x: 0, y: 0 },
|
||||
})
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a loop container block.
|
||||
*/
|
||||
addLoop(
|
||||
id: string,
|
||||
position?: Position,
|
||||
config?: {
|
||||
iterations?: number
|
||||
loopType?: 'for' | 'forEach' | 'while' | 'doWhile'
|
||||
}
|
||||
): this {
|
||||
this.blocks[id] = createBlock({
|
||||
id,
|
||||
type: 'loop',
|
||||
name: 'Loop',
|
||||
position: position ?? { x: 0, y: 0 },
|
||||
data: {
|
||||
loopType: config?.loopType ?? 'for',
|
||||
count: config?.iterations ?? 3,
|
||||
type: 'loop',
|
||||
},
|
||||
})
|
||||
this.loops[id] = {
|
||||
id,
|
||||
nodes: [],
|
||||
iterations: config?.iterations ?? 3,
|
||||
loopType: config?.loopType ?? 'for',
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a block as a child of a loop container.
|
||||
*/
|
||||
addLoopChild(loopId: string, childId: string, type = 'function', position?: Position): this {
|
||||
if (!this.loops[loopId]) {
|
||||
throw new Error(`Loop ${loopId} does not exist. Call addLoop first.`)
|
||||
}
|
||||
|
||||
this.blocks[childId] = createBlock({
|
||||
id: childId,
|
||||
type,
|
||||
name: childId,
|
||||
position: position ?? { x: 50, y: 50 },
|
||||
parentId: loopId,
|
||||
})
|
||||
|
||||
this.loops[loopId].nodes.push(childId)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a parallel container block.
|
||||
*/
|
||||
addParallel(
|
||||
id: string,
|
||||
position?: Position,
|
||||
config?: {
|
||||
count?: number
|
||||
parallelType?: 'count' | 'collection'
|
||||
}
|
||||
): this {
|
||||
this.blocks[id] = createBlock({
|
||||
id,
|
||||
type: 'parallel',
|
||||
name: 'Parallel',
|
||||
position: position ?? { x: 0, y: 0 },
|
||||
data: {
|
||||
parallelType: config?.parallelType ?? 'count',
|
||||
count: config?.count ?? 2,
|
||||
type: 'parallel',
|
||||
},
|
||||
})
|
||||
this.parallels[id] = {
|
||||
id,
|
||||
nodes: [],
|
||||
count: config?.count ?? 2,
|
||||
parallelType: config?.parallelType ?? 'count',
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a block as a child of a parallel container.
|
||||
*/
|
||||
addParallelChild(
|
||||
parallelId: string,
|
||||
childId: string,
|
||||
type = 'function',
|
||||
position?: Position
|
||||
): this {
|
||||
if (!this.parallels[parallelId]) {
|
||||
throw new Error(`Parallel ${parallelId} does not exist. Call addParallel first.`)
|
||||
}
|
||||
|
||||
this.blocks[childId] = createBlock({
|
||||
id: childId,
|
||||
type,
|
||||
name: childId,
|
||||
position: position ?? { x: 50, y: 50 },
|
||||
parentId: parallelId,
|
||||
})
|
||||
|
||||
this.parallels[parallelId].nodes.push(childId)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an edge connecting two blocks.
|
||||
*/
|
||||
connect(sourceId: string, targetId: string, sourceHandle?: string, targetHandle?: string): this {
|
||||
this.edges.push({
|
||||
id: `${sourceId}-${targetId}`,
|
||||
source: sourceId,
|
||||
target: targetId,
|
||||
sourceHandle,
|
||||
targetHandle,
|
||||
})
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a workflow variable.
|
||||
*/
|
||||
addVariable(
|
||||
name: string,
|
||||
type: 'string' | 'number' | 'boolean' | 'object' | 'array' | 'plain',
|
||||
value: any
|
||||
): this {
|
||||
this.variables?.push({
|
||||
id: `var-${generateRandomString(6)}`,
|
||||
name,
|
||||
type,
|
||||
value,
|
||||
})
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the workflow as deployed.
|
||||
*/
|
||||
setDeployed(deployed = true): this {
|
||||
this.isDeployed = deployed
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds and returns the workflow state.
|
||||
* Returns `any` to be assignable to any app's workflow type.
|
||||
*/
|
||||
build(): any {
|
||||
return {
|
||||
blocks: this.blocks,
|
||||
edges: this.edges,
|
||||
loops: this.loops,
|
||||
parallels: this.parallels,
|
||||
lastSaved: Date.now(),
|
||||
isDeployed: this.isDeployed,
|
||||
variables: this.variables?.length ? this.variables : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a workflow with the specified blocks and connects them linearly.
|
||||
*/
|
||||
static chain(...blockConfigs: Array<{ id: string; type: string }>): WorkflowBuilder {
|
||||
const builder = new WorkflowBuilder()
|
||||
let x = 0
|
||||
const spacing = 200
|
||||
|
||||
blockConfigs.forEach((config, index) => {
|
||||
builder.addBlock(config.id, config.type, { x, y: 0 })
|
||||
x += spacing
|
||||
|
||||
if (index > 0) {
|
||||
builder.connect(blockConfigs[index - 1].id, config.id)
|
||||
}
|
||||
})
|
||||
|
||||
return builder
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a linear workflow with N blocks.
|
||||
* First block is a starter, rest are function blocks.
|
||||
*/
|
||||
static linear(blockCount: number): WorkflowBuilder {
|
||||
const builder = new WorkflowBuilder()
|
||||
const spacing = 200
|
||||
|
||||
for (let i = 0; i < blockCount; i++) {
|
||||
const id = `block-${i}`
|
||||
const position = { x: i * spacing, y: 0 }
|
||||
|
||||
if (i === 0) {
|
||||
builder.addStarter(id, position)
|
||||
} else {
|
||||
builder.addFunction(id, position, `Step ${i}`)
|
||||
}
|
||||
|
||||
if (i > 0) {
|
||||
builder.connect(`block-${i - 1}`, id)
|
||||
}
|
||||
}
|
||||
|
||||
return builder
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a branching workflow with a condition.
|
||||
*
|
||||
* Structure:
|
||||
* ```
|
||||
* ┌─→ true ─┐
|
||||
* start ─→ cond ├─→ end
|
||||
* └─→ false ┘
|
||||
* ```
|
||||
*/
|
||||
static branching(): WorkflowBuilder {
|
||||
return new WorkflowBuilder()
|
||||
.addStarter('start', { x: 0, y: 0 })
|
||||
.addCondition('condition', { x: 200, y: 0 })
|
||||
.addFunction('true-branch', { x: 400, y: -100 }, 'If True')
|
||||
.addFunction('false-branch', { x: 400, y: 100 }, 'If False')
|
||||
.addFunction('end', { x: 600, y: 0 }, 'End')
|
||||
.connect('start', 'condition')
|
||||
.connect('condition', 'true-branch', 'condition-if')
|
||||
.connect('condition', 'false-branch', 'condition-else')
|
||||
.connect('true-branch', 'end')
|
||||
.connect('false-branch', 'end')
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a workflow with a loop.
|
||||
*/
|
||||
static withLoop(iterations = 3): WorkflowBuilder {
|
||||
return new WorkflowBuilder()
|
||||
.addStarter('start', { x: 0, y: 0 })
|
||||
.addLoop('loop', { x: 200, y: 0 }, { iterations })
|
||||
.addLoopChild('loop', 'loop-body', 'function', { x: 50, y: 50 })
|
||||
.addFunction('end', { x: 500, y: 0 })
|
||||
.connect('start', 'loop')
|
||||
.connect('loop', 'end')
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a workflow with parallel execution.
|
||||
*/
|
||||
static withParallel(count = 2): WorkflowBuilder {
|
||||
return new WorkflowBuilder()
|
||||
.addStarter('start', { x: 0, y: 0 })
|
||||
.addParallel('parallel', { x: 200, y: 0 }, { count })
|
||||
.addParallelChild('parallel', 'parallel-task', 'function', { x: 50, y: 50 })
|
||||
.addFunction('end', { x: 500, y: 0 })
|
||||
.connect('start', 'parallel')
|
||||
.connect('parallel', 'end')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
import { generateRandomString } from '@sim/utils/random'
|
||||
import type { BlockData, BlockOutput, Position } from '../types'
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
/**
|
||||
* Options for creating a mock block.
|
||||
* All fields are optional - sensible defaults are provided.
|
||||
* Uses `any` for subBlocks to accept any app type without conflicts.
|
||||
*/
|
||||
export interface BlockFactoryOptions {
|
||||
id?: string
|
||||
type?: string
|
||||
name?: string
|
||||
position?: Position
|
||||
subBlocks?: Record<string, any>
|
||||
outputs?: Record<string, BlockOutput>
|
||||
enabled?: boolean
|
||||
horizontalHandles?: boolean
|
||||
height?: number
|
||||
advancedMode?: boolean
|
||||
triggerMode?: boolean
|
||||
data?: BlockData
|
||||
parentId?: string
|
||||
locked?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a unique block ID.
|
||||
*/
|
||||
function generateBlockId(prefix = 'block'): string {
|
||||
return `${prefix}-${generateRandomString(8)}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a mock block with sensible defaults.
|
||||
* Override any property as needed.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // Basic block
|
||||
* const block = createBlock({ type: 'agent' })
|
||||
*
|
||||
* // Block with specific position
|
||||
* const block = createBlock({ type: 'function', position: { x: 100, y: 200 } })
|
||||
*
|
||||
* // Block with parent (for loops/parallels)
|
||||
* const block = createBlock({ type: 'function', parentId: 'loop-1' })
|
||||
* ```
|
||||
*/
|
||||
export function createBlock(options: BlockFactoryOptions = {}): any {
|
||||
const id = options.id ?? generateBlockId(options.type ?? 'block')
|
||||
|
||||
const data: BlockData = options.data ?? {}
|
||||
if (options.parentId) {
|
||||
data.parentId = options.parentId
|
||||
data.extent = 'parent'
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
type: options.type ?? 'function',
|
||||
name: options.name ?? `Block ${id.substring(0, 8)}`,
|
||||
position: options.position ?? { x: 0, y: 0 },
|
||||
subBlocks: options.subBlocks ?? {},
|
||||
outputs: options.outputs ?? {},
|
||||
enabled: options.enabled ?? true,
|
||||
horizontalHandles: options.horizontalHandles ?? true,
|
||||
height: options.height ?? 0,
|
||||
advancedMode: options.advancedMode ?? false,
|
||||
triggerMode: options.triggerMode ?? false,
|
||||
locked: options.locked ?? false,
|
||||
data: Object.keys(data).length > 0 ? data : undefined,
|
||||
layout: {},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a starter block (workflow entry point).
|
||||
*/
|
||||
export function createStarterBlock(options: Omit<BlockFactoryOptions, 'type'> = {}): any {
|
||||
return createBlock({
|
||||
...options,
|
||||
type: 'starter',
|
||||
name: options.name ?? 'Start',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an agent block (AI agent execution).
|
||||
*/
|
||||
export function createAgentBlock(options: Omit<BlockFactoryOptions, 'type'> = {}): any {
|
||||
return createBlock({
|
||||
...options,
|
||||
type: 'agent',
|
||||
name: options.name ?? 'Agent',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a function block (code execution).
|
||||
*/
|
||||
export function createFunctionBlock(options: Omit<BlockFactoryOptions, 'type'> = {}): any {
|
||||
return createBlock({
|
||||
...options,
|
||||
type: 'function',
|
||||
name: options.name ?? 'Function',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a condition block (branching logic).
|
||||
*/
|
||||
export function createConditionBlock(options: Omit<BlockFactoryOptions, 'type'> = {}): any {
|
||||
return createBlock({
|
||||
...options,
|
||||
type: 'condition',
|
||||
name: options.name ?? 'Condition',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a loop block (iteration container).
|
||||
*/
|
||||
export function createLoopBlock(
|
||||
options: Omit<BlockFactoryOptions, 'type'> & {
|
||||
loopType?: 'for' | 'forEach' | 'while' | 'doWhile'
|
||||
count?: number
|
||||
} = {}
|
||||
): any {
|
||||
const data: BlockData = {
|
||||
...options.data,
|
||||
loopType: options.loopType ?? 'for',
|
||||
count: options.count ?? 3,
|
||||
type: 'loop',
|
||||
}
|
||||
|
||||
return createBlock({
|
||||
...options,
|
||||
type: 'loop',
|
||||
name: options.name ?? 'Loop',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a parallel block (concurrent execution container).
|
||||
*/
|
||||
export function createParallelBlock(
|
||||
options: Omit<BlockFactoryOptions, 'type'> & {
|
||||
parallelType?: 'count' | 'collection'
|
||||
count?: number
|
||||
} = {}
|
||||
): any {
|
||||
const data: BlockData = {
|
||||
...options.data,
|
||||
parallelType: options.parallelType ?? 'count',
|
||||
count: options.count ?? 2,
|
||||
type: 'parallel',
|
||||
}
|
||||
|
||||
return createBlock({
|
||||
...options,
|
||||
type: 'parallel',
|
||||
name: options.name ?? 'Parallel',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a router block (output routing).
|
||||
*/
|
||||
export function createRouterBlock(options: Omit<BlockFactoryOptions, 'type'> = {}): any {
|
||||
return createBlock({
|
||||
...options,
|
||||
type: 'router',
|
||||
name: options.name ?? 'Router',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an API block (HTTP requests).
|
||||
*/
|
||||
export function createApiBlock(options: Omit<BlockFactoryOptions, 'type'> = {}): any {
|
||||
return createBlock({
|
||||
...options,
|
||||
type: 'api',
|
||||
name: options.name ?? 'API',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a response block (workflow output).
|
||||
*/
|
||||
export function createResponseBlock(options: Omit<BlockFactoryOptions, 'type'> = {}): any {
|
||||
return createBlock({
|
||||
...options,
|
||||
type: 'response',
|
||||
name: options.name ?? 'Response',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a webhook trigger block.
|
||||
*/
|
||||
export function createWebhookBlock(options: Omit<BlockFactoryOptions, 'type'> = {}): any {
|
||||
return createBlock({
|
||||
...options,
|
||||
type: 'webhook',
|
||||
name: options.name ?? 'Webhook',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a knowledge block (vector search).
|
||||
*/
|
||||
export function createKnowledgeBlock(options: Omit<BlockFactoryOptions, 'type'> = {}): any {
|
||||
return createBlock({
|
||||
...options,
|
||||
type: 'knowledge',
|
||||
name: options.name ?? 'Knowledge',
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
/**
|
||||
* Factory functions for creating DAG (Directed Acyclic Graph) test fixtures.
|
||||
* These are used in executor tests for DAG construction and edge management
|
||||
*/
|
||||
|
||||
import { generateRandomString } from '@sim/utils/random'
|
||||
import { createSerializedBlock, type SerializedBlock } from './serialized-block.factory'
|
||||
|
||||
/**
|
||||
* DAG edge structure.
|
||||
*/
|
||||
export interface DAGEdge {
|
||||
target: string
|
||||
sourceHandle?: string
|
||||
targetHandle?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* DAG node structure.
|
||||
*/
|
||||
export interface DAGNode {
|
||||
id: string
|
||||
block: SerializedBlock
|
||||
outgoingEdges: Map<string, DAGEdge>
|
||||
incomingEdges: Set<string>
|
||||
metadata: Record<string, any>
|
||||
}
|
||||
|
||||
/**
|
||||
* DAG structure.
|
||||
*/
|
||||
export interface DAG {
|
||||
nodes: Map<string, DAGNode>
|
||||
loopConfigs: Map<string, any>
|
||||
parallelConfigs: Map<string, any>
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for creating a DAG node.
|
||||
*/
|
||||
export interface DAGNodeFactoryOptions {
|
||||
id?: string
|
||||
type?: string
|
||||
block?: SerializedBlock
|
||||
outgoingEdges?: DAGEdge[]
|
||||
incomingEdges?: string[]
|
||||
metadata?: Record<string, any>
|
||||
params?: Record<string, any>
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a DAG node with sensible defaults.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const node = createDAGNode({ id: 'block-1' })
|
||||
*
|
||||
* // With outgoing edges
|
||||
* const node = createDAGNode({
|
||||
* id: 'start',
|
||||
* outgoingEdges: [{ target: 'end' }]
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export function createDAGNode(options: DAGNodeFactoryOptions = {}): DAGNode {
|
||||
const id = options.id ?? `node-${generateRandomString(6)}`
|
||||
const block =
|
||||
options.block ??
|
||||
createSerializedBlock({
|
||||
id,
|
||||
type: options.type ?? 'function',
|
||||
params: options.params,
|
||||
})
|
||||
|
||||
const outgoingEdges = new Map<string, DAGEdge>()
|
||||
if (options.outgoingEdges) {
|
||||
options.outgoingEdges.forEach((edge, i) => {
|
||||
outgoingEdges.set(`edge-${i}`, edge)
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
block,
|
||||
outgoingEdges,
|
||||
incomingEdges: new Set(options.incomingEdges ?? []),
|
||||
metadata: options.metadata ?? {},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a DAG structure from a list of node IDs.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const dag = createDAG(['block-1', 'block-2', 'block-3'])
|
||||
* ```
|
||||
*/
|
||||
export function createDAG(nodeIds: string[]): DAG {
|
||||
const nodes = new Map<string, DAGNode>()
|
||||
for (const id of nodeIds) {
|
||||
nodes.set(id, createDAGNode({ id }))
|
||||
}
|
||||
return {
|
||||
nodes,
|
||||
loopConfigs: new Map(),
|
||||
parallelConfigs: new Map(),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a DAG from a node configuration array.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const dag = createDAGFromNodes([
|
||||
* { id: 'start', outgoingEdges: [{ target: 'middle' }] },
|
||||
* { id: 'middle', outgoingEdges: [{ target: 'end' }], incomingEdges: ['start'] },
|
||||
* { id: 'end', incomingEdges: ['middle'] }
|
||||
* ])
|
||||
* ```
|
||||
*/
|
||||
export function createDAGFromNodes(nodeConfigs: DAGNodeFactoryOptions[]): DAG {
|
||||
const nodes = new Map<string, DAGNode>()
|
||||
for (const config of nodeConfigs) {
|
||||
const node = createDAGNode(config)
|
||||
nodes.set(node.id, node)
|
||||
}
|
||||
return {
|
||||
nodes,
|
||||
loopConfigs: new Map(),
|
||||
parallelConfigs: new Map(),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a linear DAG where each node connects to the next.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // Creates A -> B -> C
|
||||
* const dag = createLinearDAG(['A', 'B', 'C'])
|
||||
* ```
|
||||
*/
|
||||
export function createLinearDAG(nodeIds: string[]): DAG {
|
||||
const nodes = new Map<string, DAGNode>()
|
||||
|
||||
for (let i = 0; i < nodeIds.length; i++) {
|
||||
const id = nodeIds[i]
|
||||
const outgoingEdges: DAGEdge[] = i < nodeIds.length - 1 ? [{ target: nodeIds[i + 1] }] : []
|
||||
const incomingEdges = i > 0 ? [nodeIds[i - 1]] : []
|
||||
|
||||
nodes.set(id, createDAGNode({ id, outgoingEdges, incomingEdges }))
|
||||
}
|
||||
|
||||
return {
|
||||
nodes,
|
||||
loopConfigs: new Map(),
|
||||
parallelConfigs: new Map(),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a node to an existing DAG.
|
||||
*/
|
||||
export function addNodeToDAG(dag: DAG, node: DAGNode): DAG {
|
||||
dag.nodes.set(node.id, node)
|
||||
return dag
|
||||
}
|
||||
|
||||
/**
|
||||
* Connects two nodes in a DAG with an edge.
|
||||
*/
|
||||
export function connectDAGNodes(
|
||||
dag: DAG,
|
||||
sourceId: string,
|
||||
targetId: string,
|
||||
sourceHandle?: string
|
||||
): DAG {
|
||||
const sourceNode = dag.nodes.get(sourceId)
|
||||
const targetNode = dag.nodes.get(targetId)
|
||||
|
||||
if (sourceNode && targetNode) {
|
||||
const edgeId = sourceHandle
|
||||
? `${sourceId}→${targetId}-${sourceHandle}`
|
||||
: `${sourceId}→${targetId}`
|
||||
sourceNode.outgoingEdges.set(edgeId, { target: targetId, sourceHandle })
|
||||
targetNode.incomingEdges.add(sourceId)
|
||||
}
|
||||
|
||||
return dag
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
import { generateRandomString } from '@sim/utils/random'
|
||||
|
||||
/**
|
||||
* Options for creating a mock edge.
|
||||
*/
|
||||
export interface EdgeFactoryOptions {
|
||||
id?: string
|
||||
source: string
|
||||
target: string
|
||||
sourceHandle?: string
|
||||
targetHandle?: string
|
||||
type?: string
|
||||
data?: Record<string, any>
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an edge ID from source and target.
|
||||
*/
|
||||
function generateEdgeId(source: string, target: string): string {
|
||||
return `${source}-${target}-${generateRandomString(4)}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a mock edge connecting two blocks.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // Simple edge
|
||||
* const edge = createEdge({ source: 'block-1', target: 'block-2' })
|
||||
*
|
||||
* // Edge with specific handles
|
||||
* const edge = createEdge({
|
||||
* source: 'condition-1',
|
||||
* target: 'block-2',
|
||||
* sourceHandle: 'condition-if'
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export function createEdge(options: EdgeFactoryOptions): any {
|
||||
return {
|
||||
id: options.id ?? generateEdgeId(options.source, options.target),
|
||||
source: options.source,
|
||||
target: options.target,
|
||||
sourceHandle: options.sourceHandle,
|
||||
targetHandle: options.targetHandle,
|
||||
type: options.type ?? 'default',
|
||||
data: options.data,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates multiple edges from a connection specification.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const edges = createEdges([
|
||||
* { source: 'start', target: 'agent' },
|
||||
* { source: 'agent', target: 'end' },
|
||||
* ])
|
||||
* ```
|
||||
*/
|
||||
export function createEdges(
|
||||
connections: Array<{
|
||||
source: string
|
||||
target: string
|
||||
sourceHandle?: string
|
||||
targetHandle?: string
|
||||
}>
|
||||
): any[] {
|
||||
return connections.map((conn) => createEdge(conn))
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a linear chain of edges connecting blocks in order.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // Creates edges: a->b, b->c, c->d
|
||||
* const edges = createLinearEdges(['a', 'b', 'c', 'd'])
|
||||
* ```
|
||||
*/
|
||||
export function createLinearEdges(blockIds: string[]): any[] {
|
||||
const edges: any[] = []
|
||||
for (let i = 0; i < blockIds.length - 1; i++) {
|
||||
edges.push(createEdge({ source: blockIds[i], target: blockIds[i + 1] }))
|
||||
}
|
||||
return edges
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import { generateRandomString } from '@sim/utils/random'
|
||||
import type { ExecutionContext } from '../types'
|
||||
|
||||
/**
|
||||
* Options for creating a mock execution context.
|
||||
*/
|
||||
export interface ExecutionContextFactoryOptions {
|
||||
workflowId?: string
|
||||
executionId?: string
|
||||
blockStates?: Map<string, any>
|
||||
executedBlocks?: Set<string>
|
||||
blockLogs?: any[]
|
||||
metadata?: {
|
||||
duration?: number
|
||||
startTime?: string
|
||||
endTime?: string
|
||||
}
|
||||
environmentVariables?: Record<string, string>
|
||||
workflowVariables?: Record<string, any>
|
||||
abortSignal?: AbortSignal
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a mock execution context for testing workflow execution.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const ctx = createExecutionContext({ workflowId: 'test-wf' })
|
||||
*
|
||||
* // With abort signal
|
||||
* const ctx = createExecutionContext({
|
||||
* workflowId: 'test-wf',
|
||||
* abortSignal: AbortSignal.abort(),
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export function createExecutionContext(
|
||||
options: ExecutionContextFactoryOptions = {}
|
||||
): ExecutionContext {
|
||||
return {
|
||||
workflowId: options.workflowId ?? 'test-workflow',
|
||||
executionId: options.executionId ?? `exec-${generateRandomString(8)}`,
|
||||
blockStates: options.blockStates ?? new Map(),
|
||||
executedBlocks: options.executedBlocks ?? new Set(),
|
||||
blockLogs: options.blockLogs ?? [],
|
||||
metadata: {
|
||||
duration: options.metadata?.duration ?? 0,
|
||||
startTime: options.metadata?.startTime ?? new Date().toISOString(),
|
||||
endTime: options.metadata?.endTime,
|
||||
},
|
||||
environmentVariables: options.environmentVariables ?? {},
|
||||
workflowVariables: options.workflowVariables ?? {},
|
||||
decisions: {
|
||||
router: new Map(),
|
||||
condition: new Map(),
|
||||
},
|
||||
loopExecutions: new Map(),
|
||||
completedLoops: new Set(),
|
||||
activeExecutionPath: new Set(),
|
||||
abortSignal: options.abortSignal,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an execution context with pre-populated block states.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const ctx = createExecutionContextWithStates({
|
||||
* 'block-1': { output: 'hello' },
|
||||
* 'block-2': { output: 'world' },
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export function createExecutionContextWithStates(
|
||||
blockStates: Record<string, any>,
|
||||
options: Omit<ExecutionContextFactoryOptions, 'blockStates'> = {}
|
||||
): ExecutionContext {
|
||||
const stateMap = new Map(Object.entries(blockStates))
|
||||
return createExecutionContext({
|
||||
...options,
|
||||
blockStates: stateMap,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an execution context that is already cancelled.
|
||||
*/
|
||||
export function createCancelledExecutionContext(
|
||||
options: Omit<ExecutionContextFactoryOptions, 'abortSignal'> = {}
|
||||
): ExecutionContext {
|
||||
return createExecutionContext({
|
||||
...options,
|
||||
abortSignal: AbortSignal.abort(),
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an execution context with a timeout.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const ctx = createTimedExecutionContext(5000) // 5 second timeout
|
||||
* ```
|
||||
*/
|
||||
export function createTimedExecutionContext(
|
||||
timeoutMs: number,
|
||||
options: Omit<ExecutionContextFactoryOptions, 'abortSignal'> = {}
|
||||
): ExecutionContext {
|
||||
return createExecutionContext({
|
||||
...options,
|
||||
abortSignal: AbortSignal.timeout(timeoutMs),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
/**
|
||||
* Factory functions for creating ExecutionContext test fixtures for executor tests.
|
||||
* This is the executor-specific context, different from the generic testing context.
|
||||
*/
|
||||
|
||||
import type {
|
||||
SerializedBlock,
|
||||
SerializedConnection,
|
||||
SerializedWorkflow,
|
||||
} from './serialized-block.factory'
|
||||
|
||||
/**
|
||||
* Block state in execution context.
|
||||
*/
|
||||
export interface ExecutorBlockState {
|
||||
output: Record<string, any>
|
||||
executed: boolean
|
||||
executionTime: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Execution context for executor tests.
|
||||
*/
|
||||
export interface ExecutorContext {
|
||||
workflowId: string
|
||||
workspaceId?: string
|
||||
executionId?: string
|
||||
userId?: string
|
||||
blockStates: Map<string, ExecutorBlockState>
|
||||
executedBlocks: Set<string>
|
||||
blockLogs: any[]
|
||||
metadata: {
|
||||
duration: number
|
||||
startTime?: string
|
||||
endTime?: string
|
||||
}
|
||||
environmentVariables: Record<string, string>
|
||||
workflowVariables?: Record<string, any>
|
||||
decisions: {
|
||||
router: Map<string, string>
|
||||
condition: Map<string, string>
|
||||
}
|
||||
loopExecutions: Map<string, any>
|
||||
completedLoops: Set<string>
|
||||
activeExecutionPath: Set<string>
|
||||
workflow?: SerializedWorkflow
|
||||
currentVirtualBlockId?: string
|
||||
abortSignal?: AbortSignal
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for creating an executor context.
|
||||
*/
|
||||
export interface ExecutorContextFactoryOptions {
|
||||
workflowId?: string
|
||||
workspaceId?: string
|
||||
executionId?: string
|
||||
userId?: string
|
||||
blockStates?: Map<string, ExecutorBlockState> | Record<string, ExecutorBlockState>
|
||||
executedBlocks?: Set<string> | string[]
|
||||
blockLogs?: any[]
|
||||
metadata?: {
|
||||
duration?: number
|
||||
startTime?: string
|
||||
endTime?: string
|
||||
}
|
||||
environmentVariables?: Record<string, string>
|
||||
workflowVariables?: Record<string, any>
|
||||
workflow?: SerializedWorkflow
|
||||
currentVirtualBlockId?: string
|
||||
abortSignal?: AbortSignal
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an executor context with sensible defaults.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const ctx = createExecutorContext({ workflowId: 'test-wf' })
|
||||
*
|
||||
* // With pre-populated block states
|
||||
* const ctx = createExecutorContext({
|
||||
* blockStates: {
|
||||
* 'block-1': { output: { value: 10 }, executed: true, executionTime: 100 }
|
||||
* }
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export function createExecutorContext(
|
||||
options: ExecutorContextFactoryOptions = {}
|
||||
): ExecutorContext {
|
||||
let blockStates: Map<string, ExecutorBlockState>
|
||||
if (options.blockStates instanceof Map) {
|
||||
blockStates = options.blockStates
|
||||
} else if (options.blockStates) {
|
||||
blockStates = new Map(Object.entries(options.blockStates))
|
||||
} else {
|
||||
blockStates = new Map()
|
||||
}
|
||||
|
||||
let executedBlocks: Set<string>
|
||||
if (options.executedBlocks instanceof Set) {
|
||||
executedBlocks = options.executedBlocks
|
||||
} else if (Array.isArray(options.executedBlocks)) {
|
||||
executedBlocks = new Set(options.executedBlocks)
|
||||
} else {
|
||||
executedBlocks = new Set()
|
||||
}
|
||||
|
||||
return {
|
||||
workflowId: options.workflowId ?? 'test-workflow-id',
|
||||
workspaceId: options.workspaceId ?? 'test-workspace-id',
|
||||
executionId: options.executionId,
|
||||
userId: options.userId,
|
||||
blockStates,
|
||||
executedBlocks,
|
||||
blockLogs: options.blockLogs ?? [],
|
||||
metadata: {
|
||||
duration: options.metadata?.duration ?? 0,
|
||||
startTime: options.metadata?.startTime,
|
||||
endTime: options.metadata?.endTime,
|
||||
},
|
||||
environmentVariables: options.environmentVariables ?? {},
|
||||
workflowVariables: options.workflowVariables,
|
||||
decisions: {
|
||||
router: new Map(),
|
||||
condition: new Map(),
|
||||
},
|
||||
loopExecutions: new Map(),
|
||||
completedLoops: new Set(),
|
||||
activeExecutionPath: new Set(),
|
||||
workflow: options.workflow,
|
||||
currentVirtualBlockId: options.currentVirtualBlockId,
|
||||
abortSignal: options.abortSignal,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an executor context with pre-executed blocks.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const ctx = createExecutorContextWithBlocks({
|
||||
* 'source-block': { value: 10, text: 'hello' },
|
||||
* 'other-block': { result: true }
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export function createExecutorContextWithBlocks(
|
||||
blockOutputs: Record<string, Record<string, any>>,
|
||||
options: Omit<ExecutorContextFactoryOptions, 'blockStates' | 'executedBlocks'> = {}
|
||||
): ExecutorContext {
|
||||
const blockStates = new Map<string, ExecutorBlockState>()
|
||||
const executedBlocks = new Set<string>()
|
||||
|
||||
for (const [blockId, output] of Object.entries(blockOutputs)) {
|
||||
blockStates.set(blockId, {
|
||||
output,
|
||||
executed: true,
|
||||
executionTime: 100,
|
||||
})
|
||||
executedBlocks.add(blockId)
|
||||
}
|
||||
|
||||
return createExecutorContext({
|
||||
...options,
|
||||
blockStates,
|
||||
executedBlocks,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a block state to an existing context.
|
||||
* Returns the context for chaining.
|
||||
*/
|
||||
export function addBlockState(
|
||||
ctx: ExecutorContext,
|
||||
blockId: string,
|
||||
output: Record<string, any>,
|
||||
executionTime = 100
|
||||
): ExecutorContext {
|
||||
;(ctx.blockStates as Map<string, ExecutorBlockState>).set(blockId, {
|
||||
output,
|
||||
executed: true,
|
||||
executionTime,
|
||||
})
|
||||
;(ctx.executedBlocks as Set<string>).add(blockId)
|
||||
return ctx
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a minimal workflow for context.
|
||||
*/
|
||||
export function createMinimalWorkflow(
|
||||
blocks: SerializedBlock[],
|
||||
connections: SerializedConnection[] = []
|
||||
): SerializedWorkflow {
|
||||
return {
|
||||
version: '1.0',
|
||||
blocks,
|
||||
connections,
|
||||
loops: {},
|
||||
parallels: {},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
const URL_SAFE_ALPHABET = 'useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict'
|
||||
|
||||
/**
|
||||
* Generates a short, URL-safe random ID for test fixtures.
|
||||
*
|
||||
* Uses `crypto.getRandomValues()` instead of `crypto.randomUUID()` for
|
||||
* consistency with the app-level `generateShortId` utility.
|
||||
*/
|
||||
export function shortId(size = 8): string {
|
||||
const bytes = new Uint8Array(size)
|
||||
crypto.getRandomValues(bytes)
|
||||
|
||||
let id = ''
|
||||
for (let i = 0; i < size; i++) {
|
||||
id += URL_SAFE_ALPHABET[bytes[i] & 63]
|
||||
}
|
||||
return id
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
/**
|
||||
* Factory functions for creating test fixtures.
|
||||
*
|
||||
* Use these to create mock data with sensible defaults.
|
||||
* All functions allow overriding any field.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import {
|
||||
* createBlock,
|
||||
* createStarterBlock,
|
||||
* createAgentBlock,
|
||||
* createLinearWorkflow,
|
||||
* createExecutionContext,
|
||||
* } from '@sim/testing/factories'
|
||||
*
|
||||
* // Create a simple workflow
|
||||
* const workflow = createLinearWorkflow(3)
|
||||
*
|
||||
* // Create a specific block
|
||||
* const agent = createAgentBlock({ id: 'my-agent', position: { x: 100, y: 200 } })
|
||||
*
|
||||
* // Create execution context
|
||||
* const ctx = createExecutionContext({ workflowId: 'test' })
|
||||
* ```
|
||||
*/
|
||||
|
||||
// Block factories
|
||||
export {
|
||||
type BlockFactoryOptions,
|
||||
createAgentBlock,
|
||||
createApiBlock,
|
||||
createBlock,
|
||||
createConditionBlock,
|
||||
createFunctionBlock,
|
||||
createKnowledgeBlock,
|
||||
createLoopBlock,
|
||||
createParallelBlock,
|
||||
createResponseBlock,
|
||||
createRouterBlock,
|
||||
createStarterBlock,
|
||||
createWebhookBlock,
|
||||
} from './block.factory'
|
||||
// DAG factories (for executor DAG tests)
|
||||
export {
|
||||
addNodeToDAG,
|
||||
connectDAGNodes,
|
||||
createDAG,
|
||||
createDAGFromNodes,
|
||||
createDAGNode,
|
||||
createLinearDAG,
|
||||
type DAG,
|
||||
type DAGEdge,
|
||||
type DAGNode,
|
||||
type DAGNodeFactoryOptions,
|
||||
} from './dag.factory'
|
||||
// Edge factories
|
||||
export { createEdge, createEdges, createLinearEdges, type EdgeFactoryOptions } from './edge.factory'
|
||||
// Execution factories
|
||||
export {
|
||||
createCancelledExecutionContext,
|
||||
createExecutionContext,
|
||||
createExecutionContextWithStates,
|
||||
createTimedExecutionContext,
|
||||
type ExecutionContextFactoryOptions,
|
||||
} from './execution.factory'
|
||||
// Executor context factories (for executor tests)
|
||||
export {
|
||||
addBlockState,
|
||||
createExecutorContext,
|
||||
createExecutorContextWithBlocks,
|
||||
createMinimalWorkflow,
|
||||
type ExecutorBlockState,
|
||||
type ExecutorContext,
|
||||
type ExecutorContextFactoryOptions,
|
||||
} from './executor-context.factory'
|
||||
// Permission factories
|
||||
export {
|
||||
createAdminPermission,
|
||||
createEncryptedApiKey,
|
||||
createLegacyApiKey,
|
||||
createPermission,
|
||||
createReadPermission,
|
||||
createSession,
|
||||
createWorkflowAccessContext,
|
||||
createWorkflowRecord,
|
||||
createWorkspaceRecord,
|
||||
createWritePermission,
|
||||
type EntityType,
|
||||
type MockSession,
|
||||
type Permission,
|
||||
type PermissionFactoryOptions,
|
||||
type PermissionType,
|
||||
ROLE_ALLOWED_OPERATIONS,
|
||||
type SessionFactoryOptions,
|
||||
SOCKET_OPERATIONS,
|
||||
type SocketOperation,
|
||||
type WorkflowAccessContext,
|
||||
type WorkflowRecord,
|
||||
type WorkflowRecordFactoryOptions,
|
||||
type WorkspaceRecord,
|
||||
type WorkspaceRecordFactoryOptions,
|
||||
} from './permission.factory'
|
||||
// Serialized block factories (for executor tests)
|
||||
export {
|
||||
createSerializedAgentBlock,
|
||||
createSerializedBlock,
|
||||
createSerializedConditionBlock,
|
||||
createSerializedConnection,
|
||||
createSerializedEvaluatorBlock,
|
||||
createSerializedFunctionBlock,
|
||||
createSerializedRouterBlock,
|
||||
createSerializedStarterBlock,
|
||||
createSerializedWorkflow,
|
||||
resetSerializedBlockCounter,
|
||||
type SerializedBlock,
|
||||
type SerializedBlockFactoryOptions,
|
||||
type SerializedConnection,
|
||||
type SerializedWorkflow,
|
||||
} from './serialized-block.factory'
|
||||
// Tool mock responses
|
||||
export {
|
||||
mockDriveResponses,
|
||||
mockGitHubResponses,
|
||||
mockGmailResponses,
|
||||
mockHttpResponses,
|
||||
mockPineconeResponses,
|
||||
mockSerperResponses,
|
||||
mockSheetsResponses,
|
||||
mockSlackResponses,
|
||||
mockSupabaseResponses,
|
||||
mockTavilyResponses,
|
||||
} from './tool-responses.factory'
|
||||
// Undo/redo operation factories
|
||||
export {
|
||||
type BaseOperation,
|
||||
type BatchAddBlocksOperation,
|
||||
type BatchAddEdgesOperation,
|
||||
type BatchMoveBlocksOperation,
|
||||
type BatchRemoveBlocksOperation,
|
||||
type BatchRemoveEdgesOperation,
|
||||
type BatchUpdateParentOperation,
|
||||
createAddBlockEntry,
|
||||
createAddEdgeEntry,
|
||||
createBatchRemoveEdgesEntry,
|
||||
createBatchUpdateParentEntry,
|
||||
createMoveBlockEntry,
|
||||
createRemoveBlockEntry,
|
||||
createUpdateParentEntry,
|
||||
type Operation,
|
||||
type OperationEntry,
|
||||
type OperationType,
|
||||
type UpdateParentOperation,
|
||||
} from './undo-redo.factory'
|
||||
export {
|
||||
createUser,
|
||||
createUserWithWorkspace,
|
||||
createWorkflow,
|
||||
createWorkspace,
|
||||
type UserFactoryOptions,
|
||||
type WorkflowObjectFactoryOptions,
|
||||
type WorkspaceFactoryOptions,
|
||||
} from './user.factory'
|
||||
export {
|
||||
createAgentWithToolsWorkflowState,
|
||||
createBranchingWorkflow,
|
||||
createComplexWorkflowState,
|
||||
createConditionalWorkflowState,
|
||||
createInvalidSerializedWorkflow,
|
||||
createInvalidWorkflowState,
|
||||
createLinearWorkflow,
|
||||
createLoopWorkflow,
|
||||
createLoopWorkflowState,
|
||||
createMinimalWorkflowState,
|
||||
createMissingMetadataWorkflow,
|
||||
createParallelWorkflow,
|
||||
createWorkflowState,
|
||||
type WorkflowFactoryOptions,
|
||||
type WorkflowStateFixture,
|
||||
} from './workflow.factory'
|
||||
@@ -0,0 +1,346 @@
|
||||
import { shortId } from './id'
|
||||
|
||||
/**
|
||||
* Permission types in order of access level (highest to lowest).
|
||||
*/
|
||||
export type PermissionType = 'admin' | 'write' | 'read'
|
||||
|
||||
/**
|
||||
* Entity types that can have permissions.
|
||||
*/
|
||||
export type EntityType = 'workspace' | 'workflow' | 'organization'
|
||||
|
||||
/**
|
||||
* Permission record as stored in the database.
|
||||
*/
|
||||
export interface Permission {
|
||||
id: string
|
||||
userId: string
|
||||
entityType: EntityType
|
||||
entityId: string
|
||||
permissionType: PermissionType
|
||||
createdAt: Date
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for creating a permission.
|
||||
*/
|
||||
export interface PermissionFactoryOptions {
|
||||
id?: string
|
||||
userId?: string
|
||||
entityType?: EntityType
|
||||
entityId?: string
|
||||
permissionType?: PermissionType
|
||||
createdAt?: Date
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a mock permission record.
|
||||
*/
|
||||
export function createPermission(options: PermissionFactoryOptions = {}): Permission {
|
||||
return {
|
||||
id: options.id ?? shortId(8),
|
||||
userId: options.userId ?? `user-${shortId(6)}`,
|
||||
entityType: options.entityType ?? 'workspace',
|
||||
entityId: options.entityId ?? `ws-${shortId(6)}`,
|
||||
permissionType: options.permissionType ?? 'read',
|
||||
createdAt: options.createdAt ?? new Date(),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a workspace admin permission.
|
||||
*/
|
||||
export function createAdminPermission(
|
||||
userId: string,
|
||||
workspaceId: string,
|
||||
options: Partial<PermissionFactoryOptions> = {}
|
||||
): Permission {
|
||||
return createPermission({
|
||||
userId,
|
||||
entityType: 'workspace',
|
||||
entityId: workspaceId,
|
||||
permissionType: 'admin',
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a workspace write permission.
|
||||
*/
|
||||
export function createWritePermission(
|
||||
userId: string,
|
||||
workspaceId: string,
|
||||
options: Partial<PermissionFactoryOptions> = {}
|
||||
): Permission {
|
||||
return createPermission({
|
||||
userId,
|
||||
entityType: 'workspace',
|
||||
entityId: workspaceId,
|
||||
permissionType: 'write',
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a workspace read permission.
|
||||
*/
|
||||
export function createReadPermission(
|
||||
userId: string,
|
||||
workspaceId: string,
|
||||
options: Partial<PermissionFactoryOptions> = {}
|
||||
): Permission {
|
||||
return createPermission({
|
||||
userId,
|
||||
entityType: 'workspace',
|
||||
entityId: workspaceId,
|
||||
permissionType: 'read',
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Workspace record for testing.
|
||||
*/
|
||||
export interface WorkspaceRecord {
|
||||
id: string
|
||||
name: string
|
||||
ownerId: string
|
||||
billedAccountUserId?: string
|
||||
createdAt: Date
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for creating a workspace.
|
||||
*/
|
||||
export interface WorkspaceRecordFactoryOptions {
|
||||
id?: string
|
||||
name?: string
|
||||
ownerId?: string
|
||||
billedAccountUserId?: string
|
||||
createdAt?: Date
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a mock workspace record.
|
||||
*/
|
||||
export function createWorkspaceRecord(
|
||||
options: WorkspaceRecordFactoryOptions = {}
|
||||
): WorkspaceRecord {
|
||||
const id = options.id ?? `ws-${shortId(6)}`
|
||||
const ownerId = options.ownerId ?? `user-${shortId(6)}`
|
||||
return {
|
||||
id,
|
||||
name: options.name ?? `Workspace ${id}`,
|
||||
ownerId,
|
||||
billedAccountUserId: options.billedAccountUserId ?? ownerId,
|
||||
createdAt: options.createdAt ?? new Date(),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Workflow record for testing.
|
||||
*/
|
||||
export interface WorkflowRecord {
|
||||
id: string
|
||||
name: string
|
||||
userId: string
|
||||
workspaceId: string | null
|
||||
state: string
|
||||
isDeployed: boolean
|
||||
runCount: number
|
||||
createdAt: Date
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for creating a workflow record.
|
||||
*/
|
||||
export interface WorkflowRecordFactoryOptions {
|
||||
id?: string
|
||||
name?: string
|
||||
userId?: string
|
||||
workspaceId?: string | null
|
||||
state?: string
|
||||
isDeployed?: boolean
|
||||
runCount?: number
|
||||
createdAt?: Date
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a mock workflow database record.
|
||||
*/
|
||||
export function createWorkflowRecord(options: WorkflowRecordFactoryOptions = {}): WorkflowRecord {
|
||||
const id = options.id ?? `wf-${shortId(6)}`
|
||||
return {
|
||||
id,
|
||||
name: options.name ?? `Workflow ${id}`,
|
||||
userId: options.userId ?? `user-${shortId(6)}`,
|
||||
workspaceId: options.workspaceId ?? null,
|
||||
state: options.state ?? '{}',
|
||||
isDeployed: options.isDeployed ?? false,
|
||||
runCount: options.runCount ?? 0,
|
||||
createdAt: options.createdAt ?? new Date(),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Session object for testing.
|
||||
*/
|
||||
export interface MockSession {
|
||||
user: {
|
||||
id: string
|
||||
email: string
|
||||
name?: string
|
||||
}
|
||||
expiresAt: Date
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for creating a session.
|
||||
*/
|
||||
export interface SessionFactoryOptions {
|
||||
userId?: string
|
||||
email?: string
|
||||
name?: string
|
||||
expiresAt?: Date
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a mock session object.
|
||||
*/
|
||||
export function createSession(options: SessionFactoryOptions = {}): MockSession {
|
||||
const userId = options.userId ?? `user-${shortId(6)}`
|
||||
return {
|
||||
user: {
|
||||
id: userId,
|
||||
email: options.email ?? `${userId}@test.com`,
|
||||
name: options.name,
|
||||
},
|
||||
expiresAt: options.expiresAt ?? new Date(Date.now() + 24 * 60 * 60 * 1000),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Workflow access context for testing.
|
||||
*/
|
||||
export interface WorkflowAccessContext {
|
||||
workflow: WorkflowRecord
|
||||
workspaceOwnerId: string | null
|
||||
workspacePermission: PermissionType | null
|
||||
isOwner: boolean
|
||||
isWorkspaceOwner: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a mock workflow access context.
|
||||
*/
|
||||
export function createWorkflowAccessContext(options: {
|
||||
workflow: WorkflowRecord
|
||||
workspaceOwnerId?: string | null
|
||||
workspacePermission?: PermissionType | null
|
||||
userId?: string
|
||||
}): WorkflowAccessContext {
|
||||
const { workflow, workspaceOwnerId = null, workspacePermission = null, userId } = options
|
||||
|
||||
return {
|
||||
workflow,
|
||||
workspaceOwnerId,
|
||||
workspacePermission,
|
||||
isOwner: userId ? workflow.userId === userId : false,
|
||||
isWorkspaceOwner: userId && workspaceOwnerId ? workspaceOwnerId === userId : false,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Socket operations
|
||||
*/
|
||||
const BLOCK_OPERATIONS = {
|
||||
UPDATE_POSITION: 'update-position',
|
||||
UPDATE_NAME: 'update-name',
|
||||
TOGGLE_ENABLED: 'toggle-enabled',
|
||||
UPDATE_PARENT: 'update-parent',
|
||||
UPDATE_ADVANCED_MODE: 'update-advanced-mode',
|
||||
UPDATE_CANONICAL_MODE: 'update-canonical-mode',
|
||||
TOGGLE_HANDLES: 'toggle-handles',
|
||||
} as const
|
||||
|
||||
const BLOCKS_OPERATIONS = {
|
||||
BATCH_UPDATE_POSITIONS: 'batch-update-positions',
|
||||
BATCH_ADD_BLOCKS: 'batch-add-blocks',
|
||||
BATCH_REMOVE_BLOCKS: 'batch-remove-blocks',
|
||||
BATCH_TOGGLE_ENABLED: 'batch-toggle-enabled',
|
||||
BATCH_TOGGLE_HANDLES: 'batch-toggle-handles',
|
||||
BATCH_UPDATE_PARENT: 'batch-update-parent',
|
||||
} as const
|
||||
|
||||
const EDGE_OPERATIONS = {
|
||||
ADD: 'add',
|
||||
REMOVE: 'remove',
|
||||
} as const
|
||||
|
||||
const EDGES_OPERATIONS = {
|
||||
BATCH_ADD_EDGES: 'batch-add-edges',
|
||||
BATCH_REMOVE_EDGES: 'batch-remove-edges',
|
||||
} as const
|
||||
|
||||
const SUBFLOW_OPERATIONS = {
|
||||
UPDATE: 'update',
|
||||
} as const
|
||||
|
||||
const WORKFLOW_OPERATIONS = {
|
||||
REPLACE_STATE: 'replace-state',
|
||||
} as const
|
||||
|
||||
/**
|
||||
* All socket operations that require permission checks.
|
||||
*/
|
||||
export const SOCKET_OPERATIONS = [
|
||||
...Object.values(BLOCK_OPERATIONS),
|
||||
...Object.values(BLOCKS_OPERATIONS),
|
||||
...Object.values(EDGE_OPERATIONS),
|
||||
...Object.values(EDGES_OPERATIONS),
|
||||
...Object.values(SUBFLOW_OPERATIONS),
|
||||
...Object.values(WORKFLOW_OPERATIONS),
|
||||
] as const
|
||||
|
||||
export type SocketOperation = (typeof SOCKET_OPERATIONS)[number]
|
||||
|
||||
/**
|
||||
* Operations allowed for each role.
|
||||
*/
|
||||
export const ROLE_ALLOWED_OPERATIONS: Record<PermissionType, readonly SocketOperation[]> = {
|
||||
admin: SOCKET_OPERATIONS,
|
||||
write: SOCKET_OPERATIONS,
|
||||
read: [BLOCK_OPERATIONS.UPDATE_POSITION, BLOCKS_OPERATIONS.BATCH_UPDATE_POSITIONS],
|
||||
}
|
||||
|
||||
/**
|
||||
* API key formats for testing.
|
||||
*/
|
||||
export interface ApiKeyTestData {
|
||||
plainKey: string
|
||||
encryptedStorage: string
|
||||
last4: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates test API key data.
|
||||
*/
|
||||
export function createLegacyApiKey(): { key: string; prefix: string } {
|
||||
const random = shortId(24)
|
||||
return {
|
||||
key: `sim_${random}`,
|
||||
prefix: 'sim_',
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates test encrypted format API key data.
|
||||
*/
|
||||
export function createEncryptedApiKey(): { key: string; prefix: string } {
|
||||
const random = shortId(24)
|
||||
return {
|
||||
key: `sk-sim-${random}`,
|
||||
prefix: 'sk-sim-',
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
/**
|
||||
* Factory functions for creating SerializedBlock test fixtures.
|
||||
* These are used in executor tests where blocks are in their serialized form.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Serialized block structure used in executor tests.
|
||||
*/
|
||||
export interface SerializedBlock {
|
||||
id: string
|
||||
position: { x: number; y: number }
|
||||
config: {
|
||||
tool: string
|
||||
params: Record<string, any>
|
||||
}
|
||||
inputs: Record<string, any>
|
||||
outputs: Record<string, any>
|
||||
metadata?: {
|
||||
id: string
|
||||
name?: string
|
||||
description?: string
|
||||
category?: string
|
||||
icon?: string
|
||||
color?: string
|
||||
}
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialized connection structure.
|
||||
*/
|
||||
export interface SerializedConnection {
|
||||
source: string
|
||||
target: string
|
||||
sourceHandle?: string
|
||||
targetHandle?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialized workflow structure.
|
||||
*/
|
||||
export interface SerializedWorkflow {
|
||||
version: string
|
||||
blocks: SerializedBlock[]
|
||||
connections: SerializedConnection[]
|
||||
loops: Record<string, any>
|
||||
parallels?: Record<string, any>
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for creating a serialized block.
|
||||
*/
|
||||
export interface SerializedBlockFactoryOptions {
|
||||
id?: string
|
||||
type?: string
|
||||
name?: string
|
||||
description?: string
|
||||
position?: { x: number; y: number }
|
||||
tool?: string
|
||||
params?: Record<string, any>
|
||||
inputs?: Record<string, any>
|
||||
outputs?: Record<string, any>
|
||||
enabled?: boolean
|
||||
}
|
||||
|
||||
let blockCounter = 0
|
||||
|
||||
/**
|
||||
* Generates a unique block ID.
|
||||
*/
|
||||
function generateBlockId(prefix = 'block'): string {
|
||||
return `${prefix}-${++blockCounter}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the block counter (useful for deterministic tests).
|
||||
*/
|
||||
export function resetSerializedBlockCounter(): void {
|
||||
blockCounter = 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a serialized block with sensible defaults.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const block = createSerializedBlock({ type: 'agent', name: 'My Agent' })
|
||||
* ```
|
||||
*/
|
||||
export function createSerializedBlock(
|
||||
options: SerializedBlockFactoryOptions = {}
|
||||
): SerializedBlock {
|
||||
const type = options.type ?? 'function'
|
||||
const id = options.id ?? generateBlockId(type)
|
||||
|
||||
return {
|
||||
id,
|
||||
position: options.position ?? { x: 0, y: 0 },
|
||||
config: {
|
||||
tool: options.tool ?? type,
|
||||
params: options.params ?? {},
|
||||
},
|
||||
inputs: options.inputs ?? {},
|
||||
outputs: options.outputs ?? {},
|
||||
metadata: {
|
||||
id: type,
|
||||
name: options.name ?? `Block ${id}`,
|
||||
description: options.description,
|
||||
},
|
||||
enabled: options.enabled ?? true,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a serialized condition block.
|
||||
*/
|
||||
export function createSerializedConditionBlock(
|
||||
options: Omit<SerializedBlockFactoryOptions, 'type'> = {}
|
||||
): SerializedBlock {
|
||||
return createSerializedBlock({
|
||||
...options,
|
||||
type: 'condition',
|
||||
name: options.name ?? 'Condition',
|
||||
inputs: options.inputs ?? { conditions: 'json' },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a serialized router block.
|
||||
*/
|
||||
export function createSerializedRouterBlock(
|
||||
options: Omit<SerializedBlockFactoryOptions, 'type'> = {}
|
||||
): SerializedBlock {
|
||||
return createSerializedBlock({
|
||||
...options,
|
||||
type: 'router',
|
||||
name: options.name ?? 'Router',
|
||||
inputs: options.inputs ?? { prompt: 'string', model: 'string' },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a serialized evaluator block.
|
||||
*/
|
||||
export function createSerializedEvaluatorBlock(
|
||||
options: Omit<SerializedBlockFactoryOptions, 'type'> = {}
|
||||
): SerializedBlock {
|
||||
return createSerializedBlock({
|
||||
...options,
|
||||
type: 'evaluator',
|
||||
name: options.name ?? 'Evaluator',
|
||||
inputs: options.inputs ?? {
|
||||
content: 'string',
|
||||
metrics: 'json',
|
||||
model: 'string',
|
||||
temperature: 'number',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a serialized agent block.
|
||||
*/
|
||||
export function createSerializedAgentBlock(
|
||||
options: Omit<SerializedBlockFactoryOptions, 'type'> = {}
|
||||
): SerializedBlock {
|
||||
return createSerializedBlock({
|
||||
...options,
|
||||
type: 'agent',
|
||||
name: options.name ?? 'Agent',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a serialized function block.
|
||||
*/
|
||||
export function createSerializedFunctionBlock(
|
||||
options: Omit<SerializedBlockFactoryOptions, 'type'> = {}
|
||||
): SerializedBlock {
|
||||
return createSerializedBlock({
|
||||
...options,
|
||||
type: 'function',
|
||||
name: options.name ?? 'Function',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a serialized starter block.
|
||||
*/
|
||||
export function createSerializedStarterBlock(
|
||||
options: Omit<SerializedBlockFactoryOptions, 'type'> = {}
|
||||
): SerializedBlock {
|
||||
return createSerializedBlock({
|
||||
...options,
|
||||
type: 'starter',
|
||||
name: options.name ?? 'Start',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a simple serialized connection.
|
||||
*/
|
||||
export function createSerializedConnection(
|
||||
source: string,
|
||||
target: string,
|
||||
sourceHandle?: string
|
||||
): SerializedConnection {
|
||||
return {
|
||||
source,
|
||||
target,
|
||||
sourceHandle,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a serialized workflow with the given blocks and connections.
|
||||
*/
|
||||
export function createSerializedWorkflow(
|
||||
blocks: SerializedBlock[],
|
||||
connections: SerializedConnection[] = []
|
||||
): SerializedWorkflow {
|
||||
return {
|
||||
version: '1.0',
|
||||
blocks,
|
||||
connections,
|
||||
loops: {},
|
||||
parallels: {},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { createTableColumn } from './table.factory'
|
||||
|
||||
describe('table factory', () => {
|
||||
it('generates default column names that match table naming rules', () => {
|
||||
const generatedNames = Array.from({ length: 100 }, () => createTableColumn().name)
|
||||
|
||||
for (const name of generatedNames) {
|
||||
expect(name).toMatch(/^[a-z_][a-z0-9_]*$/)
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,62 @@
|
||||
import { generateShortId } from '@sim/utils/id'
|
||||
|
||||
const COLUMN_SUFFIX_ALPHABET = 'abcdefghijklmnopqrstuvwxyz0123456789_'
|
||||
|
||||
export type TableColumnType = 'string' | 'number' | 'boolean' | 'date' | 'json'
|
||||
|
||||
export interface TableColumnFixture {
|
||||
name: string
|
||||
type: TableColumnType
|
||||
required?: boolean
|
||||
unique?: boolean
|
||||
}
|
||||
|
||||
export interface TableRowFixture {
|
||||
id: string
|
||||
data: Record<string, unknown>
|
||||
position: number
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export interface TableColumnFactoryOptions {
|
||||
name?: string
|
||||
type?: TableColumnType
|
||||
required?: boolean
|
||||
unique?: boolean
|
||||
}
|
||||
|
||||
export interface TableRowFactoryOptions {
|
||||
id?: string
|
||||
data?: Record<string, unknown>
|
||||
position?: number
|
||||
createdAt?: string
|
||||
updatedAt?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a table column fixture with sensible defaults.
|
||||
*/
|
||||
export function createTableColumn(options: TableColumnFactoryOptions = {}): TableColumnFixture {
|
||||
return {
|
||||
name: options.name ?? `column_${generateShortId(6, COLUMN_SUFFIX_ALPHABET)}`,
|
||||
type: options.type ?? 'string',
|
||||
required: options.required,
|
||||
unique: options.unique,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a table row fixture with sensible defaults.
|
||||
*/
|
||||
export function createTableRow(options: TableRowFactoryOptions = {}): TableRowFixture {
|
||||
const timestamp = new Date().toISOString()
|
||||
|
||||
return {
|
||||
id: options.id ?? `row_${generateShortId(8)}`,
|
||||
data: options.data ?? {},
|
||||
position: options.position ?? 0,
|
||||
createdAt: options.createdAt ?? timestamp,
|
||||
updatedAt: options.updatedAt ?? timestamp,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
/**
|
||||
* Mock Data for Tool Tests
|
||||
*
|
||||
* This file contains mock data samples to be used in tool unit tests.
|
||||
*/
|
||||
|
||||
import { randomFloat } from '@sim/utils/random'
|
||||
|
||||
/**
|
||||
* HTTP Request mock responses for different scenarios.
|
||||
*/
|
||||
export const mockHttpResponses = {
|
||||
simple: {
|
||||
data: { message: 'Success', status: 'ok' },
|
||||
status: 200,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
},
|
||||
error: {
|
||||
error: { message: 'Bad Request', code: 400 },
|
||||
status: 400,
|
||||
},
|
||||
notFound: {
|
||||
error: { message: 'Not Found', code: 404 },
|
||||
status: 404,
|
||||
},
|
||||
unauthorized: {
|
||||
error: { message: 'Unauthorized', code: 401 },
|
||||
status: 401,
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Gmail Mock Data
|
||||
*/
|
||||
export const mockGmailResponses = {
|
||||
messageList: {
|
||||
messages: [
|
||||
{ id: 'msg1', threadId: 'thread1' },
|
||||
{ id: 'msg2', threadId: 'thread2' },
|
||||
{ id: 'msg3', threadId: 'thread3' },
|
||||
],
|
||||
nextPageToken: 'token123',
|
||||
},
|
||||
emptyList: {
|
||||
messages: [],
|
||||
resultSizeEstimate: 0,
|
||||
},
|
||||
singleMessage: {
|
||||
id: 'msg1',
|
||||
threadId: 'thread1',
|
||||
labelIds: ['INBOX', 'UNREAD'],
|
||||
snippet: 'This is a snippet preview of the email...',
|
||||
payload: {
|
||||
headers: [
|
||||
{ name: 'From', value: 'sender@example.com' },
|
||||
{ name: 'To', value: 'recipient@example.com' },
|
||||
{ name: 'Subject', value: 'Test Email Subject' },
|
||||
{ name: 'Date', value: 'Mon, 15 Mar 2025 10:30:00 -0800' },
|
||||
],
|
||||
mimeType: 'multipart/alternative',
|
||||
parts: [
|
||||
{
|
||||
mimeType: 'text/plain',
|
||||
body: {
|
||||
data: Buffer.from('This is the plain text content of the email').toString('base64'),
|
||||
},
|
||||
},
|
||||
{
|
||||
mimeType: 'text/html',
|
||||
body: {
|
||||
data: Buffer.from('<div>This is the HTML content of the email</div>').toString(
|
||||
'base64'
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Google Drive Mock Data
|
||||
*/
|
||||
export const mockDriveResponses = {
|
||||
fileList: {
|
||||
files: [
|
||||
{ id: 'file1', name: 'Document1.docx', mimeType: 'application/vnd.google-apps.document' },
|
||||
{
|
||||
id: 'file2',
|
||||
name: 'Spreadsheet.xlsx',
|
||||
mimeType: 'application/vnd.google-apps.spreadsheet',
|
||||
},
|
||||
{
|
||||
id: 'file3',
|
||||
name: 'Presentation.pptx',
|
||||
mimeType: 'application/vnd.google-apps.presentation',
|
||||
},
|
||||
],
|
||||
nextPageToken: 'drive-page-token',
|
||||
},
|
||||
emptyFileList: {
|
||||
files: [],
|
||||
},
|
||||
fileMetadata: {
|
||||
id: 'file1',
|
||||
name: 'Document1.docx',
|
||||
mimeType: 'application/vnd.google-apps.document',
|
||||
webViewLink: 'https://docs.google.com/document/d/123/edit',
|
||||
createdTime: '2025-03-15T12:00:00Z',
|
||||
modifiedTime: '2025-03-16T10:15:00Z',
|
||||
owners: [{ displayName: 'Test User', emailAddress: 'user@example.com' }],
|
||||
size: '12345',
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Google Sheets Mock Data
|
||||
*/
|
||||
export const mockSheetsResponses = {
|
||||
rangeData: {
|
||||
range: 'Sheet1!A1:D5',
|
||||
majorDimension: 'ROWS',
|
||||
values: [
|
||||
['Header1', 'Header2', 'Header3', 'Header4'],
|
||||
['Row1Col1', 'Row1Col2', 'Row1Col3', 'Row1Col4'],
|
||||
['Row2Col1', 'Row2Col2', 'Row2Col3', 'Row2Col4'],
|
||||
['Row3Col1', 'Row3Col2', 'Row3Col3', 'Row3Col4'],
|
||||
['Row4Col1', 'Row4Col2', 'Row4Col3', 'Row4Col4'],
|
||||
],
|
||||
},
|
||||
emptyRange: {
|
||||
range: 'Sheet1!A1:D5',
|
||||
majorDimension: 'ROWS',
|
||||
values: [],
|
||||
},
|
||||
updateResponse: {
|
||||
spreadsheetId: 'spreadsheet123',
|
||||
updatedRange: 'Sheet1!A1:D5',
|
||||
updatedRows: 5,
|
||||
updatedColumns: 4,
|
||||
updatedCells: 20,
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Pinecone Mock Data
|
||||
*/
|
||||
export const mockPineconeResponses = {
|
||||
embedding: {
|
||||
embedding: Array(1536)
|
||||
.fill(0)
|
||||
.map(() => randomFloat() * 2 - 1),
|
||||
metadata: { text: 'Sample text for embedding', id: 'embed-123' },
|
||||
},
|
||||
searchResults: {
|
||||
matches: [
|
||||
{ id: 'doc1', score: 0.92, metadata: { text: 'Matching text 1' } },
|
||||
{ id: 'doc2', score: 0.85, metadata: { text: 'Matching text 2' } },
|
||||
{ id: 'doc3', score: 0.78, metadata: { text: 'Matching text 3' } },
|
||||
],
|
||||
},
|
||||
upsertResponse: {
|
||||
statusText: 'Created',
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* GitHub Mock Data
|
||||
*/
|
||||
export const mockGitHubResponses = {
|
||||
repoInfo: {
|
||||
id: 12345,
|
||||
name: 'test-repo',
|
||||
full_name: 'user/test-repo',
|
||||
description: 'A test repository',
|
||||
html_url: 'https://github.com/user/test-repo',
|
||||
owner: {
|
||||
login: 'user',
|
||||
id: 54321,
|
||||
avatar_url: 'https://avatars.githubusercontent.com/u/54321',
|
||||
},
|
||||
private: false,
|
||||
fork: false,
|
||||
created_at: '2025-01-01T00:00:00Z',
|
||||
updated_at: '2025-03-15T10:00:00Z',
|
||||
pushed_at: '2025-03-15T09:00:00Z',
|
||||
default_branch: 'main',
|
||||
open_issues_count: 5,
|
||||
watchers_count: 10,
|
||||
forks_count: 3,
|
||||
stargazers_count: 15,
|
||||
language: 'TypeScript',
|
||||
},
|
||||
prResponse: {
|
||||
id: 12345,
|
||||
number: 42,
|
||||
title: 'Test PR Title',
|
||||
body: 'Test PR description',
|
||||
html_url: 'https://github.com/user/test-repo/pull/42',
|
||||
state: 'open',
|
||||
user: { login: 'user', id: 54321 },
|
||||
created_at: '2025-03-15T10:00:00Z',
|
||||
updated_at: '2025-03-15T10:05:00Z',
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Serper Search Mock Data
|
||||
*/
|
||||
export const mockSerperResponses = {
|
||||
searchResults: {
|
||||
searchParameters: { q: 'test query', gl: 'us', hl: 'en' },
|
||||
organic: [
|
||||
{
|
||||
title: 'Test Result 1',
|
||||
link: 'https://example.com/1',
|
||||
snippet: 'This is a snippet for the first test result.',
|
||||
position: 1,
|
||||
},
|
||||
{
|
||||
title: 'Test Result 2',
|
||||
link: 'https://example.com/2',
|
||||
snippet: 'This is a snippet for the second test result.',
|
||||
position: 2,
|
||||
},
|
||||
{
|
||||
title: 'Test Result 3',
|
||||
link: 'https://example.com/3',
|
||||
snippet: 'This is a snippet for the third test result.',
|
||||
position: 3,
|
||||
},
|
||||
],
|
||||
knowledgeGraph: {
|
||||
title: 'Test Knowledge Graph',
|
||||
type: 'Test Type',
|
||||
description: 'This is a test knowledge graph result',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Slack Mock Data
|
||||
*/
|
||||
export const mockSlackResponses = {
|
||||
messageResponse: {
|
||||
ok: true,
|
||||
channel: 'C1234567890',
|
||||
ts: '1627385301.000700',
|
||||
message: {
|
||||
text: 'This is a test message',
|
||||
user: 'U1234567890',
|
||||
ts: '1627385301.000700',
|
||||
team: 'T1234567890',
|
||||
},
|
||||
},
|
||||
errorResponse: {
|
||||
ok: false,
|
||||
error: 'channel_not_found',
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Tavily Mock Data
|
||||
*/
|
||||
export const mockTavilyResponses = {
|
||||
searchResults: {
|
||||
results: [
|
||||
{
|
||||
title: 'Test Article 1',
|
||||
url: 'https://example.com/article1',
|
||||
content: 'This is the content of test article 1.',
|
||||
score: 0.95,
|
||||
},
|
||||
{
|
||||
title: 'Test Article 2',
|
||||
url: 'https://example.com/article2',
|
||||
content: 'This is the content of test article 2.',
|
||||
score: 0.87,
|
||||
},
|
||||
{
|
||||
title: 'Test Article 3',
|
||||
url: 'https://example.com/article3',
|
||||
content: 'This is the content of test article 3.',
|
||||
score: 0.72,
|
||||
},
|
||||
],
|
||||
query: 'test query',
|
||||
search_id: 'search-123',
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Supabase Mock Data
|
||||
*/
|
||||
export const mockSupabaseResponses = {
|
||||
queryResponse: {
|
||||
data: [
|
||||
{ id: 1, name: 'Item 1', description: 'Description 1' },
|
||||
{ id: 2, name: 'Item 2', description: 'Description 2' },
|
||||
{ id: 3, name: 'Item 3', description: 'Description 3' },
|
||||
],
|
||||
error: null,
|
||||
},
|
||||
insertResponse: {
|
||||
data: [{ id: 4, name: 'Item 4', description: 'Description 4' }],
|
||||
error: null,
|
||||
},
|
||||
updateResponse: {
|
||||
data: [{ id: 1, name: 'Updated Item 1', description: 'Updated Description 1' }],
|
||||
error: null,
|
||||
},
|
||||
errorResponse: {
|
||||
data: null,
|
||||
error: {
|
||||
message: 'Database error',
|
||||
details: 'Error details',
|
||||
hint: 'Error hint',
|
||||
code: 'DB_ERROR',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,478 @@
|
||||
import { shortId } from './id'
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
/**
|
||||
* Operation types supported by the undo/redo store.
|
||||
*/
|
||||
export type OperationType =
|
||||
| 'batch-add-blocks'
|
||||
| 'batch-remove-blocks'
|
||||
| 'batch-add-edges'
|
||||
| 'batch-remove-edges'
|
||||
| 'batch-move-blocks'
|
||||
| 'update-parent'
|
||||
| 'batch-update-parent'
|
||||
|
||||
/**
|
||||
* Base operation interface.
|
||||
*/
|
||||
export interface BaseOperation {
|
||||
id: string
|
||||
type: OperationType
|
||||
timestamp: number
|
||||
workflowId: string
|
||||
userId: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch move blocks operation data.
|
||||
*/
|
||||
export interface BatchMoveBlocksOperation extends BaseOperation {
|
||||
type: 'batch-move-blocks'
|
||||
data: {
|
||||
moves: Array<{
|
||||
blockId: string
|
||||
before: { x: number; y: number; parentId?: string }
|
||||
after: { x: number; y: number; parentId?: string }
|
||||
}>
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch add blocks operation data.
|
||||
*/
|
||||
export interface BatchAddBlocksOperation extends BaseOperation {
|
||||
type: 'batch-add-blocks'
|
||||
data: {
|
||||
blockSnapshots: any[]
|
||||
edgeSnapshots: any[]
|
||||
subBlockValues: Record<string, Record<string, any>>
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch remove blocks operation data.
|
||||
*/
|
||||
export interface BatchRemoveBlocksOperation extends BaseOperation {
|
||||
type: 'batch-remove-blocks'
|
||||
data: {
|
||||
blockSnapshots: any[]
|
||||
edgeSnapshots: any[]
|
||||
subBlockValues: Record<string, Record<string, any>>
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch add edges operation data.
|
||||
*/
|
||||
export interface BatchAddEdgesOperation extends BaseOperation {
|
||||
type: 'batch-add-edges'
|
||||
data: { edgeSnapshots: any[] }
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch remove edges operation data.
|
||||
*/
|
||||
export interface BatchRemoveEdgesOperation extends BaseOperation {
|
||||
type: 'batch-remove-edges'
|
||||
data: { edgeSnapshots: any[] }
|
||||
}
|
||||
|
||||
/**
|
||||
* Update parent operation data.
|
||||
*/
|
||||
export interface UpdateParentOperation extends BaseOperation {
|
||||
type: 'update-parent'
|
||||
data: {
|
||||
blockId: string
|
||||
oldParentId?: string
|
||||
newParentId?: string
|
||||
oldPosition: { x: number; y: number }
|
||||
newPosition: { x: number; y: number }
|
||||
}
|
||||
}
|
||||
|
||||
export interface BatchUpdateParentOperation extends BaseOperation {
|
||||
type: 'batch-update-parent'
|
||||
data: {
|
||||
updates: Array<{
|
||||
blockId: string
|
||||
oldParentId?: string
|
||||
newParentId?: string
|
||||
oldPosition: { x: number; y: number }
|
||||
newPosition: { x: number; y: number }
|
||||
affectedEdges?: any[]
|
||||
}>
|
||||
}
|
||||
}
|
||||
|
||||
export type Operation =
|
||||
| BatchAddBlocksOperation
|
||||
| BatchRemoveBlocksOperation
|
||||
| BatchAddEdgesOperation
|
||||
| BatchRemoveEdgesOperation
|
||||
| BatchMoveBlocksOperation
|
||||
| UpdateParentOperation
|
||||
| BatchUpdateParentOperation
|
||||
|
||||
/**
|
||||
* Operation entry with forward and inverse operations.
|
||||
*/
|
||||
export interface OperationEntry {
|
||||
id: string
|
||||
operation: Operation
|
||||
inverse: Operation
|
||||
createdAt: number
|
||||
}
|
||||
|
||||
interface OperationEntryOptions {
|
||||
id?: string
|
||||
workflowId?: string
|
||||
userId?: string
|
||||
createdAt?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a mock batch-add-blocks operation entry.
|
||||
*/
|
||||
export function createAddBlockEntry(blockId: string, options: OperationEntryOptions = {}): any {
|
||||
const {
|
||||
id = shortId(8),
|
||||
workflowId = 'wf-1',
|
||||
userId = 'user-1',
|
||||
createdAt = Date.now(),
|
||||
} = options
|
||||
const timestamp = Date.now()
|
||||
|
||||
const mockBlockSnapshot = {
|
||||
id: blockId,
|
||||
type: 'action',
|
||||
name: `Block ${blockId}`,
|
||||
position: { x: 0, y: 0 },
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
createdAt,
|
||||
operation: {
|
||||
id: shortId(8),
|
||||
type: 'batch-add-blocks',
|
||||
timestamp,
|
||||
workflowId,
|
||||
userId,
|
||||
data: {
|
||||
blockSnapshots: [mockBlockSnapshot],
|
||||
edgeSnapshots: [],
|
||||
subBlockValues: {},
|
||||
},
|
||||
},
|
||||
inverse: {
|
||||
id: shortId(8),
|
||||
type: 'batch-remove-blocks',
|
||||
timestamp,
|
||||
workflowId,
|
||||
userId,
|
||||
data: {
|
||||
blockSnapshots: [mockBlockSnapshot],
|
||||
edgeSnapshots: [],
|
||||
subBlockValues: {},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a mock batch-remove-blocks operation entry.
|
||||
*/
|
||||
export function createRemoveBlockEntry(
|
||||
blockId: string,
|
||||
blockSnapshot: any = null,
|
||||
options: OperationEntryOptions = {}
|
||||
): any {
|
||||
const {
|
||||
id = shortId(8),
|
||||
workflowId = 'wf-1',
|
||||
userId = 'user-1',
|
||||
createdAt = Date.now(),
|
||||
} = options
|
||||
const timestamp = Date.now()
|
||||
|
||||
const snapshotToUse = blockSnapshot || {
|
||||
id: blockId,
|
||||
type: 'action',
|
||||
name: `Block ${blockId}`,
|
||||
position: { x: 0, y: 0 },
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
createdAt,
|
||||
operation: {
|
||||
id: shortId(8),
|
||||
type: 'batch-remove-blocks',
|
||||
timestamp,
|
||||
workflowId,
|
||||
userId,
|
||||
data: {
|
||||
blockSnapshots: [snapshotToUse],
|
||||
edgeSnapshots: [],
|
||||
subBlockValues: {},
|
||||
},
|
||||
},
|
||||
inverse: {
|
||||
id: shortId(8),
|
||||
type: 'batch-add-blocks',
|
||||
timestamp,
|
||||
workflowId,
|
||||
userId,
|
||||
data: {
|
||||
blockSnapshots: [snapshotToUse],
|
||||
edgeSnapshots: [],
|
||||
subBlockValues: {},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a mock batch-add-edges operation entry for a single edge.
|
||||
*/
|
||||
export function createAddEdgeEntry(
|
||||
edgeId: string,
|
||||
edgeSnapshot: any = null,
|
||||
options: OperationEntryOptions = {}
|
||||
): any {
|
||||
const {
|
||||
id = shortId(8),
|
||||
workflowId = 'wf-1',
|
||||
userId = 'user-1',
|
||||
createdAt = Date.now(),
|
||||
} = options
|
||||
const timestamp = Date.now()
|
||||
|
||||
const snapshot = edgeSnapshot || { id: edgeId, source: 'block-1', target: 'block-2' }
|
||||
|
||||
return {
|
||||
id,
|
||||
createdAt,
|
||||
operation: {
|
||||
id: shortId(8),
|
||||
type: 'batch-add-edges',
|
||||
timestamp,
|
||||
workflowId,
|
||||
userId,
|
||||
data: { edgeSnapshots: [snapshot] },
|
||||
},
|
||||
inverse: {
|
||||
id: shortId(8),
|
||||
type: 'batch-remove-edges',
|
||||
timestamp,
|
||||
workflowId,
|
||||
userId,
|
||||
data: { edgeSnapshots: [snapshot] },
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a mock batch-remove-edges operation entry.
|
||||
*/
|
||||
export function createBatchRemoveEdgesEntry(
|
||||
edgeSnapshots: any[],
|
||||
options: OperationEntryOptions = {}
|
||||
): any {
|
||||
const {
|
||||
id = shortId(8),
|
||||
workflowId = 'wf-1',
|
||||
userId = 'user-1',
|
||||
createdAt = Date.now(),
|
||||
} = options
|
||||
const timestamp = Date.now()
|
||||
|
||||
return {
|
||||
id,
|
||||
createdAt,
|
||||
operation: {
|
||||
id: shortId(8),
|
||||
type: 'batch-remove-edges',
|
||||
timestamp,
|
||||
workflowId,
|
||||
userId,
|
||||
data: { edgeSnapshots },
|
||||
},
|
||||
inverse: {
|
||||
id: shortId(8),
|
||||
type: 'batch-add-edges',
|
||||
timestamp,
|
||||
workflowId,
|
||||
userId,
|
||||
data: { edgeSnapshots },
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
interface MoveBlockOptions extends OperationEntryOptions {
|
||||
before?: { x: number; y: number; parentId?: string }
|
||||
after?: { x: number; y: number; parentId?: string }
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a mock batch-move-blocks operation entry for a single block.
|
||||
*/
|
||||
export function createMoveBlockEntry(blockId: string, options: MoveBlockOptions = {}): any {
|
||||
const {
|
||||
id = shortId(8),
|
||||
workflowId = 'wf-1',
|
||||
userId = 'user-1',
|
||||
createdAt = Date.now(),
|
||||
before = { x: 0, y: 0 },
|
||||
after = { x: 100, y: 100 },
|
||||
} = options
|
||||
const timestamp = Date.now()
|
||||
|
||||
return {
|
||||
id,
|
||||
createdAt,
|
||||
operation: {
|
||||
id: shortId(8),
|
||||
type: 'batch-move-blocks',
|
||||
timestamp,
|
||||
workflowId,
|
||||
userId,
|
||||
data: { moves: [{ blockId, before, after }] },
|
||||
},
|
||||
inverse: {
|
||||
id: shortId(8),
|
||||
type: 'batch-move-blocks',
|
||||
timestamp,
|
||||
workflowId,
|
||||
userId,
|
||||
data: { moves: [{ blockId, before: after, after: before }] },
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a mock update-parent operation entry.
|
||||
*/
|
||||
export function createUpdateParentEntry(
|
||||
blockId: string,
|
||||
options: OperationEntryOptions & {
|
||||
oldParentId?: string
|
||||
newParentId?: string
|
||||
oldPosition?: { x: number; y: number }
|
||||
newPosition?: { x: number; y: number }
|
||||
} = {}
|
||||
): any {
|
||||
const {
|
||||
id = shortId(8),
|
||||
workflowId = 'wf-1',
|
||||
userId = 'user-1',
|
||||
createdAt = Date.now(),
|
||||
oldParentId,
|
||||
newParentId,
|
||||
oldPosition = { x: 0, y: 0 },
|
||||
newPosition = { x: 50, y: 50 },
|
||||
} = options
|
||||
const timestamp = Date.now()
|
||||
|
||||
return {
|
||||
id,
|
||||
createdAt,
|
||||
operation: {
|
||||
id: shortId(8),
|
||||
type: 'update-parent',
|
||||
timestamp,
|
||||
workflowId,
|
||||
userId,
|
||||
data: { blockId, oldParentId, newParentId, oldPosition, newPosition },
|
||||
},
|
||||
inverse: {
|
||||
id: shortId(8),
|
||||
type: 'update-parent',
|
||||
timestamp,
|
||||
workflowId,
|
||||
userId,
|
||||
data: {
|
||||
blockId,
|
||||
oldParentId: newParentId,
|
||||
newParentId: oldParentId,
|
||||
oldPosition: newPosition,
|
||||
newPosition: oldPosition,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
interface BatchUpdateParentOptions extends OperationEntryOptions {
|
||||
updates?: Array<{
|
||||
blockId: string
|
||||
oldParentId?: string
|
||||
newParentId?: string
|
||||
oldPosition?: { x: number; y: number }
|
||||
newPosition?: { x: number; y: number }
|
||||
affectedEdges?: any[]
|
||||
}>
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a mock batch-update-parent operation entry.
|
||||
*/
|
||||
export function createBatchUpdateParentEntry(options: BatchUpdateParentOptions = {}): any {
|
||||
const {
|
||||
id = shortId(8),
|
||||
workflowId = 'wf-1',
|
||||
userId = 'user-1',
|
||||
createdAt = Date.now(),
|
||||
updates = [
|
||||
{
|
||||
blockId: 'block-1',
|
||||
oldParentId: undefined,
|
||||
newParentId: 'loop-1',
|
||||
oldPosition: { x: 0, y: 0 },
|
||||
newPosition: { x: 50, y: 50 },
|
||||
},
|
||||
],
|
||||
} = options
|
||||
const timestamp = Date.now()
|
||||
|
||||
const processedUpdates = updates.map((u) => ({
|
||||
blockId: u.blockId,
|
||||
oldParentId: u.oldParentId,
|
||||
newParentId: u.newParentId,
|
||||
oldPosition: u.oldPosition || { x: 0, y: 0 },
|
||||
newPosition: u.newPosition || { x: 50, y: 50 },
|
||||
affectedEdges: u.affectedEdges,
|
||||
}))
|
||||
|
||||
return {
|
||||
id,
|
||||
createdAt,
|
||||
operation: {
|
||||
id: shortId(8),
|
||||
type: 'batch-update-parent',
|
||||
timestamp,
|
||||
workflowId,
|
||||
userId,
|
||||
data: { updates: processedUpdates },
|
||||
},
|
||||
inverse: {
|
||||
id: shortId(8),
|
||||
type: 'batch-update-parent',
|
||||
timestamp,
|
||||
workflowId,
|
||||
userId,
|
||||
data: {
|
||||
updates: processedUpdates.map((u) => ({
|
||||
blockId: u.blockId,
|
||||
oldParentId: u.newParentId,
|
||||
newParentId: u.oldParentId,
|
||||
oldPosition: u.newPosition,
|
||||
newPosition: u.oldPosition,
|
||||
affectedEdges: u.affectedEdges,
|
||||
})),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import { generateRandomString } from '@sim/utils/random'
|
||||
import type { User, Workflow, WorkflowState, Workspace } from '../types'
|
||||
import { createWorkflowState } from './workflow.factory'
|
||||
|
||||
/**
|
||||
* Options for creating a mock user.
|
||||
*/
|
||||
export interface UserFactoryOptions {
|
||||
id?: string
|
||||
email?: string
|
||||
name?: string
|
||||
image?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a mock user.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const user = createUser({ email: 'test@example.com' })
|
||||
* ```
|
||||
*/
|
||||
export function createUser(options: UserFactoryOptions = {}): User {
|
||||
const id = options.id ?? `user-${generateRandomString(8)}`
|
||||
return {
|
||||
id,
|
||||
email: options.email ?? `${id}@test.example.com`,
|
||||
name: options.name ?? `Test User ${id.substring(0, 4)}`,
|
||||
image: options.image,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for creating a mock workspace.
|
||||
*/
|
||||
export interface WorkspaceFactoryOptions {
|
||||
id?: string
|
||||
name?: string
|
||||
ownerId?: string
|
||||
createdAt?: Date
|
||||
updatedAt?: Date
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a mock workspace.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const workspace = createWorkspace({ name: 'My Workspace' })
|
||||
* ```
|
||||
*/
|
||||
export function createWorkspace(options: WorkspaceFactoryOptions = {}): Workspace {
|
||||
const now = new Date()
|
||||
return {
|
||||
id: options.id ?? `ws-${generateRandomString(8)}`,
|
||||
name: options.name ?? 'Test Workspace',
|
||||
ownerId: options.ownerId ?? `user-${generateRandomString(8)}`,
|
||||
createdAt: options.createdAt ?? now,
|
||||
updatedAt: options.updatedAt ?? now,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for creating a mock workflow.
|
||||
*/
|
||||
export interface WorkflowObjectFactoryOptions {
|
||||
id?: string
|
||||
name?: string
|
||||
workspaceId?: string
|
||||
state?: WorkflowState
|
||||
createdAt?: Date
|
||||
updatedAt?: Date
|
||||
isDeployed?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a mock workflow object (not just state).
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const workflow = createWorkflow({ name: 'My Workflow' })
|
||||
* ```
|
||||
*/
|
||||
export function createWorkflow(options: WorkflowObjectFactoryOptions = {}): Workflow {
|
||||
const now = new Date()
|
||||
return {
|
||||
id: options.id ?? `wf-${generateRandomString(8)}`,
|
||||
name: options.name ?? 'Test Workflow',
|
||||
workspaceId: options.workspaceId ?? `ws-${generateRandomString(8)}`,
|
||||
state: options.state ?? createWorkflowState(),
|
||||
createdAt: options.createdAt ?? now,
|
||||
updatedAt: options.updatedAt ?? now,
|
||||
isDeployed: options.isDeployed ?? false,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a user with an associated workspace.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const { user, workspace } = createUserWithWorkspace()
|
||||
* ```
|
||||
*/
|
||||
export function createUserWithWorkspace(
|
||||
userOptions: UserFactoryOptions = {},
|
||||
workspaceOptions: Omit<WorkspaceFactoryOptions, 'ownerId'> = {}
|
||||
): { user: User; workspace: Workspace } {
|
||||
const user = createUser(userOptions)
|
||||
const workspace = createWorkspace({
|
||||
...workspaceOptions,
|
||||
ownerId: user.id,
|
||||
})
|
||||
return { user, workspace }
|
||||
}
|
||||
@@ -0,0 +1,628 @@
|
||||
import { createBlock, createFunctionBlock, createStarterBlock } from './block.factory'
|
||||
import { createLinearEdges } from './edge.factory'
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
/**
|
||||
* Options for creating a mock workflow state.
|
||||
* Uses `any` for complex types to avoid conflicts with app types.
|
||||
*/
|
||||
export interface WorkflowFactoryOptions {
|
||||
blocks?: Record<string, any>
|
||||
edges?: any[]
|
||||
loops?: Record<string, any>
|
||||
parallels?: Record<string, any>
|
||||
lastSaved?: number
|
||||
isDeployed?: boolean
|
||||
variables?: any[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an empty workflow state with defaults.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const workflow = createWorkflowState()
|
||||
* ```
|
||||
*/
|
||||
export function createWorkflowState(options: WorkflowFactoryOptions = {}): any {
|
||||
return {
|
||||
blocks: options.blocks ?? {},
|
||||
edges: options.edges ?? [],
|
||||
loops: options.loops ?? {},
|
||||
parallels: options.parallels ?? {},
|
||||
lastSaved: options.lastSaved ?? Date.now(),
|
||||
isDeployed: options.isDeployed ?? false,
|
||||
variables: options.variables,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a simple linear workflow with the specified number of blocks.
|
||||
* First block is always a starter, rest are function blocks.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // Creates: starter -> function -> function
|
||||
* const workflow = createLinearWorkflow(3)
|
||||
* ```
|
||||
*/
|
||||
export function createLinearWorkflow(blockCount: number, spacing = 200): any {
|
||||
if (blockCount < 1) {
|
||||
return createWorkflowState()
|
||||
}
|
||||
|
||||
const blocks: Record<string, any> = {}
|
||||
const blockIds: string[] = []
|
||||
|
||||
for (let i = 0; i < blockCount; i++) {
|
||||
const id = `block-${i}`
|
||||
blockIds.push(id)
|
||||
|
||||
if (i === 0) {
|
||||
blocks[id] = createStarterBlock({
|
||||
id,
|
||||
position: { x: i * spacing, y: 0 },
|
||||
})
|
||||
} else {
|
||||
blocks[id] = createFunctionBlock({
|
||||
id,
|
||||
name: `Step ${i}`,
|
||||
position: { x: i * spacing, y: 0 },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return createWorkflowState({
|
||||
blocks,
|
||||
edges: createLinearEdges(blockIds),
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a workflow with a branching condition.
|
||||
*
|
||||
* Structure:
|
||||
* ```
|
||||
* ┌─→ true-branch ─┐
|
||||
* start ─→ condition ├─→ end
|
||||
* └─→ false-branch ┘
|
||||
* ```
|
||||
*/
|
||||
export function createBranchingWorkflow(): any {
|
||||
const blocks: Record<string, any> = {
|
||||
start: createStarterBlock({ id: 'start', position: { x: 0, y: 0 } }),
|
||||
condition: createBlock({
|
||||
id: 'condition',
|
||||
type: 'condition',
|
||||
name: 'Check',
|
||||
position: { x: 200, y: 0 },
|
||||
}),
|
||||
'true-branch': createFunctionBlock({
|
||||
id: 'true-branch',
|
||||
name: 'If True',
|
||||
position: { x: 400, y: -100 },
|
||||
}),
|
||||
'false-branch': createFunctionBlock({
|
||||
id: 'false-branch',
|
||||
name: 'If False',
|
||||
position: { x: 400, y: 100 },
|
||||
}),
|
||||
end: createFunctionBlock({ id: 'end', name: 'End', position: { x: 600, y: 0 } }),
|
||||
}
|
||||
|
||||
const edges: any[] = [
|
||||
{ id: 'e1', source: 'start', target: 'condition' },
|
||||
{ id: 'e2', source: 'condition', target: 'true-branch', sourceHandle: 'condition-if' },
|
||||
{ id: 'e3', source: 'condition', target: 'false-branch', sourceHandle: 'condition-else' },
|
||||
{ id: 'e4', source: 'true-branch', target: 'end' },
|
||||
{ id: 'e5', source: 'false-branch', target: 'end' },
|
||||
]
|
||||
|
||||
return createWorkflowState({ blocks, edges })
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a workflow with a loop container.
|
||||
*
|
||||
* Structure:
|
||||
* ```
|
||||
* start ─→ loop[loop-body] ─→ end
|
||||
* ```
|
||||
*/
|
||||
export function createLoopWorkflow(iterations = 3): any {
|
||||
const blocks: Record<string, any> = {
|
||||
start: createStarterBlock({ id: 'start', position: { x: 0, y: 0 } }),
|
||||
loop: createBlock({
|
||||
id: 'loop',
|
||||
type: 'loop',
|
||||
name: 'Loop',
|
||||
position: { x: 200, y: 0 },
|
||||
data: { loopType: 'for', count: iterations, type: 'loop' },
|
||||
}),
|
||||
'loop-body': createFunctionBlock({
|
||||
id: 'loop-body',
|
||||
name: 'Loop Body',
|
||||
position: { x: 50, y: 50 },
|
||||
parentId: 'loop',
|
||||
}),
|
||||
end: createFunctionBlock({ id: 'end', name: 'End', position: { x: 500, y: 0 } }),
|
||||
}
|
||||
|
||||
const edges: any[] = [
|
||||
{ id: 'e1', source: 'start', target: 'loop' },
|
||||
{ id: 'e2', source: 'loop', target: 'end' },
|
||||
]
|
||||
|
||||
const loops: Record<string, any> = {
|
||||
loop: {
|
||||
id: 'loop',
|
||||
nodes: ['loop-body'],
|
||||
iterations,
|
||||
loopType: 'for',
|
||||
},
|
||||
}
|
||||
|
||||
return createWorkflowState({ blocks, edges, loops })
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a workflow with a parallel container.
|
||||
*
|
||||
* Structure:
|
||||
* ```
|
||||
* start ─→ parallel[parallel-task] ─→ end
|
||||
* ```
|
||||
*/
|
||||
export function createParallelWorkflow(count = 2): any {
|
||||
const blocks: Record<string, any> = {
|
||||
start: createStarterBlock({ id: 'start', position: { x: 0, y: 0 } }),
|
||||
parallel: createBlock({
|
||||
id: 'parallel',
|
||||
type: 'parallel',
|
||||
name: 'Parallel',
|
||||
position: { x: 200, y: 0 },
|
||||
data: { parallelType: 'count', count, type: 'parallel' },
|
||||
}),
|
||||
'parallel-task': createFunctionBlock({
|
||||
id: 'parallel-task',
|
||||
name: 'Parallel Task',
|
||||
position: { x: 50, y: 50 },
|
||||
parentId: 'parallel',
|
||||
}),
|
||||
end: createFunctionBlock({ id: 'end', name: 'End', position: { x: 500, y: 0 } }),
|
||||
}
|
||||
|
||||
const edges: any[] = [
|
||||
{ id: 'e1', source: 'start', target: 'parallel' },
|
||||
{ id: 'e2', source: 'parallel', target: 'end' },
|
||||
]
|
||||
|
||||
const parallels: Record<string, any> = {
|
||||
parallel: {
|
||||
id: 'parallel',
|
||||
nodes: ['parallel-task'],
|
||||
count,
|
||||
parallelType: 'count',
|
||||
},
|
||||
}
|
||||
|
||||
return createWorkflowState({ blocks, edges, parallels })
|
||||
}
|
||||
|
||||
/**
|
||||
* Detailed workflow state fixture interface for serializer tests.
|
||||
*/
|
||||
export interface WorkflowStateFixture {
|
||||
blocks: Record<string, any>
|
||||
edges: any[]
|
||||
loops: Record<string, any>
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a minimal workflow with a starter and one agent block.
|
||||
* Includes full subBlocks structure for serializer testing.
|
||||
*/
|
||||
export function createMinimalWorkflowState(): WorkflowStateFixture {
|
||||
const blocks: Record<string, any> = {
|
||||
starter: {
|
||||
id: 'starter',
|
||||
type: 'starter',
|
||||
name: 'Starter Block',
|
||||
position: { x: 0, y: 0 },
|
||||
subBlocks: {
|
||||
description: { id: 'description', type: 'long-input', value: 'This is the starter block' },
|
||||
},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
agent1: {
|
||||
id: 'agent1',
|
||||
type: 'agent',
|
||||
name: 'Agent Block',
|
||||
position: { x: 300, y: 0 },
|
||||
subBlocks: {
|
||||
provider: { id: 'provider', type: 'dropdown', value: 'anthropic' },
|
||||
model: { id: 'model', type: 'dropdown', value: 'claude-3-7-sonnet-20250219' },
|
||||
prompt: { id: 'prompt', type: 'long-input', value: 'Hello, world!' },
|
||||
tools: { id: 'tools', type: 'tool-input', value: '[]' },
|
||||
system: { id: 'system', type: 'long-input', value: 'You are a helpful assistant.' },
|
||||
responseFormat: { id: 'responseFormat', type: 'code', value: null },
|
||||
},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
blocks,
|
||||
edges: [{ id: 'edge1', source: 'starter', target: 'agent1' }],
|
||||
loops: {},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a workflow with condition blocks and branching paths.
|
||||
*/
|
||||
export function createConditionalWorkflowState(): WorkflowStateFixture {
|
||||
const blocks: Record<string, any> = {
|
||||
starter: {
|
||||
id: 'starter',
|
||||
type: 'starter',
|
||||
name: 'Starter Block',
|
||||
position: { x: 0, y: 0 },
|
||||
subBlocks: {
|
||||
description: { id: 'description', type: 'long-input', value: 'This is the starter block' },
|
||||
},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
condition1: {
|
||||
id: 'condition1',
|
||||
type: 'condition',
|
||||
name: 'Condition Block',
|
||||
position: { x: 300, y: 0 },
|
||||
subBlocks: {
|
||||
condition: { id: 'condition', type: 'long-input', value: 'input.value > 10' },
|
||||
},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
agent1: {
|
||||
id: 'agent1',
|
||||
type: 'agent',
|
||||
name: 'True Path Agent',
|
||||
position: { x: 600, y: -100 },
|
||||
subBlocks: {
|
||||
provider: { id: 'provider', type: 'dropdown', value: 'anthropic' },
|
||||
model: { id: 'model', type: 'dropdown', value: 'claude-3-7-sonnet-20250219' },
|
||||
prompt: { id: 'prompt', type: 'long-input', value: 'Value is greater than 10' },
|
||||
tools: { id: 'tools', type: 'tool-input', value: '[]' },
|
||||
system: { id: 'system', type: 'long-input', value: 'You are a helpful assistant.' },
|
||||
responseFormat: { id: 'responseFormat', type: 'code', value: null },
|
||||
},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
agent2: {
|
||||
id: 'agent2',
|
||||
type: 'agent',
|
||||
name: 'False Path Agent',
|
||||
position: { x: 600, y: 100 },
|
||||
subBlocks: {
|
||||
provider: { id: 'provider', type: 'dropdown', value: 'anthropic' },
|
||||
model: { id: 'model', type: 'dropdown', value: 'claude-3-7-sonnet-20250219' },
|
||||
prompt: { id: 'prompt', type: 'long-input', value: 'Value is less than or equal to 10' },
|
||||
tools: { id: 'tools', type: 'tool-input', value: '[]' },
|
||||
system: { id: 'system', type: 'long-input', value: 'You are a helpful assistant.' },
|
||||
responseFormat: { id: 'responseFormat', type: 'code', value: null },
|
||||
},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
blocks,
|
||||
edges: [
|
||||
{ id: 'edge1', source: 'starter', target: 'condition1' },
|
||||
{ id: 'edge2', source: 'condition1', target: 'agent1', sourceHandle: 'condition-true' },
|
||||
{ id: 'edge3', source: 'condition1', target: 'agent2', sourceHandle: 'condition-false' },
|
||||
],
|
||||
loops: {},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a workflow with a loop structure.
|
||||
*/
|
||||
export function createLoopWorkflowState(): WorkflowStateFixture {
|
||||
const blocks: Record<string, any> = {
|
||||
starter: {
|
||||
id: 'starter',
|
||||
type: 'starter',
|
||||
name: 'Starter Block',
|
||||
position: { x: 0, y: 0 },
|
||||
subBlocks: {
|
||||
description: { id: 'description', type: 'long-input', value: 'This is the starter block' },
|
||||
},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
function1: {
|
||||
id: 'function1',
|
||||
type: 'function',
|
||||
name: 'Function Block',
|
||||
position: { x: 300, y: 0 },
|
||||
subBlocks: {
|
||||
code: {
|
||||
id: 'code',
|
||||
type: 'code',
|
||||
value: 'let counter = input.counter || 0;\ncounter++;\nreturn { counter };',
|
||||
},
|
||||
language: { id: 'language', type: 'dropdown', value: 'javascript' },
|
||||
},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
condition1: {
|
||||
id: 'condition1',
|
||||
type: 'condition',
|
||||
name: 'Loop Condition',
|
||||
position: { x: 600, y: 0 },
|
||||
subBlocks: {
|
||||
condition: { id: 'condition', type: 'long-input', value: 'input.counter < 5' },
|
||||
},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
agent1: {
|
||||
id: 'agent1',
|
||||
type: 'agent',
|
||||
name: 'Loop Complete Agent',
|
||||
position: { x: 900, y: 100 },
|
||||
subBlocks: {
|
||||
provider: { id: 'provider', type: 'dropdown', value: 'anthropic' },
|
||||
model: { id: 'model', type: 'dropdown', value: 'claude-3-7-sonnet-20250219' },
|
||||
prompt: {
|
||||
id: 'prompt',
|
||||
type: 'long-input',
|
||||
value: 'Loop completed after {{input.counter}} iterations',
|
||||
},
|
||||
tools: { id: 'tools', type: 'tool-input', value: '[]' },
|
||||
system: { id: 'system', type: 'long-input', value: 'You are a helpful assistant.' },
|
||||
responseFormat: { id: 'responseFormat', type: 'code', value: null },
|
||||
},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
blocks,
|
||||
edges: [
|
||||
{ id: 'edge1', source: 'starter', target: 'function1' },
|
||||
{ id: 'edge2', source: 'function1', target: 'condition1' },
|
||||
{ id: 'edge3', source: 'condition1', target: 'function1', sourceHandle: 'condition-true' },
|
||||
{ id: 'edge4', source: 'condition1', target: 'agent1', sourceHandle: 'condition-false' },
|
||||
],
|
||||
loops: {
|
||||
loop1: { id: 'loop1', nodes: ['function1', 'condition1'], iterations: 10, loopType: 'for' },
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a complex workflow with multiple block types (API, function, agent).
|
||||
*/
|
||||
export function createComplexWorkflowState(): WorkflowStateFixture {
|
||||
const blocks: Record<string, any> = {
|
||||
starter: {
|
||||
id: 'starter',
|
||||
type: 'starter',
|
||||
name: 'Starter Block',
|
||||
position: { x: 0, y: 0 },
|
||||
subBlocks: {
|
||||
description: { id: 'description', type: 'long-input', value: 'This is the starter block' },
|
||||
},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
api1: {
|
||||
id: 'api1',
|
||||
type: 'api',
|
||||
name: 'API Request',
|
||||
position: { x: 300, y: 0 },
|
||||
subBlocks: {
|
||||
url: { id: 'url', type: 'short-input', value: 'https://api.example.com/data' },
|
||||
method: { id: 'method', type: 'dropdown', value: 'GET' },
|
||||
headers: {
|
||||
id: 'headers',
|
||||
type: 'table',
|
||||
value: [
|
||||
['Content-Type', 'application/json'],
|
||||
['Authorization', 'Bearer {{API_KEY}}'],
|
||||
],
|
||||
},
|
||||
body: { id: 'body', type: 'long-input', value: '' },
|
||||
},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
function1: {
|
||||
id: 'function1',
|
||||
type: 'function',
|
||||
name: 'Process Data',
|
||||
position: { x: 600, y: 0 },
|
||||
subBlocks: {
|
||||
code: {
|
||||
id: 'code',
|
||||
type: 'code',
|
||||
value: 'const data = input.data;\nreturn { processed: data.map(item => item.name) };',
|
||||
},
|
||||
language: { id: 'language', type: 'dropdown', value: 'javascript' },
|
||||
},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
agent1: {
|
||||
id: 'agent1',
|
||||
type: 'agent',
|
||||
name: 'Summarize Data',
|
||||
position: { x: 900, y: 0 },
|
||||
subBlocks: {
|
||||
provider: { id: 'provider', type: 'dropdown', value: 'openai' },
|
||||
model: { id: 'model', type: 'dropdown', value: 'gpt-4o' },
|
||||
prompt: {
|
||||
id: 'prompt',
|
||||
type: 'long-input',
|
||||
value: 'Summarize the following data:\n\n{{input.processed}}',
|
||||
},
|
||||
tools: {
|
||||
id: 'tools',
|
||||
type: 'tool-input',
|
||||
value:
|
||||
'[{"type":"function","name":"calculator","description":"Perform calculations","parameters":{"type":"object","properties":{"expression":{"type":"string","description":"Math expression to evaluate"}},"required":["expression"]}}]',
|
||||
},
|
||||
system: { id: 'system', type: 'long-input', value: 'You are a data analyst assistant.' },
|
||||
responseFormat: {
|
||||
id: 'responseFormat',
|
||||
type: 'code',
|
||||
value:
|
||||
'{"type":"object","properties":{"summary":{"type":"string"},"keyPoints":{"type":"array","items":{"type":"string"}},"sentiment":{"type":"string","enum":["positive","neutral","negative"]}},"required":["summary","keyPoints","sentiment"]}',
|
||||
},
|
||||
},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
blocks,
|
||||
edges: [
|
||||
{ id: 'edge1', source: 'starter', target: 'api1' },
|
||||
{ id: 'edge2', source: 'api1', target: 'function1' },
|
||||
{ id: 'edge3', source: 'function1', target: 'agent1' },
|
||||
],
|
||||
loops: {},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a workflow with an agent that has custom tools.
|
||||
*/
|
||||
export function createAgentWithToolsWorkflowState(): WorkflowStateFixture {
|
||||
const blocks: Record<string, any> = {
|
||||
starter: {
|
||||
id: 'starter',
|
||||
type: 'starter',
|
||||
name: 'Starter Block',
|
||||
position: { x: 0, y: 0 },
|
||||
subBlocks: {
|
||||
description: { id: 'description', type: 'long-input', value: 'This is the starter block' },
|
||||
},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
agent1: {
|
||||
id: 'agent1',
|
||||
type: 'agent',
|
||||
name: 'Custom Tools Agent',
|
||||
position: { x: 300, y: 0 },
|
||||
subBlocks: {
|
||||
provider: { id: 'provider', type: 'dropdown', value: 'openai' },
|
||||
model: { id: 'model', type: 'dropdown', value: 'gpt-4o' },
|
||||
prompt: {
|
||||
id: 'prompt',
|
||||
type: 'long-input',
|
||||
value: 'Use the tools to help answer: {{input.question}}',
|
||||
},
|
||||
tools: {
|
||||
id: 'tools',
|
||||
type: 'tool-input',
|
||||
value:
|
||||
'[{"type":"custom-tool","name":"weather","description":"Get current weather","parameters":{"type":"object","properties":{"location":{"type":"string"}},"required":["location"]}},{"type":"function","name":"calculator","description":"Calculate expression","parameters":{"type":"object","properties":{"expression":{"type":"string"}},"required":["expression"]}}]',
|
||||
},
|
||||
system: {
|
||||
id: 'system',
|
||||
type: 'long-input',
|
||||
value: 'You are a helpful assistant with access to tools.',
|
||||
},
|
||||
responseFormat: { id: 'responseFormat', type: 'code', value: null },
|
||||
},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
blocks,
|
||||
edges: [{ id: 'edge1', source: 'starter', target: 'agent1' }],
|
||||
loops: {},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a workflow state with an invalid block type for error testing.
|
||||
*/
|
||||
export function createInvalidWorkflowState(): WorkflowStateFixture {
|
||||
const { blocks, edges, loops } = createMinimalWorkflowState()
|
||||
|
||||
blocks.invalid = {
|
||||
id: 'invalid',
|
||||
type: 'invalid-type',
|
||||
name: 'Invalid Block',
|
||||
position: { x: 600, y: 0 },
|
||||
subBlocks: {},
|
||||
outputs: {},
|
||||
enabled: true,
|
||||
}
|
||||
|
||||
edges.push({ id: 'edge-invalid', source: 'agent1', target: 'invalid' })
|
||||
|
||||
return { blocks, edges, loops }
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a serialized workflow with invalid metadata for error testing.
|
||||
*/
|
||||
export function createInvalidSerializedWorkflow() {
|
||||
return {
|
||||
version: '1.0',
|
||||
blocks: [
|
||||
{
|
||||
id: 'invalid',
|
||||
position: { x: 0, y: 0 },
|
||||
config: { tool: 'invalid', params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
metadata: { id: 'non-existent-type' },
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
connections: [],
|
||||
loops: {},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a serialized workflow with missing metadata for error testing.
|
||||
*/
|
||||
export function createMissingMetadataWorkflow() {
|
||||
return {
|
||||
version: '1.0',
|
||||
blocks: [
|
||||
{
|
||||
id: 'invalid',
|
||||
position: { x: 0, y: 0 },
|
||||
config: { tool: 'invalid', params: {} },
|
||||
inputs: {},
|
||||
outputs: {},
|
||||
metadata: undefined,
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
connections: [],
|
||||
loops: {},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* @sim/testing - Shared testing utilities for Sim
|
||||
*
|
||||
* This package provides a comprehensive set of tools for writing tests:
|
||||
* - Factories: Create mock data with sensible defaults
|
||||
* - Builders: Fluent APIs for complex test scenarios
|
||||
* - Mocks: Reusable mock implementations
|
||||
* - Assertions: Semantic test assertions
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import {
|
||||
* // Factories
|
||||
* createBlock,
|
||||
* createStarterBlock,
|
||||
* createLinearWorkflow,
|
||||
* createExecutionContext,
|
||||
*
|
||||
* // Builders
|
||||
* WorkflowBuilder,
|
||||
* ExecutionContextBuilder,
|
||||
*
|
||||
* // Assertions
|
||||
* expectBlockExists,
|
||||
* expectEdgeConnects,
|
||||
* expectBlockExecuted,
|
||||
* } from '@sim/testing'
|
||||
*
|
||||
* describe('MyFeature', () => {
|
||||
* it('should work with a linear workflow', () => {
|
||||
* const workflow = createLinearWorkflow(3)
|
||||
* expectBlockExists(workflow.blocks, 'block-0', 'starter')
|
||||
* expectEdgeConnects(workflow.edges, 'block-0', 'block-1')
|
||||
* })
|
||||
*
|
||||
* it('should work with a complex workflow', () => {
|
||||
* const workflow = WorkflowBuilder.branching().build()
|
||||
* expectBlockCount(workflow, 5)
|
||||
* })
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
|
||||
export * from './assertions'
|
||||
export * from './builders'
|
||||
export * from './factories'
|
||||
export * from './mocks'
|
||||
export * from './types'
|
||||
@@ -0,0 +1,201 @@
|
||||
import { vi } from 'vitest'
|
||||
|
||||
/**
|
||||
* Controllable mock functions for `@sim/audit`.
|
||||
* Exposes `mockRecordAudit` so tests can assert or override behavior per test.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { auditMockFns } from '@sim/testing'
|
||||
*
|
||||
* expect(auditMockFns.mockRecordAudit).toHaveBeenCalledWith(...)
|
||||
* auditMockFns.mockRecordAudit.mockRejectedValueOnce(new Error('audit failed'))
|
||||
* ```
|
||||
*/
|
||||
export const auditMockFns = {
|
||||
mockRecordAudit: vi.fn(),
|
||||
}
|
||||
|
||||
/**
|
||||
* Static mock module for `@sim/audit`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* vi.mock('@sim/audit', () => auditMock)
|
||||
* ```
|
||||
*/
|
||||
export const auditMock = {
|
||||
recordAudit: auditMockFns.mockRecordAudit,
|
||||
AuditAction: {
|
||||
API_KEY_CREATED: 'api_key.created',
|
||||
API_KEY_UPDATED: 'api_key.updated',
|
||||
API_KEY_REVOKED: 'api_key.revoked',
|
||||
PERSONAL_API_KEY_CREATED: 'personal_api_key.created',
|
||||
PERSONAL_API_KEY_REVOKED: 'personal_api_key.revoked',
|
||||
BYOK_KEY_CREATED: 'byok_key.created',
|
||||
BYOK_KEY_UPDATED: 'byok_key.updated',
|
||||
BYOK_KEY_DELETED: 'byok_key.deleted',
|
||||
CHAT_DEPLOYED: 'chat.deployed',
|
||||
CHAT_UPDATED: 'chat.updated',
|
||||
CHAT_DELETED: 'chat.deleted',
|
||||
CREDENTIAL_CREATED: 'credential.created',
|
||||
CREDENTIAL_UPDATED: 'credential.updated',
|
||||
CREDENTIAL_RENAMED: 'credential.renamed',
|
||||
CREDENTIAL_RECONNECTED: 'credential.reconnected',
|
||||
CREDENTIAL_DELETED: 'credential.deleted',
|
||||
CREDENTIAL_MEMBER_ADDED: 'credential_member.added',
|
||||
CREDENTIAL_MEMBER_REMOVED: 'credential_member.removed',
|
||||
CREDENTIAL_MEMBER_ROLE_CHANGED: 'credential_member.role_changed',
|
||||
CREDIT_PURCHASED: 'credit.purchased',
|
||||
CUSTOM_BLOCK_PUBLISHED: 'custom_block.published',
|
||||
CUSTOM_BLOCK_UPDATED: 'custom_block.updated',
|
||||
CUSTOM_BLOCK_DELETED: 'custom_block.deleted',
|
||||
CUSTOM_TOOL_CREATED: 'custom_tool.created',
|
||||
CUSTOM_TOOL_UPDATED: 'custom_tool.updated',
|
||||
CUSTOM_TOOL_DELETED: 'custom_tool.deleted',
|
||||
DATA_DRAIN_CREATED: 'data_drain.created',
|
||||
DATA_DRAIN_UPDATED: 'data_drain.updated',
|
||||
DATA_DRAIN_DELETED: 'data_drain.deleted',
|
||||
DATA_DRAIN_RAN: 'data_drain.ran',
|
||||
DATA_DRAIN_TESTED: 'data_drain.tested',
|
||||
CONNECTOR_DOCUMENT_RESTORED: 'connector_document.restored',
|
||||
CONNECTOR_DOCUMENT_EXCLUDED: 'connector_document.excluded',
|
||||
DOCUMENT_UPLOADED: 'document.uploaded',
|
||||
DOCUMENT_UPDATED: 'document.updated',
|
||||
DOCUMENT_DELETED: 'document.deleted',
|
||||
ENVIRONMENT_UPDATED: 'environment.updated',
|
||||
ENVIRONMENT_DELETED: 'environment.deleted',
|
||||
FILE_UPLOADED: 'file.uploaded',
|
||||
FILE_UPDATED: 'file.updated',
|
||||
FILE_DELETED: 'file.deleted',
|
||||
FILE_RESTORED: 'file.restored',
|
||||
FILE_MOVED: 'file.moved',
|
||||
FILE_SHARED: 'file.shared',
|
||||
FILE_SHARE_DISABLED: 'file.share_disabled',
|
||||
FOLDER_CREATED: 'folder.created',
|
||||
FOLDER_UPDATED: 'folder.updated',
|
||||
FOLDER_DELETED: 'folder.deleted',
|
||||
FOLDER_MOVED: 'folder.moved',
|
||||
FOLDER_DUPLICATED: 'folder.duplicated',
|
||||
FOLDER_RESTORED: 'folder.restored',
|
||||
INVITATION_ACCEPTED: 'invitation.accepted',
|
||||
INVITATION_REJECTED: 'invitation.rejected',
|
||||
INVITATION_RESENT: 'invitation.resent',
|
||||
INVITATION_REVOKED: 'invitation.revoked',
|
||||
INVITATION_UPDATED: 'invitation.updated',
|
||||
CONNECTOR_CREATED: 'connector.created',
|
||||
CONNECTOR_UPDATED: 'connector.updated',
|
||||
CONNECTOR_DELETED: 'connector.deleted',
|
||||
CONNECTOR_SYNCED: 'connector.synced',
|
||||
KNOWLEDGE_BASE_CREATED: 'knowledge_base.created',
|
||||
KNOWLEDGE_BASE_UPDATED: 'knowledge_base.updated',
|
||||
KNOWLEDGE_BASE_DELETED: 'knowledge_base.deleted',
|
||||
KNOWLEDGE_BASE_RESTORED: 'knowledge_base.restored',
|
||||
MCP_SERVER_ADDED: 'mcp_server.added',
|
||||
MCP_SERVER_UPDATED: 'mcp_server.updated',
|
||||
MCP_SERVER_REMOVED: 'mcp_server.removed',
|
||||
MEMBER_INVITED: 'member.invited',
|
||||
MEMBER_ADDED: 'member.added',
|
||||
MEMBER_REMOVED: 'member.removed',
|
||||
MEMBER_ROLE_CHANGED: 'member.role_changed',
|
||||
OAUTH_DISCONNECTED: 'oauth.disconnected',
|
||||
PASSWORD_RESET: 'password.reset',
|
||||
PASSWORD_RESET_REQUESTED: 'password.reset_requested',
|
||||
ORGANIZATION_CREATED: 'organization.created',
|
||||
ORGANIZATION_UPDATED: 'organization.updated',
|
||||
ORG_MEMBER_ADDED: 'org_member.added',
|
||||
ORG_MEMBER_REMOVED: 'org_member.removed',
|
||||
ORG_MEMBER_ROLE_CHANGED: 'org_member.role_changed',
|
||||
ORG_MEMBER_USAGE_LIMIT_CHANGED: 'org_member.usage_limit_changed',
|
||||
ORG_INVITATION_CREATED: 'org_invitation.created',
|
||||
ORG_INVITATION_UPDATED: 'org_invitation.updated',
|
||||
ORG_INVITATION_ACCEPTED: 'org_invitation.accepted',
|
||||
ORG_INVITATION_REJECTED: 'org_invitation.rejected',
|
||||
ORG_INVITATION_CANCELLED: 'org_invitation.cancelled',
|
||||
ORG_INVITATION_REVOKED: 'org_invitation.revoked',
|
||||
ORG_INVITATION_RESENT: 'org_invitation.resent',
|
||||
ORG_SEAT_PROVISIONED: 'org_seat.provisioned',
|
||||
ORG_PLAN_CONVERTED: 'org_plan.converted',
|
||||
PERMISSION_GROUP_CREATED: 'permission_group.created',
|
||||
PERMISSION_GROUP_UPDATED: 'permission_group.updated',
|
||||
PERMISSION_GROUP_DELETED: 'permission_group.deleted',
|
||||
PERMISSION_GROUP_MEMBER_ADDED: 'permission_group_member.added',
|
||||
PERMISSION_GROUP_MEMBER_REMOVED: 'permission_group_member.removed',
|
||||
SCHEDULE_CREATED: 'schedule.created',
|
||||
SCHEDULE_UPDATED: 'schedule.updated',
|
||||
SCHEDULE_DELETED: 'schedule.deleted',
|
||||
SKILL_CREATED: 'skill.created',
|
||||
SKILL_UPDATED: 'skill.updated',
|
||||
SKILL_DELETED: 'skill.deleted',
|
||||
TABLE_CREATED: 'table.created',
|
||||
TABLE_UPDATED: 'table.updated',
|
||||
TABLE_DELETED: 'table.deleted',
|
||||
TABLE_RESTORED: 'table.restored',
|
||||
WEBHOOK_CREATED: 'webhook.created',
|
||||
WEBHOOK_DELETED: 'webhook.deleted',
|
||||
WORKFLOW_CREATED: 'workflow.created',
|
||||
WORKFLOW_DELETED: 'workflow.deleted',
|
||||
WORKFLOW_RESTORED: 'workflow.restored',
|
||||
WORKFLOW_DEPLOYED: 'workflow.deployed',
|
||||
WORKFLOW_UNDEPLOYED: 'workflow.undeployed',
|
||||
WORKFLOW_DUPLICATED: 'workflow.duplicated',
|
||||
WORKFLOW_LOCKED: 'workflow.locked',
|
||||
WORKFLOW_UNLOCKED: 'workflow.unlocked',
|
||||
WORKFLOW_DEPLOYMENT_ACTIVATED: 'workflow.deployment_activated',
|
||||
WORKFLOW_DEPLOYMENT_REVERTED: 'workflow.deployment_reverted',
|
||||
WORKFLOW_VARIABLES_UPDATED: 'workflow.variables_updated',
|
||||
WORKSPACE_CREATED: 'workspace.created',
|
||||
WORKSPACE_UPDATED: 'workspace.updated',
|
||||
WORKSPACE_DELETED: 'workspace.deleted',
|
||||
WORKSPACE_DUPLICATED: 'workspace.duplicated',
|
||||
WORKSPACE_FORKED: 'workspace.forked',
|
||||
WORKSPACE_FORK_PROMOTED: 'workspace.fork_promoted',
|
||||
WORKSPACE_FORK_ROLLED_BACK: 'workspace.fork_rolled_back',
|
||||
WORKSPACE_FORK_UNLINKED: 'workspace.fork_unlinked',
|
||||
WORKSPACE_EXPORTED: 'workspace.exported',
|
||||
CREDIT_ISSUED: 'credit.issued',
|
||||
INVOICE_PAYMENT_SUCCEEDED: 'invoice.payment_succeeded',
|
||||
INVOICE_PAYMENT_FAILED: 'invoice.payment_failed',
|
||||
OVERAGE_BILLED: 'billing.overage_billed',
|
||||
CHARGE_DISPUTE_OPENED: 'charge.dispute_opened',
|
||||
CHARGE_DISPUTE_CLOSED: 'charge.dispute_closed',
|
||||
SUBSCRIPTION_CREATED: 'subscription.created',
|
||||
SUBSCRIPTION_CANCELLED: 'subscription.cancelled',
|
||||
SUBSCRIPTION_TRANSFERRED: 'subscription.transferred',
|
||||
ENTERPRISE_SUBSCRIPTION_PROVISIONED: 'subscription.enterprise_provisioned',
|
||||
CREDENTIAL_ACCESSED: 'credential.accessed',
|
||||
FILE_DOWNLOADED: 'file.downloaded',
|
||||
ORG_SEAT_DEPROVISIONED: 'org_seat.deprovisioned',
|
||||
TABLE_EXPORTED: 'table.exported',
|
||||
WORKFLOW_PUBLIC_API_TOGGLED: 'workflow.public_api_toggled',
|
||||
WORKFLOW_EXPORTED: 'workflow.exported',
|
||||
},
|
||||
AuditResourceType: {
|
||||
API_KEY: 'api_key',
|
||||
BILLING: 'billing',
|
||||
BYOK_KEY: 'byok_key',
|
||||
CHAT: 'chat',
|
||||
CONNECTOR: 'connector',
|
||||
CREDENTIAL: 'credential',
|
||||
CUSTOM_BLOCK: 'custom_block',
|
||||
CUSTOM_TOOL: 'custom_tool',
|
||||
DATA_DRAIN: 'data_drain',
|
||||
DOCUMENT: 'document',
|
||||
ENVIRONMENT: 'environment',
|
||||
FILE: 'file',
|
||||
FOLDER: 'folder',
|
||||
KNOWLEDGE_BASE: 'knowledge_base',
|
||||
MCP_SERVER: 'mcp_server',
|
||||
OAUTH: 'oauth',
|
||||
ORGANIZATION: 'organization',
|
||||
PASSWORD: 'password',
|
||||
PERMISSION_GROUP: 'permission_group',
|
||||
SCHEDULE: 'schedule',
|
||||
SKILL: 'skill',
|
||||
SUBSCRIPTION: 'subscription',
|
||||
TABLE: 'table',
|
||||
WEBHOOK: 'webhook',
|
||||
WORKFLOW: 'workflow',
|
||||
WORKSPACE: 'workspace',
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { vi } from 'vitest'
|
||||
|
||||
/**
|
||||
* Mock of the `ServiceAccountTokenError` class from
|
||||
* `@/app/api/auth/oauth/utils`. Declared as a real class so consumer code
|
||||
* using `instanceof ServiceAccountTokenError` keeps working under mock.
|
||||
*/
|
||||
export class ServiceAccountTokenErrorMock extends Error {
|
||||
constructor(
|
||||
public readonly statusCode: number,
|
||||
public readonly errorDescription: string
|
||||
) {
|
||||
super(errorDescription)
|
||||
this.name = 'ServiceAccountTokenError'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Controllable mock functions for `@/app/api/auth/oauth/utils`.
|
||||
* All defaults are bare `vi.fn()` — configure per-test as needed.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { authOAuthUtilsMockFns } from '@sim/testing'
|
||||
*
|
||||
* authOAuthUtilsMockFns.mockRefreshAccessTokenIfNeeded.mockResolvedValue('access-token')
|
||||
* authOAuthUtilsMockFns.mockGetOAuthToken.mockResolvedValue(null)
|
||||
* ```
|
||||
*/
|
||||
export const authOAuthUtilsMockFns = {
|
||||
mockResolveOAuthAccountId: vi.fn(),
|
||||
mockGetServiceAccountToken: vi.fn(),
|
||||
mockSafeAccountInsert: vi.fn(),
|
||||
mockGetCredential: vi.fn(),
|
||||
mockGetOAuthToken: vi.fn(),
|
||||
mockRefreshAccessTokenIfNeeded: vi.fn(),
|
||||
mockRefreshTokenIfNeeded: vi.fn(),
|
||||
}
|
||||
|
||||
/**
|
||||
* Static mock module for `@/app/api/auth/oauth/utils`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* vi.mock('@/app/api/auth/oauth/utils', () => authOAuthUtilsMock)
|
||||
* ```
|
||||
*/
|
||||
export const authOAuthUtilsMock = {
|
||||
ServiceAccountTokenError: ServiceAccountTokenErrorMock,
|
||||
resolveOAuthAccountId: authOAuthUtilsMockFns.mockResolveOAuthAccountId,
|
||||
getServiceAccountToken: authOAuthUtilsMockFns.mockGetServiceAccountToken,
|
||||
safeAccountInsert: authOAuthUtilsMockFns.mockSafeAccountInsert,
|
||||
getCredential: authOAuthUtilsMockFns.mockGetCredential,
|
||||
getOAuthToken: authOAuthUtilsMockFns.mockGetOAuthToken,
|
||||
refreshAccessTokenIfNeeded: authOAuthUtilsMockFns.mockRefreshAccessTokenIfNeeded,
|
||||
refreshTokenIfNeeded: authOAuthUtilsMockFns.mockRefreshTokenIfNeeded,
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { vi } from 'vitest'
|
||||
|
||||
/**
|
||||
* Mock user interface for authentication testing.
|
||||
*/
|
||||
export interface MockUser {
|
||||
id: string
|
||||
email: string
|
||||
name?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Controllable mock functions for `@/lib/auth`. Override per-test with
|
||||
* `authMockFns.mockGetSession.mockResolvedValueOnce(...)`.
|
||||
*/
|
||||
export const authMockFns = {
|
||||
mockGetSession: vi.fn(),
|
||||
}
|
||||
|
||||
/**
|
||||
* Static mock module for `@/lib/auth`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* vi.mock('@/lib/auth', () => authMock)
|
||||
* ```
|
||||
*/
|
||||
export const authMock = {
|
||||
getSession: authMockFns.mockGetSession,
|
||||
auth: {
|
||||
api: {
|
||||
getSession: authMockFns.mockGetSession,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,344 @@
|
||||
/**
|
||||
* Mock block configurations for serializer and related tests.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { blocksMock, mockBlockConfigs } from '@sim/testing/mocks'
|
||||
*
|
||||
* vi.mock('@/blocks', () => blocksMock)
|
||||
*
|
||||
* // Or use individual configs
|
||||
* const starterConfig = mockBlockConfigs.starter
|
||||
* ```
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
/**
|
||||
* Mock block configurations that mirror the real block registry.
|
||||
* Used for testing serialization, deserialization, and validation.
|
||||
*/
|
||||
export const mockBlockConfigs: Record<string, any> = {
|
||||
starter: {
|
||||
name: 'Starter',
|
||||
description: 'Start of the workflow',
|
||||
category: 'flow',
|
||||
bgColor: '#4CAF50',
|
||||
tools: {
|
||||
access: ['starter'],
|
||||
config: { tool: () => 'starter' },
|
||||
},
|
||||
subBlocks: [
|
||||
{ id: 'description', type: 'long-input', label: 'Description' },
|
||||
{ id: 'inputFormat', type: 'table', label: 'Input Format' },
|
||||
],
|
||||
inputs: {},
|
||||
},
|
||||
agent: {
|
||||
name: 'Agent',
|
||||
description: 'AI Agent',
|
||||
category: 'ai',
|
||||
bgColor: '#2196F3',
|
||||
tools: {
|
||||
access: ['anthropic_chat', 'openai_chat', 'google_chat'],
|
||||
config: {
|
||||
tool: (params: Record<string, any>) => {
|
||||
const model = params.model || 'gpt-4o'
|
||||
if (model.includes('claude')) return 'anthropic'
|
||||
if (model.includes('gpt') || model.includes('o1')) return 'openai'
|
||||
if (model.includes('gemini')) return 'google'
|
||||
return 'openai'
|
||||
},
|
||||
},
|
||||
},
|
||||
subBlocks: [
|
||||
{ id: 'provider', type: 'dropdown', label: 'Provider' },
|
||||
{ id: 'model', type: 'dropdown', label: 'Model' },
|
||||
{ id: 'prompt', type: 'long-input', label: 'Prompt' },
|
||||
{ id: 'system', type: 'long-input', label: 'System Message' },
|
||||
{ id: 'tools', type: 'tool-input', label: 'Tools' },
|
||||
{ id: 'responseFormat', type: 'code', label: 'Response Format' },
|
||||
{ id: 'messages', type: 'messages-input', label: 'Messages' },
|
||||
],
|
||||
inputs: {
|
||||
input: { type: 'string' },
|
||||
tools: { type: 'array' },
|
||||
},
|
||||
},
|
||||
function: {
|
||||
name: 'Function',
|
||||
description: 'Execute custom code',
|
||||
category: 'code',
|
||||
bgColor: '#9C27B0',
|
||||
tools: {
|
||||
access: ['function'],
|
||||
config: { tool: () => 'function' },
|
||||
},
|
||||
subBlocks: [
|
||||
{ id: 'code', type: 'code', label: 'Code' },
|
||||
{ id: 'language', type: 'dropdown', label: 'Language' },
|
||||
],
|
||||
inputs: { input: { type: 'any' } },
|
||||
},
|
||||
condition: {
|
||||
name: 'Condition',
|
||||
description: 'Branch based on condition',
|
||||
category: 'flow',
|
||||
bgColor: '#FF9800',
|
||||
tools: {
|
||||
access: ['condition'],
|
||||
config: { tool: () => 'condition' },
|
||||
},
|
||||
subBlocks: [{ id: 'condition', type: 'long-input', label: 'Condition' }],
|
||||
inputs: { input: { type: 'any' } },
|
||||
},
|
||||
api: {
|
||||
name: 'API',
|
||||
description: 'Make API request',
|
||||
category: 'data',
|
||||
bgColor: '#E91E63',
|
||||
tools: {
|
||||
access: ['api'],
|
||||
config: { tool: () => 'api' },
|
||||
},
|
||||
subBlocks: [
|
||||
{ id: 'url', type: 'short-input', label: 'URL' },
|
||||
{ id: 'method', type: 'dropdown', label: 'Method' },
|
||||
{ id: 'headers', type: 'table', label: 'Headers' },
|
||||
{ id: 'body', type: 'long-input', label: 'Body' },
|
||||
],
|
||||
inputs: {},
|
||||
},
|
||||
webhook: {
|
||||
name: 'Webhook',
|
||||
description: 'Webhook trigger',
|
||||
category: 'triggers',
|
||||
bgColor: '#4CAF50',
|
||||
tools: {
|
||||
access: ['webhook'],
|
||||
config: { tool: () => 'webhook' },
|
||||
},
|
||||
subBlocks: [{ id: 'path', type: 'short-input', label: 'Path' }],
|
||||
inputs: {},
|
||||
},
|
||||
jina: {
|
||||
name: 'Jina',
|
||||
description: 'Convert website content into text',
|
||||
category: 'tools',
|
||||
bgColor: '#333333',
|
||||
tools: {
|
||||
access: ['jina_read_url'],
|
||||
config: { tool: () => 'jina_read_url' },
|
||||
},
|
||||
subBlocks: [
|
||||
{ id: 'url', type: 'short-input', title: 'URL', required: true },
|
||||
{ id: 'apiKey', type: 'short-input', title: 'API Key', required: true },
|
||||
],
|
||||
inputs: {
|
||||
url: { type: 'string' },
|
||||
apiKey: { type: 'string' },
|
||||
},
|
||||
},
|
||||
reddit: {
|
||||
name: 'Reddit',
|
||||
description: 'Access Reddit data and content',
|
||||
category: 'tools',
|
||||
bgColor: '#FF5700',
|
||||
tools: {
|
||||
access: ['reddit_get_posts', 'reddit_get_comments'],
|
||||
config: { tool: () => 'reddit_get_posts' },
|
||||
},
|
||||
subBlocks: [
|
||||
{ id: 'operation', type: 'dropdown', title: 'Operation', required: true },
|
||||
{ id: 'credential', type: 'oauth-input', title: 'Reddit Account', required: true },
|
||||
{ id: 'subreddit', type: 'short-input', title: 'Subreddit', required: true },
|
||||
],
|
||||
inputs: {
|
||||
operation: { type: 'string' },
|
||||
credential: { type: 'string' },
|
||||
subreddit: { type: 'string' },
|
||||
},
|
||||
},
|
||||
slack: {
|
||||
name: 'Slack',
|
||||
description: 'Send messages to Slack',
|
||||
category: 'tools',
|
||||
bgColor: '#611f69',
|
||||
tools: {
|
||||
access: ['slack_send_message'],
|
||||
config: { tool: () => 'slack_send_message' },
|
||||
},
|
||||
subBlocks: [
|
||||
{
|
||||
id: 'channel',
|
||||
type: 'dropdown',
|
||||
title: 'Channel',
|
||||
mode: 'basic',
|
||||
canonicalParamId: 'channel',
|
||||
},
|
||||
{
|
||||
id: 'manualChannel',
|
||||
type: 'short-input',
|
||||
title: 'Channel ID',
|
||||
mode: 'advanced',
|
||||
canonicalParamId: 'channel',
|
||||
},
|
||||
{ id: 'text', type: 'long-input', title: 'Message' },
|
||||
{ id: 'username', type: 'short-input', title: 'Username', mode: 'both' },
|
||||
],
|
||||
inputs: {
|
||||
channel: { type: 'string' },
|
||||
manualChannel: { type: 'string' },
|
||||
text: { type: 'string' },
|
||||
username: { type: 'string' },
|
||||
},
|
||||
},
|
||||
agentWithMemories: {
|
||||
name: 'Agent with Memories',
|
||||
description: 'AI Agent with memory support',
|
||||
category: 'ai',
|
||||
bgColor: '#2196F3',
|
||||
tools: {
|
||||
access: ['anthropic_chat'],
|
||||
config: { tool: () => 'anthropic_chat' },
|
||||
},
|
||||
subBlocks: [
|
||||
{ id: 'systemPrompt', type: 'long-input', title: 'System Prompt' },
|
||||
{ id: 'userPrompt', type: 'long-input', title: 'User Prompt' },
|
||||
{ id: 'memories', type: 'short-input', title: 'Memories', mode: 'advanced' },
|
||||
{ id: 'model', type: 'dropdown', title: 'Model' },
|
||||
],
|
||||
inputs: {
|
||||
systemPrompt: { type: 'string' },
|
||||
userPrompt: { type: 'string' },
|
||||
memories: { type: 'array' },
|
||||
model: { type: 'string' },
|
||||
},
|
||||
},
|
||||
conditional_block: {
|
||||
name: 'Conditional Block',
|
||||
description: 'Block with conditional fields',
|
||||
category: 'tools',
|
||||
bgColor: '#FF5700',
|
||||
tools: {
|
||||
access: ['conditional_tool'],
|
||||
config: { tool: () => 'conditional_tool' },
|
||||
},
|
||||
subBlocks: [
|
||||
{ id: 'mode', type: 'dropdown', label: 'Mode' },
|
||||
{
|
||||
id: 'optionA',
|
||||
type: 'short-input',
|
||||
label: 'Option A',
|
||||
condition: { field: 'mode', value: 'a' },
|
||||
},
|
||||
{
|
||||
id: 'optionB',
|
||||
type: 'short-input',
|
||||
label: 'Option B',
|
||||
condition: { field: 'mode', value: 'b' },
|
||||
},
|
||||
{
|
||||
id: 'notModeC',
|
||||
type: 'short-input',
|
||||
label: 'Not Mode C',
|
||||
condition: { field: 'mode', value: 'c', not: true },
|
||||
},
|
||||
{
|
||||
id: 'complexCondition',
|
||||
type: 'short-input',
|
||||
label: 'Complex',
|
||||
condition: { field: 'mode', value: 'a', and: { field: 'optionA', value: 'special' } },
|
||||
},
|
||||
{
|
||||
id: 'arrayCondition',
|
||||
type: 'short-input',
|
||||
label: 'Array Condition',
|
||||
condition: { field: 'mode', value: ['a', 'b'] },
|
||||
},
|
||||
],
|
||||
inputs: {},
|
||||
},
|
||||
wait: {
|
||||
name: 'Wait',
|
||||
description: 'Pause workflow execution for a specified time delay',
|
||||
category: 'blocks',
|
||||
bgColor: '#F59E0B',
|
||||
tools: {
|
||||
access: [],
|
||||
},
|
||||
subBlocks: [
|
||||
{
|
||||
id: 'timeValue',
|
||||
title: 'Wait Amount',
|
||||
type: 'short-input',
|
||||
placeholder: '10',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'timeUnit',
|
||||
title: 'Unit',
|
||||
type: 'dropdown',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
inputs: {
|
||||
timeValue: { type: 'string' },
|
||||
timeUnit: { type: 'string' },
|
||||
},
|
||||
outputs: {
|
||||
waitDuration: { type: 'number' },
|
||||
status: { type: 'string' },
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a getBlock function that returns mock block configs.
|
||||
* Can be extended with additional block types.
|
||||
*/
|
||||
export function createMockGetBlock(extraConfigs: Record<string, any> = {}) {
|
||||
const configs = { ...mockBlockConfigs, ...extraConfigs }
|
||||
return (type: string) => configs[type] || null
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock tool configurations for validation tests.
|
||||
*/
|
||||
export const mockToolConfigs: Record<string, any> = {
|
||||
jina_read_url: {
|
||||
params: {
|
||||
url: { visibility: 'user-or-llm', required: true },
|
||||
apiKey: { visibility: 'user-only', required: true },
|
||||
},
|
||||
},
|
||||
reddit_get_posts: {
|
||||
params: {
|
||||
subreddit: { visibility: 'user-or-llm', required: true },
|
||||
credential: { visibility: 'user-only', required: true },
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a getTool function that returns mock tool configs.
|
||||
*/
|
||||
export function createMockGetTool(extraConfigs: Record<string, any> = {}) {
|
||||
const configs = { ...mockToolConfigs, ...extraConfigs }
|
||||
return (toolId: string) => configs[toolId] || null
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-configured blocks mock for use with vi.mock('@/blocks', () => blocksMock).
|
||||
*/
|
||||
export const blocksMock = {
|
||||
getBlock: createMockGetBlock(),
|
||||
getAllBlocks: () => Object.values(mockBlockConfigs),
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-configured tools/utils mock for use with vi.mock('@/tools/utils', () => toolsUtilsMock).
|
||||
*/
|
||||
export const toolsUtilsMock = {
|
||||
getTool: createMockGetTool(),
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import { vi } from 'vitest'
|
||||
|
||||
/**
|
||||
* Frozen mirror of the `NotificationStatus` const from
|
||||
* `@/lib/copilot/request/http`. Matches the real values so route code using
|
||||
* e.g. `NotificationStatus.success` keeps resolving under mock.
|
||||
*/
|
||||
const NotificationStatusMock = {
|
||||
pending: 'pending',
|
||||
background: 'background',
|
||||
success: 'success',
|
||||
error: 'error',
|
||||
cancelled: 'cancelled',
|
||||
} as const
|
||||
|
||||
/**
|
||||
* Controllable mock functions for `@/lib/copilot/request/http`.
|
||||
* Response helpers default to returning minimal Response-like objects so
|
||||
* handler tests can assert `res.status` and `await res.json()`.
|
||||
* `createRequestTracker` returns a stable tracker with `requestId`,
|
||||
* `startTime`, and a `getDuration()` that always returns `0`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { copilotHttpMockFns } from '@sim/testing'
|
||||
*
|
||||
* copilotHttpMockFns.mockAuthenticateCopilotRequestSessionOnly.mockResolvedValue({
|
||||
* userId: 'user-1',
|
||||
* isAuthenticated: true,
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export const copilotHttpMockFns = {
|
||||
mockCreateUnauthorizedResponse: vi.fn(() => ({
|
||||
status: 401,
|
||||
ok: false,
|
||||
json: async () => ({ error: 'Unauthorized' }),
|
||||
})),
|
||||
mockCreateBadRequestResponse: vi.fn((message: string) => ({
|
||||
status: 400,
|
||||
ok: false,
|
||||
json: async () => ({ error: message }),
|
||||
})),
|
||||
mockCreateNotFoundResponse: vi.fn((message: string) => ({
|
||||
status: 404,
|
||||
ok: false,
|
||||
json: async () => ({ error: message }),
|
||||
})),
|
||||
mockCreateInternalServerErrorResponse: vi.fn((message: string) => ({
|
||||
status: 500,
|
||||
ok: false,
|
||||
json: async () => ({ error: message }),
|
||||
})),
|
||||
mockCreateRequestId: vi.fn(() => 'test-request-id'),
|
||||
mockCreateShortRequestId: vi.fn(() => 'test-req'),
|
||||
mockCreateRequestTracker: vi.fn(() => ({
|
||||
requestId: 'test-req',
|
||||
startTime: 0,
|
||||
getDuration: () => 0,
|
||||
})),
|
||||
mockAuthenticateCopilotRequestSessionOnly: vi.fn(),
|
||||
mockCheckInternalApiKey: vi.fn(),
|
||||
}
|
||||
|
||||
/**
|
||||
* Static mock module for `@/lib/copilot/request/http`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* vi.mock('@/lib/copilot/request/http', () => copilotHttpMock)
|
||||
* ```
|
||||
*/
|
||||
export const copilotHttpMock = {
|
||||
NotificationStatus: NotificationStatusMock,
|
||||
createUnauthorizedResponse: copilotHttpMockFns.mockCreateUnauthorizedResponse,
|
||||
createBadRequestResponse: copilotHttpMockFns.mockCreateBadRequestResponse,
|
||||
createNotFoundResponse: copilotHttpMockFns.mockCreateNotFoundResponse,
|
||||
createInternalServerErrorResponse: copilotHttpMockFns.mockCreateInternalServerErrorResponse,
|
||||
createRequestId: copilotHttpMockFns.mockCreateRequestId,
|
||||
createShortRequestId: copilotHttpMockFns.mockCreateShortRequestId,
|
||||
createRequestTracker: copilotHttpMockFns.mockCreateRequestTracker,
|
||||
authenticateCopilotRequestSessionOnly:
|
||||
copilotHttpMockFns.mockAuthenticateCopilotRequestSessionOnly,
|
||||
checkInternalApiKey: copilotHttpMockFns.mockCheckInternalApiKey,
|
||||
}
|
||||
@@ -0,0 +1,353 @@
|
||||
import { vi } from 'vitest'
|
||||
|
||||
/**
|
||||
* Creates mock SQL template literal function.
|
||||
* Mimics drizzle-orm's sql tagged template.
|
||||
*/
|
||||
export function createMockSql() {
|
||||
const sqlFn = (strings: TemplateStringsArray, ...values: any[]) => ({
|
||||
strings,
|
||||
values,
|
||||
toSQL: () => ({ sql: strings.join('?'), params: values }),
|
||||
})
|
||||
|
||||
// Add sql.raw method used by some queries
|
||||
sqlFn.raw = (rawSql: string) => ({
|
||||
rawSql,
|
||||
toSQL: () => ({ sql: rawSql, params: [] }),
|
||||
})
|
||||
|
||||
// Add sql.join method used to combine multiple SQL fragments
|
||||
sqlFn.join = (fragments: any[], separator: any) => ({
|
||||
fragments,
|
||||
separator,
|
||||
toSQL: () => ({
|
||||
sql: fragments.map((f) => f?.toSQL?.()?.sql || String(f)).join(separator?.rawSql || ', '),
|
||||
params: fragments.flatMap((f) => f?.toSQL?.()?.params || []),
|
||||
}),
|
||||
})
|
||||
|
||||
return sqlFn
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates mock SQL operators (eq, and, or, etc.).
|
||||
*/
|
||||
export function createMockSqlOperators() {
|
||||
return {
|
||||
eq: vi.fn((a, b) => ({ type: 'eq', left: a, right: b })),
|
||||
ne: vi.fn((a, b) => ({ type: 'ne', left: a, right: b })),
|
||||
gt: vi.fn((a, b) => ({ type: 'gt', left: a, right: b })),
|
||||
gte: vi.fn((a, b) => ({ type: 'gte', left: a, right: b })),
|
||||
lt: vi.fn((a, b) => ({ type: 'lt', left: a, right: b })),
|
||||
lte: vi.fn((a, b) => ({ type: 'lte', left: a, right: b })),
|
||||
count: vi.fn((column) => ({ type: 'count', column })),
|
||||
avg: vi.fn((column) => ({ type: 'avg', column })),
|
||||
sum: vi.fn((column) => ({ type: 'sum', column })),
|
||||
min: vi.fn((column) => ({ type: 'min', column })),
|
||||
max: vi.fn((column) => ({ type: 'max', column })),
|
||||
and: vi.fn((...conditions) => ({ type: 'and', conditions })),
|
||||
or: vi.fn((...conditions) => ({ type: 'or', conditions })),
|
||||
not: vi.fn((condition) => ({ type: 'not', condition })),
|
||||
isNull: vi.fn((column) => ({ type: 'isNull', column })),
|
||||
isNotNull: vi.fn((column) => ({ type: 'isNotNull', column })),
|
||||
inArray: vi.fn((column, values) => ({ type: 'inArray', column, values })),
|
||||
notInArray: vi.fn((column, values) => ({ type: 'notInArray', column, values })),
|
||||
exists: vi.fn((subquery) => ({ type: 'exists', subquery })),
|
||||
notExists: vi.fn((subquery) => ({ type: 'notExists', subquery })),
|
||||
like: vi.fn((column, pattern) => ({ type: 'like', column, pattern })),
|
||||
ilike: vi.fn((column, pattern) => ({ type: 'ilike', column, pattern })),
|
||||
desc: vi.fn((column) => ({ type: 'desc', column })),
|
||||
asc: vi.fn((column) => ({ type: 'asc', column })),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-wired chain of vi.fn()s for drizzle-style DB queries.
|
||||
*
|
||||
* Each builder step is a stable, module-level `vi.fn()` — safe to reference
|
||||
* inside hoisted `vi.mock()` factories (same pattern as `authMockFns`). Chains
|
||||
* are wired at module load time:
|
||||
*
|
||||
* - `select().from().where()` → returns a builder with `.limit` / `.orderBy` /
|
||||
* `.returning` / `.groupBy` / `.for` terminals
|
||||
* - `select().from().innerJoin()|leftJoin()` → returns the same where-builder
|
||||
* - `insert().values().returning()` / `update().set().where()` / `delete().where()`
|
||||
*
|
||||
* Terminals (`limit`, `orderBy`, `returning`, `groupBy`, `for`, `values`)
|
||||
* default to resolving `[]` (or `undefined` for `values`). Override per-test
|
||||
* with `dbChainMockFns.limit.mockResolvedValueOnce([...])`. `for` mirrors
|
||||
* drizzle's `.for('update')` — it returns a Promise with `.limit` / `.orderBy`
|
||||
* / `.returning` / `.groupBy` attached, so both `await .where().for('update')`
|
||||
* (terminal) and `await .where().for('update').limit(1)` (chained) work.
|
||||
* Override the terminal result with `dbChainMockFns.for.mockResolvedValueOnce(
|
||||
* [...])`; override the chained result by mocking the downstream terminal
|
||||
* (e.g. `dbChainMockFns.limit.mockResolvedValueOnce([...])`).
|
||||
*
|
||||
* `vi.clearAllMocks()` clears call history but preserves default wiring. Tests
|
||||
* that replace a wiring with `mockReturnValue(...)` (not `...Once`) must re-wire
|
||||
* in their own `beforeEach`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { dbChainMock, dbChainMockFns } from '@sim/testing'
|
||||
* vi.mock('@sim/db', () => dbChainMock)
|
||||
*
|
||||
* it('finds rows', async () => {
|
||||
* dbChainMockFns.limit.mockResolvedValueOnce([{ id: 'w-1' }])
|
||||
* // ... exercise code that hits db.select().from().where().limit() ...
|
||||
* expect(dbChainMockFns.where).toHaveBeenCalled()
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
const offset = vi.fn(() => Promise.resolve([] as unknown[]))
|
||||
// `.limit()` returns a builder that is awaitable (default empty page) and also
|
||||
// exposes `.offset()` for keyset/OFFSET paging (`.limit(n).offset(m)`).
|
||||
const limitBuilder = () => {
|
||||
const thenable: any = Promise.resolve([] as unknown[])
|
||||
thenable.offset = offset
|
||||
return thenable
|
||||
}
|
||||
const limit = vi.fn(limitBuilder)
|
||||
const returning = vi.fn(() => Promise.resolve([] as unknown[]))
|
||||
const execute = vi.fn(() => Promise.resolve([] as unknown[]))
|
||||
|
||||
const terminalBuilder = () => {
|
||||
const thenable: any = Promise.resolve([] as unknown[])
|
||||
thenable.limit = limit
|
||||
thenable.orderBy = orderBy
|
||||
thenable.returning = returning
|
||||
thenable.groupBy = groupBy
|
||||
thenable.for = forClause
|
||||
return thenable
|
||||
}
|
||||
|
||||
const orderBy = vi.fn(terminalBuilder)
|
||||
const having = vi.fn(terminalBuilder)
|
||||
const groupBy = vi.fn(() => {
|
||||
const builder = terminalBuilder()
|
||||
builder.having = having
|
||||
return builder
|
||||
})
|
||||
const forBuilder = terminalBuilder
|
||||
const forClause = vi.fn(forBuilder)
|
||||
|
||||
const onConflictDoUpdate = vi.fn(() => ({ returning }) as unknown as Promise<void>)
|
||||
const onConflictDoNothing = vi.fn(() => ({ returning }) as unknown as Promise<void>)
|
||||
|
||||
const whereBuilder = () => {
|
||||
// Some call sites (e.g. `db.select().from(t).where(eq(...))` with no
|
||||
// limit/orderBy) await the where directly. Make the builder a thenable so
|
||||
// those calls resolve to the default empty array.
|
||||
const thenable: any = Promise.resolve([] as unknown[])
|
||||
thenable.limit = limit
|
||||
thenable.orderBy = orderBy
|
||||
thenable.returning = returning
|
||||
thenable.groupBy = groupBy
|
||||
thenable.for = forClause
|
||||
return thenable
|
||||
}
|
||||
const where = vi.fn(whereBuilder)
|
||||
|
||||
const joinBuilder = (): { where: typeof where; innerJoin: any; leftJoin: any } => ({
|
||||
where,
|
||||
innerJoin,
|
||||
leftJoin,
|
||||
})
|
||||
const innerJoin: ReturnType<typeof vi.fn> = vi.fn(joinBuilder)
|
||||
const leftJoin: ReturnType<typeof vi.fn> = vi.fn(joinBuilder)
|
||||
const from = vi.fn(joinBuilder)
|
||||
|
||||
const select = vi.fn(() => ({ from }))
|
||||
const selectDistinct = vi.fn(() => ({ from }))
|
||||
const selectDistinctOn = vi.fn(() => ({ from }))
|
||||
const values = vi.fn(() => ({ returning, onConflictDoUpdate, onConflictDoNothing }))
|
||||
const insert = vi.fn(() => ({ values }))
|
||||
const set = vi.fn(() => ({ where }))
|
||||
const update = vi.fn(() => ({ set }))
|
||||
const del = vi.fn(() => ({ where }))
|
||||
const transaction: ReturnType<typeof vi.fn> = vi.fn(
|
||||
async (cb: (tx: any) => unknown): Promise<unknown> => cb(dbChainMock.db)
|
||||
)
|
||||
|
||||
export const dbChainMockFns = {
|
||||
select,
|
||||
selectDistinct,
|
||||
selectDistinctOn,
|
||||
from,
|
||||
where,
|
||||
limit,
|
||||
offset,
|
||||
orderBy,
|
||||
returning,
|
||||
innerJoin,
|
||||
leftJoin,
|
||||
groupBy,
|
||||
having,
|
||||
execute,
|
||||
for: forClause,
|
||||
insert,
|
||||
values,
|
||||
onConflictDoUpdate,
|
||||
onConflictDoNothing,
|
||||
update,
|
||||
set,
|
||||
delete: del,
|
||||
transaction,
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-applies the default chain wiring to every `dbChainMockFns` entry. Call
|
||||
* this in `beforeEach` (after `vi.clearAllMocks()`) if any test uses
|
||||
* `mockReturnValue` / `mockResolvedValue` (permanent overrides) — this
|
||||
* guarantees the next test starts with fresh defaults.
|
||||
*
|
||||
* Not needed if tests exclusively use the `...Once` variants, since those
|
||||
* auto-expire after one call.
|
||||
*/
|
||||
export function resetDbChainMock(): void {
|
||||
select.mockImplementation(() => ({ from }))
|
||||
selectDistinct.mockImplementation(() => ({ from }))
|
||||
selectDistinctOn.mockImplementation(() => ({ from }))
|
||||
from.mockImplementation(joinBuilder)
|
||||
innerJoin.mockImplementation(joinBuilder)
|
||||
leftJoin.mockImplementation(joinBuilder)
|
||||
where.mockImplementation(whereBuilder)
|
||||
insert.mockImplementation(() => ({ values }))
|
||||
values.mockImplementation(() => ({ returning, onConflictDoUpdate, onConflictDoNothing }))
|
||||
onConflictDoUpdate.mockImplementation(() => ({ returning }) as unknown as Promise<void>)
|
||||
onConflictDoNothing.mockImplementation(() => ({ returning }) as unknown as Promise<void>)
|
||||
update.mockImplementation(() => ({ set }))
|
||||
set.mockImplementation(() => ({ where }))
|
||||
del.mockImplementation(() => ({ where }))
|
||||
limit.mockImplementation(limitBuilder)
|
||||
offset.mockImplementation(() => Promise.resolve([] as unknown[]))
|
||||
orderBy.mockImplementation(terminalBuilder)
|
||||
returning.mockImplementation(() => Promise.resolve([] as unknown[]))
|
||||
having.mockImplementation(terminalBuilder)
|
||||
groupBy.mockImplementation(() => {
|
||||
const builder = terminalBuilder()
|
||||
builder.having = having
|
||||
return builder
|
||||
})
|
||||
execute.mockImplementation(() => Promise.resolve([] as unknown[]))
|
||||
forClause.mockImplementation(forBuilder)
|
||||
transaction.mockImplementation(async (cb: (tx: typeof dbChainMock.db) => unknown) =>
|
||||
cb(dbChainMock.db)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Static mock module for `@sim/db` backed by `dbChainMockFns`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* vi.mock('@sim/db', () => dbChainMock)
|
||||
* ```
|
||||
*/
|
||||
const dbChainInstance = {
|
||||
select,
|
||||
selectDistinct,
|
||||
selectDistinctOn,
|
||||
insert,
|
||||
update,
|
||||
delete: del,
|
||||
execute,
|
||||
transaction,
|
||||
}
|
||||
|
||||
export const dbChainMock = {
|
||||
db: dbChainInstance,
|
||||
/** Same instance as `db` so per-test chain overrides cover both clients. */
|
||||
dbReplica: dbChainInstance,
|
||||
runOutsideTransactionContext: <T>(fn: () => T): T => fn(),
|
||||
instrumentPoolClient: <T>(client: T): T => client,
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a mock database connection.
|
||||
*/
|
||||
export function createMockDb() {
|
||||
// A `where(...)` result that is both awaitable (resolves to `[]`) and exposes
|
||||
// `.limit`/`.orderBy`, so `select().from()[.leftJoin()].where()[.limit()]`
|
||||
// works whether or not a terminal is chained.
|
||||
const whereResult = () => {
|
||||
const thenable: any = Promise.resolve([])
|
||||
thenable.limit = vi.fn(() => Promise.resolve([]))
|
||||
thenable.orderBy = vi.fn(() => Promise.resolve([]))
|
||||
return thenable
|
||||
}
|
||||
const fromBuilder = () => ({
|
||||
where: vi.fn(whereResult),
|
||||
leftJoin: vi.fn(() => ({ where: vi.fn(whereResult) })),
|
||||
innerJoin: vi.fn(() => ({ where: vi.fn(whereResult) })),
|
||||
})
|
||||
|
||||
return {
|
||||
select: vi.fn(() => ({
|
||||
from: vi.fn(fromBuilder),
|
||||
})),
|
||||
selectDistinct: vi.fn(() => ({
|
||||
from: vi.fn(fromBuilder),
|
||||
})),
|
||||
insert: vi.fn(() => ({
|
||||
values: vi.fn(() => ({
|
||||
returning: vi.fn(() => Promise.resolve([])),
|
||||
onConflictDoUpdate: vi.fn(() => ({
|
||||
returning: vi.fn(() => Promise.resolve([])),
|
||||
})),
|
||||
onConflictDoNothing: vi.fn(() => ({
|
||||
returning: vi.fn(() => Promise.resolve([])),
|
||||
})),
|
||||
})),
|
||||
})),
|
||||
update: vi.fn(() => ({
|
||||
set: vi.fn(() => ({
|
||||
where: vi.fn(() => ({
|
||||
returning: vi.fn(() => Promise.resolve([])),
|
||||
})),
|
||||
})),
|
||||
})),
|
||||
delete: vi.fn(() => ({
|
||||
where: vi.fn(() => ({
|
||||
returning: vi.fn(() => Promise.resolve([])),
|
||||
})),
|
||||
})),
|
||||
transaction: vi.fn(async (callback) => callback(createMockDb())),
|
||||
query: vi.fn(() => Promise.resolve([])),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock module for @sim/db.
|
||||
* Use with vi.mock() to replace the real database.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* vi.mock('@sim/db', () => databaseMock)
|
||||
* ```
|
||||
*/
|
||||
const mockDbInstance = createMockDb()
|
||||
|
||||
export const databaseMock = {
|
||||
db: mockDbInstance,
|
||||
/** Same instance as `db` so per-test overrides cover both clients. */
|
||||
dbReplica: mockDbInstance,
|
||||
sql: createMockSql(),
|
||||
runOutsideTransactionContext: <T>(fn: () => T): T => fn(),
|
||||
instrumentPoolClient: <T>(client: T): T => client,
|
||||
...createMockSqlOperators(),
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a mock for drizzle-orm module.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* vi.mock('drizzle-orm', () => drizzleOrmMock)
|
||||
* ```
|
||||
*/
|
||||
export const drizzleOrmMock = {
|
||||
sql: createMockSql(),
|
||||
...createMockSqlOperators(),
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { vi } from 'vitest'
|
||||
|
||||
/**
|
||||
* Controllable mock functions for `@/lib/core/security/encryption`.
|
||||
* Default: `decryptSecret` resolves to `{ decrypted: 'test-decrypted' }`,
|
||||
* `encryptSecret` resolves to `{ encrypted: 'test-encrypted', iv: 'test-iv' }`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { encryptionMockFns } from '@sim/testing'
|
||||
*
|
||||
* encryptionMockFns.mockDecryptSecret.mockResolvedValueOnce({ decrypted: 'my-secret' })
|
||||
* ```
|
||||
*/
|
||||
export const encryptionMockFns = {
|
||||
mockDecryptSecret: vi.fn().mockResolvedValue({ decrypted: 'test-decrypted' }),
|
||||
mockEncryptSecret: vi.fn().mockResolvedValue({ encrypted: 'test-encrypted', iv: 'test-iv' }),
|
||||
}
|
||||
|
||||
/**
|
||||
* Static mock module for `@/lib/core/security/encryption`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* vi.mock('@/lib/core/security/encryption', () => encryptionMock)
|
||||
* ```
|
||||
*/
|
||||
export const encryptionMock = {
|
||||
decryptSecret: encryptionMockFns.mockDecryptSecret,
|
||||
encryptSecret: encryptionMockFns.mockEncryptSecret,
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { vi } from 'vitest'
|
||||
|
||||
/**
|
||||
* Static mock module for `@/lib/core/config/env-flags`.
|
||||
* All boolean flags default to `false` for safe test isolation.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* vi.mock('@/lib/core/config/env-flags', () => envFlagsMock)
|
||||
* ```
|
||||
*/
|
||||
export const envFlagsMock = {
|
||||
isProd: false,
|
||||
isDev: false,
|
||||
isTest: true,
|
||||
isHosted: false,
|
||||
isBillingEnabled: false,
|
||||
isEmailVerificationEnabled: false,
|
||||
isAuthDisabled: false,
|
||||
isPrivateDatabaseHostsAllowed: false,
|
||||
isRegistrationDisabled: false,
|
||||
isEmailPasswordEnabled: false,
|
||||
isTriggerDevEnabled: false,
|
||||
isSsoEnabled: false,
|
||||
isAccessControlEnabled: false,
|
||||
isOrganizationsEnabled: false,
|
||||
isInboxEnabled: false,
|
||||
isWhitelabelingEnabled: false,
|
||||
isAuditLogsEnabled: false,
|
||||
isDataRetentionEnabled: false,
|
||||
isE2bEnabled: false,
|
||||
isE2BDocEnabled: false,
|
||||
isOllamaConfigured: false,
|
||||
isAzureConfigured: false,
|
||||
isInvitationsDisabled: false,
|
||||
isPublicApiDisabled: false,
|
||||
isGoogleAuthDisabled: false,
|
||||
isGithubAuthDisabled: false,
|
||||
isReactGrabEnabled: false,
|
||||
isReactScanEnabled: false,
|
||||
getAllowedIntegrationsFromEnv: vi.fn().mockReturnValue(null),
|
||||
getBlacklistedProvidersFromEnv: vi.fn().mockReturnValue([]),
|
||||
getAllowedMcpDomainsFromEnv: vi.fn().mockReturnValue(null),
|
||||
getCostMultiplier: vi.fn().mockReturnValue(1),
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import { vi } from 'vitest'
|
||||
|
||||
/**
|
||||
* Default mock environment values for testing
|
||||
*/
|
||||
export const defaultMockEnv = {
|
||||
// Core
|
||||
DATABASE_URL: 'postgresql://test:test@localhost:5432/test',
|
||||
BETTER_AUTH_URL: 'https://test.sim.ai',
|
||||
BETTER_AUTH_SECRET: 'test-secret-that-is-at-least-32-chars-long',
|
||||
ENCRYPTION_KEY: 'test-encryption-key-32-chars-long!',
|
||||
INTERNAL_API_SECRET: 'test-internal-api-secret-32-chars!',
|
||||
|
||||
// Email
|
||||
RESEND_API_KEY: 'test-resend-key',
|
||||
FROM_EMAIL_ADDRESS: 'Sim <noreply@test.sim.ai>',
|
||||
EMAIL_DOMAIN: 'test.sim.ai',
|
||||
PERSONAL_EMAIL_FROM: 'Test <test@test.sim.ai>',
|
||||
|
||||
// URLs
|
||||
NEXT_PUBLIC_APP_URL: 'https://test.sim.ai',
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a mock getEnv function that returns values from the provided env object
|
||||
*/
|
||||
export function createMockGetEnv(envValues: Record<string, string | undefined> = defaultMockEnv) {
|
||||
return vi.fn((key: string) => envValues[key])
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a complete env mock object for use with vi.doMock
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* vi.doMock('@/lib/core/config/env', () => createEnvMock())
|
||||
*
|
||||
* // With custom values
|
||||
* vi.doMock('@/lib/core/config/env', () => createEnvMock({
|
||||
* NEXT_PUBLIC_APP_URL: 'https://custom.example.com',
|
||||
* }))
|
||||
* ```
|
||||
*/
|
||||
export function createEnvMock(overrides: Record<string, string | undefined> = {}) {
|
||||
const envValues = { ...defaultMockEnv, ...overrides }
|
||||
|
||||
return {
|
||||
env: envValues,
|
||||
getEnv: createMockGetEnv(envValues),
|
||||
isTruthy: (value: string | boolean | number | undefined) =>
|
||||
typeof value === 'string' ? value.toLowerCase() === 'true' || value === '1' : Boolean(value),
|
||||
isFalsy: (value: string | boolean | number | undefined) =>
|
||||
typeof value === 'string'
|
||||
? value.toLowerCase() === 'false' || value === '0'
|
||||
: value === false,
|
||||
envBoolean: (value: boolean | string | undefined | null): boolean | undefined => {
|
||||
if (typeof value === 'boolean') return value
|
||||
if (value === undefined || value === null || value === '') return undefined
|
||||
const normalized = String(value).trim().toLowerCase()
|
||||
return (
|
||||
normalized === 'true' || normalized === '1' || normalized === 'yes' || normalized === 'on'
|
||||
)
|
||||
},
|
||||
envNumber: (
|
||||
value: number | string | undefined | null,
|
||||
fallback: number,
|
||||
options: { min?: number } = {}
|
||||
): number => {
|
||||
const min = options.min ?? 0
|
||||
if (typeof value === 'number' && Number.isFinite(value) && value >= min) return value
|
||||
if (value === undefined || value === null || value === '') return fallback
|
||||
const parsed = Number(value)
|
||||
return Number.isFinite(parsed) && parsed >= min ? parsed : fallback
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-configured env mock for direct use with vi.mock
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* vi.mock('@/lib/core/config/env', () => envMock)
|
||||
* ```
|
||||
*/
|
||||
export const envMock = createEnvMock()
|
||||
@@ -0,0 +1,31 @@
|
||||
import { vi } from 'vitest'
|
||||
|
||||
/**
|
||||
* Controllable mock functions for `@/lib/execution/preprocessing`.
|
||||
* Default is a bare `vi.fn()` — configure per-test.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { executionPreprocessingMockFns } from '@sim/testing'
|
||||
*
|
||||
* executionPreprocessingMockFns.mockPreprocessExecution.mockResolvedValue({
|
||||
* success: true,
|
||||
* actorUserId: 'user-1',
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export const executionPreprocessingMockFns = {
|
||||
mockPreprocessExecution: vi.fn(),
|
||||
}
|
||||
|
||||
/**
|
||||
* Static mock module for `@/lib/execution/preprocessing`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* vi.mock('@/lib/execution/preprocessing', () => executionPreprocessingMock)
|
||||
* ```
|
||||
*/
|
||||
export const executionPreprocessingMock = {
|
||||
preprocessExecution: executionPreprocessingMockFns.mockPreprocessExecution,
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* Mock utilities for executor handler testing.
|
||||
* Sets up common mocks needed for testing executor block handlers.
|
||||
*
|
||||
* This module is designed to be imported for side effects - the vi.mock calls
|
||||
* are executed at the top level and hoisted by vitest.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // Import at the very top of your test file for side effects
|
||||
* import '@sim/testing/mocks/executor.mock'
|
||||
*
|
||||
* // Then your other imports
|
||||
* import { describe, it, expect } from 'vitest'
|
||||
* ```
|
||||
*/
|
||||
import { vi } from 'vitest'
|
||||
import { setupGlobalFetchMock } from './fetch.mock'
|
||||
import { loggerMock } from './logger.mock'
|
||||
|
||||
// Logger
|
||||
vi.mock('@sim/logger', () => loggerMock)
|
||||
|
||||
// Blocks
|
||||
vi.mock('@/blocks/index', () => ({
|
||||
getBlock: vi.fn(),
|
||||
}))
|
||||
|
||||
// Tools
|
||||
vi.mock('@/tools/utils', () => ({
|
||||
getTool: vi.fn(),
|
||||
getToolAsync: vi.fn(),
|
||||
validateToolRequest: vi.fn(),
|
||||
formatRequestParams: vi.fn(),
|
||||
transformTable: vi.fn(),
|
||||
createParamSchema: vi.fn(),
|
||||
getClientEnvVars: vi.fn(),
|
||||
createCustomToolRequestBody: vi.fn(),
|
||||
validateRequiredParametersAfterMerge: vi.fn(),
|
||||
}))
|
||||
|
||||
// Utils
|
||||
vi.mock('@/lib/core/config/environment', () => ({
|
||||
isHosted: false,
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/core/config/api-keys', () => ({
|
||||
getRotatingApiKey: vi.fn(),
|
||||
}))
|
||||
|
||||
// Tools module
|
||||
vi.mock('@/tools')
|
||||
|
||||
// Providers
|
||||
vi.mock('@/providers', () => ({
|
||||
executeProviderRequest: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/providers/utils', async (importOriginal) => {
|
||||
const actual = await importOriginal()
|
||||
return {
|
||||
...(actual as object),
|
||||
getProviderFromModel: vi.fn(),
|
||||
transformBlockTool: vi.fn(),
|
||||
getBaseModelProviders: vi.fn(() => ({})),
|
||||
}
|
||||
})
|
||||
|
||||
// Executor utilities
|
||||
vi.mock('@/executor/path')
|
||||
vi.mock('@/executor/resolver', () => ({
|
||||
InputResolver: vi.fn(),
|
||||
}))
|
||||
vi.mock('@/executor/utils/http', () => ({
|
||||
buildAuthHeaders: vi.fn().mockResolvedValue({ 'Content-Type': 'application/json' }),
|
||||
buildAPIUrl: vi.fn((path: string) => new URL(path, 'http://localhost:3000')),
|
||||
extractAPIErrorMessage: vi.fn(async (response: Response) => {
|
||||
const defaultMessage = `API request failed with status ${response.status}`
|
||||
try {
|
||||
const errorData = await response.json()
|
||||
return errorData.error || defaultMessage
|
||||
} catch {
|
||||
return defaultMessage
|
||||
}
|
||||
}),
|
||||
}))
|
||||
|
||||
// Specific block utilities
|
||||
vi.mock('@/blocks/blocks/router')
|
||||
|
||||
// Mock blocks module
|
||||
vi.mock('@/blocks')
|
||||
|
||||
// Mock fetch for server requests
|
||||
setupGlobalFetchMock()
|
||||
|
||||
// Mock process.env
|
||||
process.env.NEXT_PUBLIC_APP_URL = 'http://localhost:3000'
|
||||
@@ -0,0 +1,135 @@
|
||||
import { vi } from 'vitest'
|
||||
|
||||
/**
|
||||
* Type for mock fetch response configuration.
|
||||
*/
|
||||
export interface MockFetchResponse {
|
||||
status?: number
|
||||
statusText?: string
|
||||
ok?: boolean
|
||||
headers?: Record<string, string>
|
||||
json?: any
|
||||
text?: string
|
||||
body?: any
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a mock fetch function that returns configured responses.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const mockFetch = createMockFetch({
|
||||
* json: { data: 'test' },
|
||||
* status: 200
|
||||
* })
|
||||
* global.fetch = mockFetch
|
||||
* ```
|
||||
*/
|
||||
export function createMockFetch(defaultResponse: MockFetchResponse = {}) {
|
||||
const mockFn = vi.fn(async (_url: string | URL | Request, _init?: RequestInit) => {
|
||||
return createMockResponse(defaultResponse)
|
||||
})
|
||||
|
||||
return mockFn
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a mock Response object.
|
||||
*/
|
||||
export function createMockResponse(config: MockFetchResponse = {}): Response {
|
||||
const status = config.status ?? 200
|
||||
const ok = config.ok ?? (status >= 200 && status < 300)
|
||||
|
||||
return {
|
||||
status,
|
||||
statusText: config.statusText ?? (ok ? 'OK' : 'Error'),
|
||||
ok,
|
||||
headers: new Headers(config.headers ?? {}),
|
||||
json: vi.fn(async () => config.json ?? {}),
|
||||
text: vi.fn(async () => config.text ?? JSON.stringify(config.json ?? {})),
|
||||
body: config.body ?? null,
|
||||
bodyUsed: false,
|
||||
arrayBuffer: vi.fn(async () => new ArrayBuffer(0)),
|
||||
blob: vi.fn(async () => new Blob()),
|
||||
formData: vi.fn(async () => new FormData()),
|
||||
clone: vi.fn(function (this: Response) {
|
||||
return createMockResponse(config)
|
||||
}),
|
||||
redirected: false,
|
||||
type: 'basic' as ResponseType,
|
||||
url: '',
|
||||
bytes: vi.fn(async () => new Uint8Array()),
|
||||
} as Response
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a mock fetch that handles multiple URLs with different responses.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const mockFetch = createMultiMockFetch({
|
||||
* '/api/users': { json: [{ id: 1 }] },
|
||||
* '/api/error': { status: 500, json: { error: 'Server Error' } },
|
||||
* })
|
||||
* global.fetch = mockFetch
|
||||
* ```
|
||||
*/
|
||||
export function createMultiMockFetch(
|
||||
routes: Record<string, MockFetchResponse>,
|
||||
defaultResponse?: MockFetchResponse
|
||||
) {
|
||||
return vi.fn(async (url: string | URL | Request, _init?: RequestInit) => {
|
||||
const urlString = url instanceof Request ? url.url : url.toString()
|
||||
|
||||
// Find matching route (exact or partial match)
|
||||
const matchedRoute = Object.keys(routes).find(
|
||||
(route) => urlString === route || urlString.includes(route)
|
||||
)
|
||||
|
||||
if (matchedRoute) {
|
||||
return createMockResponse(routes[matchedRoute])
|
||||
}
|
||||
|
||||
if (defaultResponse) {
|
||||
return createMockResponse(defaultResponse)
|
||||
}
|
||||
|
||||
return createMockResponse({ status: 404, json: { error: 'Not Found' } })
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up global fetch mock.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const mockFetch = setupGlobalFetchMock({ json: { success: true } })
|
||||
* // Later...
|
||||
* expect(mockFetch).toHaveBeenCalledWith('/api/test', expect.anything())
|
||||
* ```
|
||||
*/
|
||||
export function setupGlobalFetchMock(defaultResponse?: MockFetchResponse) {
|
||||
const mockFetch = createMockFetch(defaultResponse)
|
||||
vi.stubGlobal('fetch', mockFetch)
|
||||
return mockFetch
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures fetch to return a specific response for the next call.
|
||||
*/
|
||||
export function mockNextFetchResponse(response: MockFetchResponse) {
|
||||
const currentFetch = globalThis.fetch
|
||||
if (vi.isMockFunction(currentFetch)) {
|
||||
currentFetch.mockResolvedValueOnce(createMockResponse(response))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures fetch to reject with an error.
|
||||
*/
|
||||
export function mockFetchError(error: Error | string) {
|
||||
const currentFetch = globalThis.fetch
|
||||
if (vi.isMockFunction(currentFetch)) {
|
||||
currentFetch.mockRejectedValueOnce(error instanceof Error ? error : new Error(error))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { vi } from 'vitest'
|
||||
import { authMockFns } from './auth.mock'
|
||||
|
||||
/**
|
||||
* Auth type constants matching `@/lib/auth/hybrid` AuthType. Included in
|
||||
* `hybridAuthMock.AuthType` so route code can reference `AuthType.SESSION` etc.
|
||||
*/
|
||||
const AuthTypeMock = {
|
||||
SESSION: 'session',
|
||||
API_KEY: 'api_key',
|
||||
INTERNAL_JWT: 'internal_jwt',
|
||||
} as const
|
||||
|
||||
/**
|
||||
* Default session-delegating implementation for `checkSessionOrInternalAuth`.
|
||||
* Mirrors the real function's session-auth path so tests that only mock
|
||||
* `getSession` (via `authMockFns.mockGetSession`) continue to work when the
|
||||
* hybrid module is globally mocked.
|
||||
*/
|
||||
const defaultCheckSessionOrInternalAuth = async () => {
|
||||
const session = await authMockFns.mockGetSession()
|
||||
if (session?.user?.id) {
|
||||
return {
|
||||
success: true,
|
||||
userId: session.user.id,
|
||||
userName: session.user.name,
|
||||
userEmail: session.user.email,
|
||||
authType: AuthTypeMock.SESSION,
|
||||
}
|
||||
}
|
||||
return { success: false, error: 'Unauthorized' }
|
||||
}
|
||||
|
||||
/**
|
||||
* Controllable mock functions for `@/lib/auth/hybrid`. Override per-test with
|
||||
* `hybridAuthMockFns.mockCheckHybridAuth.mockResolvedValueOnce(...)`.
|
||||
*/
|
||||
export const hybridAuthMockFns = {
|
||||
mockCheckHybridAuth: vi.fn(defaultCheckSessionOrInternalAuth),
|
||||
mockCheckSessionOrInternalAuth: vi.fn(defaultCheckSessionOrInternalAuth),
|
||||
mockCheckInternalAuth: vi.fn(),
|
||||
mockHasExternalApiCredentials: vi.fn(() => false),
|
||||
}
|
||||
|
||||
/**
|
||||
* Static mock module for `@/lib/auth/hybrid`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* vi.mock('@/lib/auth/hybrid', () => hybridAuthMock)
|
||||
* ```
|
||||
*/
|
||||
export const hybridAuthMock = {
|
||||
AuthType: AuthTypeMock,
|
||||
checkHybridAuth: hybridAuthMockFns.mockCheckHybridAuth,
|
||||
checkSessionOrInternalAuth: hybridAuthMockFns.mockCheckSessionOrInternalAuth,
|
||||
checkInternalAuth: hybridAuthMockFns.mockCheckInternalAuth,
|
||||
hasExternalApiCredentials: hybridAuthMockFns.mockHasExternalApiCredentials,
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
/**
|
||||
* Mock implementations for common dependencies.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { createMockLogger, setupGlobalFetchMock, databaseMock } from '@sim/testing/mocks'
|
||||
*
|
||||
* // Mock the logger
|
||||
* vi.mock('@sim/logger', () => ({ createLogger: () => createMockLogger() }))
|
||||
*
|
||||
* // Mock fetch globally
|
||||
* setupGlobalFetchMock({ json: { success: true } })
|
||||
*
|
||||
* // Mock database
|
||||
* vi.mock('@sim/db', () => databaseMock)
|
||||
* ```
|
||||
*/
|
||||
|
||||
// Audit mocks
|
||||
export { auditMock, auditMockFns } from './audit.mock'
|
||||
// Auth mocks
|
||||
export { authMock, authMockFns, type MockUser } from './auth.mock'
|
||||
// Auth OAuth utils mocks (for @/app/api/auth/oauth/utils)
|
||||
export {
|
||||
authOAuthUtilsMock,
|
||||
authOAuthUtilsMockFns,
|
||||
ServiceAccountTokenErrorMock,
|
||||
} from './auth-oauth-utils.mock'
|
||||
// Blocks mocks
|
||||
export {
|
||||
blocksMock,
|
||||
createMockGetBlock,
|
||||
createMockGetTool,
|
||||
mockBlockConfigs,
|
||||
mockToolConfigs,
|
||||
toolsUtilsMock,
|
||||
} from './blocks.mock'
|
||||
// Copilot HTTP mocks (for @/lib/copilot/request/http)
|
||||
export { copilotHttpMock, copilotHttpMockFns } from './copilot-http.mock'
|
||||
// Database mocks
|
||||
export {
|
||||
createMockDb,
|
||||
createMockSql,
|
||||
createMockSqlOperators,
|
||||
databaseMock,
|
||||
dbChainMock,
|
||||
dbChainMockFns,
|
||||
drizzleOrmMock,
|
||||
resetDbChainMock,
|
||||
} from './database.mock'
|
||||
// Encryption mocks
|
||||
export { encryptionMock, encryptionMockFns } from './encryption.mock'
|
||||
// Env mocks
|
||||
export { createEnvMock, createMockGetEnv, defaultMockEnv, envMock } from './env.mock'
|
||||
// Env flag mocks
|
||||
export { envFlagsMock } from './env-flags.mock'
|
||||
// Execution preprocessing mocks (for @/lib/execution/preprocessing)
|
||||
export {
|
||||
executionPreprocessingMock,
|
||||
executionPreprocessingMockFns,
|
||||
} from './execution-preprocessing.mock'
|
||||
// Executor mocks - use side-effect import: import '@sim/testing/mocks/executor'
|
||||
// Fetch mocks
|
||||
export {
|
||||
createMockFetch,
|
||||
createMockResponse,
|
||||
createMultiMockFetch,
|
||||
type MockFetchResponse,
|
||||
mockFetchError,
|
||||
mockNextFetchResponse,
|
||||
setupGlobalFetchMock,
|
||||
} from './fetch.mock'
|
||||
// Hybrid auth mocks
|
||||
export { hybridAuthMock, hybridAuthMockFns } from './hybrid-auth.mock'
|
||||
// Input validation mocks
|
||||
export { inputValidationMock, inputValidationMockFns } from './input-validation.mock'
|
||||
// Knowledge API utils mocks (for @/app/api/knowledge/utils)
|
||||
export { knowledgeApiUtilsMock, knowledgeApiUtilsMockFns } from './knowledge-api-utils.mock'
|
||||
// Logger mocks
|
||||
export { clearLoggerMocks, createMockLogger, getLoggerCalls, loggerMock } from './logger.mock'
|
||||
// Logging session mocks (for @/lib/logs/execution/logging-session)
|
||||
export {
|
||||
LoggingSessionMock,
|
||||
loggingSessionMock,
|
||||
loggingSessionMockFns,
|
||||
} from './logging-session.mock'
|
||||
// MCP OAuth mocks (for @/lib/mcp/oauth)
|
||||
export {
|
||||
McpOauthInsecureUrlErrorMock,
|
||||
McpOauthRedirectRequiredMock,
|
||||
mcpOauthMock,
|
||||
mcpOauthMockFns,
|
||||
} from './mcp-oauth.mock'
|
||||
// Permission mocks
|
||||
export { permissionsMock, permissionsMockFns } from './permissions.mock'
|
||||
// PostHog server mocks (for @/lib/posthog/server)
|
||||
export { posthogServerMock, posthogServerMockFns } from './posthog-server.mock'
|
||||
// Redis client mocks (for Redis client objects)
|
||||
export { clearRedisMocks, createMockRedis, type MockRedis } from './redis.mock'
|
||||
// Redis config mocks (for @/lib/core/config/redis)
|
||||
export { redisConfigMock, redisConfigMockFns } from './redis-config.mock'
|
||||
// Request mocks
|
||||
export {
|
||||
createMockFormDataRequest,
|
||||
createMockRequest,
|
||||
requestUtilsMock,
|
||||
requestUtilsMockFns,
|
||||
} from './request.mock'
|
||||
// Schema mocks
|
||||
export { schemaMock } from './schema.mock'
|
||||
// Socket mocks
|
||||
export {
|
||||
createMockSocket,
|
||||
createMockSocketServer,
|
||||
type MockSocket,
|
||||
type MockSocketServer,
|
||||
} from './socket.mock'
|
||||
// Storage mocks (browser localStorage/sessionStorage)
|
||||
export { clearStorageMocks, createMockStorage, setupGlobalStorageMocks } from './storage.mock'
|
||||
// Storage service mocks (for @/lib/uploads/core/storage-service)
|
||||
export { storageServiceMock, storageServiceMockFns } from './storage-service.mock'
|
||||
// Stripe mocks
|
||||
export {
|
||||
createMockStripeEvent,
|
||||
stripeClientMock,
|
||||
stripeClientMockFns,
|
||||
stripePaymentMethodMock,
|
||||
stripePaymentMethodMockFns,
|
||||
} from './stripe.mock'
|
||||
// Telemetry mocks
|
||||
export { telemetryMock } from './telemetry.mock'
|
||||
// Terminal console mocks (for @/stores/terminal and @/stores/terminal/console/store)
|
||||
export {
|
||||
resetTerminalConsoleMock,
|
||||
terminalConsoleMock,
|
||||
terminalConsoleMockFns,
|
||||
} from './terminal-console.mock'
|
||||
// URL mocks
|
||||
export { urlsMock, urlsMockFns } from './urls.mock'
|
||||
// Workflow authz package mocks (for @sim/platform-authz/workflow)
|
||||
export { workflowAuthzMock, workflowAuthzMockFns } from './workflow-authz.mock'
|
||||
// Workflows API utils mocks (for @/app/api/workflows/utils)
|
||||
export { workflowsApiUtilsMock, workflowsApiUtilsMockFns } from './workflows-api-utils.mock'
|
||||
// Workflows orchestration mocks (for @/lib/workflows/orchestration)
|
||||
export {
|
||||
workflowsOrchestrationMock,
|
||||
workflowsOrchestrationMockFns,
|
||||
} from './workflows-orchestration.mock'
|
||||
// Workflows persistence utils mocks (for @/lib/workflows/persistence/utils)
|
||||
export {
|
||||
workflowsPersistenceUtilsMock,
|
||||
workflowsPersistenceUtilsMockFns,
|
||||
} from './workflows-persistence-utils.mock'
|
||||
// Workflows-utils mocks
|
||||
export { workflowsUtilsMock, workflowsUtilsMockFns } from './workflows-utils.mock'
|
||||
@@ -0,0 +1,47 @@
|
||||
import { vi } from 'vitest'
|
||||
|
||||
/**
|
||||
* Controllable mock functions for `@/lib/core/security/input-validation.server`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { inputValidationMockFns } from '@sim/testing'
|
||||
*
|
||||
* inputValidationMockFns.mockValidateUrlWithDNS.mockResolvedValue({ valid: true })
|
||||
* inputValidationMockFns.mockSecureFetchWithPinnedIP.mockResolvedValue({ response: new Response() })
|
||||
* ```
|
||||
*/
|
||||
export const inputValidationMockFns = {
|
||||
mockValidateUrlWithDNS: vi.fn(),
|
||||
mockValidateDatabaseHost: vi.fn(),
|
||||
mockSecureFetchWithPinnedIP: vi.fn(),
|
||||
mockSecureFetchWithValidation: vi.fn(),
|
||||
mockIsPrivateOrReservedIP: vi.fn().mockReturnValue(false),
|
||||
mockCreatePinnedLookup: vi.fn(),
|
||||
}
|
||||
|
||||
/**
|
||||
* Static mock module for `@/lib/core/security/input-validation.server`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* vi.mock('@/lib/core/security/input-validation.server', () => inputValidationMock)
|
||||
* ```
|
||||
*/
|
||||
export const inputValidationMock = {
|
||||
validateUrlWithDNS: inputValidationMockFns.mockValidateUrlWithDNS,
|
||||
validateDatabaseHost: inputValidationMockFns.mockValidateDatabaseHost,
|
||||
secureFetchWithPinnedIP: inputValidationMockFns.mockSecureFetchWithPinnedIP,
|
||||
secureFetchWithValidation: inputValidationMockFns.mockSecureFetchWithValidation,
|
||||
isPrivateOrReservedIP: inputValidationMockFns.mockIsPrivateOrReservedIP,
|
||||
createPinnedLookup: inputValidationMockFns.mockCreatePinnedLookup,
|
||||
SecureFetchHeaders: class {
|
||||
headers: Record<string, string> = {}
|
||||
set(k: string, v: string) {
|
||||
this.headers[k] = v
|
||||
}
|
||||
get(k: string) {
|
||||
return this.headers[k]
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { vi } from 'vitest'
|
||||
|
||||
/**
|
||||
* Controllable mock functions for `@/app/api/knowledge/utils`.
|
||||
* All defaults are bare `vi.fn()` — override per-test with
|
||||
* `knowledgeApiUtilsMockFns.mockCheckKnowledgeBaseAccess.mockResolvedValueOnce(...)`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { knowledgeApiUtilsMockFns } from '@sim/testing'
|
||||
*
|
||||
* knowledgeApiUtilsMockFns.mockCheckKnowledgeBaseAccess.mockResolvedValue({
|
||||
* hasAccess: true,
|
||||
* knowledgeBase: { id: 'kb-1', userId: 'u-1', workspaceId: 'ws-1', name: 'KB' },
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export const knowledgeApiUtilsMockFns = {
|
||||
mockCheckKnowledgeBaseAccess: vi.fn(),
|
||||
mockCheckKnowledgeBaseWriteAccess: vi.fn(),
|
||||
mockCheckDocumentWriteAccess: vi.fn(),
|
||||
mockCheckDocumentAccess: vi.fn(),
|
||||
mockCheckChunkAccess: vi.fn(),
|
||||
}
|
||||
|
||||
/**
|
||||
* Static mock module for `@/app/api/knowledge/utils`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* vi.mock('@/app/api/knowledge/utils', () => knowledgeApiUtilsMock)
|
||||
* ```
|
||||
*/
|
||||
export const knowledgeApiUtilsMock = {
|
||||
checkKnowledgeBaseAccess: knowledgeApiUtilsMockFns.mockCheckKnowledgeBaseAccess,
|
||||
checkKnowledgeBaseWriteAccess: knowledgeApiUtilsMockFns.mockCheckKnowledgeBaseWriteAccess,
|
||||
checkDocumentWriteAccess: knowledgeApiUtilsMockFns.mockCheckDocumentWriteAccess,
|
||||
checkDocumentAccess: knowledgeApiUtilsMockFns.mockCheckDocumentAccess,
|
||||
checkChunkAccess: knowledgeApiUtilsMockFns.mockCheckChunkAccess,
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { vi } from 'vitest'
|
||||
|
||||
/**
|
||||
* Creates a mock logger that captures all log calls.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const logger = createMockLogger()
|
||||
* // Use in your code
|
||||
* logger.info('test message')
|
||||
* // Assert
|
||||
* expect(logger.info).toHaveBeenCalledWith('test message')
|
||||
* ```
|
||||
*/
|
||||
export function createMockLogger() {
|
||||
return {
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
trace: vi.fn(),
|
||||
fatal: vi.fn(),
|
||||
child: vi.fn(() => createMockLogger()),
|
||||
withMetadata: vi.fn(() => createMockLogger()),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock module for @sim/logger.
|
||||
* Use with vi.mock() to replace the real logger.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* vi.mock('@sim/logger', () => loggerMock)
|
||||
* ```
|
||||
*/
|
||||
export const loggerMock = {
|
||||
createLogger: vi.fn(() => createMockLogger()),
|
||||
logger: createMockLogger(),
|
||||
runWithRequestContext: vi.fn(<T>(_ctx: unknown, fn: () => T): T => fn()),
|
||||
getRequestContext: vi.fn(() => undefined),
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the mock logger calls for assertion.
|
||||
*/
|
||||
export function getLoggerCalls(logger: ReturnType<typeof createMockLogger>) {
|
||||
return {
|
||||
info: logger.info.mock.calls,
|
||||
warn: logger.warn.mock.calls,
|
||||
error: logger.error.mock.calls,
|
||||
debug: logger.debug.mock.calls,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all logger mock calls.
|
||||
*/
|
||||
export function clearLoggerMocks(logger: ReturnType<typeof createMockLogger>) {
|
||||
logger.info.mockClear()
|
||||
logger.warn.mockClear()
|
||||
logger.error.mockClear()
|
||||
logger.debug.mockClear()
|
||||
logger.trace.mockClear()
|
||||
logger.fatal.mockClear()
|
||||
logger.withMetadata.mockClear()
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import { vi } from 'vitest'
|
||||
|
||||
/**
|
||||
* Controllable mock functions for the `LoggingSession` class from
|
||||
* `@/lib/logs/execution/logging-session`. Every instance method is backed by a
|
||||
* shared `vi.fn()` so tests that construct multiple sessions observe identical
|
||||
* mock state. `mockSafeStart` defaults to `true` because callers branch on the
|
||||
* boolean result. All other methods resolve to `undefined`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { loggingSessionMockFns } from '@sim/testing'
|
||||
*
|
||||
* loggingSessionMockFns.mockSafeStart.mockResolvedValueOnce(false)
|
||||
* expect(loggingSessionMockFns.mockSafeCompleteWithError).toHaveBeenCalled()
|
||||
* ```
|
||||
*/
|
||||
export const loggingSessionMockFns = {
|
||||
mockStart: vi.fn().mockResolvedValue(undefined),
|
||||
mockComplete: vi.fn().mockResolvedValue(undefined),
|
||||
mockCompleteWithError: vi.fn().mockResolvedValue(undefined),
|
||||
mockCompleteWithCancellation: vi.fn().mockResolvedValue(undefined),
|
||||
mockCompleteWithPause: vi.fn().mockResolvedValue(undefined),
|
||||
mockSafeStart: vi.fn().mockResolvedValue(true),
|
||||
mockWaitForCompletion: vi.fn().mockResolvedValue(undefined),
|
||||
mockWaitForPostExecution: vi.fn().mockResolvedValue(undefined),
|
||||
mockSafeComplete: vi.fn().mockResolvedValue(undefined),
|
||||
mockSafeCompleteWithError: vi.fn().mockResolvedValue(undefined),
|
||||
mockSafeCompleteWithCancellation: vi.fn().mockResolvedValue(undefined),
|
||||
mockSafeCompleteWithPause: vi.fn().mockResolvedValue(undefined),
|
||||
mockMarkAsFailed: vi.fn().mockResolvedValue(undefined),
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the object returned by each `new LoggingSession(...)` call. Declared as
|
||||
* a named function (not an arrow) so it stays constructable under vitest 4's
|
||||
* `Reflect.construct` path while remaining assignable to `mockImplementation`; a
|
||||
* returned object overrides the constructed instance.
|
||||
*/
|
||||
function buildLoggingSessionInstance() {
|
||||
return {
|
||||
start: loggingSessionMockFns.mockStart,
|
||||
complete: loggingSessionMockFns.mockComplete,
|
||||
completeWithError: loggingSessionMockFns.mockCompleteWithError,
|
||||
completeWithCancellation: loggingSessionMockFns.mockCompleteWithCancellation,
|
||||
completeWithPause: loggingSessionMockFns.mockCompleteWithPause,
|
||||
safeStart: loggingSessionMockFns.mockSafeStart,
|
||||
waitForCompletion: loggingSessionMockFns.mockWaitForCompletion,
|
||||
waitForPostExecution: loggingSessionMockFns.mockWaitForPostExecution,
|
||||
safeComplete: loggingSessionMockFns.mockSafeComplete,
|
||||
safeCompleteWithError: loggingSessionMockFns.mockSafeCompleteWithError,
|
||||
safeCompleteWithCancellation: loggingSessionMockFns.mockSafeCompleteWithCancellation,
|
||||
safeCompleteWithPause: loggingSessionMockFns.mockSafeCompleteWithPause,
|
||||
markAsFailed: loggingSessionMockFns.mockMarkAsFailed,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor-shaped mock for `LoggingSession`. Each `new LoggingSession(...)`
|
||||
* call returns an object whose methods point at the shared `vi.fn()` refs in
|
||||
* `loggingSessionMockFns`.
|
||||
*/
|
||||
export const LoggingSessionMock = vi.fn().mockImplementation(buildLoggingSessionInstance)
|
||||
|
||||
/**
|
||||
* Static mock module for `@/lib/logs/execution/logging-session`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* vi.mock('@/lib/logs/execution/logging-session', () => loggingSessionMock)
|
||||
* ```
|
||||
*/
|
||||
export const loggingSessionMock = {
|
||||
LoggingSession: LoggingSessionMock,
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import { vi } from 'vitest'
|
||||
|
||||
/**
|
||||
* Controllable mock functions for `@/lib/mcp/oauth`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { mcpOauthMockFns } from '@sim/testing'
|
||||
*
|
||||
* mcpOauthMockFns.mockGetOrCreateOauthRow.mockResolvedValue({ id: 'oauth-row-1', ... })
|
||||
* ```
|
||||
*/
|
||||
export const mcpOauthMockFns = {
|
||||
mockAssertSafeOauthServerUrl: vi.fn(),
|
||||
mockMcpAuthGuarded: vi.fn(),
|
||||
mockGetOrCreateOauthRow: vi.fn(),
|
||||
mockLoadOauthRow: vi.fn(),
|
||||
mockLoadOauthRowByState: vi.fn(),
|
||||
mockLoadPreregisteredClient: vi.fn(),
|
||||
mockSetOauthRowUser: vi.fn(),
|
||||
mockSaveClientInformation: vi.fn(),
|
||||
mockSaveTokens: vi.fn(),
|
||||
mockSaveCodeVerifier: vi.fn(),
|
||||
mockSaveState: vi.fn(),
|
||||
mockClearTokens: vi.fn(),
|
||||
mockClearClient: vi.fn(),
|
||||
mockClearVerifier: vi.fn(),
|
||||
mockClearState: vi.fn(),
|
||||
mockRevokeMcpOauthTokens: vi.fn(),
|
||||
mockWithMcpOauthRefreshLock: vi.fn(async (_rowId: string, fn: () => Promise<unknown>) => fn()),
|
||||
}
|
||||
|
||||
export class McpOauthRedirectRequiredMock extends Error {
|
||||
constructor(public readonly authorizationUrl: string) {
|
||||
super('MCP OAuth redirect required')
|
||||
this.name = 'McpOauthRedirectRequiredMock'
|
||||
}
|
||||
}
|
||||
|
||||
export class McpOauthInsecureUrlErrorMock extends Error {
|
||||
constructor(public readonly url: string) {
|
||||
super(`Insecure MCP OAuth server URL: ${url}`)
|
||||
this.name = 'McpOauthInsecureUrlErrorMock'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the provider config back as the constructed instance, matching the
|
||||
* original identity passthrough. Declared as a named function (not an arrow) so
|
||||
* it stays constructable under vitest 4's `Reflect.construct` path while
|
||||
* remaining assignable to `mockImplementation`.
|
||||
*/
|
||||
function buildSimMcpOauthProvider(value: object) {
|
||||
return value
|
||||
}
|
||||
|
||||
/**
|
||||
* Static mock module for `@/lib/mcp/oauth`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* vi.mock('@/lib/mcp/oauth', () => mcpOauthMock)
|
||||
* ```
|
||||
*/
|
||||
export const mcpOauthMock = {
|
||||
assertSafeOauthServerUrl: mcpOauthMockFns.mockAssertSafeOauthServerUrl,
|
||||
mcpAuthGuarded: mcpOauthMockFns.mockMcpAuthGuarded,
|
||||
getOrCreateOauthRow: mcpOauthMockFns.mockGetOrCreateOauthRow,
|
||||
loadOauthRow: mcpOauthMockFns.mockLoadOauthRow,
|
||||
loadOauthRowByState: mcpOauthMockFns.mockLoadOauthRowByState,
|
||||
loadPreregisteredClient: mcpOauthMockFns.mockLoadPreregisteredClient,
|
||||
setOauthRowUser: mcpOauthMockFns.mockSetOauthRowUser,
|
||||
saveClientInformation: mcpOauthMockFns.mockSaveClientInformation,
|
||||
saveTokens: mcpOauthMockFns.mockSaveTokens,
|
||||
saveCodeVerifier: mcpOauthMockFns.mockSaveCodeVerifier,
|
||||
saveState: mcpOauthMockFns.mockSaveState,
|
||||
clearTokens: mcpOauthMockFns.mockClearTokens,
|
||||
clearClient: mcpOauthMockFns.mockClearClient,
|
||||
clearVerifier: mcpOauthMockFns.mockClearVerifier,
|
||||
clearState: mcpOauthMockFns.mockClearState,
|
||||
revokeMcpOauthTokens: mcpOauthMockFns.mockRevokeMcpOauthTokens,
|
||||
withMcpOauthRefreshLock: mcpOauthMockFns.mockWithMcpOauthRefreshLock,
|
||||
McpOauthRedirectRequired: McpOauthRedirectRequiredMock,
|
||||
McpOauthInsecureUrlError: McpOauthInsecureUrlErrorMock,
|
||||
SimMcpOauthProvider: vi.fn().mockImplementation(buildSimMcpOauthProvider),
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { vi } from 'vitest'
|
||||
|
||||
/**
|
||||
* Controllable mock functions for `@/lib/workspaces/permissions/utils`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { permissionsMockFns } from '@sim/testing'
|
||||
*
|
||||
* permissionsMockFns.mockCheckWorkspaceAccess.mockResolvedValue({
|
||||
* exists: true, hasAccess: true, canWrite: true, workspace: { id: 'ws-1', name: 'Test', ownerId: 'user-1' },
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export const permissionsMockFns = {
|
||||
mockWorkspaceExists: vi.fn(),
|
||||
mockGetWorkspaceById: vi.fn(),
|
||||
mockGetWorkspaceWithOwner: vi.fn(),
|
||||
mockCheckWorkspaceAccess: vi.fn(),
|
||||
mockAssertActiveWorkspaceAccess: vi.fn(),
|
||||
mockGetUserEntityPermissions: vi.fn(),
|
||||
mockGetUsersWithPermissions: vi.fn(),
|
||||
mockGetWorkspaceMemberProfiles: vi.fn(),
|
||||
mockHasWorkspaceAdminAccess: vi.fn(),
|
||||
mockGetManageableWorkspaces: vi.fn(),
|
||||
}
|
||||
|
||||
/**
|
||||
* Static mock module for `@/lib/workspaces/permissions/utils`.
|
||||
* Defaults resolve to "allowed" state for safe test defaults.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* vi.mock('@/lib/workspaces/permissions/utils', () => permissionsMock)
|
||||
* ```
|
||||
*/
|
||||
export const permissionsMock = {
|
||||
workspaceExists: permissionsMockFns.mockWorkspaceExists,
|
||||
getWorkspaceById: permissionsMockFns.mockGetWorkspaceById,
|
||||
getWorkspaceWithOwner: permissionsMockFns.mockGetWorkspaceWithOwner,
|
||||
checkWorkspaceAccess: permissionsMockFns.mockCheckWorkspaceAccess,
|
||||
assertActiveWorkspaceAccess: permissionsMockFns.mockAssertActiveWorkspaceAccess,
|
||||
getUserEntityPermissions: permissionsMockFns.mockGetUserEntityPermissions,
|
||||
getUsersWithPermissions: permissionsMockFns.mockGetUsersWithPermissions,
|
||||
getWorkspaceMemberProfiles: permissionsMockFns.mockGetWorkspaceMemberProfiles,
|
||||
hasWorkspaceAdminAccess: permissionsMockFns.mockHasWorkspaceAdminAccess,
|
||||
getManageableWorkspaces: permissionsMockFns.mockGetManageableWorkspaces,
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { vi } from 'vitest'
|
||||
|
||||
/**
|
||||
* Controllable mock functions for `@/lib/posthog/server`.
|
||||
* All defaults are bare `vi.fn()` — configure per-test as needed.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { posthogServerMockFns } from '@sim/testing'
|
||||
*
|
||||
* expect(posthogServerMockFns.mockCaptureServerEvent).toHaveBeenCalledWith(...)
|
||||
* ```
|
||||
*/
|
||||
export const posthogServerMockFns = {
|
||||
mockCaptureServerEvent: vi.fn(),
|
||||
mockGetPostHogClient: vi.fn(() => null),
|
||||
}
|
||||
|
||||
/**
|
||||
* Static mock module for `@/lib/posthog/server`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* vi.mock('@/lib/posthog/server', () => posthogServerMock)
|
||||
* ```
|
||||
*/
|
||||
export const posthogServerMock = {
|
||||
captureServerEvent: posthogServerMockFns.mockCaptureServerEvent,
|
||||
getPostHogClient: posthogServerMockFns.mockGetPostHogClient,
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { vi } from 'vitest'
|
||||
|
||||
/**
|
||||
* Controllable mock functions for `@/lib/core/config/redis`.
|
||||
* Default: `getRedisClient` returns `null` (tests that need a client override it).
|
||||
* `acquireLock` defaults to succeeding (`true`); `releaseLock` defaults to `true`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { redisConfigMockFns } from '@sim/testing'
|
||||
*
|
||||
* redisConfigMockFns.mockGetRedisClient.mockReturnValue(myFakeRedis)
|
||||
* ```
|
||||
*/
|
||||
export const redisConfigMockFns = {
|
||||
mockGetRedisClient: vi.fn().mockReturnValue(null),
|
||||
mockOnRedisReconnect: vi.fn(),
|
||||
mockAcquireLock: vi.fn().mockResolvedValue(true),
|
||||
mockReleaseLock: vi.fn().mockResolvedValue(true),
|
||||
mockExtendLock: vi.fn().mockResolvedValue(true),
|
||||
mockCloseRedisConnection: vi.fn().mockResolvedValue(undefined),
|
||||
mockResetForTesting: vi.fn(),
|
||||
}
|
||||
|
||||
/**
|
||||
* Static mock module for `@/lib/core/config/redis`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* vi.mock('@/lib/core/config/redis', () => redisConfigMock)
|
||||
* ```
|
||||
*/
|
||||
export const redisConfigMock = {
|
||||
getRedisClient: redisConfigMockFns.mockGetRedisClient,
|
||||
onRedisReconnect: redisConfigMockFns.mockOnRedisReconnect,
|
||||
acquireLock: redisConfigMockFns.mockAcquireLock,
|
||||
releaseLock: redisConfigMockFns.mockReleaseLock,
|
||||
extendLock: redisConfigMockFns.mockExtendLock,
|
||||
closeRedisConnection: redisConfigMockFns.mockCloseRedisConnection,
|
||||
resetForTesting: redisConfigMockFns.mockResetForTesting,
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import { vi } from 'vitest'
|
||||
|
||||
/**
|
||||
* Creates a mock Redis client with common operations.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const redis = createMockRedis()
|
||||
* const queue = new RedisJobQueue(redis as never)
|
||||
*
|
||||
* // After operations
|
||||
* expect(redis.hset).toHaveBeenCalled()
|
||||
* expect(redis.expire).toHaveBeenCalledWith('key', 86400)
|
||||
* ```
|
||||
*/
|
||||
export function createMockRedis() {
|
||||
return {
|
||||
// Hash operations
|
||||
hset: vi.fn().mockResolvedValue(1),
|
||||
hget: vi.fn().mockResolvedValue(null),
|
||||
hgetall: vi.fn().mockResolvedValue({}),
|
||||
hdel: vi.fn().mockResolvedValue(1),
|
||||
hmset: vi.fn().mockResolvedValue('OK'),
|
||||
hincrby: vi.fn().mockResolvedValue(1),
|
||||
|
||||
// Key operations
|
||||
get: vi.fn().mockResolvedValue(null),
|
||||
set: vi.fn().mockResolvedValue('OK'),
|
||||
del: vi.fn().mockResolvedValue(1),
|
||||
exists: vi.fn().mockResolvedValue(0),
|
||||
expire: vi.fn().mockResolvedValue(1),
|
||||
ttl: vi.fn().mockResolvedValue(-1),
|
||||
|
||||
// List operations
|
||||
lpush: vi.fn().mockResolvedValue(1),
|
||||
rpush: vi.fn().mockResolvedValue(1),
|
||||
lpop: vi.fn().mockResolvedValue(null),
|
||||
rpop: vi.fn().mockResolvedValue(null),
|
||||
lrange: vi.fn().mockResolvedValue([]),
|
||||
llen: vi.fn().mockResolvedValue(0),
|
||||
|
||||
// Set operations
|
||||
sadd: vi.fn().mockResolvedValue(1),
|
||||
srem: vi.fn().mockResolvedValue(1),
|
||||
smembers: vi.fn().mockResolvedValue([]),
|
||||
sismember: vi.fn().mockResolvedValue(0),
|
||||
|
||||
// Pub/Sub
|
||||
publish: vi.fn().mockResolvedValue(0),
|
||||
subscribe: vi.fn().mockResolvedValue(undefined),
|
||||
unsubscribe: vi.fn().mockResolvedValue(undefined),
|
||||
on: vi.fn(),
|
||||
|
||||
// Transaction
|
||||
multi: vi.fn(() => ({
|
||||
exec: vi.fn().mockResolvedValue([]),
|
||||
})),
|
||||
|
||||
// Scripting
|
||||
eval: vi.fn().mockResolvedValue(0),
|
||||
|
||||
// Connection
|
||||
ping: vi.fn().mockResolvedValue('PONG'),
|
||||
quit: vi.fn().mockResolvedValue('OK'),
|
||||
disconnect: vi.fn().mockResolvedValue(undefined),
|
||||
|
||||
// Status
|
||||
status: 'ready',
|
||||
}
|
||||
}
|
||||
|
||||
export type MockRedis = ReturnType<typeof createMockRedis>
|
||||
|
||||
/**
|
||||
* Clears all Redis mock calls.
|
||||
*/
|
||||
export function clearRedisMocks(redis: MockRedis) {
|
||||
Object.values(redis).forEach((value) => {
|
||||
if (typeof value === 'function' && 'mockClear' in value) {
|
||||
value.mockClear()
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* Mock request utilities for API testing
|
||||
*/
|
||||
import { NextRequest } from 'next/server'
|
||||
import { vi } from 'vitest'
|
||||
|
||||
/**
|
||||
* Creates a mock NextRequest for API route testing.
|
||||
* This is a general-purpose utility for testing Next.js API routes.
|
||||
*
|
||||
* Returning `NextRequest` (not plain `Request`) keeps `request.nextUrl`
|
||||
* available for routes that go through `parseRequest` and similar helpers
|
||||
* that read query params via `request.nextUrl.searchParams`.
|
||||
*
|
||||
* @param method - HTTP method (GET, POST, PUT, DELETE, etc.)
|
||||
* @param body - Optional request body (will be JSON stringified)
|
||||
* @param headers - Optional headers to include
|
||||
* @param url - Optional custom URL (defaults to http://localhost:3000/api/test)
|
||||
* @returns NextRequest instance
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const req = createMockRequest('POST', { name: 'test' })
|
||||
* const response = await POST(req)
|
||||
* ```
|
||||
*/
|
||||
type NextRequestInit = NonNullable<ConstructorParameters<typeof NextRequest>[1]>
|
||||
|
||||
export function createMockRequest(
|
||||
method = 'GET',
|
||||
body?: unknown,
|
||||
headers: Record<string, string> = {},
|
||||
url = 'http://localhost:3000/api/test'
|
||||
): NextRequest {
|
||||
const init: NextRequestInit = {
|
||||
method,
|
||||
headers: new Headers({
|
||||
'Content-Type': 'application/json',
|
||||
...headers,
|
||||
}),
|
||||
}
|
||||
|
||||
if (body !== undefined) {
|
||||
init.body = JSON.stringify(body)
|
||||
}
|
||||
|
||||
return new NextRequest(new URL(url), init)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a mock NextRequest with form data for file upload testing.
|
||||
*
|
||||
* @param formData - FormData instance
|
||||
* @param method - HTTP method (defaults to POST)
|
||||
* @param url - Optional custom URL
|
||||
* @returns Request instance
|
||||
*/
|
||||
export function createMockFormDataRequest(
|
||||
formData: FormData,
|
||||
method = 'POST',
|
||||
url = 'http://localhost:3000/api/test'
|
||||
): Request {
|
||||
return new Request(new URL(url), {
|
||||
method,
|
||||
body: formData,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Controllable mock functions for `@/lib/core/utils/request`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { requestUtilsMockFns } from '@sim/testing'
|
||||
*
|
||||
* requestUtilsMockFns.mockGenerateRequestId.mockReturnValueOnce('test-req-42')
|
||||
* requestUtilsMockFns.mockGetClientIp.mockReturnValueOnce('10.0.0.5')
|
||||
* ```
|
||||
*/
|
||||
export const requestUtilsMockFns = {
|
||||
mockGenerateRequestId: vi.fn(() => 'mock-request-id'),
|
||||
mockGetClientIp: vi.fn(() => '127.0.0.1'),
|
||||
}
|
||||
|
||||
/**
|
||||
* Static mock module for `@/lib/core/utils/request`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* vi.mock('@/lib/core/utils/request', () => requestUtilsMock)
|
||||
* ```
|
||||
*/
|
||||
export const requestUtilsMock = {
|
||||
generateRequestId: requestUtilsMockFns.mockGenerateRequestId,
|
||||
getClientIp: requestUtilsMockFns.mockGetClientIp,
|
||||
noop: () => {},
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,180 @@
|
||||
import { generateRandomString } from '@sim/utils/random'
|
||||
import { type Mock, vi } from 'vitest'
|
||||
|
||||
/**
|
||||
* Mock socket interface for type safety.
|
||||
*/
|
||||
export interface IMockSocket {
|
||||
id: string
|
||||
connected: boolean
|
||||
disconnected: boolean
|
||||
emit: Mock
|
||||
on: Mock
|
||||
once: Mock
|
||||
off: Mock
|
||||
connect: Mock
|
||||
disconnect: Mock
|
||||
join: Mock
|
||||
leave: Mock
|
||||
_handlers: Record<string, ((...args: any[]) => any)[]>
|
||||
_trigger: (event: string, ...args: any[]) => void
|
||||
_reset: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a mock Socket.IO client socket.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const socket = createMockSocket()
|
||||
* socket.emit('test', { data: 'value' })
|
||||
* expect(socket.emit).toHaveBeenCalledWith('test', { data: 'value' })
|
||||
* ```
|
||||
*/
|
||||
export function createMockSocket(): IMockSocket {
|
||||
const eventHandlers: Record<string, ((...args: any[]) => any)[]> = {}
|
||||
|
||||
const socket = {
|
||||
id: `socket-${generateRandomString(8)}`,
|
||||
connected: true,
|
||||
disconnected: false,
|
||||
|
||||
// Core methods
|
||||
emit: vi.fn((event: string, ..._args: any[]) => {
|
||||
return socket
|
||||
}),
|
||||
|
||||
on: vi.fn((event: string, handler: (...args: any[]) => any) => {
|
||||
if (!eventHandlers[event]) {
|
||||
eventHandlers[event] = []
|
||||
}
|
||||
eventHandlers[event].push(handler)
|
||||
return socket
|
||||
}),
|
||||
|
||||
once: vi.fn((event: string, handler: (...args: any[]) => any) => {
|
||||
if (!eventHandlers[event]) {
|
||||
eventHandlers[event] = []
|
||||
}
|
||||
eventHandlers[event].push(handler)
|
||||
return socket
|
||||
}),
|
||||
|
||||
off: vi.fn((event: string, handler?: (...args: any[]) => any) => {
|
||||
if (handler && eventHandlers[event]) {
|
||||
eventHandlers[event] = eventHandlers[event].filter((h) => h !== handler)
|
||||
} else {
|
||||
delete eventHandlers[event]
|
||||
}
|
||||
return socket
|
||||
}),
|
||||
|
||||
connect: vi.fn(() => {
|
||||
socket.connected = true
|
||||
socket.disconnected = false
|
||||
return socket
|
||||
}),
|
||||
|
||||
disconnect: vi.fn(() => {
|
||||
socket.connected = false
|
||||
socket.disconnected = true
|
||||
return socket
|
||||
}),
|
||||
|
||||
// Room methods
|
||||
join: vi.fn((_room: string) => socket),
|
||||
leave: vi.fn((_room: string) => socket),
|
||||
|
||||
// Utility methods for testing
|
||||
_handlers: eventHandlers,
|
||||
|
||||
_trigger: (event: string, ...args: any[]) => {
|
||||
const handlers = eventHandlers[event] || []
|
||||
handlers.forEach((handler) => handler(...args))
|
||||
},
|
||||
|
||||
_reset: () => {
|
||||
Object.keys(eventHandlers).forEach((key) => delete eventHandlers[key])
|
||||
socket.emit.mockClear()
|
||||
socket.on.mockClear()
|
||||
socket.once.mockClear()
|
||||
socket.off.mockClear()
|
||||
},
|
||||
}
|
||||
|
||||
return socket
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock socket server interface.
|
||||
*/
|
||||
export interface IMockSocketServer {
|
||||
sockets: Map<string, IMockSocket>
|
||||
rooms: Map<string, Set<string>>
|
||||
emit: Mock
|
||||
to: Mock
|
||||
in: Mock
|
||||
_addSocket: (socket: IMockSocket) => void
|
||||
_joinRoom: (socketId: string, room: string) => void
|
||||
_leaveRoom: (socketId: string, room: string) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a mock Socket.IO server.
|
||||
*/
|
||||
export function createMockSocketServer(): IMockSocketServer {
|
||||
const sockets = new Map<string, IMockSocket>()
|
||||
const rooms = new Map<string, Set<string>>()
|
||||
|
||||
return {
|
||||
sockets,
|
||||
rooms,
|
||||
|
||||
emit: vi.fn((_event: string, ..._args: any[]) => {}),
|
||||
|
||||
to: vi.fn((room: string) => ({
|
||||
emit: vi.fn((event: string, ...args: any[]) => {
|
||||
const socketIds = rooms.get(room) || new Set()
|
||||
socketIds.forEach((id) => {
|
||||
const socket = sockets.get(id)
|
||||
if (socket) {
|
||||
socket._trigger(event, ...args)
|
||||
}
|
||||
})
|
||||
}),
|
||||
})),
|
||||
|
||||
in: vi.fn((room: string) => ({
|
||||
emit: vi.fn((event: string, ...args: any[]) => {
|
||||
const socketIds = rooms.get(room) || new Set()
|
||||
socketIds.forEach((id) => {
|
||||
const socket = sockets.get(id)
|
||||
if (socket) {
|
||||
socket._trigger(event, ...args)
|
||||
}
|
||||
})
|
||||
}),
|
||||
})),
|
||||
|
||||
_addSocket: (socket: ReturnType<typeof createMockSocket>) => {
|
||||
sockets.set(socket.id, socket)
|
||||
},
|
||||
|
||||
_joinRoom: (socketId: string, room: string) => {
|
||||
if (!rooms.has(room)) {
|
||||
rooms.set(room, new Set())
|
||||
}
|
||||
rooms.get(room)?.add(socketId)
|
||||
},
|
||||
|
||||
_leaveRoom: (socketId: string, room: string) => {
|
||||
rooms.get(room)?.delete(socketId)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Type aliases for convenience.
|
||||
*/
|
||||
export type MockSocket = IMockSocket
|
||||
export type MockSocketServer = IMockSocketServer
|
||||
@@ -0,0 +1,47 @@
|
||||
import { vi } from 'vitest'
|
||||
|
||||
/**
|
||||
* Controllable mock functions for `@/lib/uploads/core/storage-service`.
|
||||
* All defaults are bare `vi.fn()` — configure per-test as needed.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { storageServiceMockFns } from '@sim/testing'
|
||||
*
|
||||
* storageServiceMockFns.mockHasCloudStorage.mockReturnValue(true)
|
||||
* storageServiceMockFns.mockGeneratePresignedUploadUrl.mockResolvedValue({
|
||||
* uploadUrl: 'https://s3/test', key: 'workspace/x/y', ...
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export const storageServiceMockFns = {
|
||||
mockUploadFile: vi.fn(),
|
||||
mockDownloadFile: vi.fn(),
|
||||
mockDeleteFile: vi.fn(),
|
||||
mockHeadObject: vi.fn(),
|
||||
mockGeneratePresignedUploadUrl: vi.fn(),
|
||||
mockGenerateBatchPresignedUploadUrls: vi.fn(),
|
||||
mockGeneratePresignedDownloadUrl: vi.fn(),
|
||||
mockHasCloudStorage: vi.fn(() => false),
|
||||
mockGetS3InfoForKey: vi.fn(),
|
||||
}
|
||||
|
||||
/**
|
||||
* Static mock module for `@/lib/uploads/core/storage-service`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* vi.mock('@/lib/uploads/core/storage-service', () => storageServiceMock)
|
||||
* ```
|
||||
*/
|
||||
export const storageServiceMock = {
|
||||
uploadFile: storageServiceMockFns.mockUploadFile,
|
||||
downloadFile: storageServiceMockFns.mockDownloadFile,
|
||||
deleteFile: storageServiceMockFns.mockDeleteFile,
|
||||
headObject: storageServiceMockFns.mockHeadObject,
|
||||
generatePresignedUploadUrl: storageServiceMockFns.mockGeneratePresignedUploadUrl,
|
||||
generateBatchPresignedUploadUrls: storageServiceMockFns.mockGenerateBatchPresignedUploadUrls,
|
||||
generatePresignedDownloadUrl: storageServiceMockFns.mockGeneratePresignedDownloadUrl,
|
||||
hasCloudStorage: storageServiceMockFns.mockHasCloudStorage,
|
||||
getS3InfoForKey: storageServiceMockFns.mockGetS3InfoForKey,
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { vi } from 'vitest'
|
||||
|
||||
/**
|
||||
* Creates a mock storage implementation (localStorage/sessionStorage).
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const storage = createMockStorage()
|
||||
* storage.setItem('key', 'value')
|
||||
* expect(storage.getItem('key')).toBe('value')
|
||||
* ```
|
||||
*/
|
||||
export function createMockStorage(): Storage {
|
||||
const store: Record<string, string> = {}
|
||||
|
||||
return {
|
||||
getItem: vi.fn((key: string) => store[key] ?? null),
|
||||
setItem: vi.fn((key: string, value: string) => {
|
||||
store[key] = value
|
||||
}),
|
||||
removeItem: vi.fn((key: string) => {
|
||||
delete store[key]
|
||||
}),
|
||||
clear: vi.fn(() => {
|
||||
Object.keys(store).forEach((key) => delete store[key])
|
||||
}),
|
||||
key: vi.fn((index: number) => Object.keys(store)[index] ?? null),
|
||||
get length() {
|
||||
return Object.keys(store).length
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up global localStorage and sessionStorage mocks.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // In vitest.setup.ts
|
||||
* setupGlobalStorageMocks()
|
||||
* ```
|
||||
*/
|
||||
export function setupGlobalStorageMocks() {
|
||||
const localStorageMock = createMockStorage()
|
||||
const sessionStorageMock = createMockStorage()
|
||||
|
||||
Object.defineProperty(globalThis, 'localStorage', {
|
||||
value: localStorageMock,
|
||||
writable: true,
|
||||
})
|
||||
|
||||
Object.defineProperty(globalThis, 'sessionStorage', {
|
||||
value: sessionStorageMock,
|
||||
writable: true,
|
||||
})
|
||||
|
||||
return { localStorage: localStorageMock, sessionStorage: sessionStorageMock }
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all storage mock data and calls.
|
||||
*/
|
||||
export function clearStorageMocks() {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.clear()
|
||||
vi.mocked(localStorage.getItem).mockClear()
|
||||
vi.mocked(localStorage.setItem).mockClear()
|
||||
vi.mocked(localStorage.removeItem).mockClear()
|
||||
}
|
||||
if (typeof sessionStorage !== 'undefined') {
|
||||
sessionStorage.clear()
|
||||
vi.mocked(sessionStorage.getItem).mockClear()
|
||||
vi.mocked(sessionStorage.setItem).mockClear()
|
||||
vi.mocked(sessionStorage.removeItem).mockClear()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import type Stripe from 'stripe'
|
||||
import { vi } from 'vitest'
|
||||
|
||||
/**
|
||||
* Mock for `@/lib/billing/stripe-client`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { stripeClientMock, stripeClientMockFns } from '@sim/testing'
|
||||
* vi.mock('@/lib/billing/stripe-client', () => stripeClientMock)
|
||||
*
|
||||
* stripeClientMockFns.mockRequireStripeClient.mockReturnValue(fakeStripe)
|
||||
* ```
|
||||
*/
|
||||
export const stripeClientMockFns = {
|
||||
mockRequireStripeClient: vi.fn(),
|
||||
mockGetStripeClient: vi.fn(),
|
||||
mockHasValidStripeCredentials: vi.fn(() => true),
|
||||
}
|
||||
|
||||
export const stripeClientMock = {
|
||||
requireStripeClient: stripeClientMockFns.mockRequireStripeClient,
|
||||
getStripeClient: stripeClientMockFns.mockGetStripeClient,
|
||||
hasValidStripeCredentials: stripeClientMockFns.mockHasValidStripeCredentials,
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock for `@/lib/billing/stripe-payment-method`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { stripePaymentMethodMock, stripePaymentMethodMockFns } from '@sim/testing'
|
||||
* vi.mock('@/lib/billing/stripe-payment-method', () => stripePaymentMethodMock)
|
||||
* ```
|
||||
*/
|
||||
export const stripePaymentMethodMockFns = {
|
||||
mockResolveDefaultPaymentMethod: vi.fn(async () => ({
|
||||
paymentMethodId: undefined as string | undefined,
|
||||
collectionMethod: 'charge_automatically' as 'charge_automatically' | 'send_invoice' | null,
|
||||
})),
|
||||
mockGetCustomerId: vi.fn(),
|
||||
}
|
||||
|
||||
export const stripePaymentMethodMock = {
|
||||
resolveDefaultPaymentMethod: stripePaymentMethodMockFns.mockResolveDefaultPaymentMethod,
|
||||
getCustomerId: stripePaymentMethodMockFns.mockGetCustomerId,
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a minimal `Stripe.Event` with the given type and object payload.
|
||||
* Fills in a deterministic `id` (`evt_${type}`) and nests `object` under
|
||||
* `data.object` as Stripe does.
|
||||
*/
|
||||
export function createMockStripeEvent<T = unknown>(
|
||||
type: string,
|
||||
object: T,
|
||||
overrides: Partial<Stripe.Event> = {}
|
||||
): Stripe.Event {
|
||||
return {
|
||||
id: `evt_${type}`,
|
||||
object: 'event',
|
||||
api_version: '2024-06-20',
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
livemode: false,
|
||||
pending_webhooks: 0,
|
||||
request: null,
|
||||
type,
|
||||
data: { object: object as unknown as Stripe.Event.Data.Object },
|
||||
...overrides,
|
||||
} as Stripe.Event
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Mock for @/lib/core/telemetry module.
|
||||
* Provides no-op implementations for telemetry functions and PlatformEvents.
|
||||
*/
|
||||
import { vi } from 'vitest'
|
||||
|
||||
/**
|
||||
* Pre-configured telemetry mock for use with vi.mock.
|
||||
* All PlatformEvents methods are no-op vi.fn() stubs.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* vi.mock('@/lib/core/telemetry', () => telemetryMock)
|
||||
* ```
|
||||
*/
|
||||
export const telemetryMock = {
|
||||
PlatformEvents: new Proxy(
|
||||
{},
|
||||
{
|
||||
get: (_target, prop) => {
|
||||
if (typeof prop === 'string') {
|
||||
return vi.fn()
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
}
|
||||
),
|
||||
createWorkflowSpans: vi.fn(),
|
||||
trackPlatformEvent: vi.fn(),
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { generateRandomString } from '@sim/utils/random'
|
||||
import { vi } from 'vitest'
|
||||
|
||||
interface ConsoleEntryLike {
|
||||
id?: string
|
||||
workflowId: string
|
||||
blockId: string
|
||||
blockType: string
|
||||
executionId?: string
|
||||
isRunning?: boolean
|
||||
error?: string | null
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
const entriesByWorkflow: Record<string, ConsoleEntryLike[]> = {}
|
||||
|
||||
const mockGetWorkflowEntries = vi.fn((workflowId: string) => entriesByWorkflow[workflowId] ?? [])
|
||||
|
||||
const mockAddConsole = vi.fn((entry: ConsoleEntryLike) => {
|
||||
const stored = { ...entry, id: entry.id ?? `mock-${generateRandomString(16)}` }
|
||||
if (!entriesByWorkflow[entry.workflowId]) entriesByWorkflow[entry.workflowId] = []
|
||||
entriesByWorkflow[entry.workflowId].push(stored)
|
||||
return stored
|
||||
})
|
||||
|
||||
const mockUpdateConsole = vi.fn()
|
||||
const mockCancelRunningEntries = vi.fn()
|
||||
const mockClearWorkflowConsole = vi.fn((workflowId: string) => {
|
||||
delete entriesByWorkflow[workflowId]
|
||||
})
|
||||
|
||||
/**
|
||||
* Resets the in-memory mock console store. Call from `beforeEach` if your tests
|
||||
* push entries via `terminalConsoleMockFns.mockAddConsole`.
|
||||
*/
|
||||
export function resetTerminalConsoleMock(): void {
|
||||
for (const key of Object.keys(entriesByWorkflow)) delete entriesByWorkflow[key]
|
||||
mockGetWorkflowEntries.mockClear()
|
||||
mockAddConsole.mockClear()
|
||||
mockUpdateConsole.mockClear()
|
||||
mockCancelRunningEntries.mockClear()
|
||||
mockClearWorkflowConsole.mockClear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Controllable mock fns for `@/stores/terminal` and `@/stores/terminal/console/store`.
|
||||
* Includes a tiny in-memory store backing `getWorkflowEntries`/`addConsole` so callers
|
||||
* exercising the read-after-write contract behave correctly without the real Zustand store.
|
||||
*/
|
||||
export const terminalConsoleMockFns = {
|
||||
mockGetWorkflowEntries,
|
||||
mockAddConsole,
|
||||
mockUpdateConsole,
|
||||
mockCancelRunningEntries,
|
||||
mockClearWorkflowConsole,
|
||||
reset: resetTerminalConsoleMock,
|
||||
}
|
||||
|
||||
const stateValue = {
|
||||
addConsole: mockAddConsole,
|
||||
updateConsole: mockUpdateConsole,
|
||||
cancelRunningEntries: mockCancelRunningEntries,
|
||||
clearWorkflowConsole: mockClearWorkflowConsole,
|
||||
getWorkflowEntries: mockGetWorkflowEntries,
|
||||
workflowEntries: entriesByWorkflow,
|
||||
entryIdsByBlockExecution: {},
|
||||
entryLocationById: {},
|
||||
isOpen: false,
|
||||
_hasHydrated: true,
|
||||
}
|
||||
|
||||
/**
|
||||
* Static mock module for `@/stores/terminal` / `@/stores/terminal/console/store`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* vi.mock('@/stores/terminal', () => terminalConsoleMock)
|
||||
* ```
|
||||
*/
|
||||
export const terminalConsoleMock = {
|
||||
useTerminalConsoleStore: Object.assign(
|
||||
vi.fn(() => stateValue),
|
||||
{
|
||||
getState: vi.fn(() => stateValue),
|
||||
setState: vi.fn(),
|
||||
subscribe: vi.fn(),
|
||||
}
|
||||
),
|
||||
saveExecutionPointer: vi.fn(),
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { vi } from 'vitest'
|
||||
|
||||
/**
|
||||
* Controllable mock functions for `@/lib/core/utils/urls`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { urlsMockFns } from '@sim/testing'
|
||||
*
|
||||
* urlsMockFns.mockGetBaseUrl.mockReturnValue('https://custom.example.com')
|
||||
* ```
|
||||
*/
|
||||
export const urlsMockFns = {
|
||||
mockGetBaseUrl: vi.fn(),
|
||||
mockGetInternalApiBaseUrl: vi.fn(),
|
||||
mockEnsureAbsoluteUrl: vi.fn(),
|
||||
mockGetBaseDomain: vi.fn(),
|
||||
mockGetEmailDomain: vi.fn(),
|
||||
mockGetSocketServerUrl: vi.fn(),
|
||||
mockGetSocketUrl: vi.fn(),
|
||||
mockGetOllamaUrl: vi.fn(),
|
||||
}
|
||||
|
||||
/**
|
||||
* Static mock module for `@/lib/core/utils/urls`.
|
||||
* Functions return sensible localhost defaults.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* vi.mock('@/lib/core/utils/urls', () => urlsMock)
|
||||
* ```
|
||||
*/
|
||||
export const urlsMock = {
|
||||
SITE_URL: 'https://www.sim.ai',
|
||||
getBaseUrl: urlsMockFns.mockGetBaseUrl,
|
||||
getInternalApiBaseUrl: urlsMockFns.mockGetInternalApiBaseUrl,
|
||||
ensureAbsoluteUrl: urlsMockFns.mockEnsureAbsoluteUrl,
|
||||
getBaseDomain: urlsMockFns.mockGetBaseDomain,
|
||||
getEmailDomain: urlsMockFns.mockGetEmailDomain,
|
||||
getSocketServerUrl: urlsMockFns.mockGetSocketServerUrl,
|
||||
getSocketUrl: urlsMockFns.mockGetSocketUrl,
|
||||
getOllamaUrl: urlsMockFns.mockGetOllamaUrl,
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import { vi } from 'vitest'
|
||||
|
||||
/**
|
||||
* Real `WorkflowLockedError` subclass used by tests so `instanceof` checks in
|
||||
* route handlers behave the same as in production. Mirrors the shape exported
|
||||
* by `@sim/platform-authz/workflow`.
|
||||
*/
|
||||
export class MockWorkflowLockedError extends Error {
|
||||
readonly status = 423
|
||||
|
||||
constructor(message = 'Workflow is locked') {
|
||||
super(message)
|
||||
this.name = 'WorkflowLockedError'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Real `FolderLockedError` subclass used by tests so `instanceof` checks in
|
||||
* route handlers behave the same as in production. Mirrors the shape exported
|
||||
* by `@sim/platform-authz/workflow`.
|
||||
*/
|
||||
export class MockFolderLockedError extends Error {
|
||||
readonly status = 423
|
||||
|
||||
constructor(message = 'Folder is locked') {
|
||||
super(message)
|
||||
this.name = 'FolderLockedError'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Real `FolderNotFoundError` subclass used by tests so `instanceof` checks in
|
||||
* route handlers behave the same as in production. Mirrors the shape exported
|
||||
* by `@sim/platform-authz/workflow`.
|
||||
*/
|
||||
export class MockFolderNotFoundError extends Error {
|
||||
readonly status = 400
|
||||
|
||||
constructor(message = 'Target folder not found') {
|
||||
super(message)
|
||||
this.name = 'FolderNotFoundError'
|
||||
}
|
||||
}
|
||||
|
||||
const unlockedStatus = {
|
||||
locked: false,
|
||||
directLocked: false,
|
||||
inheritedLocked: false,
|
||||
lockedBy: null as 'workflow' | 'folder' | null,
|
||||
lockedFolderId: null as string | null,
|
||||
}
|
||||
|
||||
/**
|
||||
* Controllable mocks for the `@sim/platform-authz/workflow` entry.
|
||||
*
|
||||
* Defaults assume permissive access (no lock, write allowed). Override with
|
||||
* `mockResolvedValue` per test when exercising the lock/permission paths.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { workflowAuthzMockFns } from '@sim/testing'
|
||||
*
|
||||
* workflowAuthzMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({
|
||||
* allowed: true,
|
||||
* status: 200,
|
||||
* workflow: { id: 'wf-1' },
|
||||
* workspacePermission: 'admin',
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export const workflowAuthzMockFns = {
|
||||
mockAuthorizeWorkflowByWorkspacePermission: vi.fn(),
|
||||
mockGetActiveWorkflowContext: vi.fn(),
|
||||
mockGetActiveWorkflowRecord: vi.fn(),
|
||||
mockAssertActiveWorkflowContext: vi.fn(),
|
||||
mockGetFolderLockStatus: vi.fn().mockResolvedValue(unlockedStatus),
|
||||
mockGetWorkflowLockStatus: vi.fn().mockResolvedValue(unlockedStatus),
|
||||
mockAssertWorkflowMutable: vi.fn().mockResolvedValue(undefined),
|
||||
mockAssertFolderMutable: vi.fn().mockResolvedValue(undefined),
|
||||
mockIsFolderInWorkspace: vi.fn().mockResolvedValue(true),
|
||||
mockAssertFolderInWorkspace: vi.fn().mockResolvedValue(undefined),
|
||||
}
|
||||
|
||||
/**
|
||||
* Static mock module for `@sim/platform-authz/workflow`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* vi.mock('@sim/platform-authz/workflow', () => workflowAuthzMock)
|
||||
* ```
|
||||
*/
|
||||
export const workflowAuthzMock = {
|
||||
authorizeWorkflowByWorkspacePermission:
|
||||
workflowAuthzMockFns.mockAuthorizeWorkflowByWorkspacePermission,
|
||||
getActiveWorkflowContext: workflowAuthzMockFns.mockGetActiveWorkflowContext,
|
||||
getActiveWorkflowRecord: workflowAuthzMockFns.mockGetActiveWorkflowRecord,
|
||||
assertActiveWorkflowContext: workflowAuthzMockFns.mockAssertActiveWorkflowContext,
|
||||
getFolderLockStatus: workflowAuthzMockFns.mockGetFolderLockStatus,
|
||||
getWorkflowLockStatus: workflowAuthzMockFns.mockGetWorkflowLockStatus,
|
||||
assertWorkflowMutable: workflowAuthzMockFns.mockAssertWorkflowMutable,
|
||||
assertFolderMutable: workflowAuthzMockFns.mockAssertFolderMutable,
|
||||
isFolderInWorkspace: workflowAuthzMockFns.mockIsFolderInWorkspace,
|
||||
assertFolderInWorkspace: workflowAuthzMockFns.mockAssertFolderInWorkspace,
|
||||
WorkflowLockedError: MockWorkflowLockedError,
|
||||
FolderLockedError: MockFolderLockedError,
|
||||
FolderNotFoundError: MockFolderNotFoundError,
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { vi } from 'vitest'
|
||||
|
||||
/**
|
||||
* Controllable mock functions for `@/app/api/workflows/utils`.
|
||||
*
|
||||
* Default `createSuccessResponse`/`createErrorResponse` return a mock Response-like
|
||||
* object where `.json()` resolves to the payload — compatible with most assertions
|
||||
* like `expect(await res.json()).toEqual(...)` and `expect(res.status).toBe(...)`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { workflowsApiUtilsMockFns } from '@sim/testing'
|
||||
*
|
||||
* workflowsApiUtilsMockFns.mockVerifyWorkspaceMembership.mockResolvedValue('admin')
|
||||
* workflowsApiUtilsMockFns.mockCheckNeedsRedeployment.mockResolvedValue(true)
|
||||
* ```
|
||||
*/
|
||||
export const workflowsApiUtilsMockFns = {
|
||||
mockCreateSuccessResponse: vi.fn((data: unknown) => ({
|
||||
status: 200,
|
||||
ok: true,
|
||||
json: async () => data,
|
||||
})),
|
||||
mockCreateErrorResponse: vi.fn((error: string, status: number, code?: string) => ({
|
||||
status,
|
||||
ok: false,
|
||||
json: async () => ({
|
||||
error,
|
||||
code: code || error.toUpperCase().replace(/\s+/g, '_'),
|
||||
}),
|
||||
})),
|
||||
mockCheckNeedsRedeployment: vi.fn().mockResolvedValue(false),
|
||||
mockVerifyWorkspaceMembership: vi.fn().mockResolvedValue('member'),
|
||||
}
|
||||
|
||||
/**
|
||||
* Static mock module for `@/app/api/workflows/utils`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* vi.mock('@/app/api/workflows/utils', () => workflowsApiUtilsMock)
|
||||
* ```
|
||||
*/
|
||||
export const workflowsApiUtilsMock = {
|
||||
createSuccessResponse: workflowsApiUtilsMockFns.mockCreateSuccessResponse,
|
||||
createErrorResponse: workflowsApiUtilsMockFns.mockCreateErrorResponse,
|
||||
checkNeedsRedeployment: workflowsApiUtilsMockFns.mockCheckNeedsRedeployment,
|
||||
verifyWorkspaceMembership: workflowsApiUtilsMockFns.mockVerifyWorkspaceMembership,
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { vi } from 'vitest'
|
||||
|
||||
/**
|
||||
* Controllable mock functions for `@/lib/workflows/orchestration`.
|
||||
* All defaults are bare `vi.fn()` — configure per-test as needed.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { workflowsOrchestrationMockFns } from '@sim/testing'
|
||||
*
|
||||
* workflowsOrchestrationMockFns.mockPerformFullDeploy.mockResolvedValue({
|
||||
* success: true,
|
||||
* version: 1,
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export const workflowsOrchestrationMockFns = {
|
||||
mockPerformChatDeploy: vi.fn(),
|
||||
mockPerformChatUndeploy: vi.fn(),
|
||||
mockNotifySocketDeploymentChanged: vi.fn(),
|
||||
mockPerformActivateVersion: vi.fn(),
|
||||
mockPerformFullDeploy: vi.fn(),
|
||||
mockPerformFullUndeploy: vi.fn(),
|
||||
mockPerformRevertToVersion: vi.fn(),
|
||||
mockPerformCreateFolder: vi.fn(),
|
||||
mockPerformUpdateFolder: vi.fn(),
|
||||
mockPerformDeleteFolder: vi.fn(),
|
||||
mockPerformCreateWorkflow: vi.fn(),
|
||||
mockPerformUpdateWorkflow: vi.fn(),
|
||||
mockPerformDeleteWorkflow: vi.fn(),
|
||||
}
|
||||
|
||||
/**
|
||||
* Static mock module for `@/lib/workflows/orchestration`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* vi.mock('@/lib/workflows/orchestration', () => workflowsOrchestrationMock)
|
||||
* ```
|
||||
*/
|
||||
export const workflowsOrchestrationMock = {
|
||||
performChatDeploy: workflowsOrchestrationMockFns.mockPerformChatDeploy,
|
||||
performChatUndeploy: workflowsOrchestrationMockFns.mockPerformChatUndeploy,
|
||||
notifySocketDeploymentChanged: workflowsOrchestrationMockFns.mockNotifySocketDeploymentChanged,
|
||||
performActivateVersion: workflowsOrchestrationMockFns.mockPerformActivateVersion,
|
||||
performFullDeploy: workflowsOrchestrationMockFns.mockPerformFullDeploy,
|
||||
performFullUndeploy: workflowsOrchestrationMockFns.mockPerformFullUndeploy,
|
||||
performRevertToVersion: workflowsOrchestrationMockFns.mockPerformRevertToVersion,
|
||||
performCreateFolder: workflowsOrchestrationMockFns.mockPerformCreateFolder,
|
||||
performUpdateFolder: workflowsOrchestrationMockFns.mockPerformUpdateFolder,
|
||||
performDeleteFolder: workflowsOrchestrationMockFns.mockPerformDeleteFolder,
|
||||
performCreateWorkflow: workflowsOrchestrationMockFns.mockPerformCreateWorkflow,
|
||||
performUpdateWorkflow: workflowsOrchestrationMockFns.mockPerformUpdateWorkflow,
|
||||
performDeleteWorkflow: workflowsOrchestrationMockFns.mockPerformDeleteWorkflow,
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { vi } from 'vitest'
|
||||
|
||||
/**
|
||||
* Controllable mock functions for `@/lib/workflows/persistence/utils`.
|
||||
* All defaults are bare `vi.fn()` — configure per-test as needed.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { workflowsPersistenceUtilsMockFns } from '@sim/testing'
|
||||
*
|
||||
* workflowsPersistenceUtilsMockFns.mockLoadWorkflowFromNormalizedTables.mockResolvedValue({
|
||||
* blocks: {},
|
||||
* edges: [],
|
||||
* loops: {},
|
||||
* parallels: {},
|
||||
* isFromNormalizedTables: true,
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export const workflowsPersistenceUtilsMockFns = {
|
||||
mockBlockExistsInDeployment: vi.fn(),
|
||||
mockLoadDeployedWorkflowState: vi.fn(),
|
||||
mockMigrateAgentBlocksToMessagesFormat: vi.fn(),
|
||||
mockLoadWorkflowFromNormalizedTables: vi.fn(),
|
||||
mockSaveWorkflowToNormalizedTables: vi.fn(),
|
||||
mockWorkflowExistsInNormalizedTables: vi.fn(),
|
||||
mockDeployWorkflow: vi.fn(),
|
||||
mockRegenerateWorkflowStateIds: vi.fn(),
|
||||
mockUndeployWorkflow: vi.fn(),
|
||||
mockActivateWorkflowVersion: vi.fn(),
|
||||
mockActivateWorkflowVersionById: vi.fn(),
|
||||
mockListWorkflowVersions: vi.fn(),
|
||||
}
|
||||
|
||||
/**
|
||||
* Static mock module for `@/lib/workflows/persistence/utils`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* vi.mock('@/lib/workflows/persistence/utils', () => workflowsPersistenceUtilsMock)
|
||||
* ```
|
||||
*/
|
||||
export const workflowsPersistenceUtilsMock = {
|
||||
blockExistsInDeployment: workflowsPersistenceUtilsMockFns.mockBlockExistsInDeployment,
|
||||
loadDeployedWorkflowState: workflowsPersistenceUtilsMockFns.mockLoadDeployedWorkflowState,
|
||||
migrateAgentBlocksToMessagesFormat:
|
||||
workflowsPersistenceUtilsMockFns.mockMigrateAgentBlocksToMessagesFormat,
|
||||
loadWorkflowFromNormalizedTables:
|
||||
workflowsPersistenceUtilsMockFns.mockLoadWorkflowFromNormalizedTables,
|
||||
saveWorkflowToNormalizedTables:
|
||||
workflowsPersistenceUtilsMockFns.mockSaveWorkflowToNormalizedTables,
|
||||
workflowExistsInNormalizedTables:
|
||||
workflowsPersistenceUtilsMockFns.mockWorkflowExistsInNormalizedTables,
|
||||
deployWorkflow: workflowsPersistenceUtilsMockFns.mockDeployWorkflow,
|
||||
regenerateWorkflowStateIds: workflowsPersistenceUtilsMockFns.mockRegenerateWorkflowStateIds,
|
||||
undeployWorkflow: workflowsPersistenceUtilsMockFns.mockUndeployWorkflow,
|
||||
activateWorkflowVersion: workflowsPersistenceUtilsMockFns.mockActivateWorkflowVersion,
|
||||
activateWorkflowVersionById: workflowsPersistenceUtilsMockFns.mockActivateWorkflowVersionById,
|
||||
listWorkflowVersions: workflowsPersistenceUtilsMockFns.mockListWorkflowVersions,
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { vi } from 'vitest'
|
||||
|
||||
/**
|
||||
* Controllable mock functions for `@/lib/workflows/utils`.
|
||||
* Use these references in tests to configure return values and assert calls.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { workflowsUtilsMockFns } from '@sim/testing'
|
||||
*
|
||||
* workflowsUtilsMockFns.mockGetWorkflowById.mockResolvedValue({ id: 'wf-1', name: 'Test' })
|
||||
* ```
|
||||
*/
|
||||
export const workflowsUtilsMockFns = {
|
||||
mockGetWorkflowById: vi.fn(),
|
||||
mockListWorkflows: vi.fn(),
|
||||
mockDeduplicateWorkflowName: vi.fn(),
|
||||
mockResolveWorkflowIdForUser: vi.fn(),
|
||||
mockUpdateWorkflowRunCounts: vi.fn(),
|
||||
mockWorkflowHasResponseBlock: vi.fn(),
|
||||
mockCreateHttpResponseFromBlock: vi.fn(),
|
||||
mockValidateWorkflowPermissions: vi.fn(),
|
||||
mockCreateWorkflowRecord: vi.fn(),
|
||||
mockUpdateWorkflowRecord: vi.fn(),
|
||||
mockDeleteWorkflowRecord: vi.fn(),
|
||||
mockSetWorkflowVariables: vi.fn(),
|
||||
mockCreateFolderRecord: vi.fn(),
|
||||
mockUpdateFolderRecord: vi.fn(),
|
||||
mockDeleteFolderRecord: vi.fn(),
|
||||
mockCheckForCircularReference: vi.fn(),
|
||||
mockListFolders: vi.fn(),
|
||||
}
|
||||
|
||||
/**
|
||||
* Static mock module for `@/lib/workflows/utils`.
|
||||
* Use with `vi.mock()` to replace the real module in tests.
|
||||
*
|
||||
* Default behaviors:
|
||||
* - `getWorkflowById` resolves to `null`
|
||||
* - `validateWorkflowPermissions` resolves to an authorized result
|
||||
* - Other functions resolve to sensible empty/success defaults
|
||||
*
|
||||
* `authorizeWorkflowByWorkspacePermission` moved to `@sim/platform-authz/workflow`;
|
||||
* use `workflowAuthzMock` / `workflowAuthzMockFns` for that surface.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* vi.mock('@/lib/workflows/utils', () => workflowsUtilsMock)
|
||||
* ```
|
||||
*/
|
||||
export const workflowsUtilsMock = {
|
||||
getWorkflowById: workflowsUtilsMockFns.mockGetWorkflowById,
|
||||
listWorkflows: workflowsUtilsMockFns.mockListWorkflows,
|
||||
deduplicateWorkflowName: workflowsUtilsMockFns.mockDeduplicateWorkflowName,
|
||||
resolveWorkflowIdForUser: workflowsUtilsMockFns.mockResolveWorkflowIdForUser,
|
||||
updateWorkflowRunCounts: workflowsUtilsMockFns.mockUpdateWorkflowRunCounts,
|
||||
workflowHasResponseBlock: workflowsUtilsMockFns.mockWorkflowHasResponseBlock,
|
||||
createHttpResponseFromBlock: workflowsUtilsMockFns.mockCreateHttpResponseFromBlock,
|
||||
validateWorkflowPermissions: workflowsUtilsMockFns.mockValidateWorkflowPermissions,
|
||||
createWorkflowRecord: workflowsUtilsMockFns.mockCreateWorkflowRecord,
|
||||
updateWorkflowRecord: workflowsUtilsMockFns.mockUpdateWorkflowRecord,
|
||||
deleteWorkflowRecord: workflowsUtilsMockFns.mockDeleteWorkflowRecord,
|
||||
setWorkflowVariables: workflowsUtilsMockFns.mockSetWorkflowVariables,
|
||||
createFolderRecord: workflowsUtilsMockFns.mockCreateFolderRecord,
|
||||
updateFolderRecord: workflowsUtilsMockFns.mockUpdateFolderRecord,
|
||||
deleteFolderRecord: workflowsUtilsMockFns.mockDeleteFolderRecord,
|
||||
checkForCircularReference: workflowsUtilsMockFns.mockCheckForCircularReference,
|
||||
listFolders: workflowsUtilsMockFns.mockListFolders,
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* Global setup utilities that run once before all tests.
|
||||
*
|
||||
* Use this for expensive setup that should only happen once.
|
||||
*/
|
||||
|
||||
import { vi } from 'vitest'
|
||||
|
||||
/**
|
||||
* Suppresses specific console warnings/errors during tests.
|
||||
*/
|
||||
export function suppressConsoleWarnings(patterns: RegExp[]): void {
|
||||
const originalWarn = console.warn
|
||||
const originalError = console.error
|
||||
|
||||
console.warn = (...args: any[]) => {
|
||||
const message = args.join(' ')
|
||||
if (patterns.some((pattern) => pattern.test(message))) {
|
||||
return
|
||||
}
|
||||
originalWarn.apply(console, args)
|
||||
}
|
||||
|
||||
console.error = (...args: any[]) => {
|
||||
const message = args.join(' ')
|
||||
if (patterns.some((pattern) => pattern.test(message))) {
|
||||
return
|
||||
}
|
||||
originalError.apply(console, args)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Common patterns to suppress in tests.
|
||||
*/
|
||||
export const COMMON_SUPPRESS_PATTERNS = [
|
||||
/Zustand.*persist middleware/i,
|
||||
/React does not recognize the.*prop/,
|
||||
/Warning: Invalid DOM property/,
|
||||
/act\(\) warning/,
|
||||
]
|
||||
|
||||
/**
|
||||
* Sets up global mocks for Node.js environment.
|
||||
*/
|
||||
export function setupNodeEnvironment(): void {
|
||||
// Mock window if not present
|
||||
if (typeof window === 'undefined') {
|
||||
vi.stubGlobal('window', {
|
||||
location: { href: 'http://localhost:3000' },
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
})
|
||||
}
|
||||
|
||||
// Mock document if not present
|
||||
if (typeof document === 'undefined') {
|
||||
vi.stubGlobal('document', {
|
||||
createElement: vi.fn(() => ({
|
||||
style: {},
|
||||
setAttribute: vi.fn(),
|
||||
appendChild: vi.fn(),
|
||||
})),
|
||||
body: { appendChild: vi.fn() },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up global mocks after tests.
|
||||
*/
|
||||
export function cleanupGlobalMocks(): void {
|
||||
vi.unstubAllGlobals()
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Shared Vitest setup file for the testing package.
|
||||
*
|
||||
* Import this in your vitest.config.ts to get common mocks and setup.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // vitest.config.ts
|
||||
* export default defineConfig({
|
||||
* test: {
|
||||
* setupFiles: ['@sim/testing/setup'],
|
||||
* },
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
|
||||
import { afterEach, beforeEach, vi } from 'vitest'
|
||||
import { setupGlobalFetchMock } from '../mocks/fetch.mock'
|
||||
import { createMockLogger } from '../mocks/logger.mock'
|
||||
import { clearStorageMocks, setupGlobalStorageMocks } from '../mocks/storage.mock'
|
||||
|
||||
// Setup global storage mocks
|
||||
setupGlobalStorageMocks()
|
||||
|
||||
// Setup global fetch mock with empty JSON response by default
|
||||
setupGlobalFetchMock({ json: {} })
|
||||
|
||||
// Clear mocks between tests
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
clearStorageMocks()
|
||||
})
|
||||
|
||||
// Export utilities for use in tests
|
||||
export { createMockLogger }
|
||||
export { setupGlobalStorageMocks, clearStorageMocks }
|
||||
export { mockFetchError, mockNextFetchResponse, setupGlobalFetchMock } from '../mocks/fetch.mock'
|
||||
@@ -0,0 +1,153 @@
|
||||
/**
|
||||
* Core types for the testing package.
|
||||
*
|
||||
* These are intentionally loose/permissive types that accept any shape of data
|
||||
* from the app. The testing package should not try to mirror app types exactly -
|
||||
* that creates maintenance burden and type drift issues.
|
||||
*
|
||||
* Tests themselves provide type safety through their actual usage of app types.
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
export interface Position {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
export interface BlockData {
|
||||
parentId?: string
|
||||
extent?: string
|
||||
width?: number
|
||||
height?: number
|
||||
count?: number
|
||||
loopType?: string
|
||||
parallelType?: string
|
||||
collection?: any
|
||||
whileCondition?: string
|
||||
doWhileCondition?: string
|
||||
type?: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export interface SubBlockState {
|
||||
id: string
|
||||
type: string
|
||||
value: any
|
||||
}
|
||||
|
||||
export type BlockOutput = any
|
||||
|
||||
export interface BlockState {
|
||||
id: string
|
||||
type: string
|
||||
name: string
|
||||
position: Position
|
||||
subBlocks: Record<string, SubBlockState>
|
||||
outputs: Record<string, BlockOutput>
|
||||
enabled: boolean
|
||||
horizontalHandles?: boolean
|
||||
height?: number
|
||||
advancedMode?: boolean
|
||||
triggerMode?: boolean
|
||||
data?: BlockData
|
||||
layout?: Record<string, any>
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export interface Edge {
|
||||
id: string
|
||||
source: string
|
||||
target: string
|
||||
sourceHandle?: string | null
|
||||
targetHandle?: string | null
|
||||
type?: string
|
||||
data?: Record<string, any>
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export interface Loop {
|
||||
id: string
|
||||
nodes: string[]
|
||||
iterations: number
|
||||
loopType: string
|
||||
forEachItems?: any
|
||||
whileCondition?: string
|
||||
doWhileCondition?: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export interface Parallel {
|
||||
id: string
|
||||
nodes: string[]
|
||||
distribution?: any
|
||||
count?: number
|
||||
parallelType?: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export interface WorkflowState {
|
||||
blocks: Record<string, BlockState>
|
||||
edges: Edge[]
|
||||
loops: Record<string, Loop>
|
||||
parallels: Record<string, Parallel>
|
||||
lastSaved?: number
|
||||
lastUpdate?: number
|
||||
isDeployed?: boolean
|
||||
deployedAt?: Date
|
||||
needsRedeployment?: boolean
|
||||
variables?: any[]
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export interface ExecutionContext {
|
||||
workflowId: string
|
||||
executionId?: string
|
||||
blockStates: Map<string, any>
|
||||
executedBlocks: Set<string>
|
||||
blockLogs: any[]
|
||||
metadata: {
|
||||
duration: number
|
||||
startTime?: string
|
||||
endTime?: string
|
||||
}
|
||||
environmentVariables: Record<string, string>
|
||||
workflowVariables?: Record<string, any>
|
||||
decisions: {
|
||||
router: Map<string, any>
|
||||
condition: Map<string, any>
|
||||
}
|
||||
loopExecutions: Map<string, any>
|
||||
completedLoops: Set<string>
|
||||
activeExecutionPath: Set<string>
|
||||
abortSignal?: AbortSignal
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export interface User {
|
||||
id: string
|
||||
email: string
|
||||
name?: string
|
||||
image?: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export interface Workspace {
|
||||
id: string
|
||||
name: string
|
||||
ownerId: string
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export interface Workflow {
|
||||
id: string
|
||||
name: string
|
||||
workspaceId: string
|
||||
state: WorkflowState
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
isDeployed?: boolean
|
||||
[key: string]: any
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "@sim/tsconfig/library.json",
|
||||
"compilerOptions": {
|
||||
"lib": ["ES2022", "DOM"],
|
||||
"paths": {
|
||||
"@sim/testing/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user