import { describe, expect, it, vi } from 'vitest' import { createLargeArrayManifest, isLargeArrayManifest, } from '@/lib/execution/payloads/large-array-manifest' import { compactExecutionPayload } from '@/lib/execution/payloads/serializer' import { navigatePathAsync } from '@/executor/variables/resolvers/reference-async.server' import type { ResolutionContext } from './reference' import { WorkflowResolver } from './workflow' vi.mock('@/lib/workflows/variables/variable-manager', () => ({ VariableManager: { resolveForExecution: vi.fn((value) => value), }, })) /** * Creates a minimal ResolutionContext for testing. * The WorkflowResolver only uses context.executionContext.workflowVariables, * so we only need to provide that field. */ function createTestContext(workflowVariables: Record): ResolutionContext { return { executionContext: { workflowVariables, workspaceId: 'workspace-1', workflowId: 'workflow-1', executionId: 'execution-1', }, executionState: {}, currentNodeId: 'test-node', } as ResolutionContext } describe('WorkflowResolver', () => { describe('canResolve', () => { it.concurrent('should return true for variable references', () => { const resolver = new WorkflowResolver({}) expect(resolver.canResolve('')).toBe(true) expect(resolver.canResolve('')).toBe(true) }) it.concurrent('should return false for non-variable references', () => { const resolver = new WorkflowResolver({}) expect(resolver.canResolve('')).toBe(false) expect(resolver.canResolve('')).toBe(false) expect(resolver.canResolve('plain text')).toBe(false) }) }) describe('resolve with normalized matching', () => { it.concurrent('should resolve variable with exact name match', () => { const variables = { 'var-1': { id: 'var-1', name: 'myvar', type: 'plain', value: 'test-value' }, } const resolver = new WorkflowResolver(variables) const result = resolver.resolve('', createTestContext(variables)) expect(result).toBe('test-value') }) it.concurrent('should resolve variable with normalized name (lowercase)', () => { const variables = { 'var-1': { id: 'var-1', name: 'MyVar', type: 'plain', value: 'test-value' }, } const resolver = new WorkflowResolver(variables) const result = resolver.resolve('', createTestContext(variables)) expect(result).toBe('test-value') }) it.concurrent('should resolve variable with normalized name (spaces removed)', () => { const variables = { 'var-1': { id: 'var-1', name: 'My Variable', type: 'plain', value: 'test-value' }, } const resolver = new WorkflowResolver(variables) const result = resolver.resolve('', createTestContext(variables)) expect(result).toBe('test-value') }) it.concurrent( 'should resolve variable with fully normalized name (JIRA TEAM UUID case)', () => { const variables = { 'var-1': { id: 'var-1', name: 'JIRA TEAM UUID', type: 'plain', value: 'uuid-123' }, } const resolver = new WorkflowResolver(variables) const result = resolver.resolve('', createTestContext(variables)) expect(result).toBe('uuid-123') } ) it.concurrent('should resolve variable regardless of reference case', () => { const variables = { 'var-1': { id: 'var-1', name: 'jirateamuuid', type: 'plain', value: 'uuid-123' }, } const resolver = new WorkflowResolver(variables) const result = resolver.resolve('', createTestContext(variables)) expect(result).toBe('uuid-123') }) it.concurrent('should resolve by variable ID (exact match)', () => { const variables = { 'my-uuid-id': { id: 'my-uuid-id', name: 'Some Name', type: 'plain', value: 'id-value' }, } const resolver = new WorkflowResolver(variables) const result = resolver.resolve('', createTestContext(variables)) expect(result).toBe('id-value') }) it.concurrent('should return undefined for non-existent variable', () => { const variables = { 'var-1': { id: 'var-1', name: 'existing', type: 'plain', value: 'test' }, } const resolver = new WorkflowResolver(variables) const result = resolver.resolve('', createTestContext(variables)) expect(result).toBeUndefined() }) it.concurrent('should handle nested path access', () => { const variables = { 'var-1': { id: 'var-1', name: 'config', type: 'object', value: { nested: { value: 'deep' } }, }, } const resolver = new WorkflowResolver(variables) const result = resolver.resolve( '', createTestContext(variables) ) expect(result).toBe('deep') }) it.concurrent('should resolve with mixed case and spaces in reference', () => { const variables = { 'var-1': { id: 'var-1', name: 'api key', type: 'plain', value: 'secret-key' }, } const resolver = new WorkflowResolver(variables) const result = resolver.resolve('', createTestContext(variables)) expect(result).toBe('secret-key') }) it.concurrent('should handle real-world variable naming patterns', () => { const testCases = [ { varName: 'User ID', refName: 'userid', value: 'user-123' }, { varName: 'API Key', refName: 'apikey', value: 'key-456' }, { varName: 'STRIPE SECRET KEY', refName: 'stripesecretkey', value: 'sk_test' }, { varName: 'Database URL', refName: 'databaseurl', value: 'postgres://...' }, ] for (const { varName, refName, value } of testCases) { const variables = { 'var-1': { id: 'var-1', name: varName, type: 'plain', value }, } const resolver = new WorkflowResolver(variables) const result = resolver.resolve(``, createTestContext(variables)) expect(result).toBe(value) } }) it('returns whole large workflow variable manifests only when refs are allowed', async () => { const compacted = await compactExecutionPayload( Array.from({ length: 100 }, (_, index) => ({ key: `SIM-${index}`, summary: 'Issue summary that keeps each item small', })), { thresholdBytes: 256, workspaceId: 'workspace-1', workflowId: 'workflow-1', executionId: 'execution-1', } ) const variables = { 'var-1': { id: 'var-1', name: 'issues', type: 'array', value: compacted }, } const resolver = new WorkflowResolver(variables, navigatePathAsync) const context = createTestContext(variables) expect(() => resolver.resolve('', context)).toThrow('too large to inline') await expect(resolver.resolveAsync('', context)).rejects.toThrow( 'too large to inline' ) const allowedContext = { ...context, allowLargeValueRefs: true } expect(isLargeArrayManifest(resolver.resolve('', allowedContext))).toBe(true) await expect(resolver.resolveAsync('', allowedContext)).resolves.toEqual( compacted ) }) it('resolves nested paths through async large workflow variable navigation', async () => { const compacted = await compactExecutionPayload( Array.from({ length: 100 }, (_, index) => ({ key: `SIM-${index + 1}`, fields: { summary: index === 0 ? 'Large issue' : 'Other issue' }, })), { thresholdBytes: 256, workspaceId: 'workspace-1', workflowId: 'workflow-1', executionId: 'execution-1', } ) const variables = { 'var-1': { id: 'var-1', name: 'issues', type: 'array', value: compacted }, } const resolver = new WorkflowResolver(variables, navigatePathAsync) await expect( resolver.resolveAsync('', createTestContext(variables)) ).resolves.toBe('Large issue') }) it('preserves large array manifest workflow variables without array coercion', async () => { const manifest = await createLargeArrayManifest([{ key: 'SIM-1' }], { workspaceId: 'workspace-1', workflowId: 'workflow-1', executionId: 'execution-1', }) const variables = { 'var-1': { id: 'var-1', name: 'issues', type: 'array', value: manifest }, } const resolver = new WorkflowResolver(variables, navigatePathAsync) const allowedContext = { ...createTestContext(variables), allowLargeValueRefs: true } expect(resolver.resolve('', allowedContext)).toEqual(manifest) await expect(resolver.resolveAsync('', allowedContext)).resolves.toEqual( manifest ) }) it('resolves bracket-indexed paths through manifest workflow variables', async () => { const manifest = await createLargeArrayManifest([{ key: 'SIM-1' }], { workspaceId: 'workspace-1', workflowId: 'workflow-1', executionId: 'execution-1', }) const variables = { 'var-1': { id: 'var-1', name: 'issues', type: 'array', value: manifest }, } const resolver = new WorkflowResolver(variables, navigatePathAsync) await expect( resolver.resolveAsync('', createTestContext(variables)) ).resolves.toBe('SIM-1') }) it('resolves manifest array length without materializing chunks', async () => { const manifest = await createLargeArrayManifest([{ key: 'SIM-1' }, { key: 'SIM-2' }], { workspaceId: 'workspace-1', workflowId: 'workflow-1', executionId: 'execution-1', }) const variables = { 'var-1': { id: 'var-1', name: 'issues', type: 'array', value: manifest }, } const resolver = new WorkflowResolver(variables, navigatePathAsync) await expect( resolver.resolveAsync('', createTestContext(variables)) ).resolves.toBe(2) }) }) describe('edge cases', () => { it.concurrent('should handle empty workflow variables', () => { const resolver = new WorkflowResolver({}) const result = resolver.resolve('', createTestContext({})) expect(result).toBeUndefined() }) it.concurrent('should handle invalid reference format', () => { const resolver = new WorkflowResolver({}) const result = resolver.resolve('', createTestContext({})) expect(result).toBeUndefined() }) it.concurrent('should handle null variable values in the map', () => { const variables: Record = { 'var-1': null, 'var-2': { id: 'var-2', name: 'valid', type: 'plain', value: 'exists' }, } const resolver = new WorkflowResolver(variables) const result = resolver.resolve('', createTestContext(variables)) expect(result).toBe('exists') }) it.concurrent('should handle variable with empty name', () => { const variables = { 'var-1': { id: 'var-1', name: '', type: 'plain', value: 'empty-name' }, } const resolver = new WorkflowResolver(variables) // Empty name normalizes to empty string, which matches "" reference const result = resolver.resolve('', createTestContext(variables)) expect(result).toBe('empty-name') }) it.concurrent('should prefer name match over ID match when both could apply', () => { const variables = { apikey: { id: 'apikey', name: 'different', type: 'plain', value: 'by-id' }, 'var-2': { id: 'var-2', name: 'apikey', type: 'plain', value: 'by-name' }, } const resolver = new WorkflowResolver(variables) const result = resolver.resolve('', createTestContext(variables)) expect(['by-id', 'by-name']).toContain(result) }) }) })