Files
codetyper.cli/test/agent-stream.test.ts
Carlos Gutierrez 187cc68304 Improve agent autonomy and diff view readability
Agent behavior improvements:
  - Add project context detection (tsconfig.json, pom.xml, etc.)
  - Enforce validation after changes (tsc --noEmit, mvn compile, etc.)
  - Run tests automatically - never ask "do you want me to run tests"
  - Complete full loop: create → type-check → test → confirm
  - Add command detection for direct execution (run tree, run ls)

  Diff view improvements:
  - Use darker backgrounds for added/removed lines
  - Add diffLineBgAdded, diffLineBgRemoved, diffLineText theme colors
  - Improve text visibility with white text on dark backgrounds
  - Update both React/Ink and SolidJS diff components

  Streaming fixes:
  - Fix tool call argument accumulation using OpenAI index field
  - Fix streaming content display after tool calls
  - Add consecutive error tracking to prevent token waste

  Other changes:
  - ESC to abort operations, Ctrl+C to exit
  - Fix model selection when provider changes in cascade mode
  - Add debug logging for troubleshooting
  - Move tests to root tests/ folder
  - Fix banner test GRADIENT_COLORS reference
2026-01-29 07:33:30 -05:00

204 lines
5.5 KiB
TypeScript

/**
* Unit tests for Streaming Agent
*/
import { describe, it, expect } from "bun:test";
import {
createInitialStreamingState,
createStreamAccumulator,
} from "@/types/streaming";
import type {
StreamingState,
StreamAccumulator,
PartialToolCall,
} from "@/types/streaming";
describe("Streaming Agent Types", () => {
describe("createInitialStreamingState", () => {
it("should create state with idle status", () => {
const state = createInitialStreamingState();
expect(state.status).toBe("idle");
expect(state.content).toBe("");
expect(state.pendingToolCalls).toHaveLength(0);
expect(state.completedToolCalls).toHaveLength(0);
expect(state.error).toBeNull();
expect(state.modelSwitched).toBeNull();
});
});
describe("createStreamAccumulator", () => {
it("should create empty accumulator", () => {
const accumulator = createStreamAccumulator();
expect(accumulator.content).toBe("");
expect(accumulator.toolCalls.size).toBe(0);
expect(accumulator.modelSwitch).toBeNull();
});
it("should accumulate content", () => {
const accumulator = createStreamAccumulator();
accumulator.content += "Hello ";
accumulator.content += "World";
expect(accumulator.content).toBe("Hello World");
});
it("should store partial tool calls", () => {
const accumulator = createStreamAccumulator();
const partial: PartialToolCall = {
index: 0,
id: "call_123",
name: "read",
argumentsBuffer: '{"path": "/test',
isComplete: false,
};
accumulator.toolCalls.set(0, partial);
expect(accumulator.toolCalls.size).toBe(1);
expect(accumulator.toolCalls.get(0)?.name).toBe("read");
});
it("should accumulate tool call arguments", () => {
const accumulator = createStreamAccumulator();
const partial: PartialToolCall = {
index: 0,
id: "call_123",
name: "read",
argumentsBuffer: "",
isComplete: false,
};
accumulator.toolCalls.set(0, partial);
// Simulate streaming arguments
partial.argumentsBuffer += '{"path": ';
partial.argumentsBuffer += '"/test.ts"}';
expect(partial.argumentsBuffer).toBe('{"path": "/test.ts"}');
// Verify JSON is valid
const parsed = JSON.parse(partial.argumentsBuffer);
expect(parsed.path).toBe("/test.ts");
});
});
describe("StreamingState transitions", () => {
it("should represent idle to streaming transition", () => {
const state: StreamingState = {
...createInitialStreamingState(),
status: "streaming",
content: "Processing your request",
};
expect(state.status).toBe("streaming");
expect(state.content).toBe("Processing your request");
});
it("should represent tool call accumulation", () => {
const partial: PartialToolCall = {
index: 0,
id: "call_456",
name: "bash",
argumentsBuffer: '{"command": "ls -la"}',
isComplete: false,
};
const state: StreamingState = {
...createInitialStreamingState(),
status: "accumulating_tool",
pendingToolCalls: [partial],
};
expect(state.status).toBe("accumulating_tool");
expect(state.pendingToolCalls).toHaveLength(1);
expect(state.pendingToolCalls[0].name).toBe("bash");
});
it("should represent completion state", () => {
const state: StreamingState = {
...createInitialStreamingState(),
status: "complete",
content: "Task completed successfully.",
completedToolCalls: [
{ id: "call_789", name: "write", arguments: { path: "/out.txt" } },
],
};
expect(state.status).toBe("complete");
expect(state.completedToolCalls).toHaveLength(1);
});
it("should represent error state", () => {
const state: StreamingState = {
...createInitialStreamingState(),
status: "error",
error: "Connection timeout",
};
expect(state.status).toBe("error");
expect(state.error).toBe("Connection timeout");
});
it("should represent model switch", () => {
const state: StreamingState = {
...createInitialStreamingState(),
status: "streaming",
modelSwitched: {
from: "gpt-4",
to: "gpt-4-unlimited",
reason: "Quota exceeded",
},
};
expect(state.modelSwitched).not.toBeNull();
expect(state.modelSwitched?.from).toBe("gpt-4");
expect(state.modelSwitched?.to).toBe("gpt-4-unlimited");
});
});
describe("Tool call finalization", () => {
it("should convert partial to complete tool call", () => {
const partial: PartialToolCall = {
index: 0,
id: "call_abc",
name: "edit",
argumentsBuffer:
'{"file_path": "/src/app.ts", "old_string": "foo", "new_string": "bar"}',
isComplete: true,
};
const args = JSON.parse(partial.argumentsBuffer);
expect(args.file_path).toBe("/src/app.ts");
expect(args.old_string).toBe("foo");
expect(args.new_string).toBe("bar");
});
it("should handle malformed JSON gracefully", () => {
const partial: PartialToolCall = {
index: 0,
id: "call_def",
name: "read",
argumentsBuffer: '{"path": "/incomplete',
isComplete: true,
};
let args: Record<string, unknown> = {};
try {
args = JSON.parse(partial.argumentsBuffer);
} catch {
args = {};
}
expect(args).toEqual({});
});
});
});