fix(stream-text): close after error (#192)

* feat(tests): add tool test cases and improve existing test structure

* refactor(tests): remove unused imports from index.test.ts

* refactor(tests): remove console.log from tool test case

* fix(tests): correct text initialization in stream test case

* fix(stream): correct formatting in finally block of streamText function

* fix(tests): ensure text comparison is case insensitive in stream test
This commit is contained in:
exoticknight authored and GitHub committed 2025-07-18 17:19:49 +08:00
1 parent d1439dd65d
commit 5949bb6b53
5 files changed
+213 -88

No files matched your search

+1
View File
@@ -36,6 +36,7 @@
"scripts": {
"build": "pkgroll",
"test": "vitest run",
"test:coverage": "vitest run --coverage",
"test:watch": "vitest"
},
"dependencies": {
+3 -3
View File
@@ -187,6 +187,9 @@ export const streamText = async (options: StreamTextOptions): Promise<StreamText
try {
await trampoline(async () => startStream())
eventCtrl?.close()
textCtrl?.close()
}
catch (err) {
eventCtrl?.error(err)
@@ -197,9 +200,6 @@ export const streamText = async (options: StreamTextOptions): Promise<StreamText
resultUsage.reject(err)
}
finally {
eventCtrl?.close()
textCtrl?.close()
resultSteps.resolve(steps)
resultMessages.resolve(messages)
resultUsage.resolve(usage)
@@ -1,11 +1,87 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`@xsai/stream-text > basic 1`] = `Promise {}`;
exports[`@xsai/stream-text basic > basic 1`] = `Promise {}`;
exports[`@xsai/stream-text > basic 2`] = `
exports[`@xsai/stream-text basic > basic 2`] = `
[
{
"text": "YES",
"text": "Yes",
"type": "text-delta",
},
{
"text": "",
"type": "text-delta",
},
{
"finishReason": "stop",
"type": "finish",
"usage": undefined,
},
]
`;
exports[`@xsai/stream-text basic > stream 1`] = `Promise {}`;
exports[`@xsai/stream-text basic > stream 2`] = `
[
{
"text": "Why",
"type": "text-delta",
},
{
"text": " don",
"type": "text-delta",
},
{
"text": "'t",
"type": "text-delta",
},
{
"text": " sc",
"type": "text-delta",
},
{
"text": "ient",
"type": "text-delta",
},
{
"text": "ists",
"type": "text-delta",
},
{
"text": " trust",
"type": "text-delta",
},
{
"text": " atoms",
"type": "text-delta",
},
{
"text": "?",
"type": "text-delta",
},
{
"text": " Because",
"type": "text-delta",
},
{
"text": " they",
"type": "text-delta",
},
{
"text": " make",
"type": "text-delta",
},
{
"text": " up",
"type": "text-delta",
},
{
"text": " everything",
"type": "text-delta",
},
{
"text": "!",
"type": "text-delta",
},
{
+17 -82
View File
@@ -1,6 +1,3 @@
import { clean } from '@xsai/shared'
import { tool } from '@xsai/tool'
import { description, object, pipe, string } from 'valibot'
import { describe, expect, it } from 'vitest'
import type { StreamTextEvent } from '../src/types/event'
@@ -16,7 +13,7 @@ declare global {
}
}
describe('@xsai/stream-text', async () => {
describe('@xsai/stream-text basic', async () => {
it('basic', async () => {
const { fullStream, steps, textStream } = await streamText({
baseURL: 'http://localhost:11434/v1/',
@@ -40,7 +37,7 @@ describe('@xsai/stream-text', async () => {
for await (const t of textStream) {
text += t
}
expect(text).toBe('YES')
expect(text.toUpperCase()).toBe('YES')
const events: StreamTextEvent[] = []
for await (const event of fullStream) {
@@ -49,97 +46,35 @@ describe('@xsai/stream-text', async () => {
expect(events).toMatchSnapshot()
})
it('basic tool calls', async () => {
const add = await tool({
description: 'Adds two numbers',
execute: ({ a, b }) => (Number.parseInt(a) + Number.parseInt(b)).toString(),
name: 'add',
parameters: object({
a: pipe(
string(),
description('First number'),
),
b: pipe(
string(),
description('Second number'),
),
}),
})
const { fullStream, steps } = await streamText({
it('stream', async () => {
const { fullStream, steps, textStream } = await streamText({
baseURL: 'http://localhost:11434/v1/',
maxSteps: 2,
messages: [
{
content: 'You are a helpful assistant.',
role: 'system',
},
{
content: 'How many times does 114514 plus 1919810 equal? Please try to call the `add` tool to solve the problem.',
content: 'tell me a joke',
role: 'user',
},
],
model: 'qwen3:0.6b',
seed: 1145141919810,
toolChoice: 'required',
tools: [add],
model: 'granite3.3:2b',
seed: 114514,
})
expect(steps).toMatchSnapshot()
const text = []
for await (const t of textStream) {
text.push(t)
}
expect(text.length).toBeGreaterThan(1)
const events: StreamTextEvent[] = []
for await (const event of fullStream) {
// eslint-disable-next-line @masknet/type-no-force-cast-via-top-type
events.push(clean({
...event,
toolCallId: undefined,
}) as unknown as StreamTextEvent)
events.push(event)
}
expect(events.find(e => e.type === 'tool-call-streaming-start')).toStrictEqual({
toolName: 'add',
type: 'tool-call-streaming-start',
})
expect(events.find(e => e.type === 'tool-call')).toStrictEqual({
args: '{"a":"114514","b":"1919810"}',
toolCallType: 'function',
toolName: 'add',
type: 'tool-call',
})
expect(events.find(e => e.type === 'tool-result')).toStrictEqual({
args: {
a: '114514',
b: '1919810',
},
result: '2034324',
toolName: 'add',
type: 'tool-result',
})
const allSteps = await steps
const cleanToolCallId = (obj: object) => clean({
...obj,
toolCallId: undefined,
})
expect(allSteps.length).toBe(2)
expect(allSteps[0].toolCalls.map(cleanToolCallId)).toStrictEqual([
{
args: '{"a":"114514","b":"1919810"}',
toolCallType: 'function',
toolName: 'add',
},
])
expect(allSteps[0].toolResults.map(cleanToolCallId)).toStrictEqual([
{
args: {
a: '114514',
b: '1919810',
},
result: '2034324',
toolName: 'add',
},
])
expect(events).toMatchSnapshot()
})
})
+113
View File
@@ -0,0 +1,113 @@
import { clean } from '@xsai/shared'
import { tool } from '@xsai/tool'
import { description, object, pipe, string } from 'valibot'
import { describe, expect, it } from 'vitest'
import type { StreamTextEvent } from '../src/types/event'
import { streamText } from '../src'
// make TS happy
// https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream#browser_compatibility
declare global {
interface ReadableStream<R = any> {
// eslint-disable-next-line ts/method-signature-style
[Symbol.asyncIterator](): AsyncIterableIterator<R>
}
}
describe('@xsai/stream-text tool', async () => {
it('basic tool calls', async () => {
const add = await tool({
description: 'Adds two numbers',
execute: ({ a, b }) => (Number.parseInt(a) + Number.parseInt(b)).toString(),
name: 'add',
parameters: object({
a: pipe(
string(),
description('First number'),
),
b: pipe(
string(),
description('Second number'),
),
}),
})
const { fullStream, steps } = await streamText({
baseURL: 'http://localhost:11434/v1/',
maxSteps: 2,
messages: [
{
content: 'You are a helpful assistant.',
role: 'system',
},
{
content: 'How many times does 114514 plus 1919810 equal? Please try to call the `add` tool to solve the problem.',
role: 'user',
},
],
model: 'qwen3:0.6b',
seed: 1145141919810,
toolChoice: 'required',
tools: [add],
})
const events: StreamTextEvent[] = []
for await (const event of fullStream) {
// eslint-disable-next-line @masknet/type-no-force-cast-via-top-type
events.push(clean({
...event,
toolCallId: undefined,
}) as unknown as StreamTextEvent)
}
expect(events.find(e => e.type === 'tool-call-streaming-start')).toStrictEqual({
toolName: 'add',
type: 'tool-call-streaming-start',
})
expect(events.find(e => e.type === 'tool-call')).toStrictEqual({
args: '{"a":"114514","b":"1919810"}',
toolCallType: 'function',
toolName: 'add',
type: 'tool-call',
})
expect(events.find(e => e.type === 'tool-result')).toStrictEqual({
args: {
a: '114514',
b: '1919810',
},
result: '2034324',
toolName: 'add',
type: 'tool-result',
})
const allSteps = await steps
const cleanToolCallId = (obj: object) => clean({
...obj,
toolCallId: undefined,
})
expect(allSteps.length).toBe(2)
expect(allSteps[0].toolCalls.map(cleanToolCallId)).toStrictEqual([
{
args: '{"a":"114514","b":"1919810"}',
toolCallType: 'function',
toolName: 'add',
},
])
expect(allSteps[0].toolResults.map(cleanToolCallId)).toStrictEqual([
{
args: {
a: '114514',
b: '1919810',
},
result: '2034324',
toolName: 'add',
},
])
})
})