From bb52a46fa62f03bd5ab0b04e094967e1a08b4679 Mon Sep 17 00:00:00 2001 From: Federico Jaramillo Martinez Date: Fri, 8 May 2026 08:46:13 +0200 Subject: [PATCH] test: cover client API and message parsing --- src/client/src/api/parsers.test.ts | 63 +++++++++++++++++++++++++++++ src/client/src/chatMessages.test.ts | 60 +++++++++++++++++++++++++++ 2 files changed, 123 insertions(+) create mode 100644 src/client/src/api/parsers.test.ts create mode 100644 src/client/src/chatMessages.test.ts diff --git a/src/client/src/api/parsers.test.ts b/src/client/src/api/parsers.test.ts new file mode 100644 index 0000000..7fd4588 --- /dev/null +++ b/src/client/src/api/parsers.test.ts @@ -0,0 +1,63 @@ +import { describe, expect, it } from "vitest"; +import { parseCommandResult, parseFileContentResponse, parseFileSuggestion, parseGitStatusResponse, parseMessagePage, parseSessionStatus, parseSlashCommand } from "./parsers"; + +describe("API parsers", () => { + it("accepts legacy array message pages and paged message responses", () => { + expect(parseMessagePage(["a", "b"])).toEqual({ messages: ["a", "b"], start: 0, total: 2 }); + expect(parseMessagePage({ messages: ["c"], start: 3, total: 9 })).toEqual({ messages: ["c"], start: 3, total: 9 }); + }); + + it("validates session status including optional model and nullable context usage", () => { + expect(parseSessionStatus({ + sessionId: "s1", + isStreaming: false, + isCompacting: true, + isBashRunning: false, + pendingMessageCount: 2, + tokens: { input: 1, output: 2, cacheRead: 3, cacheWrite: 4, total: 10 }, + cost: 0.12, + model: { provider: "p", id: "m", contextWindow: 100, reasoning: { effort: "low" } }, + contextUsage: { tokens: null, contextWindow: 100, percent: 0.5 }, + thinkingLevel: "medium", + })).toEqual({ + sessionId: "s1", + isStreaming: false, + isCompacting: true, + isBashRunning: false, + pendingMessageCount: 2, + tokens: { input: 1, output: 2, cacheRead: 3, cacheWrite: 4, total: 10 }, + cost: 0.12, + model: { provider: "p", id: "m", contextWindow: 100, reasoning: { effort: "low" } }, + contextUsage: { tokens: null, contextWindow: 100, percent: 0.5 }, + thinkingLevel: "medium", + }); + }); + + it("rejects invalid enum-like fields", () => { + expect(() => parseSlashCommand({ name: "bad", source: "remote" })).toThrow("Invalid command source"); + expect(() => parseFileSuggestion({ path: "a", kind: "deleted" })).toThrow("Invalid file kind"); + expect(() => parseGitStatusResponse({ isGitRepo: true, hash: "h", files: [{ path: "a", index: "weird", workingTree: "modified" }] })).toThrow("Invalid git file state"); + }); + + it("validates file content responses", () => { + expect(parseFileContentResponse({ + path: "README.md", + language: "markdown", + encoding: "utf8", + size: 4, + modifiedAt: "now", + content: "text", + truncated: false, + binary: false, + })).toMatchObject({ path: "README.md", language: "markdown", content: "text" }); + + expect(() => parseFileContentResponse({ encoding: "base64" })).toThrow("Invalid file encoding"); + }); + + it("parses command result variants", () => { + expect(parseCommandResult({ type: "unsupported", message: "nope" })).toEqual({ type: "unsupported", message: "nope" }); + expect(parseCommandResult({ type: "select", requestId: "r1", title: "Pick", options: [{ value: "v", label: "Label", description: "desc" }] })).toEqual({ type: "select", requestId: "r1", title: "Pick", options: [{ value: "v", label: "Label", description: "desc" }] }); + expect(parseCommandResult({ type: "done", message: "ok" })).toEqual({ type: "done", message: "ok" }); + expect(() => parseCommandResult({ type: "later" })).toThrow("Invalid command result type"); + }); +}); diff --git a/src/client/src/chatMessages.test.ts b/src/client/src/chatMessages.test.ts new file mode 100644 index 0000000..1850992 --- /dev/null +++ b/src/client/src/chatMessages.test.ts @@ -0,0 +1,60 @@ +import { describe, expect, it } from "vitest"; +import { appendText, normalizeMessage, normalizeMessages, textMessage } from "./chatMessages"; + +describe("chat message normalization", () => { + it("normalizes simple text messages and drops empty content", () => { + expect(normalizeMessages([ + { role: "user", content: "hello" }, + { role: "assistant", content: "" }, + { role: "unknown", content: "system text" }, + ])).toEqual([ + textMessage("user", "hello"), + textMessage("system", "system text"), + ]); + }); + + it("normalizes tool calls and tool results", () => { + expect(normalizeMessage({ role: "assistant", content: [{ type: "toolCall", name: "bash", arguments: { command: "npm test" } }] })).toEqual([ + { role: "assistant", parts: [{ type: "toolCall", toolName: "bash", summary: "npm test" }] }, + ]); + expect(normalizeMessage({ role: "toolResult", toolName: "bash", isError: true, content: [{ type: "text", text: "failed" }] })).toEqual([ + { role: "tool", parts: [{ type: "toolResult", toolName: "bash", text: "failed", isError: true }] }, + ]); + }); + + it("extracts skill invocation blocks into dedicated skill and user messages", () => { + expect(normalizeMessage({ role: "user", content: "\nUse browser\n\n\nNow test the UI" })).toEqual([ + { role: "user", parts: [{ type: "skillInvocation", name: "playwright", location: "/skills/playwright", content: "Use browser" }] }, + textMessage("user", "Now test the UI"), + ]); + }); + + it("formats bash execution records as bash chat lines", () => { + expect(normalizeMessage({ + role: "bashExecution", + command: "npm test", + excludeFromContext: true, + output: "ok", + exitCode: 0, + truncated: true, + fullOutputPath: "/tmp/out.log", + })).toEqual([ + textMessage("bash", "excluded from context\n\n$ npm test\n\nok\n\nexit 0\n\noutput truncated\n\nfull output: /tmp/out.log"), + ]); + }); +}); + +describe("appendText", () => { + it("appends to the previous same-role text message", () => { + expect(appendText([textMessage("assistant", "hello")], "assistant", " world")).toEqual([ + textMessage("assistant", "hello world"), + ]); + }); + + it("starts a new message when role or last part does not match", () => { + expect(appendText([textMessage("user", "hello")], "assistant", "hi")).toEqual([ + textMessage("user", "hello"), + textMessage("assistant", "hi"), + ]); + }); +});