Files
simstudioai--sim/apps/sim/lib/copilot/request/session/writer.test.ts
T
wehub-resource-sync d25d482dc2
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
chore: import upstream snapshot with attribution
2026-07-13 13:20:55 +08:00

190 lines
5.1 KiB
TypeScript

/**
* @vitest-environment node
*/
import { beforeEach, describe, expect, it, vi } from 'vitest'
import {
MothershipStreamV1EventType,
MothershipStreamV1TextChannel,
} from '@/lib/copilot/generated/mothership-stream-v1'
import type { StreamEvent } from '@/lib/copilot/request/session'
const { appendEvents } = vi.hoisted(() => ({
appendEvents: vi.fn(),
}))
vi.mock('@/lib/copilot/request/session/buffer', () => ({
appendEvents,
}))
import { StreamWriter } from '@/lib/copilot/request/session/writer'
function decodeChunk(value: Uint8Array): string {
return new TextDecoder().decode(value)
}
describe('StreamWriter', () => {
beforeEach(() => {
vi.clearAllMocks()
vi.useRealTimers()
})
it('enqueues before persistence completes and flushes pending writes on close', async () => {
let releasePersist: (() => void) | null = null
appendEvents.mockImplementation(
() =>
new Promise<void>((resolve) => {
releasePersist = resolve
})
)
const writer = new StreamWriter({
streamId: 'stream-1',
chatId: 'chat-1',
requestId: 'req-1',
})
const chunks: string[] = []
let closeCount = 0
const controller = {
enqueue: vi.fn((value: Uint8Array) => {
chunks.push(decodeChunk(value))
}),
close: vi.fn(() => {
closeCount += 1
}),
} as unknown as ReadableStreamDefaultController
writer.attach(controller)
await writer.publish({
type: MothershipStreamV1EventType.text,
payload: { channel: MothershipStreamV1TextChannel.assistant, text: 'hello' },
})
expect(controller.enqueue).toHaveBeenCalledOnce()
expect(appendEvents).not.toHaveBeenCalled()
expect(chunks[0]).toContain('"text":"hello"')
expect(closeCount).toBe(0)
const closePromise = writer.close()
await Promise.resolve()
await Promise.resolve()
expect(appendEvents).toHaveBeenCalledOnce()
expect(closeCount).toBe(0)
const resolvePersist = releasePersist
if (typeof resolvePersist === 'function') {
resolvePersist()
}
await closePromise
expect(closeCount).toBe(1)
})
it('batches publishes on the flush timer and preserves sequence order', async () => {
vi.useFakeTimers()
const persistedSeqs: number[] = []
appendEvents.mockImplementation(async (envelopes) => {
persistedSeqs.push(...envelopes.map((envelope) => envelope.seq))
return envelopes
})
const writer = new StreamWriter({
streamId: 'stream-1',
requestId: 'req-1',
})
const chunks: string[] = []
const controller = {
enqueue: vi.fn((value: Uint8Array) => {
chunks.push(decodeChunk(value))
}),
close: vi.fn(),
} as unknown as ReadableStreamDefaultController
writer.attach(controller)
await Promise.all([
writer.publish({
type: MothershipStreamV1EventType.text,
payload: { channel: MothershipStreamV1TextChannel.assistant, text: 'one' },
}),
writer.publish({
type: MothershipStreamV1EventType.text,
payload: { channel: MothershipStreamV1TextChannel.assistant, text: 'two' },
}),
])
expect(appendEvents).not.toHaveBeenCalled()
await vi.advanceTimersByTimeAsync(15)
await writer.close()
expect(persistedSeqs).toEqual([1, 2])
expect(appendEvents).toHaveBeenCalledWith([
expect.objectContaining({ seq: 1 }),
expect.objectContaining({ seq: 2 }),
])
expect(chunks[0]).toContain('"seq":1')
expect(chunks[1]).toContain('"seq":2')
})
it('flush waits for persistence and surfaces failures', async () => {
appendEvents.mockRejectedValueOnce(new Error('redis down'))
const writer = new StreamWriter({
streamId: 'stream-1',
requestId: 'req-1',
})
writer.attach({
enqueue: vi.fn(),
close: vi.fn(),
} as unknown as ReadableStreamDefaultController)
await writer.publish({
type: MothershipStreamV1EventType.text,
payload: { channel: MothershipStreamV1TextChannel.assistant, text: 'boom' },
})
await expect(writer.flush()).rejects.toThrow('redis down')
})
it('persists synthetic preview events alongside contract events', async () => {
appendEvents.mockResolvedValue([])
const writer = new StreamWriter({
streamId: 'stream-1',
requestId: 'req-1',
})
const chunks: string[] = []
writer.attach({
enqueue: vi.fn((value: Uint8Array) => {
chunks.push(decodeChunk(value))
}),
close: vi.fn(),
} as unknown as ReadableStreamDefaultController)
await writer.publish({
type: MothershipStreamV1EventType.tool,
payload: {
toolCallId: 'preview-1',
toolName: 'workspace_file',
previewPhase: 'file_preview_start',
},
} satisfies StreamEvent)
await writer.flush()
expect(chunks[0]).toContain('"previewPhase":"file_preview_start"')
expect(appendEvents).toHaveBeenCalledWith([
expect.objectContaining({
type: MothershipStreamV1EventType.tool,
payload: expect.objectContaining({
toolCallId: 'preview-1',
previewPhase: 'file_preview_start',
}),
}),
])
})
})