Skip to content

Commit b090ad5

Browse files
committed
refactor: move Stream/Iterable to root and pluralize builder namespaces
Stream and Iterable operate on any ReadableStream<PART> / AsyncIterable<ITEM>, so they no longer belong under ai-test-kit/language. Move them to the root ai-test-kit entry (with StreamDelayOptions); MockLanguageModel keeps using simulateStream internally. Drop the language-specific Stream.text() and Stream.finishReason() reducers: no consumers use them, and the only inline equivalents work on the high-level TextStreamPart layer, not provider-level LanguageModelV3StreamPart, so they would not be a drop-in anyway. Also bring the builder/utility namespaces in line with the plural ones that already exist (UIParts, UIChunks, UIMessages, Options, StreamParts): rename Stream -> Streams, Iterable -> Iterables, Content -> ContentParts. Singular Mock* factories are unchanged. This leaves room for a future Errors namespace. The .from() construction verb stays (mirrors Array.from and the Mock*.from factories). Import Stream/Iterable from 'ai-test-kit' (now Streams/Iterables); Content is now ContentParts from 'ai-test-kit/language'.
1 parent c23af48 commit b090ad5

14 files changed

Lines changed: 217 additions & 268 deletions

README.md

Lines changed: 119 additions & 125 deletions
Large diffs are not rendered by default.

src/index.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ import { describe, expect, test } from 'vitest';
22
import * as root from './index.js';
33

44
describe('root barrel', () => {
5-
test('should be intentionally empty (import from subpaths like ai-test-kit/language)', () => {
5+
test('should export the generic stream and iterable helpers', () => {
66
// Assert
7-
expect(Object.keys(root).length).toBe(0);
7+
expect(typeof root.Streams).toBe('object');
8+
expect(typeof root.Iterables).toBe('object');
89
});
910
});

src/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/**
2-
* Root entry. Intentionally empty — import a model family from its own subpath:
2+
* Root entry for the generic, layer-agnostic helpers. Import a model family from its own subpath:
33
* `ai-test-kit/language`, `ai-test-kit/embedding`, `ai-test-kit/image`, or `ai-test-kit/ui`.
44
*/
5-
export {};
5+
export { Iterables } from './iterable.js';
6+
export { Streams, type StreamDelayOptions } from './stream.js';
Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import { describe, expect, test } from 'vitest';
2-
import { Iterable } from './iterable.js';
3-
import { Stream } from './stream.js';
2+
import { Iterables } from './iterable.js';
3+
import { Streams } from './stream.js';
44

5-
describe('Iterable', () => {
5+
describe('Iterables', () => {
66
test('from() and toArray() should round-trip items', async () => {
77
// Arrange
88
const items = [1, 2, 3];
99

1010
// Act
11-
const roundTripped = await Iterable.toArray(Iterable.from(items));
11+
const roundTripped = await Iterables.toArray(Iterables.from(items));
1212

1313
// Assert
1414
expect(roundTripped).toEqual(items);
@@ -22,19 +22,19 @@ describe('Iterable', () => {
2222
}
2323

2424
// Act
25-
const items = await Iterable.toArray(generate());
25+
const items = await Iterables.toArray(generate());
2626

2727
// Assert
2828
expect(items).toEqual(['a', 'b']);
2929
});
3030

3131
test('toStream() should convert an async iterable into a drainable ReadableStream', async () => {
3232
// Arrange
33-
const iterable = Iterable.from(['x', 'y']);
33+
const iterable = Iterables.from(['x', 'y']);
3434

3535
// Act
36-
const stream = Iterable.toStream(iterable);
37-
const items = await Stream.toArray(stream);
36+
const stream = Iterables.toStream(iterable);
37+
const items = await Streams.toArray(stream);
3838

3939
// Assert
4040
expect(stream).toBeInstanceOf(ReadableStream);
@@ -49,7 +49,7 @@ describe('Iterable', () => {
4949
}
5050

5151
// Act
52-
const result = Stream.toArray(Iterable.toStream(failing()));
52+
const result = Streams.toArray(Iterables.toStream(failing()));
5353

5454
// Assert
5555
await expect(result).rejects.toThrow();
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ async function* fromArray<ITEM>(items: Array<ITEM>): AsyncGenerator<ITEM> {
88
/**
99
* Operations for building, draining, and converting async iterables in tests.
1010
*
11-
* The complement to `Stream`: where `Stream` works with `ReadableStream`s, `Iterable` works with
11+
* The complement to `Streams`: where `Streams` works with `ReadableStream`s, `Iterables` works with
1212
* `AsyncIterable`s (async generators and anything consumed via `for await`). Cross over to the full
13-
* `Stream` toolbox with `Iterable.toStream`.
13+
* `Streams` toolbox with `Iterables.toStream`.
1414
*/
15-
export const Iterable = {
15+
export const Iterables = {
1616
/** Builds an `AsyncIterable` that yields each item in order, e.g. to feed code that consumes one. */
1717
from: <ITEM>(items: Array<ITEM>): AsyncIterable<ITEM> => fromArray(items),
1818

src/language/content.test.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,74 +1,74 @@
11
import { describe, expect, test } from 'vitest';
2-
import { Content } from './content.js';
2+
import { ContentParts } from './content.js';
33

4-
describe('Content', () => {
4+
describe('ContentParts', () => {
55
test('text() should build a text part', () => {
66
// Act
7-
const part = Content.text('hi');
7+
const part = ContentParts.text('hi');
88

99
// Assert
1010
expect(part).toEqual({ type: 'text', text: 'hi' });
1111
});
1212

1313
test('reasoning() should build a reasoning part', () => {
1414
// Act
15-
const part = Content.reasoning('thinking');
15+
const part = ContentParts.reasoning('thinking');
1616

1717
// Assert
1818
expect(part).toEqual({ type: 'reasoning', text: 'thinking' });
1919
});
2020

2121
test('toolCall() should stringify object input to JSON', () => {
2222
// Act
23-
const part = Content.toolCall({ toolCallId: '1', toolName: 'weather', input: { city: 'Tokyo' } });
23+
const part = ContentParts.toolCall({ toolCallId: '1', toolName: 'weather', input: { city: 'Tokyo' } });
2424

2525
// Assert
2626
expect(part).toEqual({ type: 'tool-call', toolCallId: '1', toolName: 'weather', input: '{"city":"Tokyo"}' });
2727
});
2828

2929
test('toolCall() should pass string input through', () => {
3030
// Act
31-
const part = Content.toolCall({ toolCallId: '1', toolName: 'weather', input: '{"city":"Tokyo"}' });
31+
const part = ContentParts.toolCall({ toolCallId: '1', toolName: 'weather', input: '{"city":"Tokyo"}' });
3232

3333
// Assert
3434
expect(part.input).toBe('{"city":"Tokyo"}');
3535
});
3636

3737
test('toolResult() should omit isError when not provided', () => {
3838
// Act
39-
const part = Content.toolResult({ toolCallId: '1', toolName: 'weather', result: { temp: 20 } });
39+
const part = ContentParts.toolResult({ toolCallId: '1', toolName: 'weather', result: { temp: 20 } });
4040

4141
// Assert
4242
expect(part).toEqual({ type: 'tool-result', toolCallId: '1', toolName: 'weather', result: { temp: 20 } });
4343
});
4444

4545
test('toolResult() should include isError when provided', () => {
4646
// Act
47-
const part = Content.toolResult({ toolCallId: '1', toolName: 'weather', result: 'nope', isError: true });
47+
const part = ContentParts.toolResult({ toolCallId: '1', toolName: 'weather', result: 'nope', isError: true });
4848

4949
// Assert
5050
expect(part.isError).toBe(true);
5151
});
5252

5353
test('file() should build a file part', () => {
5454
// Act
55-
const part = Content.file({ mediaType: 'image/png', data: 'abc' });
55+
const part = ContentParts.file({ mediaType: 'image/png', data: 'abc' });
5656

5757
// Assert
5858
expect(part).toEqual({ type: 'file', mediaType: 'image/png', data: 'abc' });
5959
});
6060

6161
test('source() should build a url source, omitting title when absent', () => {
6262
// Act
63-
const part = Content.source({ id: 's1', url: 'https://example.com' });
63+
const part = ContentParts.source({ id: 's1', url: 'https://example.com' });
6464

6565
// Assert
6666
expect(part).toEqual({ type: 'source', sourceType: 'url', id: 's1', url: 'https://example.com' });
6767
});
6868

6969
test('source() should include title when provided', () => {
7070
// Act
71-
const part = Content.source({ id: 's1', url: 'https://example.com', title: 'Example' });
71+
const part = ContentParts.source({ id: 's1', url: 'https://example.com', title: 'Example' });
7272

7373
// Assert
7474
expect(part).toEqual({ type: 'source', sourceType: 'url', id: 's1', url: 'https://example.com', title: 'Example' });

src/language/content.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import type {
99
import { toJSONString } from '../internal/json.js';
1010

1111
/** Builders for the static content parts a language model returns from `doGenerate`. */
12-
export const Content = {
12+
export const ContentParts = {
1313
/** A text part. */
1414
text: (text: string): LanguageModelV3Text => ({ type: 'text', text }),
1515

src/language/index.test.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,8 @@ describe('language barrel', () => {
66
// Assert
77
expect(typeof language.MockLanguageModel).toBe('object');
88
expect(typeof language.MockLanguageModel.from).toBe('function');
9-
expect(typeof language.Content).toBe('object');
9+
expect(typeof language.ContentParts).toBe('object');
1010
expect(typeof language.StreamParts).toBe('object');
11-
expect(typeof language.Stream).toBe('object');
12-
expect(typeof language.Iterable).toBe('object');
1311
expect(typeof language.Options).toBe('object');
1412
});
1513
});

src/language/index.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ export {
66
type StreamResponse,
77
} from './mock-language-model.js';
88

9-
export { Content } from './content.js';
9+
export { ContentParts } from './content.js';
1010
export { StreamParts, type StreamPartOptions } from './stream-parts.js';
11-
export { Stream, type StreamDelayOptions } from './stream.js';
12-
export { Iterable } from './iterable.js';
1311
export { Options } from './options.js';

src/language/mock-language-model.test.ts

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { generateText, streamText } from 'ai';
22
import { describe, expect, test } from 'vitest';
3-
import { Content } from './content.js';
3+
import { Streams } from '../stream.js';
4+
import { ContentParts } from './content.js';
45
import { MockLanguageModel } from './mock-language-model.js';
56
import { Options } from './options.js';
6-
import { Stream } from './stream.js';
77
import { StreamParts } from './stream-parts.js';
88

99
describe('MockLanguageModel', () => {
@@ -30,9 +30,9 @@ describe('MockLanguageModel', () => {
3030
await expect(result).rejects.toThrow();
3131
});
3232

33-
test('should return explicit content built from Content atoms', async () => {
33+
test('should return explicit content built from ContentParts atoms', async () => {
3434
// Arrange
35-
const model = MockLanguageModel.from({ content: [Content.text('explicit')] });
35+
const model = MockLanguageModel.from({ content: [ContentParts.text('explicit')] });
3636

3737
// Act
3838
const result = await generateText({ model, prompt: 'Hi', ...Options.generate });
@@ -44,7 +44,7 @@ describe('MockLanguageModel', () => {
4444

4545
test('should accept a unified finish-reason string in the content form', async () => {
4646
// Arrange
47-
const model = MockLanguageModel.from({ content: [Content.text('truncated')], finishReason: 'length' });
47+
const model = MockLanguageModel.from({ content: [ContentParts.text('truncated')], finishReason: 'length' });
4848

4949
// Act
5050
const result = await generateText({ model, prompt: 'Hi', ...Options.generate });
@@ -66,10 +66,10 @@ describe('MockLanguageModel', () => {
6666
expect(result.text).toBe('prompt-parts:1');
6767
});
6868

69-
test('should surface a tool call from Content.toolCall', async () => {
69+
test('should surface a tool call from ContentParts.toolCall', async () => {
7070
// Arrange
7171
const model = MockLanguageModel.from({
72-
content: [Content.toolCall({ toolCallId: 'call-1', toolName: 'weather', input: { city: 'Tokyo' } })],
72+
content: [ContentParts.toolCall({ toolCallId: 'call-1', toolName: 'weather', input: { city: 'Tokyo' } })],
7373
});
7474

7575
// Act
@@ -88,7 +88,7 @@ describe('MockLanguageModel', () => {
8888

8989
// Act
9090
const result = streamText({ model, prompt: 'Hi', ...Options.stream });
91-
const text = (await Stream.toArray(result.textStream)).join('');
91+
const text = (await Streams.toArray(result.textStream)).join('');
9292

9393
// Assert
9494
expect(text).toBe('Hello World');
@@ -101,19 +101,19 @@ describe('MockLanguageModel', () => {
101101

102102
// Act
103103
const result = streamText({ model, prompt: 'Hi', ...Options.stream });
104-
const text = (await Stream.toArray(result.textStream)).join('');
104+
const text = (await Streams.toArray(result.textStream)).join('');
105105

106106
// Assert
107107
expect(text).toBe('abcdef');
108108
});
109109

110110
test('should derive a stream from content', async () => {
111111
// Arrange
112-
const model = MockLanguageModel.from({ content: [Content.text('derived')] });
112+
const model = MockLanguageModel.from({ content: [ContentParts.text('derived')] });
113113

114114
// Act
115115
const result = streamText({ model, prompt: 'Hi', ...Options.stream });
116-
const text = (await Stream.toArray(result.textStream)).join('');
116+
const text = (await Streams.toArray(result.textStream)).join('');
117117

118118
// Assert
119119
expect(text).toBe('derived');
@@ -122,12 +122,12 @@ describe('MockLanguageModel', () => {
122122
test('should make a string and the equivalent content stream identically', async () => {
123123
// Arrange
124124
const fromString = MockLanguageModel.from('Hello');
125-
const fromContent = MockLanguageModel.from({ content: [Content.text('Hello')] });
125+
const fromContent = MockLanguageModel.from({ content: [ContentParts.text('Hello')] });
126126
const callOptions = { prompt: [] } as never;
127127

128128
// Act
129-
const stringParts = await Stream.toArray((await fromString.doStream(callOptions)).stream);
130-
const contentParts = await Stream.toArray((await fromContent.doStream(callOptions)).stream);
129+
const stringParts = await Streams.toArray((await fromString.doStream(callOptions)).stream);
130+
const contentParts = await Streams.toArray((await fromContent.doStream(callOptions)).stream);
131131

132132
// Assert
133133
expect(stringParts).toEqual(contentParts);
@@ -141,7 +141,7 @@ describe('MockLanguageModel', () => {
141141

142142
// Act
143143
const result = streamText({ model, prompt: 'Hi', ...Options.stream });
144-
const text = (await Stream.toArray(result.textStream)).join('');
144+
const text = (await Streams.toArray(result.textStream)).join('');
145145

146146
// Assert
147147
expect(text).toBe('fast');
@@ -155,7 +155,7 @@ describe('MockLanguageModel', () => {
155155

156156
// Act
157157
const result = streamText({ model, prompt: 'Hi', ...Options.stream });
158-
const text = (await Stream.toArray(result.textStream)).join('');
158+
const text = (await Streams.toArray(result.textStream)).join('');
159159

160160
// Assert
161161
expect(text).toBe('has-prompt');
@@ -164,11 +164,11 @@ describe('MockLanguageModel', () => {
164164
test('should stream from a bare ReadableStream in the stream form', async () => {
165165
// Arrange
166166
const parts = [StreamParts.streamStart(), ...StreamParts.text('piped'), StreamParts.finish()];
167-
const model = MockLanguageModel.from({ stream: Stream.from(parts) });
167+
const model = MockLanguageModel.from({ stream: Streams.from(parts) });
168168

169169
// Act
170170
const result = streamText({ model, prompt: 'Hi', ...Options.stream });
171-
const text = (await Stream.toArray(result.textStream)).join('');
171+
const text = (await Streams.toArray(result.textStream)).join('');
172172

173173
// Assert
174174
expect(text).toBe('piped');
@@ -315,14 +315,14 @@ describe('MockLanguageModel', () => {
315315
test('streamResult() should wrap a ReadableStream as a stream result', async () => {
316316
// Arrange
317317
const parts = [...StreamParts.text('wrapped'), StreamParts.finish()];
318-
const stream = Stream.from(parts);
318+
const stream = Streams.from(parts);
319319

320320
// Act
321321
const result = MockLanguageModel.streamResult(stream);
322322

323323
// Assert
324324
expect(result.stream).toBe(stream);
325-
expect(await Stream.toArray(result.stream)).toEqual(parts);
325+
expect(await Streams.toArray(result.stream)).toEqual(parts);
326326
});
327327
});
328328

0 commit comments

Comments
 (0)