diff --git a/src/api/providers/__tests__/kenari.spec.ts b/src/api/providers/__tests__/kenari.spec.ts index 28691437a2..d6b95ce0b1 100644 --- a/src/api/providers/__tests__/kenari.spec.ts +++ b/src/api/providers/__tests__/kenari.spec.ts @@ -17,6 +17,7 @@ import { kenariDefaultModelId } from "@roo-code/types" import { KenariHandler } from "../kenari" import { getModels } from "../fetchers/modelCache" import { ApiHandlerOptions } from "../../../shared/api" +import { asyncStreamFrom, collectStream } from "../../../test-utils/stream" vitest.mock("openai") vitest.mock("delay", () => ({ default: vitest.fn(() => Promise.resolve()) })) @@ -84,9 +85,9 @@ describe("KenariHandler", () => { describe("createMessage", () => { beforeEach(() => { - mockCreate.mockImplementation(async () => ({ - [Symbol.asyncIterator]: async function* () { - yield { + mockCreate.mockImplementation(async () => + asyncStreamFrom([ + { choices: [ { delta: { @@ -104,8 +105,8 @@ describe("KenariHandler", () => { }, ], usage: null, - } - yield { + }, + { choices: [{ delta: {}, index: 0 }], usage: { prompt_tokens: 12, @@ -113,19 +114,16 @@ describe("KenariHandler", () => { total_tokens: 19, prompt_tokens_details: { cached_tokens: 4 }, }, - } - }, - })) + }, + ]), + ) }) it("streams text, reasoning, tool-call and usage chunks", async () => { const handler = new KenariHandler(mockOptions) const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hi" }] - const chunks = [] - for await (const chunk of handler.createMessage("You are helpful.", messages)) { - chunks.push(chunk) - } + const chunks = await collectStream(handler.createMessage("You are helpful.", messages)) expect(chunks).toContainEqual({ type: "text", text: "Hello" }) expect(chunks).toContainEqual({ type: "reasoning", text: "thinking…" }) @@ -145,37 +143,31 @@ describe("KenariHandler", () => { }) it("yields nothing for a chunk whose delta has no content, reasoning or tool calls", async () => { - mockCreate.mockImplementation(async () => ({ - [Symbol.asyncIterator]: async function* () { - yield { choices: [{ delta: {}, index: 0 }], usage: null } - yield { choices: [], usage: null } - }, - })) + mockCreate.mockImplementation(async () => + asyncStreamFrom([ + { choices: [{ delta: {}, index: 0 }], usage: null }, + { choices: [], usage: null }, + ]), + ) const handler = new KenariHandler(mockOptions) - const chunks = [] - for await (const chunk of handler.createMessage("sys", [{ role: "user", content: "Hi" }])) { - chunks.push(chunk) - } + const chunks = await collectStream(handler.createMessage("sys", [{ role: "user", content: "Hi" }])) expect(chunks).toEqual([]) }) it("streams tool call chunks even when the function name and arguments are missing", async () => { - mockCreate.mockImplementation(async () => ({ - [Symbol.asyncIterator]: async function* () { - yield { + mockCreate.mockImplementation(async () => + asyncStreamFrom([ + { choices: [{ delta: { tool_calls: [{ index: 1 }] }, index: 0 }], usage: null, - } - }, - })) + }, + ]), + ) const handler = new KenariHandler(mockOptions) - const chunks = [] - for await (const chunk of handler.createMessage("sys", [{ role: "user", content: "Hi" }])) { - chunks.push(chunk) - } + const chunks = await collectStream(handler.createMessage("sys", [{ role: "user", content: "Hi" }])) expect(chunks).toEqual([ { @@ -189,20 +181,17 @@ describe("KenariHandler", () => { }) it("reports undefined cache reads when usage has no prompt_tokens_details", async () => { - mockCreate.mockImplementation(async () => ({ - [Symbol.asyncIterator]: async function* () { - yield { + mockCreate.mockImplementation(async () => + asyncStreamFrom([ + { choices: [{ delta: {}, index: 0 }], usage: { prompt_tokens: 3, completion_tokens: 2, total_tokens: 5 }, - } - }, - })) + }, + ]), + ) const handler = new KenariHandler(mockOptions) - const chunks = [] - for await (const chunk of handler.createMessage("sys", [{ role: "user", content: "Hi" }])) { - chunks.push(chunk) - } + const chunks = await collectStream(handler.createMessage("sys", [{ role: "user", content: "Hi" }])) expect(chunks).toEqual([ { @@ -215,39 +204,33 @@ describe("KenariHandler", () => { }) it("skips the reasoning chunk when reasoning_content is an empty string", async () => { - mockCreate.mockImplementation(async () => ({ - [Symbol.asyncIterator]: async function* () { - yield { + mockCreate.mockImplementation(async () => + asyncStreamFrom([ + { choices: [{ delta: { content: "Hi", reasoning_content: "" }, index: 0 }], usage: null, - } - }, - })) + }, + ]), + ) const handler = new KenariHandler(mockOptions) - const chunks = [] - for await (const chunk of handler.createMessage("sys", [{ role: "user", content: "Hi" }])) { - chunks.push(chunk) - } + const chunks = await collectStream(handler.createMessage("sys", [{ role: "user", content: "Hi" }])) expect(chunks).toEqual([{ type: "text", text: "Hi" }]) }) it("emits reasoning from the OpenRouter-style `reasoning` field when reasoning_content is absent", async () => { - mockCreate.mockImplementation(async () => ({ - [Symbol.asyncIterator]: async function* () { - yield { + mockCreate.mockImplementation(async () => + asyncStreamFrom([ + { choices: [{ delta: { content: "Hi", reasoning: "thinking…" }, index: 0 }], usage: null, - } - }, - })) + }, + ]), + ) const handler = new KenariHandler(mockOptions) - const chunks = [] - for await (const chunk of handler.createMessage("sys", [{ role: "user", content: "Hi" }])) { - chunks.push(chunk) - } + const chunks = await collectStream(handler.createMessage("sys", [{ role: "user", content: "Hi" }])) expect(chunks).toContainEqual({ type: "reasoning", text: "thinking…" }) }) @@ -264,9 +247,7 @@ describe("KenariHandler", () => { }) const handler = new KenariHandler({ kenariApiKey: "test-key", kenariModelId: "openai/o3-mini" }) - for await (const _chunk of handler.createMessage("sys", [{ role: "user", content: "Hi" }])) { - void _chunk // drain - } + await collectStream(handler.createMessage("sys", [{ role: "user", content: "Hi" }])) expect(mockCreate).toHaveBeenCalledWith( expect.objectContaining({ @@ -278,40 +259,35 @@ describe("KenariHandler", () => { it("sends an explicitly configured model temperature", async () => { const handler = new KenariHandler({ ...mockOptions, modelTemperature: 0.7 }) - for await (const _chunk of handler.createMessage("sys", [{ role: "user", content: "Hi" }])) { - void _chunk // drain - } + await collectStream(handler.createMessage("sys", [{ role: "user", content: "Hi" }])) expect(mockCreate).toHaveBeenCalledWith(expect.objectContaining({ temperature: 0.7 })) }) it("honors metadata.parallelToolCalls false", async () => { const handler = new KenariHandler(mockOptions) - for await (const _chunk of handler.createMessage("sys", [{ role: "user", content: "Hi" }], { - taskId: "task-1", - parallelToolCalls: false, - })) { - void _chunk // drain - } + await collectStream( + handler.createMessage("sys", [{ role: "user", content: "Hi" }], { + taskId: "task-1", + parallelToolCalls: false, + }), + ) expect(mockCreate).toHaveBeenCalledWith(expect.objectContaining({ parallel_tool_calls: false })) }) it("reports zero usage when the upstream counts are zero", async () => { - mockCreate.mockImplementation(async () => ({ - [Symbol.asyncIterator]: async function* () { - yield { + mockCreate.mockImplementation(async () => + asyncStreamFrom([ + { choices: [{ delta: { content: "x" }, index: 0 }], usage: { prompt_tokens: 0, completion_tokens: 0 }, - } - }, - })) + }, + ]), + ) const handler = new KenariHandler(mockOptions) - const chunks = [] - for await (const chunk of handler.createMessage("sys", [{ role: "user", content: "Hi" }])) { - chunks.push(chunk) - } + const chunks = await collectStream(handler.createMessage("sys", [{ role: "user", content: "Hi" }])) expect(chunks).toContainEqual({ type: "usage", @@ -324,9 +300,7 @@ describe("KenariHandler", () => { it("requests a streaming completion with usage included", async () => { const handler = new KenariHandler(mockOptions) const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hi" }] - for await (const _chunk of handler.createMessage("sys", messages)) { - void _chunk // drain - } + await collectStream(handler.createMessage("sys", messages)) expect(mockCreate).toHaveBeenCalledWith( expect.objectContaining({ diff --git a/src/api/providers/__tests__/minimax.spec.ts b/src/api/providers/__tests__/minimax.spec.ts index 5f90d2b818..53dbd8740f 100644 --- a/src/api/providers/__tests__/minimax.spec.ts +++ b/src/api/providers/__tests__/minimax.spec.ts @@ -13,6 +13,7 @@ import { Anthropic } from "@anthropic-ai/sdk" import { type MinimaxModelId, minimaxDefaultModelId, minimaxModels } from "@roo-code/types" import { MiniMaxHandler } from "../minimax" +import { asyncStreamFrom, collectStream } from "../../../test-utils/stream" vitest.mock("@anthropic-ai/sdk", () => { const mockCreate = vitest.fn() @@ -240,21 +241,15 @@ describe("MiniMaxHandler", () => { it("createMessage should yield text content from stream", async () => { const testContent = "This is test content from MiniMax stream" - mockCreate.mockResolvedValueOnce({ - [Symbol.asyncIterator]: () => ({ - next: vitest - .fn() - .mockResolvedValueOnce({ - done: false, - value: { - type: "content_block_start", - index: 0, - content_block: { type: "text", text: testContent }, - }, - }) - .mockResolvedValueOnce({ done: true }), - }), - }) + mockCreate.mockResolvedValueOnce( + asyncStreamFrom([ + { + type: "content_block_start", + index: 0, + content_block: { type: "text", text: testContent }, + }, + ]), + ) const stream = handler.createMessage("system prompt", []) const firstChunk = await stream.next() @@ -264,25 +259,19 @@ describe("MiniMaxHandler", () => { }) it("createMessage should yield usage data from stream", async () => { - mockCreate.mockResolvedValueOnce({ - [Symbol.asyncIterator]: () => ({ - next: vitest - .fn() - .mockResolvedValueOnce({ - done: false, - value: { - type: "message_start", - message: { - usage: { - input_tokens: 10, - output_tokens: 20, - }, - }, + mockCreate.mockResolvedValueOnce( + asyncStreamFrom([ + { + type: "message_start", + message: { + usage: { + input_tokens: 10, + output_tokens: 20, }, - }) - .mockResolvedValueOnce({ done: true }), - }), - }) + }, + }, + ]), + ) const stream = handler.createMessage("system prompt", []) const firstChunk = await stream.next() @@ -299,13 +288,7 @@ describe("MiniMaxHandler", () => { minimaxApiKey: "test-minimax-api-key", }) - mockCreate.mockResolvedValueOnce({ - [Symbol.asyncIterator]: () => ({ - async next() { - return { done: true } - }, - }), - }) + mockCreate.mockResolvedValueOnce(asyncStreamFrom([])) const systemPrompt = "Test system prompt for MiniMax" const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Test message for MiniMax" }] @@ -326,13 +309,7 @@ describe("MiniMaxHandler", () => { }) it("should use temperature 1 by default", async () => { - mockCreate.mockResolvedValueOnce({ - [Symbol.asyncIterator]: () => ({ - async next() { - return { done: true } - }, - }), - }) + mockCreate.mockResolvedValueOnce(asyncStreamFrom([])) const messageGenerator = handler.createMessage("test", []) await messageGenerator.next() @@ -347,21 +324,15 @@ describe("MiniMaxHandler", () => { it("should handle thinking blocks in stream", async () => { const thinkingContent = "Let me think about this..." - mockCreate.mockResolvedValueOnce({ - [Symbol.asyncIterator]: () => ({ - next: vitest - .fn() - .mockResolvedValueOnce({ - done: false, - value: { - type: "content_block_start", - index: 0, - content_block: { type: "thinking", thinking: thinkingContent }, - }, - }) - .mockResolvedValueOnce({ done: true }), - }), - }) + mockCreate.mockResolvedValueOnce( + asyncStreamFrom([ + { + type: "content_block_start", + index: 0, + content_block: { type: "thinking", thinking: thinkingContent }, + }, + ]), + ) const stream = handler.createMessage("system prompt", []) const firstChunk = await stream.next() @@ -371,33 +342,24 @@ describe("MiniMaxHandler", () => { }) it("should handle tool calls in stream", async () => { - mockCreate.mockResolvedValueOnce({ - [Symbol.asyncIterator]: () => ({ - next: vitest - .fn() - .mockResolvedValueOnce({ - done: false, - value: { - type: "content_block_start", - index: 0, - content_block: { - type: "tool_use", - id: "tool-123", - name: "get_weather", - input: { city: "London" }, - }, - }, - }) - .mockResolvedValueOnce({ - done: false, - value: { - type: "content_block_stop", - index: 0, - }, - }) - .mockResolvedValueOnce({ done: true }), - }), - }) + mockCreate.mockResolvedValueOnce( + asyncStreamFrom([ + { + type: "content_block_start", + index: 0, + content_block: { + type: "tool_use", + id: "tool-123", + name: "get_weather", + input: { city: "London" }, + }, + }, + { + type: "content_block_stop", + index: 0, + }, + ]), + ) const stream = handler.createMessage("system prompt", []) const firstChunk = await stream.next() diff --git a/src/api/providers/__tests__/mistral.spec.ts b/src/api/providers/__tests__/mistral.spec.ts index 96e42e356b..f2a7591bd8 100644 --- a/src/api/providers/__tests__/mistral.spec.ts +++ b/src/api/providers/__tests__/mistral.spec.ts @@ -11,28 +11,26 @@ vi.mock("@roo-code/telemetry", () => ({ // Mock Mistral client - must come before other imports const mockCreate = vi.fn() const mockComplete = vi.fn() +import { asyncStreamFrom, collectStream } from "../../../test-utils/stream" vi.mock("@mistralai/mistralai", () => { return { Mistral: vi.fn().mockImplementation(function () { return { chat: { - stream: mockCreate.mockImplementation(async (_options) => { - const stream = { - [Symbol.asyncIterator]: async function* () { - yield { - data: { - choices: [ - { - delta: { content: "Test response" }, - index: 0, - }, - ], - }, - } + stream: mockCreate.mockImplementation(async (_options) => + asyncStreamFrom([ + { + data: { + choices: [ + { + delta: { content: "Test response" }, + index: 0, + }, + ], + }, }, - } - return stream - }), + ]), + ), complete: mockComplete.mockImplementation(async (_options) => { return { choices: [ @@ -158,31 +156,28 @@ describe("MistralHandler", () => { it("should handle thinking content as reasoning chunks", async () => { // Mock stream with thinking content matching new SDK structure - mockCreate.mockImplementationOnce(async (_options) => { - const stream = { - [Symbol.asyncIterator]: async function* () { - yield { - data: { - choices: [ - { - delta: { - content: [ - { - type: "thinking", - thinking: [{ type: "text", text: "Let me think about this..." }], - }, - { type: "text", text: "Here's the answer" }, - ], - }, - index: 0, + mockCreate.mockImplementationOnce(async (_options) => + asyncStreamFrom([ + { + data: { + choices: [ + { + delta: { + content: [ + { + type: "thinking", + thinking: [{ type: "text", text: "Let me think about this..." }], + }, + { type: "text", text: "Here's the answer" }, + ], }, - ], - }, - } + index: 0, + }, + ], + }, }, - } - return stream - }) + ]), + ) const iterator = handler.createMessage(systemPrompt, messages) const results: (ApiStreamTextChunk | ApiStreamReasoningChunk)[] = [] @@ -200,32 +195,29 @@ describe("MistralHandler", () => { it("should handle mixed content arrays correctly", async () => { // Mock stream with mixed content matching new SDK structure - mockCreate.mockImplementationOnce(async (_options) => { - const stream = { - [Symbol.asyncIterator]: async function* () { - yield { - data: { - choices: [ - { - delta: { - content: [ - { type: "text", text: "First text" }, - { - type: "thinking", - thinking: [{ type: "text", text: "Some reasoning" }], - }, - { type: "text", text: "Second text" }, - ], - }, - index: 0, + mockCreate.mockImplementationOnce(async (_options) => + asyncStreamFrom([ + { + data: { + choices: [ + { + delta: { + content: [ + { type: "text", text: "First text" }, + { + type: "thinking", + thinking: [{ type: "text", text: "Some reasoning" }], + }, + { type: "text", text: "Second text" }, + ], }, - ], - }, - } + index: 0, + }, + ], + }, }, - } - return stream - }) + ]), + ) const iterator = handler.createMessage(systemPrompt, messages) const results: (ApiStreamTextChunk | ApiStreamReasoningChunk)[] = [] @@ -314,34 +306,31 @@ describe("MistralHandler", () => { it("should handle tool calls in streaming response", async () => { // Mock stream with tool calls - mockCreate.mockImplementationOnce(async (_options) => { - const stream = { - [Symbol.asyncIterator]: async function* () { - yield { - data: { - choices: [ - { - delta: { - toolCalls: [ - { - id: "call_123", - type: "function", - function: { - name: "get_weather", - arguments: '{"location":"New York"}', - }, + mockCreate.mockImplementationOnce(async (_options) => + asyncStreamFrom([ + { + data: { + choices: [ + { + delta: { + toolCalls: [ + { + id: "call_123", + type: "function", + function: { + name: "get_weather", + arguments: '{"location":"New York"}', }, - ], - }, - index: 0, + }, + ], }, - ], - }, - } + index: 0, + }, + ], + }, }, - } - return stream - }) + ]), + ) const metadata: ApiHandlerCreateMessageMetadata = { taskId: "test-task", @@ -369,42 +358,39 @@ describe("MistralHandler", () => { it("should handle multiple tool calls in a single response", async () => { // Mock stream with multiple tool calls - mockCreate.mockImplementationOnce(async (_options) => { - const stream = { - [Symbol.asyncIterator]: async function* () { - yield { - data: { - choices: [ - { - delta: { - toolCalls: [ - { - id: "call_1", - type: "function", - function: { - name: "get_weather", - arguments: '{"location":"NYC"}', - }, + mockCreate.mockImplementationOnce(async (_options) => + asyncStreamFrom([ + { + data: { + choices: [ + { + delta: { + toolCalls: [ + { + id: "call_1", + type: "function", + function: { + name: "get_weather", + arguments: '{"location":"NYC"}', }, - { - id: "call_2", - type: "function", - function: { - name: "get_weather", - arguments: '{"location":"LA"}', - }, + }, + { + id: "call_2", + type: "function", + function: { + name: "get_weather", + arguments: '{"location":"LA"}', }, - ], - }, - index: 0, + }, + ], }, - ], - }, - } + index: 0, + }, + ], + }, }, - } - return stream - }) + ]), + ) const metadata: ApiHandlerCreateMessageMetadata = { taskId: "test-task", diff --git a/src/api/providers/__tests__/vercel-ai-gateway.spec.ts b/src/api/providers/__tests__/vercel-ai-gateway.spec.ts index ad14486262..92cc785951 100644 --- a/src/api/providers/__tests__/vercel-ai-gateway.spec.ts +++ b/src/api/providers/__tests__/vercel-ai-gateway.spec.ts @@ -14,6 +14,7 @@ import OpenAI from "openai" import { VercelAiGatewayHandler } from "../vercel-ai-gateway" import { ApiHandlerOptions } from "../../../shared/api" +import { asyncStreamFrom, collectStream } from "../../../test-utils/stream" import { vercelAiGatewayDefaultModelId, VERCEL_AI_GATEWAY_DEFAULT_TEMPERATURE } from "@roo-code/types" // Mock dependencies @@ -180,9 +181,9 @@ describe("VercelAiGatewayHandler", () => { describe("createMessage", () => { beforeEach(() => { - mockCreate.mockImplementation(async () => ({ - [Symbol.asyncIterator]: async function* () { - yield { + mockCreate.mockImplementation(async () => + asyncStreamFrom([ + { choices: [ { delta: { content: "Test response" }, @@ -190,8 +191,8 @@ describe("VercelAiGatewayHandler", () => { }, ], usage: null, - } - yield { + }, + { choices: [ { delta: {}, @@ -208,9 +209,9 @@ describe("VercelAiGatewayHandler", () => { }, cost: 0.005, }, - } - }, - })) + }, + ]), + ) }) it("streams text content correctly", async () => { @@ -219,10 +220,7 @@ describe("VercelAiGatewayHandler", () => { const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello" }] const stream = handler.createMessage(systemPrompt, messages) - const chunks = [] - for await (const chunk of stream) { - chunks.push(chunk) - } + const chunks = await collectStream(stream) expect(chunks).toHaveLength(2) expect(chunks[0]).toEqual({ @@ -240,41 +238,33 @@ describe("VercelAiGatewayHandler", () => { }) it("throws the upstream reason when an in-stream error chunk is received", async () => { - mockCreate.mockImplementation(async () => ({ - [Symbol.asyncIterator]: async function* () { - yield { + mockCreate.mockImplementation(async () => + asyncStreamFrom([ + { error: { message: "Too many requests, please wait before trying again", code: 429, }, - } - }, - })) + }, + ]), + ) const handler = new VercelAiGatewayHandler(mockOptions) const stream = handler.createMessage("You are a helpful assistant.", [{ role: "user", content: "Hello" }]) await expect(async () => { - for await (const _chunk of stream) { - // drain - } + await collectStream(stream) }).rejects.toThrow("Too many requests, please wait before trying again") }) it("throws a default message when an in-stream error chunk has no message", async () => { - mockCreate.mockImplementation(async () => ({ - [Symbol.asyncIterator]: async function* () { - yield { error: {} } - }, - })) + mockCreate.mockImplementation(async () => asyncStreamFrom([{ error: {} }])) const handler = new VercelAiGatewayHandler(mockOptions) const stream = handler.createMessage("You are a helpful assistant.", [{ role: "user", content: "Hello" }]) await expect(async () => { - for await (const _chunk of stream) { - // drain - } + await collectStream(stream) }).rejects.toThrow("Vercel AI Gateway stream error") }) @@ -401,10 +391,7 @@ describe("VercelAiGatewayHandler", () => { const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello" }] const stream = handler.createMessage(systemPrompt, messages) - const chunks = [] - for await (const chunk of stream) { - chunks.push(chunk) - } + const chunks = await collectStream(stream) const usageChunk = chunks.find((chunk) => chunk.type === "usage") expect(usageChunk).toEqual({ @@ -436,18 +423,18 @@ describe("VercelAiGatewayHandler", () => { ] beforeEach(() => { - mockCreate.mockImplementation(async () => ({ - [Symbol.asyncIterator]: async function* () { - yield { + mockCreate.mockImplementation(async () => + asyncStreamFrom([ + { choices: [ { delta: {}, index: 0, }, ], - } - }, - })) + }, + ]), + ) }) it("should include tools when provided", async () => { @@ -525,9 +512,9 @@ describe("VercelAiGatewayHandler", () => { }) it("should yield tool_call_partial chunks when streaming tool calls", async () => { - mockCreate.mockImplementation(async () => ({ - [Symbol.asyncIterator]: async function* () { - yield { + mockCreate.mockImplementation(async () => + asyncStreamFrom([ + { choices: [ { delta: { @@ -545,8 +532,8 @@ describe("VercelAiGatewayHandler", () => { index: 0, }, ], - } - yield { + }, + { choices: [ { delta: { @@ -562,8 +549,8 @@ describe("VercelAiGatewayHandler", () => { index: 0, }, ], - } - yield { + }, + { choices: [ { delta: {}, @@ -574,9 +561,9 @@ describe("VercelAiGatewayHandler", () => { prompt_tokens: 10, completion_tokens: 5, }, - } - }, - })) + }, + ]), + ) const handler = new VercelAiGatewayHandler(mockOptions) @@ -585,10 +572,7 @@ describe("VercelAiGatewayHandler", () => { tools: testTools, }) - const chunks = [] - for await (const chunk of stream) { - chunks.push(chunk) - } + const chunks = await collectStream(stream) const toolCallChunks = chunks.filter((chunk) => chunk.type === "tool_call_partial") expect(toolCallChunks).toHaveLength(2)