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
244 lines
7.8 KiB
TypeScript
244 lines
7.8 KiB
TypeScript
/**
|
|
* @vitest-environment node
|
|
*/
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { cacheLargeValue, clearLargeValueCacheForTests } from '@/lib/execution/payloads/cache'
|
|
import {
|
|
LARGE_ARRAY_MANIFEST_MARKER,
|
|
LARGE_ARRAY_MANIFEST_VERSION,
|
|
} from '@/lib/execution/payloads/large-array-manifest-metadata'
|
|
import { LARGE_VALUE_REF_MARKER } from '@/lib/execution/payloads/large-value-ref'
|
|
import type { ExecutionContext } from '@/executor/types'
|
|
import { findEffectiveContainerId } from '@/executor/utils/subflow-utils'
|
|
import { resolveArrayInputAsync } from '@/executor/utils/subflow-utils.server'
|
|
import type { VariableResolver } from '@/executor/variables/resolver'
|
|
|
|
describe('resolveArrayInputAsync', () => {
|
|
const fakeCtx = {
|
|
workspaceId: 'workspace-1',
|
|
workflowId: 'workflow-1',
|
|
executionId: 'execution-1',
|
|
userId: 'user-1',
|
|
} as unknown as ExecutionContext
|
|
|
|
beforeEach(() => {
|
|
clearLargeValueCacheForTests()
|
|
})
|
|
|
|
function createManifest(items: unknown[]) {
|
|
const json = JSON.stringify(items)
|
|
const size = Buffer.byteLength(json, 'utf8')
|
|
const id = 'lv_ABCDEFGHIJKL'
|
|
cacheLargeValue(id, items, size, fakeCtx)
|
|
|
|
return {
|
|
__simLargeArrayManifest: true,
|
|
version: LARGE_ARRAY_MANIFEST_VERSION,
|
|
kind: 'array',
|
|
totalCount: items.length,
|
|
chunkCount: 1,
|
|
byteSize: size,
|
|
chunks: [
|
|
{
|
|
ref: {
|
|
__simLargeValueRef: true,
|
|
version: 1,
|
|
id,
|
|
kind: 'array',
|
|
size,
|
|
executionId: fakeCtx.executionId,
|
|
},
|
|
count: items.length,
|
|
byteSize: size,
|
|
},
|
|
],
|
|
preview: items.slice(0, 3),
|
|
}
|
|
}
|
|
|
|
it('returns arrays as-is', async () => {
|
|
await expect(resolveArrayInputAsync(fakeCtx, [1, 2, 3], null)).resolves.toEqual([1, 2, 3])
|
|
})
|
|
|
|
it('converts plain objects to entries', async () => {
|
|
await expect(resolveArrayInputAsync(fakeCtx, { a: 1, b: 2 }, null)).resolves.toEqual([
|
|
['a', 1],
|
|
['b', 2],
|
|
])
|
|
})
|
|
|
|
it('materializes large array manifests instead of iterating metadata entries', async () => {
|
|
const items = [{ id: 1 }, { id: 2 }]
|
|
const manifest = createManifest(items)
|
|
|
|
await expect(resolveArrayInputAsync(fakeCtx, manifest, null)).resolves.toEqual(items)
|
|
})
|
|
|
|
it('records exact nested keys discovered while materializing collection values', async () => {
|
|
const ctx = {
|
|
...fakeCtx,
|
|
largeValueKeys: [] as string[],
|
|
fileKeys: [] as string[],
|
|
} as ExecutionContext
|
|
const nestedRef = {
|
|
__simLargeValueRef: true,
|
|
version: 1,
|
|
id: 'lv_MNOPQRSTUVWX',
|
|
kind: 'object',
|
|
size: 12,
|
|
key: 'execution/workspace-1/workflow-1/source-execution/large-value-lv_MNOPQRSTUVWX.json',
|
|
executionId: 'source-execution',
|
|
}
|
|
const file = {
|
|
id: 'file-1',
|
|
name: 'nested.txt',
|
|
key: 'execution/workspace-1/workflow-1/source-execution/nested.txt',
|
|
url: '/api/files/serve/execution/workspace-1/workflow-1/source-execution/nested.txt?context=execution',
|
|
size: 5,
|
|
type: 'text/plain',
|
|
context: 'execution',
|
|
}
|
|
const items = [{ nestedRef, file }]
|
|
const manifest = createManifest(items)
|
|
|
|
await expect(resolveArrayInputAsync(ctx, manifest, null)).resolves.toEqual(items)
|
|
|
|
expect(ctx.largeValueKeys).toEqual([nestedRef.key])
|
|
expect(ctx.fileKeys).toEqual([file.key])
|
|
})
|
|
|
|
it('rejects invalid manifest-shaped collection inputs instead of iterating metadata', async () => {
|
|
await expect(
|
|
resolveArrayInputAsync(
|
|
fakeCtx,
|
|
{
|
|
[LARGE_ARRAY_MANIFEST_MARKER]: true,
|
|
version: LARGE_ARRAY_MANIFEST_VERSION,
|
|
kind: 'array',
|
|
totalCount: 1,
|
|
chunkCount: 0,
|
|
byteSize: 0,
|
|
chunks: [],
|
|
preview: [],
|
|
},
|
|
null
|
|
)
|
|
).rejects.toThrow('Invalid large array manifest')
|
|
})
|
|
|
|
it('rejects invalid large-ref-shaped collection inputs instead of iterating metadata', async () => {
|
|
await expect(
|
|
resolveArrayInputAsync(
|
|
fakeCtx,
|
|
{
|
|
[LARGE_VALUE_REF_MARKER]: true,
|
|
version: 1,
|
|
id: 'not-a-valid-large-value-id',
|
|
kind: 'array',
|
|
size: 1,
|
|
},
|
|
null
|
|
)
|
|
).rejects.toThrow('Invalid large value ref')
|
|
})
|
|
|
|
it('returns empty array when a pure reference resolves to null (skipped block)', async () => {
|
|
// `resolveSingleReference` returns `null` for a reference that points at a
|
|
// block that exists in the workflow but did not execute on this path.
|
|
// A loop/parallel over such a reference should run zero iterations rather
|
|
// than fail the workflow.
|
|
const resolver = {
|
|
resolveSingleReference: vi.fn().mockResolvedValue(null),
|
|
} as unknown as VariableResolver
|
|
|
|
const result = await resolveArrayInputAsync(fakeCtx, '<SkippedBlock.result.items>', resolver)
|
|
|
|
expect(result).toEqual([])
|
|
expect(resolver.resolveSingleReference).toHaveBeenCalled()
|
|
})
|
|
|
|
it('returns the array from a pure reference that resolved to an array', async () => {
|
|
const resolver = {
|
|
resolveSingleReference: vi.fn().mockResolvedValue([1, 2, 3]),
|
|
} as unknown as VariableResolver
|
|
|
|
await expect(resolveArrayInputAsync(fakeCtx, '<Block.items>', resolver)).resolves.toEqual([
|
|
1, 2, 3,
|
|
])
|
|
})
|
|
|
|
it('materializes a manifest returned by a pure reference', async () => {
|
|
const items = [{ id: 1 }, { id: 2 }]
|
|
const manifest = createManifest(items)
|
|
const resolver = {
|
|
resolveSingleReference: vi.fn().mockResolvedValue(manifest),
|
|
} as unknown as VariableResolver
|
|
|
|
await expect(resolveArrayInputAsync(fakeCtx, '<variable.issues>', resolver)).resolves.toEqual(
|
|
items
|
|
)
|
|
expect(resolver.resolveSingleReference).toHaveBeenCalledWith(
|
|
fakeCtx,
|
|
'',
|
|
'<variable.issues>',
|
|
undefined,
|
|
{ allowLargeValueRefs: true }
|
|
)
|
|
})
|
|
|
|
it('converts resolved objects to entries', async () => {
|
|
const resolver = {
|
|
resolveSingleReference: vi.fn().mockResolvedValue({ x: 1, y: 2 }),
|
|
} as unknown as VariableResolver
|
|
|
|
await expect(resolveArrayInputAsync(fakeCtx, '<Block.obj>', resolver)).resolves.toEqual([
|
|
['x', 1],
|
|
['y', 2],
|
|
])
|
|
})
|
|
|
|
it('throws when a pure reference resolves to a non-array, non-object, non-null value', async () => {
|
|
const resolver = {
|
|
resolveSingleReference: vi.fn().mockResolvedValue(42),
|
|
} as unknown as VariableResolver
|
|
|
|
await expect(resolveArrayInputAsync(fakeCtx, '<Block.count>', resolver)).rejects.toThrow(
|
|
/did not resolve to an array or object/
|
|
)
|
|
})
|
|
|
|
it('throws when a pure reference resolves to undefined (unknown block)', async () => {
|
|
// `undefined` means the reference could not be matched to any block at
|
|
// all (typo / deleted block). This must still fail loudly.
|
|
const resolver = {
|
|
resolveSingleReference: vi.fn().mockResolvedValue(undefined),
|
|
} as unknown as VariableResolver
|
|
|
|
await expect(resolveArrayInputAsync(fakeCtx, '<Missing.items>', resolver)).rejects.toThrow(
|
|
/did not resolve to an array or object/
|
|
)
|
|
})
|
|
|
|
it('parses a JSON array string', async () => {
|
|
await expect(resolveArrayInputAsync(fakeCtx, '[1, 2, 3]', null)).resolves.toEqual([1, 2, 3])
|
|
})
|
|
|
|
it('throws on a string that is neither a reference nor valid JSON array/object', async () => {
|
|
await expect(resolveArrayInputAsync(fakeCtx, 'not json', null)).rejects.toThrow()
|
|
})
|
|
})
|
|
|
|
describe('findEffectiveContainerId', () => {
|
|
it('finds pre-cloned nested subflow IDs with clone sequence suffixes', () => {
|
|
const executionMap = new Map<string, unknown>([
|
|
['inner-parallel', {}],
|
|
['inner-parallel__obranch-2', {}],
|
|
['inner-parallel__clone3__obranch-2', {}],
|
|
])
|
|
|
|
expect(
|
|
findEffectiveContainerId('inner-parallel', 'leaf__clone7__obranch-2₍0₎', executionMap)
|
|
).toBe('inner-parallel__clone3__obranch-2')
|
|
})
|
|
})
|