d25d482dc2
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
131 lines
3.8 KiB
TypeScript
131 lines
3.8 KiB
TypeScript
/**
|
|
* @vitest-environment node
|
|
*/
|
|
import { describe, expect, it } from 'vitest'
|
|
import {
|
|
buildNextCallChain,
|
|
MAX_CALL_CHAIN_DEPTH,
|
|
parseCallChain,
|
|
SIM_VIA_HEADER,
|
|
serializeCallChain,
|
|
validateCallChain,
|
|
} from '@/lib/execution/call-chain'
|
|
|
|
describe('call-chain', () => {
|
|
describe('SIM_VIA_HEADER', () => {
|
|
it('has the expected header name', () => {
|
|
expect(SIM_VIA_HEADER).toBe('X-Sim-Via')
|
|
})
|
|
})
|
|
|
|
describe('MAX_CALL_CHAIN_DEPTH', () => {
|
|
it('equals 25', () => {
|
|
expect(MAX_CALL_CHAIN_DEPTH).toBe(25)
|
|
})
|
|
})
|
|
|
|
describe('parseCallChain', () => {
|
|
it('returns empty array for null', () => {
|
|
expect(parseCallChain(null)).toEqual([])
|
|
})
|
|
|
|
it('returns empty array for undefined', () => {
|
|
expect(parseCallChain(undefined)).toEqual([])
|
|
})
|
|
|
|
it('returns empty array for empty string', () => {
|
|
expect(parseCallChain('')).toEqual([])
|
|
})
|
|
|
|
it('returns empty array for whitespace-only string', () => {
|
|
expect(parseCallChain(' ')).toEqual([])
|
|
})
|
|
|
|
it('parses a single workflow ID', () => {
|
|
expect(parseCallChain('wf-abc')).toEqual(['wf-abc'])
|
|
})
|
|
|
|
it('parses multiple comma-separated workflow IDs', () => {
|
|
expect(parseCallChain('wf-a,wf-b,wf-c')).toEqual(['wf-a', 'wf-b', 'wf-c'])
|
|
})
|
|
|
|
it('trims whitespace around workflow IDs', () => {
|
|
expect(parseCallChain(' wf-a , wf-b , wf-c ')).toEqual(['wf-a', 'wf-b', 'wf-c'])
|
|
})
|
|
|
|
it('filters out empty segments', () => {
|
|
expect(parseCallChain('wf-a,,wf-b')).toEqual(['wf-a', 'wf-b'])
|
|
})
|
|
})
|
|
|
|
describe('serializeCallChain', () => {
|
|
it('serializes an empty array', () => {
|
|
expect(serializeCallChain([])).toBe('')
|
|
})
|
|
|
|
it('serializes a single ID', () => {
|
|
expect(serializeCallChain(['wf-a'])).toBe('wf-a')
|
|
})
|
|
|
|
it('serializes multiple IDs with commas', () => {
|
|
expect(serializeCallChain(['wf-a', 'wf-b', 'wf-c'])).toBe('wf-a,wf-b,wf-c')
|
|
})
|
|
})
|
|
|
|
describe('validateCallChain', () => {
|
|
it('returns null for an empty chain', () => {
|
|
expect(validateCallChain([])).toBeNull()
|
|
})
|
|
|
|
it('returns null when chain is under max depth', () => {
|
|
expect(validateCallChain(['wf-a', 'wf-b'])).toBeNull()
|
|
})
|
|
|
|
it('allows legitimate self-recursion', () => {
|
|
expect(validateCallChain(['wf-a', 'wf-a', 'wf-a'])).toBeNull()
|
|
})
|
|
|
|
it('returns depth error when chain is at max depth', () => {
|
|
const chain = Array.from({ length: MAX_CALL_CHAIN_DEPTH }, (_, i) => `wf-${i}`)
|
|
const error = validateCallChain(chain)
|
|
expect(error).toContain(
|
|
`Maximum workflow call chain depth (${MAX_CALL_CHAIN_DEPTH}) exceeded`
|
|
)
|
|
})
|
|
|
|
it('allows chain just under max depth', () => {
|
|
const chain = Array.from({ length: MAX_CALL_CHAIN_DEPTH - 1 }, (_, i) => `wf-${i}`)
|
|
expect(validateCallChain(chain)).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('buildNextCallChain', () => {
|
|
it('appends workflow ID to empty chain', () => {
|
|
expect(buildNextCallChain([], 'wf-a')).toEqual(['wf-a'])
|
|
})
|
|
|
|
it('appends workflow ID to existing chain', () => {
|
|
expect(buildNextCallChain(['wf-a', 'wf-b'], 'wf-c')).toEqual(['wf-a', 'wf-b', 'wf-c'])
|
|
})
|
|
|
|
it('does not mutate the original chain', () => {
|
|
const original = ['wf-a']
|
|
const result = buildNextCallChain(original, 'wf-b')
|
|
expect(original).toEqual(['wf-a'])
|
|
expect(result).toEqual(['wf-a', 'wf-b'])
|
|
})
|
|
})
|
|
|
|
describe('round-trip', () => {
|
|
it('parse → serialize is identity', () => {
|
|
const header = 'wf-a,wf-b,wf-c'
|
|
expect(serializeCallChain(parseCallChain(header))).toBe(header)
|
|
})
|
|
|
|
it('serialize → parse is identity', () => {
|
|
const chain = ['wf-a', 'wf-b', 'wf-c']
|
|
expect(parseCallChain(serializeCallChain(chain))).toEqual(chain)
|
|
})
|
|
})
|
|
})
|