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
353 lines
10 KiB
TypeScript
353 lines
10 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import {
|
|
StreamingResponseFormatProcessor,
|
|
streamingResponseFormatProcessor,
|
|
} from '@/executor/utils'
|
|
|
|
describe('StreamingResponseFormatProcessor', () => {
|
|
let processor: StreamingResponseFormatProcessor
|
|
|
|
beforeEach(() => {
|
|
processor = new StreamingResponseFormatProcessor()
|
|
})
|
|
|
|
afterEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
describe('processStream', () => {
|
|
it.concurrent('should return original stream when no response format selection', async () => {
|
|
const mockStream = new ReadableStream({
|
|
start(controller) {
|
|
controller.enqueue(new TextEncoder().encode('{"content": "test"}'))
|
|
controller.close()
|
|
},
|
|
})
|
|
|
|
const result = processor.processStream(
|
|
mockStream,
|
|
'block-1',
|
|
['block-1.content'], // No underscore, not response format
|
|
{ schema: { properties: { username: { type: 'string' } } } }
|
|
)
|
|
|
|
expect(result).toBe(mockStream)
|
|
})
|
|
|
|
it.concurrent('should return original stream when no response format provided', async () => {
|
|
const mockStream = new ReadableStream({
|
|
start(controller) {
|
|
controller.enqueue(new TextEncoder().encode('{"content": "test"}'))
|
|
controller.close()
|
|
},
|
|
})
|
|
|
|
const result = processor.processStream(
|
|
mockStream,
|
|
'block-1',
|
|
['block-1_username'], // Has underscore but no response format
|
|
undefined
|
|
)
|
|
|
|
expect(result).toBe(mockStream)
|
|
})
|
|
|
|
it.concurrent('should process stream and extract single selected field', async () => {
|
|
const mockStream = new ReadableStream({
|
|
start(controller) {
|
|
controller.enqueue(new TextEncoder().encode('{"username": "alice", "age": 25}'))
|
|
controller.close()
|
|
},
|
|
})
|
|
|
|
const processedStream = processor.processStream(mockStream, 'block-1', ['block-1_username'], {
|
|
schema: { properties: { username: { type: 'string' }, age: { type: 'number' } } },
|
|
})
|
|
|
|
const reader = processedStream.getReader()
|
|
const decoder = new TextDecoder()
|
|
let result = ''
|
|
|
|
while (true) {
|
|
const { done, value } = await reader.read()
|
|
if (done) break
|
|
result += decoder.decode(value)
|
|
}
|
|
|
|
expect(result).toBe('alice')
|
|
})
|
|
|
|
it.concurrent('should process stream and extract multiple selected fields', async () => {
|
|
const mockStream = new ReadableStream({
|
|
start(controller) {
|
|
controller.enqueue(
|
|
new TextEncoder().encode('{"username": "bob", "age": 30, "email": "bob@test.com"}')
|
|
)
|
|
controller.close()
|
|
},
|
|
})
|
|
|
|
const processedStream = processor.processStream(
|
|
mockStream,
|
|
'block-1',
|
|
['block-1_username', 'block-1_age'],
|
|
{
|
|
schema: { properties: { username: { type: 'string' }, age: { type: 'number' } } },
|
|
}
|
|
)
|
|
|
|
const reader = processedStream.getReader()
|
|
const decoder = new TextDecoder()
|
|
let result = ''
|
|
|
|
while (true) {
|
|
const { done, value } = await reader.read()
|
|
if (done) break
|
|
result += decoder.decode(value)
|
|
}
|
|
|
|
expect(result).toBe('bob\n30')
|
|
})
|
|
|
|
it.concurrent('should handle non-string field values by JSON stringifying them', async () => {
|
|
const mockStream = new ReadableStream({
|
|
start(controller) {
|
|
controller.enqueue(
|
|
new TextEncoder().encode(
|
|
'{"config": {"theme": "dark", "notifications": true}, "count": 42}'
|
|
)
|
|
)
|
|
controller.close()
|
|
},
|
|
})
|
|
|
|
const processedStream = processor.processStream(
|
|
mockStream,
|
|
'block-1',
|
|
['block-1_config', 'block-1_count'],
|
|
{
|
|
schema: { properties: { config: { type: 'object' }, count: { type: 'number' } } },
|
|
}
|
|
)
|
|
|
|
const reader = processedStream.getReader()
|
|
const decoder = new TextDecoder()
|
|
let result = ''
|
|
|
|
while (true) {
|
|
const { done, value } = await reader.read()
|
|
if (done) break
|
|
result += decoder.decode(value)
|
|
}
|
|
|
|
expect(result).toBe('{"theme":"dark","notifications":true}\n42')
|
|
})
|
|
|
|
it.concurrent('should handle streaming JSON that comes in chunks', async () => {
|
|
const mockStream = new ReadableStream({
|
|
start(controller) {
|
|
// Simulate streaming JSON in chunks
|
|
controller.enqueue(new TextEncoder().encode('{"username": "charlie"'))
|
|
controller.enqueue(new TextEncoder().encode(', "age": 35}'))
|
|
controller.close()
|
|
},
|
|
})
|
|
|
|
const processedStream = processor.processStream(mockStream, 'block-1', ['block-1_username'], {
|
|
schema: { properties: { username: { type: 'string' }, age: { type: 'number' } } },
|
|
})
|
|
|
|
const reader = processedStream.getReader()
|
|
const decoder = new TextDecoder()
|
|
let result = ''
|
|
|
|
while (true) {
|
|
const { done, value } = await reader.read()
|
|
if (done) break
|
|
result += decoder.decode(value)
|
|
}
|
|
|
|
expect(result).toBe('charlie')
|
|
})
|
|
|
|
it.concurrent('should handle missing fields gracefully', async () => {
|
|
const mockStream = new ReadableStream({
|
|
start(controller) {
|
|
controller.enqueue(new TextEncoder().encode('{"username": "diana"}'))
|
|
controller.close()
|
|
},
|
|
})
|
|
|
|
const processedStream = processor.processStream(
|
|
mockStream,
|
|
'block-1',
|
|
['block-1_username', 'block-1_missing_field'],
|
|
{ schema: { properties: { username: { type: 'string' } } } }
|
|
)
|
|
|
|
const reader = processedStream.getReader()
|
|
const decoder = new TextDecoder()
|
|
let result = ''
|
|
|
|
while (true) {
|
|
const { done, value } = await reader.read()
|
|
if (done) break
|
|
result += decoder.decode(value)
|
|
}
|
|
|
|
expect(result).toBe('diana')
|
|
})
|
|
|
|
it.concurrent('should handle invalid JSON gracefully', async () => {
|
|
const mockStream = new ReadableStream({
|
|
start(controller) {
|
|
controller.enqueue(new TextEncoder().encode('invalid json'))
|
|
controller.close()
|
|
},
|
|
})
|
|
|
|
const processedStream = processor.processStream(mockStream, 'block-1', ['block-1_username'], {
|
|
schema: { properties: { username: { type: 'string' } } },
|
|
})
|
|
|
|
const reader = processedStream.getReader()
|
|
const decoder = new TextDecoder()
|
|
let result = ''
|
|
|
|
while (true) {
|
|
const { done, value } = await reader.read()
|
|
if (done) break
|
|
result += decoder.decode(value)
|
|
}
|
|
|
|
expect(result).toBe('')
|
|
})
|
|
|
|
it.concurrent('should filter selected fields for correct block ID', async () => {
|
|
const mockStream = new ReadableStream({
|
|
start(controller) {
|
|
controller.enqueue(new TextEncoder().encode('{"username": "eve", "age": 28}'))
|
|
controller.close()
|
|
},
|
|
})
|
|
|
|
const processedStream = processor.processStream(
|
|
mockStream,
|
|
'block-1',
|
|
['block-1_username', 'block-2_age'], // Different block ID should be filtered out
|
|
{ schema: { properties: { username: { type: 'string' }, age: { type: 'number' } } } }
|
|
)
|
|
|
|
const reader = processedStream.getReader()
|
|
const decoder = new TextDecoder()
|
|
let result = ''
|
|
|
|
while (true) {
|
|
const { done, value } = await reader.read()
|
|
if (done) break
|
|
result += decoder.decode(value)
|
|
}
|
|
|
|
expect(result).toBe('eve')
|
|
})
|
|
|
|
it.concurrent('should handle empty result when no matching fields', async () => {
|
|
const mockStream = new ReadableStream({
|
|
start(controller) {
|
|
controller.enqueue(new TextEncoder().encode('{"other_field": "value"}'))
|
|
controller.close()
|
|
},
|
|
})
|
|
|
|
const processedStream = processor.processStream(mockStream, 'block-1', ['block-1_username'], {
|
|
schema: { properties: { username: { type: 'string' } } },
|
|
})
|
|
|
|
const reader = processedStream.getReader()
|
|
const decoder = new TextDecoder()
|
|
let result = ''
|
|
|
|
while (true) {
|
|
const { done, value } = await reader.read()
|
|
if (done) break
|
|
result += decoder.decode(value)
|
|
}
|
|
|
|
expect(result).toBe('')
|
|
})
|
|
})
|
|
|
|
describe('singleton instance', () => {
|
|
it.concurrent('should export a singleton instance', () => {
|
|
expect(streamingResponseFormatProcessor).toBeInstanceOf(StreamingResponseFormatProcessor)
|
|
})
|
|
|
|
it.concurrent('should return the same instance on multiple imports', () => {
|
|
const instance1 = streamingResponseFormatProcessor
|
|
const instance2 = streamingResponseFormatProcessor
|
|
expect(instance1).toBe(instance2)
|
|
})
|
|
})
|
|
|
|
describe('edge cases', () => {
|
|
it.concurrent('should handle empty stream', async () => {
|
|
const mockStream = new ReadableStream({
|
|
start(controller) {
|
|
controller.close()
|
|
},
|
|
})
|
|
|
|
const processedStream = processor.processStream(mockStream, 'block-1', ['block-1_username'], {
|
|
schema: { properties: { username: { type: 'string' } } },
|
|
})
|
|
|
|
const reader = processedStream.getReader()
|
|
const decoder = new TextDecoder()
|
|
let result = ''
|
|
|
|
while (true) {
|
|
const { done, value } = await reader.read()
|
|
if (done) break
|
|
result += decoder.decode(value)
|
|
}
|
|
|
|
expect(result).toBe('')
|
|
})
|
|
|
|
it.concurrent('should handle very large JSON objects', async () => {
|
|
const largeObject = {
|
|
username: 'frank',
|
|
data: 'x'.repeat(10000), // Large string
|
|
nested: {
|
|
deep: {
|
|
value: 'test',
|
|
},
|
|
},
|
|
}
|
|
|
|
const mockStream = new ReadableStream({
|
|
start(controller) {
|
|
controller.enqueue(new TextEncoder().encode(JSON.stringify(largeObject)))
|
|
controller.close()
|
|
},
|
|
})
|
|
|
|
const processedStream = processor.processStream(mockStream, 'block-1', ['block-1_username'], {
|
|
schema: { properties: { username: { type: 'string' } } },
|
|
})
|
|
|
|
const reader = processedStream.getReader()
|
|
const decoder = new TextDecoder()
|
|
let result = ''
|
|
|
|
while (true) {
|
|
const { done, value } = await reader.read()
|
|
if (done) break
|
|
result += decoder.decode(value)
|
|
}
|
|
|
|
expect(result).toBe('frank')
|
|
})
|
|
})
|
|
})
|