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
211 lines
7.0 KiB
TypeScript
211 lines
7.0 KiB
TypeScript
/**
|
|
* @vitest-environment node
|
|
*/
|
|
import { describe, expect, it, vi } from 'vitest'
|
|
import type { NormalizedBlockOutput } from '@/executor/types'
|
|
import { createStreamingExecution } from '@/providers/streaming-execution'
|
|
|
|
/**
|
|
* Builds a fake stream factory mirroring the providers' `createReadableStreamFrom*`
|
|
* helpers: it returns a sentinel stream and synchronously invokes the drain
|
|
* callback so the test can assert the populated output without a real stream.
|
|
*/
|
|
function fakeStreamFactory(
|
|
drain: (handles: { output: NormalizedBlockOutput; finalizeTiming: () => void }) => void
|
|
) {
|
|
const stream = new ReadableStream()
|
|
return {
|
|
stream,
|
|
createStream: (handles: { output: NormalizedBlockOutput; finalizeTiming: () => void }) => {
|
|
drain(handles)
|
|
return stream
|
|
},
|
|
}
|
|
}
|
|
|
|
describe('createStreamingExecution', () => {
|
|
const providerStartTime = 1_000
|
|
const providerStartTimeISO = new Date(providerStartTime).toISOString()
|
|
|
|
it('assembles the simple (no-tools) shape and finalizes timing on drain', () => {
|
|
const drainTime = 5_000
|
|
vi.spyOn(Date, 'now').mockReturnValue(drainTime)
|
|
|
|
const { stream, createStream } = fakeStreamFactory(({ output, finalizeTiming }) => {
|
|
output.content = 'hello'
|
|
output.tokens = { input: 10, output: 20, total: 30 }
|
|
output.cost = { input: 0.1, output: 0.2, total: 0.3 }
|
|
finalizeTiming()
|
|
})
|
|
|
|
const result = createStreamingExecution({
|
|
model: 'test-model',
|
|
providerStartTime,
|
|
providerStartTimeISO,
|
|
timing: { kind: 'simple', segmentName: 'test-model' },
|
|
initialTokens: { input: 0, output: 0, total: 0 },
|
|
initialCost: { input: 0, output: 0, total: 0 },
|
|
isStreaming: true,
|
|
createStream,
|
|
})
|
|
|
|
expect(result.stream).toBe(stream)
|
|
|
|
const output = result.execution.output
|
|
expect(output.content).toBe('hello')
|
|
expect(output.model).toBe('test-model')
|
|
expect(output.tokens).toEqual({ input: 10, output: 20, total: 30 })
|
|
expect(output.cost).toEqual({ input: 0.1, output: 0.2, total: 0.3 })
|
|
expect(output.toolCalls).toBeUndefined()
|
|
|
|
const timing = output.providerTiming
|
|
expect(timing?.startTime).toBe(providerStartTimeISO)
|
|
expect(timing?.endTime).toBe(new Date(drainTime).toISOString())
|
|
expect(timing?.duration).toBe(drainTime - providerStartTime)
|
|
expect(timing?.modelTime).toBeUndefined()
|
|
|
|
const segment = timing?.timeSegments?.[0]
|
|
expect(segment).toMatchObject({
|
|
type: 'model',
|
|
name: 'test-model',
|
|
startTime: providerStartTime,
|
|
})
|
|
expect(segment?.endTime).toBe(drainTime)
|
|
expect(segment?.duration).toBe(drainTime - providerStartTime)
|
|
|
|
expect(result.execution.success).toBe(true)
|
|
expect(result.execution.logs).toEqual([])
|
|
expect(result.execution.isStreaming).toBe(true)
|
|
expect(result.execution.metadata?.startTime).toBe(providerStartTimeISO)
|
|
|
|
vi.restoreAllMocks()
|
|
})
|
|
|
|
it('assembles the accumulated (post-tools) shape with pre-built segments', () => {
|
|
const drainTime = 7_000
|
|
vi.spyOn(Date, 'now').mockReturnValue(drainTime)
|
|
|
|
const timeSegments = [
|
|
{ type: 'model' as const, name: 'iter 1', startTime: 1_000, endTime: 2_000, duration: 1_000 },
|
|
{ type: 'tool' as const, name: 'lookup', startTime: 2_000, endTime: 2_500, duration: 500 },
|
|
]
|
|
|
|
const { createStream } = fakeStreamFactory(({ output }) => {
|
|
output.content = 'final'
|
|
output.tokens = { input: 110, output: 220, total: 330 }
|
|
output.cost = { input: 1.1, output: 2.2, toolCost: 0.5, total: 3.8 }
|
|
})
|
|
|
|
const result = createStreamingExecution({
|
|
model: 'tool-model',
|
|
providerStartTime,
|
|
providerStartTimeISO,
|
|
timing: {
|
|
kind: 'accumulated',
|
|
modelTime: 1_500,
|
|
toolsTime: 500,
|
|
firstResponseTime: 800,
|
|
iterations: 2,
|
|
timeSegments,
|
|
},
|
|
initialTokens: { input: 100, output: 200, total: 300 },
|
|
initialCost: { input: 1, output: 2, toolCost: undefined, total: 3 },
|
|
toolCalls: { list: [{ name: 'lookup' }], count: 1 },
|
|
isStreaming: true,
|
|
createStream,
|
|
})
|
|
|
|
const output = result.execution.output
|
|
expect(output.content).toBe('final')
|
|
expect(output.tokens).toEqual({ input: 110, output: 220, total: 330 })
|
|
expect(output.cost).toEqual({ input: 1.1, output: 2.2, toolCost: 0.5, total: 3.8 })
|
|
expect(output.toolCalls).toEqual({ list: [{ name: 'lookup' }], count: 1 })
|
|
|
|
const timing = output.providerTiming
|
|
expect(timing?.modelTime).toBe(1_500)
|
|
expect(timing?.toolsTime).toBe(500)
|
|
expect(timing?.firstResponseTime).toBe(800)
|
|
expect(timing?.iterations).toBe(2)
|
|
expect(timing?.timeSegments).toBe(timeSegments)
|
|
expect(timing?.startTime).toBe(providerStartTimeISO)
|
|
expect(timing?.endTime).toBe(new Date(drainTime).toISOString())
|
|
expect(timing?.duration).toBe(drainTime - providerStartTime)
|
|
|
|
vi.restoreAllMocks()
|
|
})
|
|
|
|
it('only finalizes timing when the provider calls finalizeTiming', () => {
|
|
const constructTime = 1_200
|
|
vi.spyOn(Date, 'now').mockReturnValue(constructTime)
|
|
|
|
const result = createStreamingExecution({
|
|
model: 'no-finalize',
|
|
providerStartTime,
|
|
providerStartTimeISO,
|
|
timing: {
|
|
kind: 'accumulated',
|
|
modelTime: 0,
|
|
toolsTime: 0,
|
|
firstResponseTime: 0,
|
|
iterations: 1,
|
|
timeSegments: [],
|
|
},
|
|
initialTokens: { input: 0, output: 0, total: 0 },
|
|
initialCost: { input: 0, output: 0, total: 0 },
|
|
createStream: ({ output }) => {
|
|
output.content = 'no-timing-mutation'
|
|
return new ReadableStream()
|
|
},
|
|
})
|
|
|
|
const timing = result.execution.output.providerTiming
|
|
expect(timing?.endTime).toBe(new Date(constructTime).toISOString())
|
|
expect(timing?.duration).toBe(constructTime - providerStartTime)
|
|
expect(result.execution.isStreaming).toBeUndefined()
|
|
|
|
vi.restoreAllMocks()
|
|
})
|
|
|
|
it('finalizeTiming touches only top-level aggregate for accumulated timing', () => {
|
|
const constructTime = 1_000
|
|
const drainTime = 9_000
|
|
const nowMock = vi.spyOn(Date, 'now').mockReturnValue(constructTime)
|
|
|
|
const segment = { type: 'model' as const, name: 's', startTime: 1, endTime: 2, duration: 1 }
|
|
|
|
const result = createStreamingExecution({
|
|
model: 'm',
|
|
providerStartTime,
|
|
providerStartTimeISO,
|
|
timing: {
|
|
kind: 'accumulated',
|
|
modelTime: 0,
|
|
toolsTime: 0,
|
|
firstResponseTime: 0,
|
|
iterations: 1,
|
|
timeSegments: [segment],
|
|
},
|
|
initialTokens: { input: 0, output: 0, total: 0 },
|
|
initialCost: { input: 0, output: 0, total: 0 },
|
|
createStream: ({ finalizeTiming }) => {
|
|
nowMock.mockReturnValue(drainTime)
|
|
finalizeTiming()
|
|
return new ReadableStream()
|
|
},
|
|
})
|
|
|
|
const timing = result.execution.output.providerTiming
|
|
expect(timing?.endTime).toBe(new Date(drainTime).toISOString())
|
|
expect(timing?.duration).toBe(drainTime - providerStartTime)
|
|
expect(timing?.timeSegments?.[0]).toEqual({
|
|
type: 'model',
|
|
name: 's',
|
|
startTime: 1,
|
|
endTime: 2,
|
|
duration: 1,
|
|
})
|
|
|
|
vi.restoreAllMocks()
|
|
})
|
|
})
|