Files
kandev/apps/web/components/task/chat/message-history.test.ts
T
José Almeida 7de788ceed feat: add cross-integration entity references (#1862)
* feat: add cross-integration entity references

* fix(web): anchor entity reference menu to composer

* fix: keep Kandev tasks under @ mentions

* fix: address entity reference review findings

* test(e2e): use option role for prompt menu

* test(e2e): update passthrough mention roles

* fix: preserve queued references and paginate Azure PRs
2026-07-23 09:03:56 +01:00

290 lines
9.2 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
createMessageHistorySelector,
extractUserHistory,
navigateHistory,
fuzzyScore,
searchHistory,
type HistoryState,
} from "./message-history";
import * as messageHistory from "./message-history";
import type { Message } from "@/lib/types/http";
function msg(partial: Partial<Message>): Message {
return {
id: partial.id ?? "m",
session_id: "s1" as Message["session_id"],
task_id: "t1" as Message["task_id"],
author_type: partial.author_type ?? "user",
type: partial.type ?? "message",
content: partial.content ?? "",
created_at: partial.created_at ?? "2026-01-01T00:00:00Z",
...partial,
};
}
describe("createMessageHistorySelector", () => {
it("keeps the selector snapshot stable when only agent output changes", () => {
const selector = createMessageHistorySelector("s1");
const first = selector({
messages: {
bySession: { s1: [msg({ id: "user-1", content: "keep me" })] },
metaBySession: {},
},
});
const afterAgentStream = selector({
messages: {
bySession: {
s1: [
msg({ id: "user-1", content: "keep me" }),
msg({ id: "agent-1", author_type: "agent", content: "streaming" }),
],
},
metaBySession: {},
},
});
expect(afterAgentStream).toBe(first);
});
it("returns a new selector snapshot when reference metadata changes", () => {
const selector = createMessageHistorySelector("s1");
const reference = {
version: 1,
ref: "mention:v1:sentry:issue:sentry.example.test%2Forg-1:issue-1",
provider: "sentry",
kind: "issue",
id: "issue-1",
key: "WEB-1",
title: "Login crash",
url: "https://sentry.example.test/issues/1",
scope: "sentry.example.test/org-1",
};
const first = selector({
messages: {
bySession: {
s1: [msg({ id: "user-1", content: "See #WEB-1", metadata: { entity_references: [] } })],
},
metaBySession: {},
},
});
const withReference = selector({
messages: {
bySession: {
s1: [
msg({
id: "user-1",
content: "See #WEB-1",
metadata: { entity_references: [reference] },
}),
],
},
metaBySession: {},
},
});
expect(withReference).not.toBe(first);
expect(withReference[0]?.entityReferences).toEqual([reference]);
});
});
describe("extractUserHistoryEntries", () => {
it("provides structured history entries for rich-editor recall", () => {
expect(typeof (messageHistory as Record<string, unknown>).extractUserHistoryEntries).toBe(
"function",
);
});
it("keeps entity reference metadata with recalled message content", () => {
const reference = {
version: 1,
ref: "mention:v1:sentry:issue:sentry.example.test%2Forg-1:issue-1",
provider: "sentry",
kind: "issue",
id: "issue-1",
key: "WEB-1",
title: "Login crash",
url: "https://sentry.example.test/issues/1",
scope: "sentry.example.test/org-1",
};
const entries = messageHistory.extractUserHistoryEntries([
msg({
content: "See [#WEB-1](https://sentry.example.test/issues/1)",
metadata: { entity_references: [reference, reference] },
}),
]);
expect(entries).toEqual([
{
content: "See [#WEB-1](https://sentry.example.test/issues/1)",
entityReferences: [reference],
},
]);
});
});
describe("extractUserHistory", () => {
it("returns user message contents newest-first", () => {
const messages: Message[] = [
msg({ id: "1", content: "first" }),
msg({ id: "2", content: "second" }),
msg({ id: "3", content: "third" }),
];
expect(extractUserHistory(messages)).toEqual(["third", "second", "first"]);
});
it("skips agent messages", () => {
const messages: Message[] = [
msg({ id: "1", content: "user-a" }),
msg({ id: "2", content: "agent-reply", author_type: "agent" }),
msg({ id: "3", content: "user-b" }),
];
expect(extractUserHistory(messages)).toEqual(["user-b", "user-a"]);
});
it("skips non-message types (tool_call, thinking, etc.)", () => {
const messages: Message[] = [
msg({ id: "1", content: "real prompt" }),
msg({ id: "2", content: "thought", type: "thinking" }),
msg({ id: "3", content: "ran cmd", type: "tool_execute" }),
];
expect(extractUserHistory(messages)).toEqual(["real prompt"]);
});
it("skips empty/whitespace content", () => {
const messages: Message[] = [
msg({ id: "1", content: "real" }),
msg({ id: "2", content: "" }),
msg({ id: "3", content: " " }),
];
expect(extractUserHistory(messages)).toEqual(["real"]);
});
it("collapses consecutive duplicates", () => {
const messages: Message[] = [
msg({ id: "1", content: "fix bug" }),
msg({ id: "2", content: "fix bug" }),
msg({ id: "3", content: "fix bug" }),
msg({ id: "4", content: "ship it" }),
];
expect(extractUserHistory(messages)).toEqual(["ship it", "fix bug"]);
});
it("keeps non-consecutive duplicates", () => {
const messages: Message[] = [
msg({ id: "1", content: "a" }),
msg({ id: "2", content: "b" }),
msg({ id: "3", content: "a" }),
];
expect(extractUserHistory(messages)).toEqual(["a", "b", "a"]);
});
it("keeps duplicates separated by agent/tool turns (adjacency is in original stream)", () => {
const messages: Message[] = [
msg({ id: "1", content: "fix bug" }),
msg({ id: "2", content: "working on it", author_type: "agent" }),
msg({ id: "3", content: "ran cmd", type: "tool_execute" }),
msg({ id: "4", content: "fix bug" }),
];
expect(extractUserHistory(messages)).toEqual(["fix bug", "fix bug"]);
});
it("returns empty for empty input", () => {
expect(extractUserHistory([])).toEqual([]);
});
});
describe("navigateHistory", () => {
const empty: HistoryState = { index: null };
it("returns null when there is no history", () => {
expect(navigateHistory(empty, "up", 0)).toBeNull();
expect(navigateHistory(empty, "down", 0)).toBeNull();
});
it("first ArrowUp enters history at index 0", () => {
expect(navigateHistory(empty, "up", 3)).toEqual({ index: 0 });
});
it("subsequent ArrowUp moves further back", () => {
expect(navigateHistory({ index: 0 }, "up", 3)).toEqual({ index: 1 });
expect(navigateHistory({ index: 1 }, "up", 3)).toEqual({ index: 2 });
});
it("ArrowUp at the oldest entry returns null", () => {
expect(navigateHistory({ index: 2 }, "up", 3)).toBeNull();
});
it("ArrowDown from null defers (returns null)", () => {
expect(navigateHistory(empty, "down", 3)).toBeNull();
});
it("ArrowDown moves toward newer entries", () => {
expect(navigateHistory({ index: 2 }, "down", 3)).toEqual({ index: 1 });
expect(navigateHistory({ index: 1 }, "down", 3)).toEqual({ index: 0 });
});
it("ArrowDown from index 0 exits history (restores draft)", () => {
expect(navigateHistory({ index: 0 }, "down", 3)).toEqual({ index: null });
});
});
describe("fuzzyScore", () => {
it("returns a positive score for an empty needle", () => {
expect(fuzzyScore("", "anything")).toBeGreaterThan(0);
});
it("matches subsequences in order, case-insensitive", () => {
expect(fuzzyScore("fb", "fix bug")).not.toBeNull();
expect(fuzzyScore("FB", "Fix Bug")).not.toBeNull();
});
it("returns null when characters cannot be matched in order", () => {
expect(fuzzyScore("xyz", "fix bug")).toBeNull();
expect(fuzzyScore("gf", "fix bug")).toBeNull(); // g comes after f in haystack? no - 'g' is in 'bug', 'f' is before
});
it("ranks word-boundary matches above mid-word ones", () => {
const boundary = fuzzyScore("fb", "fix bug")!;
const midword = fuzzyScore("fb", "affable")!;
expect(boundary).not.toBeNull();
expect(midword).not.toBeNull();
expect(boundary).toBeGreaterThan(midword);
});
it("ranks consecutive matches above scattered ones", () => {
const consecutive = fuzzyScore("fix", "fix bug")!;
const scattered = fuzzyScore("fix", "f i x in mix")!;
expect(consecutive).toBeGreaterThan(scattered);
});
});
describe("searchHistory", () => {
it("returns every entry unchanged for an empty query", () => {
const history = ["c", "b", "a"];
expect(searchHistory(history, "")).toEqual([
{ index: 0, content: "c", score: 0 },
{ index: 1, content: "b", score: 0 },
{ index: 2, content: "a", score: 0 },
]);
});
it("filters out non-matching entries", () => {
const history = ["fix bug", "ship it", "rename foo"];
const hits = searchHistory(history, "fb");
expect(hits.map((h) => h.content)).toEqual(["fix bug"]);
});
it("orders matches best-first (consecutive chars beat scattered)", () => {
const history = ["find the bug later", "fb shortcut win"];
const hits = searchHistory(history, "fb");
expect(hits[0].content).toBe("fb shortcut win");
});
it("preserves the original index so callers can jump history position", () => {
const history = ["a", "fb match", "c"];
const [hit] = searchHistory(history, "fb");
expect(hit.index).toBe(1);
});
});