chore: import upstream snapshot with attribution
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
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
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
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,606 @@
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { readSSEStream } from '@/lib/core/utils/sse'
|
||||
import { clearLargeValueCacheForTests } from '@/lib/execution/payloads/cache'
|
||||
import { createStreamingResponse } from '@/lib/workflows/streaming/streaming'
|
||||
|
||||
const { mockDownloadFile } = vi.hoisted(() => ({
|
||||
mockDownloadFile: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/uploads', () => ({
|
||||
StorageService: {
|
||||
downloadFile: mockDownloadFile,
|
||||
},
|
||||
}))
|
||||
|
||||
const manifestChunk = [{ id: 1 }]
|
||||
const manifestChunkBytes = Buffer.byteLength(JSON.stringify(manifestChunk), 'utf8')
|
||||
const manifest = {
|
||||
__simLargeArrayManifest: true,
|
||||
version: 2,
|
||||
kind: 'array',
|
||||
totalCount: 1,
|
||||
chunkCount: 1,
|
||||
byteSize: manifestChunkBytes,
|
||||
chunks: [
|
||||
{
|
||||
ref: {
|
||||
__simLargeValueRef: true,
|
||||
version: 1,
|
||||
id: 'lv_ABCDEFGHIJKL',
|
||||
kind: 'array',
|
||||
size: manifestChunkBytes,
|
||||
key: 'execution/workspace-1/workflow-1/execution-1/large-value-lv_ABCDEFGHIJKL.json',
|
||||
executionId: 'execution-1',
|
||||
},
|
||||
count: 1,
|
||||
byteSize: manifestChunkBytes,
|
||||
},
|
||||
],
|
||||
preview: [{ id: 1 }],
|
||||
}
|
||||
|
||||
async function collectSSEEvents(
|
||||
stream: ReadableStream<Uint8Array>
|
||||
): Promise<Record<string, unknown>[]> {
|
||||
const reader = stream.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
const events: Record<string, unknown>[] = []
|
||||
let buffer = ''
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) {
|
||||
buffer += decoder.decode()
|
||||
break
|
||||
}
|
||||
buffer += decoder.decode(value, { stream: true })
|
||||
}
|
||||
} finally {
|
||||
reader.releaseLock()
|
||||
}
|
||||
|
||||
for (const chunk of buffer.split('\n\n')) {
|
||||
if (!chunk.startsWith('data: ')) {
|
||||
continue
|
||||
}
|
||||
const payload = chunk.substring(6)
|
||||
if (payload === '[DONE]') {
|
||||
continue
|
||||
}
|
||||
const event = JSON.parse(payload) as unknown
|
||||
if (event === '[DONE]') {
|
||||
continue
|
||||
}
|
||||
events.push(event as Record<string, unknown>)
|
||||
}
|
||||
|
||||
return events
|
||||
}
|
||||
|
||||
describe('createStreamingResponse', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
clearLargeValueCacheForTests()
|
||||
})
|
||||
|
||||
it('extracts block-level selected outputs from JSON content payloads', async () => {
|
||||
const output = { content: JSON.stringify({ answer: 'ok' }) }
|
||||
const stream = await createStreamingResponse({
|
||||
requestId: 'request-1',
|
||||
executionId: 'execution-1',
|
||||
streamConfig: {
|
||||
selectedOutputs: ['block'],
|
||||
includeFileBase64: false,
|
||||
},
|
||||
executeFn: async ({ onBlockComplete }) => {
|
||||
await onBlockComplete('block', output)
|
||||
return {
|
||||
success: true,
|
||||
output: {},
|
||||
logs: [
|
||||
{
|
||||
blockId: 'block',
|
||||
output,
|
||||
startedAt: new Date().toISOString(),
|
||||
endedAt: new Date().toISOString(),
|
||||
durationMs: 1,
|
||||
success: true,
|
||||
},
|
||||
],
|
||||
} as any
|
||||
},
|
||||
})
|
||||
|
||||
await expect(readSSEStream(stream)).resolves.toBe(JSON.stringify({ answer: 'ok' }, null, 2))
|
||||
})
|
||||
|
||||
it('extracts selected outputs from JSON content payloads', async () => {
|
||||
const output = { content: JSON.stringify({ answer: 'ok' }) }
|
||||
const stream = await createStreamingResponse({
|
||||
requestId: 'request-1',
|
||||
executionId: 'execution-1',
|
||||
streamConfig: {
|
||||
selectedOutputs: ['block_answer'],
|
||||
includeFileBase64: false,
|
||||
},
|
||||
executeFn: async ({ onBlockComplete }) => {
|
||||
await onBlockComplete('block', output)
|
||||
return {
|
||||
success: true,
|
||||
output: {},
|
||||
logs: [
|
||||
{
|
||||
blockId: 'block',
|
||||
output,
|
||||
startedAt: new Date().toISOString(),
|
||||
endedAt: new Date().toISOString(),
|
||||
durationMs: 1,
|
||||
success: true,
|
||||
},
|
||||
],
|
||||
} as any
|
||||
},
|
||||
})
|
||||
|
||||
await expect(readSSEStream(stream)).resolves.toBe('ok')
|
||||
})
|
||||
|
||||
it('auto-materializes whole manifest selected outputs under the inline cap', async () => {
|
||||
mockDownloadFile.mockResolvedValue(Buffer.from(JSON.stringify(manifestChunk), 'utf8'))
|
||||
const stream = await createStreamingResponse({
|
||||
requestId: 'request-1',
|
||||
executionId: 'execution-1',
|
||||
workspaceId: 'workspace-1',
|
||||
workflowId: 'workflow-1',
|
||||
streamConfig: {
|
||||
selectedOutputs: ['block_issues'],
|
||||
includeFileBase64: false,
|
||||
},
|
||||
executeFn: async ({ onBlockComplete }) => {
|
||||
const output = { issues: manifest }
|
||||
await onBlockComplete('block', output)
|
||||
return {
|
||||
success: true,
|
||||
output: {},
|
||||
logs: [
|
||||
{
|
||||
blockId: 'block',
|
||||
output,
|
||||
startedAt: new Date().toISOString(),
|
||||
endedAt: new Date().toISOString(),
|
||||
durationMs: 1,
|
||||
success: true,
|
||||
},
|
||||
],
|
||||
} as any
|
||||
},
|
||||
})
|
||||
|
||||
await expect(readSSEStream(stream)).resolves.toBe(JSON.stringify(manifestChunk, null, 2))
|
||||
})
|
||||
|
||||
it('auto-materializes whole-block selected outputs containing manifests', async () => {
|
||||
mockDownloadFile.mockResolvedValue(Buffer.from(JSON.stringify(manifestChunk), 'utf8'))
|
||||
const output = { issues: manifest }
|
||||
const stream = await createStreamingResponse({
|
||||
requestId: 'request-1',
|
||||
executionId: 'execution-1',
|
||||
workspaceId: 'workspace-1',
|
||||
workflowId: 'workflow-1',
|
||||
streamConfig: {
|
||||
selectedOutputs: ['block'],
|
||||
includeFileBase64: true,
|
||||
},
|
||||
executeFn: async ({ onBlockComplete }) => {
|
||||
await onBlockComplete('block', output)
|
||||
return {
|
||||
success: true,
|
||||
output: {},
|
||||
logs: [
|
||||
{
|
||||
blockId: 'block',
|
||||
output,
|
||||
startedAt: new Date().toISOString(),
|
||||
endedAt: new Date().toISOString(),
|
||||
durationMs: 1,
|
||||
success: true,
|
||||
},
|
||||
],
|
||||
} as any
|
||||
},
|
||||
})
|
||||
|
||||
await expect(readSSEStream(stream)).resolves.toBe(
|
||||
JSON.stringify({ issues: manifestChunk }, null, 2)
|
||||
)
|
||||
expect(mockDownloadFile).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('inlines materialized selected outputs without recompacting them into refs', async () => {
|
||||
const largeString = 'x'.repeat(8 * 1024 * 1024 + 1)
|
||||
const largeStringJson = JSON.stringify(largeString)
|
||||
const ref = {
|
||||
__simLargeValueRef: true,
|
||||
version: 1,
|
||||
id: 'lv_LARGESTRING1',
|
||||
kind: 'string',
|
||||
size: Buffer.byteLength(largeStringJson, 'utf8'),
|
||||
key: 'execution/workspace-1/workflow-1/execution-1/large-value-lv_LARGESTRING1.json',
|
||||
executionId: 'execution-1',
|
||||
}
|
||||
mockDownloadFile.mockResolvedValue(Buffer.from(largeStringJson, 'utf8'))
|
||||
|
||||
const stream = await createStreamingResponse({
|
||||
requestId: 'request-1',
|
||||
executionId: 'execution-1',
|
||||
workspaceId: 'workspace-1',
|
||||
workflowId: 'workflow-1',
|
||||
streamConfig: {
|
||||
selectedOutputs: ['block_text'],
|
||||
includeFileBase64: false,
|
||||
},
|
||||
executeFn: async ({ onBlockComplete }) => {
|
||||
const output = { text: ref }
|
||||
await onBlockComplete('block', output)
|
||||
return {
|
||||
success: true,
|
||||
output: {},
|
||||
logs: [
|
||||
{
|
||||
blockId: 'block',
|
||||
output,
|
||||
startedAt: new Date().toISOString(),
|
||||
endedAt: new Date().toISOString(),
|
||||
durationMs: 1,
|
||||
success: true,
|
||||
},
|
||||
],
|
||||
} as any
|
||||
},
|
||||
})
|
||||
|
||||
const streamed = await readSSEStream(stream)
|
||||
expect(streamed).toHaveLength(largeString.length)
|
||||
expect(streamed).not.toContain('__simLargeValueRef')
|
||||
})
|
||||
|
||||
it('deduplicates repeated equivalent selected outputs before streaming', async () => {
|
||||
const stream = await createStreamingResponse({
|
||||
requestId: 'request-1',
|
||||
executionId: 'execution-1',
|
||||
streamConfig: {
|
||||
selectedOutputs: ['block_text', 'block.text', 'block_text'],
|
||||
includeFileBase64: false,
|
||||
},
|
||||
executeFn: async ({ onBlockComplete }) => {
|
||||
const output = { text: 'ok' }
|
||||
await onBlockComplete('block', output)
|
||||
return {
|
||||
success: true,
|
||||
output: {},
|
||||
logs: [
|
||||
{
|
||||
blockId: 'block',
|
||||
output,
|
||||
startedAt: new Date().toISOString(),
|
||||
endedAt: new Date().toISOString(),
|
||||
durationMs: 1,
|
||||
success: true,
|
||||
},
|
||||
],
|
||||
} as any
|
||||
},
|
||||
})
|
||||
|
||||
const events = await collectSSEEvents(stream)
|
||||
const chunkEvents = events.filter((event) => 'chunk' in event)
|
||||
expect(chunkEvents).toHaveLength(1)
|
||||
expect(chunkEvents[0]).toMatchObject({ blockId: 'block', chunk: 'ok' })
|
||||
})
|
||||
|
||||
it('fails when distinct selected outputs aggregate over the inline cap', async () => {
|
||||
const largeString = 'x'.repeat(9 * 1024 * 1024)
|
||||
const largeStringJson = JSON.stringify(largeString)
|
||||
const largeStringBytes = Buffer.byteLength(largeStringJson, 'utf8')
|
||||
const firstRef = {
|
||||
__simLargeValueRef: true,
|
||||
version: 1,
|
||||
id: 'lv_MULTIREF0001',
|
||||
kind: 'string',
|
||||
size: largeStringBytes,
|
||||
key: 'execution/workspace-1/workflow-1/execution-1/large-value-lv_MULTIREF0001.json',
|
||||
executionId: 'execution-1',
|
||||
}
|
||||
const secondRef = {
|
||||
__simLargeValueRef: true,
|
||||
version: 1,
|
||||
id: 'lv_MULTIREF0002',
|
||||
kind: 'string',
|
||||
size: largeStringBytes,
|
||||
key: 'execution/workspace-1/workflow-1/execution-1/large-value-lv_MULTIREF0002.json',
|
||||
executionId: 'execution-1',
|
||||
}
|
||||
mockDownloadFile.mockImplementation(async ({ key }) => {
|
||||
if (key === firstRef.key || key === secondRef.key) {
|
||||
return Buffer.from(largeStringJson, 'utf8')
|
||||
}
|
||||
throw new Error(`Unexpected key: ${key}`)
|
||||
})
|
||||
|
||||
const stream = await createStreamingResponse({
|
||||
requestId: 'request-1',
|
||||
executionId: 'execution-1',
|
||||
workspaceId: 'workspace-1',
|
||||
workflowId: 'workflow-1',
|
||||
streamConfig: {
|
||||
selectedOutputs: ['block_first', 'block_second'],
|
||||
includeFileBase64: false,
|
||||
},
|
||||
executeFn: async ({ onBlockComplete }) => {
|
||||
const output = { first: firstRef, second: secondRef }
|
||||
await onBlockComplete('block', output)
|
||||
return {
|
||||
success: true,
|
||||
output: {},
|
||||
logs: [
|
||||
{
|
||||
blockId: 'block',
|
||||
output,
|
||||
startedAt: new Date().toISOString(),
|
||||
endedAt: new Date().toISOString(),
|
||||
durationMs: 1,
|
||||
success: true,
|
||||
},
|
||||
],
|
||||
} as any
|
||||
},
|
||||
})
|
||||
|
||||
const events = await collectSSEEvents(stream)
|
||||
expect(events).toContainEqual({
|
||||
event: 'error',
|
||||
blockId: 'block',
|
||||
error:
|
||||
'Selected output is too large to inline; select a nested field or use pagination/preview.',
|
||||
})
|
||||
expect(events.some((event) => event.event === 'final')).toBe(false)
|
||||
})
|
||||
|
||||
it('accounts escaped string JSON bytes against the aggregate selected-output cap', async () => {
|
||||
const first = '\\'.repeat(Math.floor((16 * 1024 * 1024 - 2) / 2))
|
||||
const stream = await createStreamingResponse({
|
||||
requestId: 'request-1',
|
||||
executionId: 'execution-1',
|
||||
streamConfig: {
|
||||
selectedOutputs: ['block_first', 'block_second'],
|
||||
includeFileBase64: false,
|
||||
},
|
||||
executeFn: async ({ onBlockComplete }) => {
|
||||
const output = { first, second: 'ok' }
|
||||
await onBlockComplete('block', output)
|
||||
return {
|
||||
success: true,
|
||||
output: {},
|
||||
logs: [
|
||||
{
|
||||
blockId: 'block',
|
||||
output,
|
||||
startedAt: new Date().toISOString(),
|
||||
endedAt: new Date().toISOString(),
|
||||
durationMs: 1,
|
||||
success: true,
|
||||
},
|
||||
],
|
||||
} as any
|
||||
},
|
||||
})
|
||||
|
||||
const events = await collectSSEEvents(stream)
|
||||
expect(events).toContainEqual({
|
||||
event: 'error',
|
||||
blockId: 'block',
|
||||
error:
|
||||
'Selected output is too large to inline; select a nested field or use pagination/preview.',
|
||||
})
|
||||
expect(events.some((event) => event.event === 'final')).toBe(false)
|
||||
})
|
||||
|
||||
it('fails when nested refs aggregate over the inline selected-output cap', async () => {
|
||||
const largeString = 'x'.repeat(9 * 1024 * 1024)
|
||||
const largeStringJson = JSON.stringify(largeString)
|
||||
const largeStringBytes = Buffer.byteLength(largeStringJson, 'utf8')
|
||||
const nestedRefA = {
|
||||
__simLargeValueRef: true,
|
||||
version: 1,
|
||||
id: 'lv_NESTEDREF001',
|
||||
kind: 'string',
|
||||
size: largeStringBytes,
|
||||
key: 'execution/workspace-1/workflow-1/execution-1/large-value-lv_NESTEDREF001.json',
|
||||
executionId: 'execution-1',
|
||||
}
|
||||
const nestedRefB = {
|
||||
__simLargeValueRef: true,
|
||||
version: 1,
|
||||
id: 'lv_NESTEDREF002',
|
||||
kind: 'string',
|
||||
size: largeStringBytes,
|
||||
key: 'execution/workspace-1/workflow-1/execution-1/large-value-lv_NESTEDREF002.json',
|
||||
executionId: 'execution-1',
|
||||
}
|
||||
const nestedChunk = [nestedRefA, nestedRefB]
|
||||
const nestedChunkBytes = Buffer.byteLength(JSON.stringify(nestedChunk), 'utf8')
|
||||
const nestedManifest = {
|
||||
...manifest,
|
||||
totalCount: 2,
|
||||
byteSize: nestedChunkBytes,
|
||||
chunks: [
|
||||
{
|
||||
ref: {
|
||||
...manifest.chunks[0].ref,
|
||||
size: nestedChunkBytes,
|
||||
},
|
||||
count: 2,
|
||||
byteSize: nestedChunkBytes,
|
||||
},
|
||||
],
|
||||
preview: [],
|
||||
}
|
||||
mockDownloadFile.mockImplementation(async ({ key }) => {
|
||||
if (key === nestedManifest.chunks[0].ref.key) {
|
||||
return Buffer.from(JSON.stringify(nestedChunk), 'utf8')
|
||||
}
|
||||
if (key === nestedRefA.key) {
|
||||
return Buffer.from(largeStringJson, 'utf8')
|
||||
}
|
||||
if (key === nestedRefB.key) {
|
||||
return Buffer.from(largeStringJson, 'utf8')
|
||||
}
|
||||
throw new Error(`Unexpected key: ${key}`)
|
||||
})
|
||||
|
||||
const stream = await createStreamingResponse({
|
||||
requestId: 'request-1',
|
||||
executionId: 'execution-1',
|
||||
workspaceId: 'workspace-1',
|
||||
workflowId: 'workflow-1',
|
||||
streamConfig: {
|
||||
selectedOutputs: ['block_issues'],
|
||||
includeFileBase64: false,
|
||||
},
|
||||
executeFn: async ({ onBlockComplete }) => {
|
||||
const output = { issues: nestedManifest }
|
||||
await onBlockComplete('block', output)
|
||||
return {
|
||||
success: true,
|
||||
output: {},
|
||||
logs: [
|
||||
{
|
||||
blockId: 'block',
|
||||
output,
|
||||
startedAt: new Date().toISOString(),
|
||||
endedAt: new Date().toISOString(),
|
||||
durationMs: 1,
|
||||
success: true,
|
||||
},
|
||||
],
|
||||
} as any
|
||||
},
|
||||
})
|
||||
|
||||
const events = await collectSSEEvents(stream)
|
||||
expect(events).toContainEqual({
|
||||
event: 'error',
|
||||
blockId: 'block',
|
||||
error:
|
||||
'Selected output is too large to inline; select a nested field or use pagination/preview.',
|
||||
})
|
||||
expect(events.some((event) => event.event === 'final')).toBe(false)
|
||||
expect(JSON.stringify(events)).not.toContain('__simLargeValueRef')
|
||||
})
|
||||
|
||||
it('fails clearly instead of streaming raw manifest internals when selected output is over cap', async () => {
|
||||
const oversizedManifest = {
|
||||
...manifest,
|
||||
byteSize: 16 * 1024 * 1024 + 1,
|
||||
chunks: [
|
||||
{
|
||||
...manifest.chunks[0],
|
||||
ref: {
|
||||
...manifest.chunks[0].ref,
|
||||
size: 16 * 1024 * 1024 + 1,
|
||||
},
|
||||
byteSize: 16 * 1024 * 1024 + 1,
|
||||
},
|
||||
],
|
||||
}
|
||||
const stream = await createStreamingResponse({
|
||||
requestId: 'request-1',
|
||||
executionId: 'execution-1',
|
||||
workspaceId: 'workspace-1',
|
||||
workflowId: 'workflow-1',
|
||||
streamConfig: {
|
||||
selectedOutputs: ['block_issues'],
|
||||
includeFileBase64: false,
|
||||
},
|
||||
executeFn: async ({ onBlockComplete }) => {
|
||||
const output = { issues: oversizedManifest }
|
||||
await onBlockComplete('block', output)
|
||||
return {
|
||||
success: true,
|
||||
output: {},
|
||||
logs: [
|
||||
{
|
||||
blockId: 'block',
|
||||
output,
|
||||
startedAt: new Date().toISOString(),
|
||||
endedAt: new Date().toISOString(),
|
||||
durationMs: 1,
|
||||
success: true,
|
||||
},
|
||||
],
|
||||
} as any
|
||||
},
|
||||
})
|
||||
|
||||
const events = await collectSSEEvents(stream)
|
||||
expect(events).toContainEqual({
|
||||
event: 'error',
|
||||
blockId: 'block',
|
||||
error:
|
||||
'Selected output is too large to inline; select a nested field or use pagination/preview.',
|
||||
})
|
||||
expect(events.some((event) => event.event === 'final')).toBe(false)
|
||||
expect(JSON.stringify(events)).not.toContain('__simLargeArrayManifest')
|
||||
expect(mockDownloadFile).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('uses live large-value keys for selected-output materialization', async () => {
|
||||
const largeValueKeys: string[] = []
|
||||
const ref = {
|
||||
__simLargeValueRef: true,
|
||||
version: 1,
|
||||
id: 'lv_MNOPQRSTUVWX',
|
||||
kind: 'object',
|
||||
size: 15,
|
||||
key: 'execution/workspace-1/workflow-1/source-execution/large-value-lv_MNOPQRSTUVWX.json',
|
||||
executionId: 'source-execution',
|
||||
}
|
||||
mockDownloadFile.mockResolvedValue(Buffer.from(JSON.stringify({ nested: 'ok' }), 'utf8'))
|
||||
|
||||
const stream = await createStreamingResponse({
|
||||
requestId: 'request-1',
|
||||
executionId: 'execution-1',
|
||||
workspaceId: 'workspace-1',
|
||||
workflowId: 'workflow-1',
|
||||
largeValueKeys,
|
||||
streamConfig: {
|
||||
selectedOutputs: ['block.value.nested'],
|
||||
},
|
||||
executeFn: async ({ onBlockComplete }) => {
|
||||
largeValueKeys.push(ref.key)
|
||||
await onBlockComplete('block', { value: ref })
|
||||
return {
|
||||
success: true,
|
||||
output: {},
|
||||
logs: [
|
||||
{
|
||||
blockId: 'block',
|
||||
output: { value: ref },
|
||||
startedAt: new Date().toISOString(),
|
||||
endedAt: new Date().toISOString(),
|
||||
durationMs: 1,
|
||||
success: true,
|
||||
},
|
||||
],
|
||||
} as any
|
||||
},
|
||||
})
|
||||
|
||||
await expect(readSSEStream(stream)).resolves.toBe('ok')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user