Files
dev/third_party/pi/packages/ai/test/event-stream.test.ts
alexion 6081e3e325 chore(vendor): import pi 1.3.0
Imported as a linear vendor commit so forge rebases do not replay the upstream root tree.

git-subtree-dir: third_party/pi

git-subtree-split: 71023aec1546d6d677a65669c5665c0927ce0dd0
2026-10-15 17:58:48 -04:00

170 lines
5.5 KiB
TypeScript

import { describe, expect, it } from "vitest";
import type { AssistantMessage } from "../src/types.ts";
import { AssistantMessageEventStream, EventStream } from "../src/utils/event-stream.ts";
// Regression tests for https://github.com/earendil-works/pi/issues/9055
describe("EventStream", () => {
it("drains buffered events in order and ignores events pushed after completion", async () => {
const stream = new EventStream<number, number>(
(event) => event === 3,
(event) => event,
);
stream.push(1);
stream.push(2);
stream.push(3);
stream.push(4);
expect(await stream.result()).toBe(3);
const events: number[] = [];
for await (const event of stream) {
events.push(event);
}
expect(events).toEqual([1, 2, 3]);
});
it("preserves order when events arrive after buffered draining starts", async () => {
const stream = new EventStream<number, number>(
() => false,
(event) => event,
);
stream.push(1);
stream.push(2);
const iterator = stream[Symbol.asyncIterator]();
expect(await iterator.next()).toEqual({ value: 1, done: false });
stream.push(3);
expect(await iterator.next()).toEqual({ value: 2, done: false });
expect(await iterator.next()).toEqual({ value: 3, done: false });
stream.end(3);
expect(await iterator.next()).toEqual({ value: undefined, done: true });
});
it("delivers events to waiting consumers in registration order", async () => {
const stream = new EventStream<number, number>(
() => false,
(event) => event,
);
const firstIterator = stream[Symbol.asyncIterator]();
const secondIterator = stream[Symbol.asyncIterator]();
const firstEvent = firstIterator.next();
const secondEvent = secondIterator.next();
stream.push(1);
stream.push(2);
expect(await firstEvent).toEqual({ value: 1, done: false });
expect(await secondEvent).toEqual({ value: 2, done: false });
});
it("drains buffered events after end and resolves the explicit result", async () => {
const stream = new EventStream<number, string>(
() => false,
(event) => String(event),
);
stream.push(1);
stream.push(2);
stream.end("complete");
expect(await stream.result()).toBe("complete");
const events: number[] = [];
for await (const event of stream) {
events.push(event);
}
expect(events).toEqual([1, 2]);
});
it("wakes all waiting consumers when ended without a result", async () => {
const stream = new EventStream<number, number>(
() => false,
(event) => event,
);
const firstIterator = stream[Symbol.asyncIterator]();
const secondIterator = stream[Symbol.asyncIterator]();
const firstEvent = firstIterator.next();
const secondEvent = secondIterator.next();
stream.end();
expect(await firstEvent).toEqual({ value: undefined, done: true });
expect(await secondEvent).toEqual({ value: undefined, done: true });
});
});
function message(timestamp: number, durationMs?: number): AssistantMessage {
return {
role: "assistant",
content: [],
api: "openai-responses",
provider: "openai",
model: "m",
usage: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 0,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
},
stopReason: "stop",
timestamp,
...(durationMs === undefined ? {} : { durationMs }),
};
}
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
describe("AssistantMessageEventStream timing", () => {
it("sets durationMs on the final done or error message of a response it saw start", async () => {
const done = new AssistantMessageEventStream();
const answer = message(Date.now());
await sleep(20);
done.push({ type: "done", reason: "stop", message: answer });
expect(answer.durationMs).toBeGreaterThanOrEqual(15);
expect((await done.result()).durationMs).toBe(answer.durationMs);
const failed = new AssistantMessageEventStream();
const error = { ...message(Date.now()), stopReason: "error" as const };
failed.push({ type: "error", reason: "error", error });
expect(error.durationMs).toBeGreaterThanOrEqual(0);
const ended = new AssistantMessageEventStream();
const result = message(Date.now());
ended.end(result);
expect(result.durationMs).toBeGreaterThanOrEqual(0);
});
it("keeps an existing duration, so a forwarding stream keeps the inner measurement", async () => {
const outer = new AssistantMessageEventStream();
await sleep(20);
const inner = new AssistantMessageEventStream();
const answer = message(Date.now());
inner.push({ type: "done", reason: "stop", message: answer });
const measured = answer.durationMs;
outer.push({ type: "done", reason: "stop", message: answer });
expect(answer.durationMs).toBe(measured);
expect(measured).toBeLessThan(20);
const preset = message(Date.now(), 1234);
new AssistantMessageEventStream().push({ type: "done", reason: "stop", message: preset });
expect(preset.durationMs).toBe(1234);
});
it("leaves a message untimed when it started before the stream, such as a fetched deferred result", () => {
const fetched = message(Date.now() - 60_000);
new AssistantMessageEventStream().push({ type: "done", reason: "stop", message: fetched });
expect(fetched.durationMs).toBeUndefined();
});
it("does not time a message pushed after the stream completed", () => {
const stream = new AssistantMessageEventStream();
stream.push({ type: "done", reason: "stop", message: message(Date.now()) });
const late = message(Date.now());
stream.push({ type: "done", reason: "stop", message: late });
expect(late.durationMs).toBeUndefined();
});
});