6bf8bebf51
CI / Test and Build (push) Failing after 1s
CI / Migrate Dev DB (push) Has been skipped
CI / Migrate DB (push) Has been skipped
CodeQL / Analyze actions (push) Has been cancelled
CodeQL / Analyze javascript-typescript (push) Has been cancelled
CI / Detect Version (push) Has been cancelled
CI / Detect Desktop Changes (push) Has been cancelled
CI / Build AMD64 (blacksmith-2vcpu-ubuntu-2404, ./docker/cron.Dockerfile, ubuntu-latest, ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Build AMD64 (blacksmith-2vcpu-ubuntu-2404, ./docker/db.Dockerfile, ECR_MIGRATIONS, ubuntu-latest, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (blacksmith-4vcpu-ubuntu-2404, ./docker/pii.Dockerfile, ECR_PII, ubuntu-latest, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (blacksmith-4vcpu-ubuntu-2404, ./docker/realtime.Dockerfile, ECR_REALTIME, ubuntu-latest, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build AMD64 (blacksmith-8vcpu-ubuntu-2404, ./docker/app.Dockerfile, ECR_APP, linux-x64-8-core, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/cron.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/db.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/pii.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/realtime.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-8vcpu-ubuntu-2404-arm, ./docker/app.Dockerfile, linux-arm64-8-core, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
Helm Chart / Lint, test, and validate chart (push) Has been cancelled
Helm Chart / Chart version bumped (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Build Dev ECR (blacksmith-8vcpu-ubuntu-2404, ./docker/app.Dockerfile, ECR_APP, linux-x64-8-core) (push) Has been cancelled
CI / Promote Images (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/cron) (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 / Build Dev ECR (blacksmith-2vcpu-ubuntu-2404, ./docker/db.Dockerfile, ECR_MIGRATIONS, ubuntu-latest) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-4vcpu-ubuntu-2404, ./docker/pii.Dockerfile, ECR_PII, ubuntu-latest) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-4vcpu-ubuntu-2404, ./docker/realtime.Dockerfile, ECR_REALTIME, ubuntu-latest) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Check Desktop Signing Secrets (push) Has been cancelled
CI / Desktop Release (push) Has been cancelled
CI / Create Desktop Prerelease (push) Has been cancelled
CI / Desktop Prerelease Build (push) Has been cancelled
CI / Publish Desktop Prerelease (push) Has been cancelled
CI / Prune Desktop Prereleases (push) Has been cancelled
Helm Chart / Install on kind and run helm test (push) Has been cancelled
640 lines
18 KiB
TypeScript
640 lines
18 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import { StartBlockPath } from '@/lib/workflows/triggers/triggers'
|
|
import type { UserFile } from '@/executor/types'
|
|
import {
|
|
buildResolutionFromBlock,
|
|
buildStartBlockOutput,
|
|
resolveExecutorStartBlock,
|
|
} from '@/executor/utils/start-block'
|
|
import type { SerializedBlock } from '@/serializer/types'
|
|
|
|
function createBlock(
|
|
type: string,
|
|
id = type,
|
|
options?: { subBlocks?: Record<string, unknown> }
|
|
): SerializedBlock {
|
|
return {
|
|
id,
|
|
position: { x: 0, y: 0 },
|
|
config: {
|
|
tool: type,
|
|
params: options?.subBlocks?.inputFormat ? { inputFormat: options.subBlocks.inputFormat } : {},
|
|
},
|
|
inputs: {},
|
|
outputs: {},
|
|
metadata: {
|
|
id: type,
|
|
name: `block-${type}`,
|
|
category: 'triggers',
|
|
...(options?.subBlocks ? { subBlocks: options.subBlocks } : {}),
|
|
} as SerializedBlock['metadata'] & { subBlocks?: Record<string, unknown> },
|
|
enabled: true,
|
|
}
|
|
}
|
|
|
|
describe('start-block utilities', () => {
|
|
it.concurrent('buildResolutionFromBlock returns null when metadata id missing', () => {
|
|
const block = createBlock('api_trigger')
|
|
;(block.metadata as Record<string, unknown>).id = undefined
|
|
|
|
expect(buildResolutionFromBlock(block)).toBeNull()
|
|
})
|
|
|
|
it.concurrent('resolveExecutorStartBlock prefers unified start block', () => {
|
|
const blocks = [
|
|
createBlock('api_trigger', 'api'),
|
|
createBlock('starter', 'starter'),
|
|
createBlock('start_trigger', 'start'),
|
|
]
|
|
|
|
const resolution = resolveExecutorStartBlock(blocks, {
|
|
execution: 'api',
|
|
isChildWorkflow: false,
|
|
})
|
|
|
|
expect(resolution?.blockId).toBe('start')
|
|
expect(resolution?.path).toBe(StartBlockPath.UNIFIED)
|
|
})
|
|
|
|
it.concurrent('buildStartBlockOutput normalizes unified start payload', () => {
|
|
const block = createBlock('start_trigger', 'start')
|
|
const resolution = {
|
|
blockId: 'start',
|
|
block,
|
|
path: StartBlockPath.UNIFIED,
|
|
} as const
|
|
|
|
const output = buildStartBlockOutput({
|
|
resolution,
|
|
workflowInput: { payload: 'value' },
|
|
})
|
|
|
|
expect(output.payload).toBe('value')
|
|
expect(output.input).toBeUndefined()
|
|
expect(output.conversationId).toBeUndefined()
|
|
})
|
|
|
|
it.concurrent('buildStartBlockOutput uses trigger schema for API triggers', () => {
|
|
const apiBlock = createBlock('api_trigger', 'api', {
|
|
subBlocks: {
|
|
inputFormat: {
|
|
value: [
|
|
{ name: 'name', type: 'string' },
|
|
{ name: 'count', type: 'number' },
|
|
],
|
|
},
|
|
},
|
|
})
|
|
|
|
const resolution = {
|
|
blockId: 'api',
|
|
block: apiBlock,
|
|
path: StartBlockPath.SPLIT_API,
|
|
} as const
|
|
|
|
const files: UserFile[] = [
|
|
{
|
|
id: 'file-1',
|
|
name: 'document.txt',
|
|
url: 'https://example.com/document.txt',
|
|
size: 42,
|
|
type: 'text/plain',
|
|
key: 'file-key',
|
|
},
|
|
]
|
|
|
|
const output = buildStartBlockOutput({
|
|
resolution,
|
|
workflowInput: {
|
|
input: {
|
|
name: 'Ada',
|
|
count: '5',
|
|
},
|
|
files,
|
|
},
|
|
})
|
|
|
|
expect(output.name).toBe('Ada')
|
|
expect(output.input).toEqual({ name: 'Ada', count: 5 })
|
|
expect(output.files).toEqual(files)
|
|
})
|
|
|
|
it.concurrent('buildStartBlockOutput normalizes Start files from internal serve URLs', () => {
|
|
const block = createBlock('start_trigger', 'start')
|
|
const resolution = {
|
|
blockId: 'start',
|
|
block,
|
|
path: StartBlockPath.UNIFIED,
|
|
} as const
|
|
|
|
const output = buildStartBlockOutput({
|
|
resolution,
|
|
workflowInput: {
|
|
files: [
|
|
{
|
|
id: 'file_1',
|
|
name: 'screenshot.png',
|
|
url: '/api/files/serve/s3/execution%2Fworkspace-id%2Fworkflow-id%2Fexecution-id%2Fscreenshot.png?context=execution',
|
|
size: 243289,
|
|
type: 'image/png',
|
|
},
|
|
],
|
|
},
|
|
})
|
|
|
|
expect(output.files).toEqual([
|
|
{
|
|
id: 'file_1',
|
|
name: 'screenshot.png',
|
|
url: '/api/files/serve/s3/execution%2Fworkspace-id%2Fworkflow-id%2Fexecution-id%2Fscreenshot.png?context=execution',
|
|
size: 243289,
|
|
type: 'image/png',
|
|
key: 'execution/workspace-id/workflow-id/execution-id/screenshot.png',
|
|
context: 'execution',
|
|
},
|
|
])
|
|
})
|
|
|
|
it.concurrent('rejects inputFormat fields that collide with executor routing keys', () => {
|
|
const block = createBlock('start_trigger', 'start', {
|
|
subBlocks: {
|
|
inputFormat: {
|
|
value: [
|
|
{ name: 'error', type: 'string' },
|
|
{ name: 'error', type: 'string' },
|
|
{ name: ' selectedOption ', type: 'string' },
|
|
{ name: 'selectedRoute', type: 'string' },
|
|
{ name: '_pauseMetadata', type: 'object' },
|
|
],
|
|
},
|
|
},
|
|
})
|
|
|
|
const resolution = {
|
|
blockId: 'start',
|
|
block,
|
|
path: StartBlockPath.UNIFIED,
|
|
} as const
|
|
|
|
expect(() =>
|
|
buildStartBlockOutput({
|
|
resolution,
|
|
workflowInput: { error: false, selectedRoute: 'source' },
|
|
})
|
|
).toThrow(
|
|
'Start block "block-start_trigger" cannot use reserved input format field name(s): error, selectedOption, selectedRoute, _pauseMetadata'
|
|
)
|
|
})
|
|
|
|
it.concurrent(
|
|
'rejects reserved top-level runtime input keys copied to unified Start output',
|
|
() => {
|
|
const block = createBlock('start_trigger', 'start')
|
|
const resolution = {
|
|
blockId: 'start',
|
|
block,
|
|
path: StartBlockPath.UNIFIED,
|
|
} as const
|
|
|
|
expect(() =>
|
|
buildStartBlockOutput({
|
|
resolution,
|
|
workflowInput: { error: 'false', payload: 'value' },
|
|
})
|
|
).toThrow(
|
|
'Start block "block-start_trigger" cannot use reserved runtime input field name(s): error'
|
|
)
|
|
}
|
|
)
|
|
|
|
it.concurrent('rejects reserved nested API input keys copied to trigger output', () => {
|
|
const block = createBlock('api_trigger', 'api')
|
|
const resolution = {
|
|
blockId: 'api',
|
|
block,
|
|
path: StartBlockPath.SPLIT_API,
|
|
} as const
|
|
|
|
expect(() =>
|
|
buildStartBlockOutput({
|
|
resolution,
|
|
workflowInput: { input: { selectedRoute: 'route-1', payload: 'value' } },
|
|
})
|
|
).toThrow(
|
|
'Start block "block-api_trigger" cannot use reserved runtime input field name(s): selectedRoute'
|
|
)
|
|
})
|
|
|
|
it.concurrent('allows reserved inputFormat field names on split chat trigger output', () => {
|
|
const block = createBlock('chat_trigger', 'chat', {
|
|
subBlocks: {
|
|
inputFormat: {
|
|
value: [{ name: 'error', type: 'string' }],
|
|
},
|
|
},
|
|
})
|
|
const resolution = {
|
|
blockId: 'chat',
|
|
block,
|
|
path: StartBlockPath.SPLIT_CHAT,
|
|
} as const
|
|
|
|
const output = buildStartBlockOutput({
|
|
resolution,
|
|
workflowInput: { input: 'hello', conversationId: 'conversation-1' },
|
|
})
|
|
|
|
expect(output).toEqual({ input: 'hello', conversationId: 'conversation-1' })
|
|
})
|
|
|
|
it.concurrent('allows reserved inputFormat field names on legacy chat starter output', () => {
|
|
const block = createBlock('starter', 'starter', {
|
|
subBlocks: {
|
|
startWorkflow: { value: 'chat' },
|
|
inputFormat: {
|
|
value: [{ name: 'error', type: 'string' }],
|
|
},
|
|
},
|
|
})
|
|
const resolution = {
|
|
blockId: 'starter',
|
|
block,
|
|
path: StartBlockPath.LEGACY_STARTER,
|
|
} as const
|
|
|
|
const output = buildStartBlockOutput({
|
|
resolution,
|
|
workflowInput: { input: 'hello' },
|
|
})
|
|
|
|
expect(output).toEqual({ input: 'hello' })
|
|
})
|
|
|
|
it.concurrent('allows reserved inputFormat field names on serialized legacy chat starter', () => {
|
|
const block = createBlock('starter', 'starter')
|
|
block.config.params = {
|
|
startWorkflow: 'chat',
|
|
inputFormat: [{ name: 'error', type: 'string' }],
|
|
}
|
|
const resolution = {
|
|
blockId: 'starter',
|
|
block,
|
|
path: StartBlockPath.LEGACY_STARTER,
|
|
} as const
|
|
|
|
const output = buildStartBlockOutput({
|
|
resolution,
|
|
workflowInput: { input: 'hello' },
|
|
})
|
|
|
|
expect(output).toEqual({ input: 'hello' })
|
|
})
|
|
|
|
it.concurrent('ignores malformed non-string inputFormat field names', () => {
|
|
const block = createBlock('start_trigger', 'start', {
|
|
subBlocks: {
|
|
inputFormat: {
|
|
value: [
|
|
{ name: 123, type: 'string', value: 'ignored' },
|
|
{ name: 'customField', type: 'string' },
|
|
],
|
|
},
|
|
},
|
|
})
|
|
const resolution = {
|
|
blockId: 'start',
|
|
block,
|
|
path: StartBlockPath.UNIFIED,
|
|
} as const
|
|
|
|
const output = buildStartBlockOutput({
|
|
resolution,
|
|
workflowInput: { customField: 'value' },
|
|
})
|
|
|
|
expect(output.customField).toBe('value')
|
|
expect(output[123]).toBeUndefined()
|
|
})
|
|
|
|
describe('inputFormat default values', () => {
|
|
it.concurrent('uses default value when runtime does not provide the field', () => {
|
|
const block = createBlock('start_trigger', 'start', {
|
|
subBlocks: {
|
|
inputFormat: {
|
|
value: [
|
|
{ name: 'input', type: 'string' },
|
|
{ name: 'customField', type: 'string', value: 'defaultValue' },
|
|
],
|
|
},
|
|
},
|
|
})
|
|
|
|
const resolution = {
|
|
blockId: 'start',
|
|
block,
|
|
path: StartBlockPath.UNIFIED,
|
|
} as const
|
|
|
|
const output = buildStartBlockOutput({
|
|
resolution,
|
|
workflowInput: { input: 'hello' },
|
|
})
|
|
|
|
expect(output.input).toBe('hello')
|
|
expect(output.customField).toBe('defaultValue')
|
|
})
|
|
|
|
it.concurrent('runtime value overrides default value', () => {
|
|
const block = createBlock('start_trigger', 'start', {
|
|
subBlocks: {
|
|
inputFormat: {
|
|
value: [{ name: 'customField', type: 'string', value: 'defaultValue' }],
|
|
},
|
|
},
|
|
})
|
|
|
|
const resolution = {
|
|
blockId: 'start',
|
|
block,
|
|
path: StartBlockPath.UNIFIED,
|
|
} as const
|
|
|
|
const output = buildStartBlockOutput({
|
|
resolution,
|
|
workflowInput: { customField: 'runtimeValue' },
|
|
})
|
|
|
|
expect(output.customField).toBe('runtimeValue')
|
|
})
|
|
|
|
it.concurrent('empty string from runtime overrides default value', () => {
|
|
const block = createBlock('start_trigger', 'start', {
|
|
subBlocks: {
|
|
inputFormat: {
|
|
value: [{ name: 'customField', type: 'string', value: 'defaultValue' }],
|
|
},
|
|
},
|
|
})
|
|
|
|
const resolution = {
|
|
blockId: 'start',
|
|
block,
|
|
path: StartBlockPath.UNIFIED,
|
|
} as const
|
|
|
|
const output = buildStartBlockOutput({
|
|
resolution,
|
|
workflowInput: { customField: '' },
|
|
})
|
|
|
|
expect(output.customField).toBe('')
|
|
})
|
|
|
|
it.concurrent('null from runtime does not override default value', () => {
|
|
const block = createBlock('start_trigger', 'start', {
|
|
subBlocks: {
|
|
inputFormat: {
|
|
value: [{ name: 'customField', type: 'string', value: 'defaultValue' }],
|
|
},
|
|
},
|
|
})
|
|
|
|
const resolution = {
|
|
blockId: 'start',
|
|
block,
|
|
path: StartBlockPath.UNIFIED,
|
|
} as const
|
|
|
|
const output = buildStartBlockOutput({
|
|
resolution,
|
|
workflowInput: { customField: null },
|
|
})
|
|
|
|
expect(output.customField).toBe('defaultValue')
|
|
})
|
|
|
|
it.concurrent('preserves coerced types for unified start payload', () => {
|
|
const block = createBlock('start_trigger', 'start', {
|
|
subBlocks: {
|
|
inputFormat: {
|
|
value: [
|
|
{ name: 'conversation_id', type: 'number' },
|
|
{ name: 'sender', type: 'object' },
|
|
{ name: 'is_active', type: 'boolean' },
|
|
],
|
|
},
|
|
},
|
|
})
|
|
|
|
const resolution = {
|
|
blockId: 'start',
|
|
block,
|
|
path: StartBlockPath.UNIFIED,
|
|
} as const
|
|
|
|
const output = buildStartBlockOutput({
|
|
resolution,
|
|
workflowInput: {
|
|
conversation_id: '149',
|
|
sender: '{"id":10,"email":"user@example.com"}',
|
|
is_active: 'true',
|
|
},
|
|
})
|
|
|
|
expect(output.conversation_id).toBe(149)
|
|
expect(output.sender).toEqual({ id: 10, email: 'user@example.com' })
|
|
expect(output.is_active).toBe(true)
|
|
})
|
|
|
|
it.concurrent(
|
|
'prefers coerced inputFormat values over duplicated top-level workflowInput keys',
|
|
() => {
|
|
const block = createBlock('start_trigger', 'start', {
|
|
subBlocks: {
|
|
inputFormat: {
|
|
value: [
|
|
{ name: 'conversation_id', type: 'number' },
|
|
{ name: 'sender', type: 'object' },
|
|
{ name: 'is_active', type: 'boolean' },
|
|
],
|
|
},
|
|
},
|
|
})
|
|
|
|
const resolution = {
|
|
blockId: 'start',
|
|
block,
|
|
path: StartBlockPath.UNIFIED,
|
|
} as const
|
|
|
|
const output = buildStartBlockOutput({
|
|
resolution,
|
|
workflowInput: {
|
|
input: {
|
|
conversation_id: '149',
|
|
sender: '{"id":10,"email":"user@example.com"}',
|
|
is_active: 'false',
|
|
},
|
|
conversation_id: '150',
|
|
sender: '{"id":99,"email":"wrong@example.com"}',
|
|
is_active: 'true',
|
|
extra: 'keep-me',
|
|
},
|
|
})
|
|
|
|
expect(output.conversation_id).toBe(149)
|
|
expect(output.sender).toEqual({ id: 10, email: 'user@example.com' })
|
|
expect(output.is_active).toBe(false)
|
|
expect(output.extra).toBe('keep-me')
|
|
}
|
|
)
|
|
})
|
|
|
|
describe('EXTERNAL_TRIGGER path', () => {
|
|
it.concurrent('rejects reserved runtime input keys copied to external trigger output', () => {
|
|
const block = createBlock('webhook', 'start')
|
|
const resolution = {
|
|
blockId: 'start',
|
|
block,
|
|
path: StartBlockPath.EXTERNAL_TRIGGER,
|
|
} as const
|
|
|
|
expect(() =>
|
|
buildStartBlockOutput({
|
|
resolution,
|
|
workflowInput: { _pauseMetadata: { contextId: 'fake-pause' }, payload: 'value' },
|
|
})
|
|
).toThrow(
|
|
'Start block "block-webhook" cannot use reserved runtime input field name(s): _pauseMetadata'
|
|
)
|
|
})
|
|
|
|
it.concurrent('preserves coerced types for integration trigger payload', () => {
|
|
const block = createBlock('webhook', 'start', {
|
|
subBlocks: {
|
|
inputFormat: {
|
|
value: [
|
|
{ name: 'count', type: 'number' },
|
|
{ name: 'payload', type: 'object' },
|
|
],
|
|
},
|
|
},
|
|
})
|
|
|
|
const resolution = {
|
|
blockId: 'start',
|
|
block,
|
|
path: StartBlockPath.EXTERNAL_TRIGGER,
|
|
} as const
|
|
|
|
const output = buildStartBlockOutput({
|
|
resolution,
|
|
workflowInput: {
|
|
count: '5',
|
|
payload: '{"event":"push"}',
|
|
extra: 'untouched',
|
|
},
|
|
})
|
|
|
|
expect(output.count).toBe(5)
|
|
expect(output.payload).toEqual({ event: 'push' })
|
|
expect(output.extra).toBe('untouched')
|
|
})
|
|
})
|
|
|
|
describe('run metadata injection', () => {
|
|
const runMetadata = {
|
|
userEmail: 'real@sim.ai',
|
|
workspaceId: 'ws-1',
|
|
workflowId: 'wf-1',
|
|
executionId: 'exec-1',
|
|
executionType: 'api',
|
|
executionMode: 'sync' as const,
|
|
startTime: '2026-07-15T00:00:00.000Z',
|
|
}
|
|
|
|
function createUnifiedResolution(subBlocks?: Record<string, unknown>) {
|
|
const block = createBlock('start_trigger', 'start', subBlocks ? { subBlocks } : undefined)
|
|
return {
|
|
blockId: 'start',
|
|
block,
|
|
path: StartBlockPath.UNIFIED,
|
|
} as const
|
|
}
|
|
|
|
it.concurrent('server metadata overrides caller-supplied metadata key', () => {
|
|
const resolution = createUnifiedResolution({ runMetadata: { value: true } })
|
|
|
|
const output = buildStartBlockOutput({
|
|
resolution,
|
|
workflowInput: {
|
|
metadata: { userEmail: 'attacker@x.com' },
|
|
simUserEmail: 'attacker@x.com',
|
|
payload: 'value',
|
|
},
|
|
runMetadata,
|
|
})
|
|
|
|
expect(output.metadata).toEqual(runMetadata)
|
|
expect(output.payload).toBe('value')
|
|
expect(output.simUserEmail).toBe('attacker@x.com')
|
|
})
|
|
|
|
it.concurrent('strips caller-supplied metadata key when no trusted metadata exists', () => {
|
|
const resolution = createUnifiedResolution({ runMetadata: { value: true } })
|
|
|
|
const output = buildStartBlockOutput({
|
|
resolution,
|
|
workflowInput: { metadata: { userEmail: 'attacker@x.com' } },
|
|
})
|
|
|
|
expect(output).not.toHaveProperty('metadata')
|
|
})
|
|
|
|
it.concurrent('throws when an input format field is named metadata', () => {
|
|
const resolution = createUnifiedResolution({
|
|
runMetadata: { value: true },
|
|
inputFormat: { value: [{ name: 'metadata', type: 'string' }] },
|
|
})
|
|
|
|
expect(() =>
|
|
buildStartBlockOutput({
|
|
resolution,
|
|
workflowInput: {},
|
|
runMetadata,
|
|
})
|
|
).toThrow('reserves the "metadata" output')
|
|
})
|
|
|
|
it.concurrent('toggle off leaves caller-supplied metadata untouched', () => {
|
|
const resolution = createUnifiedResolution()
|
|
|
|
const output = buildStartBlockOutput({
|
|
resolution,
|
|
workflowInput: { metadata: { custom: 'value' } },
|
|
runMetadata,
|
|
})
|
|
|
|
expect(output.metadata).toEqual({ custom: 'value' })
|
|
})
|
|
|
|
it.concurrent('reads the toggle from config params when metadata subBlocks are absent', () => {
|
|
const block = createBlock('start_trigger', 'start')
|
|
block.config.params.runMetadata = true
|
|
const resolution = {
|
|
blockId: 'start',
|
|
block,
|
|
path: StartBlockPath.UNIFIED,
|
|
} as const
|
|
|
|
const output = buildStartBlockOutput({
|
|
resolution,
|
|
workflowInput: { metadata: 'spoof' },
|
|
runMetadata,
|
|
})
|
|
|
|
expect(output.metadata).toEqual(runMetadata)
|
|
})
|
|
})
|
|
})
|