diff --git a/src/client/src/chatGroups.test.ts b/src/client/src/chatGroups.test.ts new file mode 100644 index 0000000..bf43384 --- /dev/null +++ b/src/client/src/chatGroups.test.ts @@ -0,0 +1,58 @@ +import { describe, expect, it } from "vitest"; +import { groupChatMessages, summarizeChatGroup } from "./chatGroups"; +import type { ChatLine } from "./components/shared"; + +const text = (role: ChatLine["role"], value: string): ChatLine => ({ role, parts: [{ type: "text", text: value }] }); + +describe("groupChatMessages", () => { + it("groups technical parts until a readable message is encountered", () => { + const messages: ChatLine[] = [ + { role: "assistant", parts: [{ type: "thinking", text: "plan" }, { type: "toolCall", toolName: "read", summary: "file" }] }, + text("assistant", "visible answer"), + { role: "tool", parts: [{ type: "toolResult", toolName: "read", text: "ok", isError: false }] }, + ]; + + expect(groupChatMessages(messages, 10)).toEqual([ + { kind: "group", startIndex: 10, messages: [messages[0]] }, + { kind: "message", index: 11, message: text("assistant", "visible answer") }, + { kind: "group", startIndex: 12, messages: [messages[2]] }, + ]); + }); + + it("splits mixed readable and technical parts from a single message", () => { + const messages: ChatLine[] = [ + { role: "assistant", parts: [{ type: "thinking", text: "hidden" }, { type: "text", text: "shown" }] }, + ]; + + expect(groupChatMessages(messages)).toEqual([ + { kind: "group", startIndex: 0, messages: [{ role: "assistant", parts: [{ type: "thinking", text: "hidden" }] }] }, + { kind: "message", index: 0, message: { role: "assistant", parts: [{ type: "text", text: "shown" }] } }, + ]); + }); + + it("treats compaction and branch summaries as grouped events", () => { + const messages: ChatLine[] = [ + { ...text("assistant", "summary"), source: "compaction" }, + { ...text("assistant", "branch"), source: "branch_summary" }, + ]; + + const groups = groupChatMessages(messages); + + expect(groups).toHaveLength(1); + expect(groups[0]).toMatchObject({ kind: "group", startIndex: 0 }); + }); +}); + +describe("summarizeChatGroup", () => { + it("summarizes special event groups", () => { + expect(summarizeChatGroup([{ ...text("assistant", "a"), source: "compaction" }])).toBe("1 history compaction summary"); + expect(summarizeChatGroup([ + { ...text("assistant", "a"), source: "branch_summary" }, + { ...text("assistant", "b"), source: "branch_summary" }, + ])).toBe("2 branch summaries"); + }); + + it("summarizes mixed groups by role counts", () => { + expect(summarizeChatGroup([text("tool", "a"), text("system", "b"), text("tool", "c")])).toBe("3 events · 2 tool · 1 system"); + }); +}); diff --git a/src/client/src/inputModes.test.ts b/src/client/src/inputModes.test.ts new file mode 100644 index 0000000..717da29 --- /dev/null +++ b/src/client/src/inputModes.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from "vitest"; +import { inputModeForDraft, isShellInput } from "./inputModes"; + +describe("inputModeForDraft", () => { + it("detects shell input and context-excluded shell input after leading whitespace", () => { + expect(inputModeForDraft(" ! npm test")).toEqual({ kind: "shell", excludeFromContext: false }); + expect(inputModeForDraft("\n!! secret command")).toEqual({ kind: "shell", excludeFromContext: true }); + expect(isShellInput(" ! pwd")).toBe(true); + }); + + it("detects slash commands only for the current token", () => { + expect(inputModeForDraft("/compact")).toEqual({ kind: "command" }); + expect(inputModeForDraft("please /compact")).toEqual({ kind: "command" }); + expect(inputModeForDraft("please mention/path")).toEqual({ kind: "normal" }); + }); + + it("detects file completion contexts", () => { + expect(inputModeForDraft("open @src/main.ts")).toEqual({ kind: "file" }); + expect(inputModeForDraft("open @ ")).toEqual({ kind: "file" }); + expect(inputModeForDraft("open @ \"src/main.ts")).toEqual({ kind: "file" }); + expect(inputModeForDraft("open \"src/main.ts")).toEqual({ kind: "normal" }); + }); +}); diff --git a/src/client/src/route.test.ts b/src/client/src/route.test.ts new file mode 100644 index 0000000..cb0eb1f --- /dev/null +++ b/src/client/src/route.test.ts @@ -0,0 +1,76 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { readRoute, writeRoute, type AppRoute } from "./route"; + +const originalWindow = globalThis.window; + +afterEach(() => { + vi.restoreAllMocks(); + Object.defineProperty(globalThis, "window", { value: originalWindow, configurable: true }); +}); + +function installWindow(href: string): { pushed: string[] } { + const url = new URL(href); + const pushed: string[] = []; + const fakeWindow = { + location: { + href: url.href, + pathname: url.pathname, + search: url.search, + hash: url.hash, + }, + history: { + pushState: vi.fn((_state: object, _title: string, next: URL | string) => { + pushed.push(String(next)); + }), + }, + }; + Object.defineProperty(globalThis, "window", { value: fakeWindow, configurable: true }); + return { pushed }; +} + +describe("route helpers", () => { + it("reads only supported route fields from the current URL", () => { + installWindow("http://localhost/app?project=p1&workspace=w1&session=s1&tool=git&view=files&file=src%2Fmain.ts&diff=README.md"); + + expect(readRoute()).toEqual({ + projectId: "p1", + workspaceId: "w1", + sessionId: "s1", + tool: "git", + view: "files", + file: "src/main.ts", + diff: "README.md", + }); + }); + + it("ignores unsupported tool and view values", () => { + installWindow("http://localhost/app?tool=terminal&view=settings"); + + expect(readRoute()).toMatchObject({ tool: undefined, view: undefined }); + }); + + it("writes compact URLs and preserves path/hash", () => { + const { pushed } = installWindow("http://localhost/app?old=1#section"); + const route: AppRoute = { + projectId: "project/id", + workspaceId: "workspace id", + sessionId: "", + tool: "files", + view: "chat", + file: "src/main.ts", + diff: undefined, + }; + + writeRoute(route); + + expect(pushed).toEqual(["http://localhost/app?old=1&project=project%2Fid&workspace=workspace+id&tool=files&view=chat&file=src%2Fmain.ts#section"]); + }); + + it("does not push history when the route is unchanged", () => { + const { pushed } = installWindow("http://localhost/app?project=p1&tool=git"); + + writeRoute({ projectId: "p1", workspaceId: undefined, sessionId: undefined, tool: "git", view: undefined, file: undefined, diff: undefined }); + + expect(pushed).toEqual([]); + }); +});