import { describe, expect, test } from 'bun:test' describe('workflow module builders', () => { test('documentTemplaterNode builds the backend document-templater action', async () => { const { documentTemplaterNode, inputRef } = await import('./workflow-builders.ts') const node = documentTemplaterNode({ name: 'Fill Contract', vaultUrl: 'vault://template.docx', templateFilename: 'template.docx', placeholders: [{ key: 'client', reference: inputRef({ id: 'input-client', name: 'client' }) }], }) expect(node.actionId).toBe('document-templater') expect(node.input).toMatchObject({ vaultUrl: 'vault://template.docx', templateFilename: 'template.docx', outputFilename: 'template.docx', placeholders: [{ key: 'client', reference: '{{input://input-client}}' }], }) }) test('documentTemplaterNode rejects non-docx template names', async () => { const { documentTemplaterNode } = await import('./workflow-builders.ts') expect(() => documentTemplaterNode({ name: 'Bad', vaultUrl: 'vault://bad.pdf', templateFilename: 'bad.pdf', })).toThrow('templateFilename must end with .docx') }) }) describe('workflow module completeness rules', () => { test('flags blank map over input', async () => { const { mapNode, llmNode, buildGraph } = await import('./workflow-builders.ts') const { validateWorkflowGraph } = await import('./workflows.ts') const map = mapNode({ name: 'Map', over: '', mapVariable: 'item' }) const child = llmNode({ name: 'Child', prompt: 'child' }) const graph = buildGraph([map, child], [[map, child, 'subgraph']], { validate: false }) const result = validateWorkflowGraph(graph, { version: 'v3' }) expect(result.errors.map(error => error.code)).toContain('module_required_field') }) test('flags pending code execution sandbox', async () => { const { codeExecutionNode, buildGraph } = await import('./workflow-builders.ts') const { validateWorkflowGraph } = await import('./workflows.ts') const node = codeExecutionNode({ name: 'Code', code: 'function execute(input) { return input }' }) node.input = { ...node.input, sandboxStatus: 'pending' } const graph = buildGraph([node], [], { validate: false }) const result = validateWorkflowGraph(graph, { version: 'v3' }) expect(result.errors.map(error => error.code)).toContain('code_execution_sandbox_not_ready') }) test('flags missing code execution execute function', async () => { const { codeExecutionNode, buildGraph } = await import('./workflow-builders.ts') const { validateWorkflowGraph } = await import('./workflows.ts') const node = codeExecutionNode({ name: 'Code', code: 'function execute(input) { return input }' }) node.input = { ...node.input, code: 'function helper(input) { return input }' } const graph = buildGraph([node], [], { validate: false }) const result = validateWorkflowGraph(graph, { version: 'v3' }) expect(result.errors.map(error => error.code)).toContain('code_execution_missing_execute') }) test('flags building code execution sandbox', async () => { const { codeExecutionNode, buildGraph } = await import('./workflow-builders.ts') const { validateWorkflowGraph } = await import('./workflows.ts') const node = codeExecutionNode({ name: 'Code', code: 'function execute(input) { return input }' }) node.input = { ...node.input, sandboxStatus: 'building' } const graph = buildGraph([node], [], { validate: false }) const result = validateWorkflowGraph(graph, { version: 'v3' }) expect(result.errors.map(error => error.code)).toContain('code_execution_sandbox_not_ready') }) test('rejects workflow references in non-reference-enabled module fields', async () => { const { llmNode, buildGraph } = await import('./workflow-builders.ts') const { validateWorkflowGraph } = await import('./workflows.ts') const node = llmNode({ name: 'Prompt', prompt: 'Summarize {{input://topic}}' }) node.input = { ...node.input, model: '{{input://model}}' } const graph = buildGraph([node], [], { validate: false }) const result = validateWorkflowGraph(graph, { version: 'v3' }) expect(result.errors.map(error => error.code)).toContain('module_reference_field_not_allowed') }) test('allows workflow references nested under canvas variables', async () => { const { canvasNode, buildGraph } = await import('./workflow-builders.ts') const { validateWorkflowGraph } = await import('./workflows.ts') const node = canvasNode({ name: 'Canvas', canvasId: 'canvas-id', variables: { payload: { topic: '{{input://topic}}' } }, }) const graph = buildGraph([node], [], { validate: false }) const result = validateWorkflowGraph(graph, { version: 'v3' }) expect(result.errors).not.toContainEqual(expect.objectContaining({ code: 'module_reference_field_not_allowed', path: 'input.variables.payload.topic', })) expect(result.valid).toBe(true) }) test('allows workflow references nested under template variables', async () => { const { templateNode, buildGraph } = await import('./workflow-builders.ts') const { validateWorkflowGraph } = await import('./workflows.ts') const node = templateNode({ name: 'Template', templateId: 'template-id', variables: { payload: { topic: '{{input://topic}}' } }, }) const graph = buildGraph([node], [], { validate: false }) const result = validateWorkflowGraph(graph) expect(result.errors).not.toContainEqual(expect.objectContaining({ code: 'module_reference_field_not_allowed', path: 'input.variables.payload.topic', })) expect(result.valid).toBe(true) }) test('allows shorthand workflow references nested under template variables', async () => { const { templateNode, buildGraph } = await import('./workflow-builders.ts') const { validateWorkflowGraph } = await import('./workflows.ts') const node = templateNode({ name: 'Template', templateId: 'template-id', variables: { topic: '{{topic}}' }, }) const graph = buildGraph([node], [], { validate: false }) const result = validateWorkflowGraph(graph) expect(result.errors).not.toContainEqual(expect.objectContaining({ code: 'module_reference_field_not_allowed', path: 'input.variables.topic', })) expect(result.valid).toBe(true) }) test('rejects workflow references in disallowed canvas fields', async () => { const { canvasNode, buildGraph } = await import('./workflow-builders.ts') const { validateWorkflowGraph } = await import('./workflows.ts') const node = canvasNode({ name: 'Canvas', canvasId: 'canvas-id' }) node.input = { ...node.input, tags: ['{{input://tag}}'] } const graph = buildGraph([node], [], { validate: false }) const result = validateWorkflowGraph(graph, { version: 'v3' }) expect(result.errors).toContainEqual(expect.objectContaining({ code: 'module_reference_field_not_allowed', path: 'input.tags[0]', })) }) test('rejects shorthand workflow references in non-reference-enabled canvas fields', async () => { const { canvasNode, buildGraph } = await import('./workflow-builders.ts') const { validateWorkflowGraph } = await import('./workflows.ts') const node = canvasNode({ name: 'Canvas', canvasId: '{{canvasId}}', tags: ['{{tag}}'] }) const graph = buildGraph([node], [], { validate: false }) const result = validateWorkflowGraph(graph, { version: 'v3' }) expect(result.errors).toContainEqual(expect.objectContaining({ code: 'module_reference_field_not_allowed', path: 'input.canvasId', })) expect(result.errors).toContainEqual(expect.objectContaining({ code: 'module_reference_field_not_allowed', path: 'input.tags[0]', })) }) test('rejects shorthand workflow references in map control field', async () => { const { mapNode, llmNode, buildGraph } = await import('./workflow-builders.ts') const { validateWorkflowGraph } = await import('./workflows.ts') const map = mapNode({ name: 'Map', over: '{{items}}', mapVariable: 'item' }) const child = llmNode({ name: 'Child', prompt: 'child' }) const graph = buildGraph([map, child], [[map, child, 'subgraph']], { validate: false }) const result = validateWorkflowGraph(graph, { version: 'v3' }) expect(result.errors).toContainEqual(expect.objectContaining({ code: 'module_shorthand_reference_not_allowed', path: 'input.over', })) }) test('rejects shorthand workflow references in condition control fields', async () => { const { conditionNode, llmNode, buildGraph } = await import('./workflow-builders.ts') const { validateWorkflowGraph } = await import('./workflows.ts') const condition = conditionNode({ name: 'Route', cases: [{ name: 'A', condition: '{{kind}}|equals|a' }], }) const caseNode = llmNode({ name: 'Case', prompt: 'case' }) const defaultNode = llmNode({ name: 'Default', prompt: 'default' }) const graph = buildGraph( [condition, caseNode, defaultNode], [[condition, caseNode, { branch: 0 }], [condition, defaultNode, { branch: 'default' }]], { validate: false }, ) const result = validateWorkflowGraph(graph, { version: 'v3' }) expect(result.errors).toContainEqual(expect.objectContaining({ code: 'module_shorthand_reference_not_allowed', path: 'input.cases[0].condition', })) }) test('returns validation errors for malformed condition cases', async () => { const { conditionNode, buildGraph } = await import('./workflow-builders.ts') const { validateWorkflowGraph } = await import('./workflows.ts') const condition = conditionNode({ name: 'Route', cases: [{ name: 'A', condition: '{{input://kind}}|equals|a' }], includeDefault: false, }) condition.input = { ...condition.input, cases: [null] } const graph = buildGraph([condition], [], { validate: false }) const result = validateWorkflowGraph(graph, { version: 'v3', strictAuthoring: false }) expect(result.errors).toContainEqual(expect.objectContaining({ code: 'condition_case_invalid', path: 'input.cases[0]', })) }) test('rejects blank condition case expressions', async () => { const { conditionNode, buildGraph } = await import('./workflow-builders.ts') const { validateWorkflowGraph } = await import('./workflows.ts') const condition = conditionNode({ name: 'Route', cases: [{ name: 'A', condition: ' ' }], includeDefault: false, }) const graph = buildGraph([condition], [], { validate: false }) const result = validateWorkflowGraph(graph, { version: 'v3', strictAuthoring: false }) expect(result.errors).toContainEqual(expect.objectContaining({ code: 'condition_case_missing_condition', path: 'input.cases[0].condition', })) }) test('flags incomplete document templater placeholders with placeholder path', async () => { const { documentTemplaterNode, buildGraph } = await import('./workflow-builders.ts') const { validateWorkflowGraph } = await import('./workflows.ts') const node = documentTemplaterNode({ name: 'Fill Contract', vaultUrl: 'vault://template.docx', placeholders: [{ key: 'client', reference: '' }], }) const graph = buildGraph([node], [], { validate: false }) const result = validateWorkflowGraph(graph, { version: 'v3' }) expect(result.errors).toContainEqual(expect.objectContaining({ code: 'document_templater_placeholder_incomplete', path: 'input.placeholders[0]', })) }) test('returns validation errors for malformed document templater placeholders', async () => { const { documentTemplaterNode, buildGraph } = await import('./workflow-builders.ts') const { validateWorkflowGraph } = await import('./workflows.ts') const node = documentTemplaterNode({ name: 'Fill Contract', vaultUrl: 'vault://template.docx', }) node.input = { ...node.input, placeholders: [null] } const graph = buildGraph([node], [], { validate: false }) const result = validateWorkflowGraph(graph, { version: 'v3' }) expect(result.errors).toContainEqual(expect.objectContaining({ code: 'document_templater_placeholder_incomplete', path: 'input.placeholders[0]', })) }) }) describe('workflow docs examples', () => { test('stable id document templater example is executable', async () => { const { buildRunInput, buildWorkflowSpecV3, documentTemplaterNode, inputRef, } = await import('./workflow-builders.ts') const clientInput = { id: 'input-client', name: 'clientName', type: 'text' as const } const spec = buildWorkflowSpecV3({ inputs: [clientInput] }) const fillDoc = documentTemplaterNode({ name: 'Fill contract', vaultUrl: 'vault://template.docx', templateFilename: 'template.docx', placeholders: [ { key: 'clientName', reference: inputRef(clientInput) }, ], }) const runInput = buildRunInput('clientName', 'Acme', { id: clientInput.id }) expect((spec.inputs as any).clientName.id).toBe('input-client') expect(fillDoc.input).toMatchObject({ placeholders: [{ key: 'clientName', reference: '{{input://input-client}}' }], }) expect(runInput).toMatchObject({ id: 'input-client', name: 'clientName' }) }) })