Files
PaperClipAI/ui/src/components/task-chat/task-chat-adapter.test.ts
T
DottaandPaperclip 26d71d8def Fix human author identity and viewer-relative alignment in task chat (#15494)
Preserve human author identity in task chat and align only the current viewer on the right. Show other humans and agents on the left with their own identity. Include mixed-author Storybook journeys and regression coverage.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-09 17:04:20 -05:00

337 lines
12 KiB
TypeScript

import { describe, expect, it } from "vitest";
import type { IssueChatComment } from "@/lib/issue-chat-messages";
import {
commentsToTaskChatItems,
formatTaskChatTimestamp,
} from "./task-chat-adapter";
describe("commentsToTaskChatItems", () => {
it("preserves distinct human IDs, photos and viewer ownership, including after a profile refresh", () => {
const comments = ["alex", "sam"].map(authorUserId => ({
id: `comment-${authorUserId}`, authorType: "user", authorUserId,
authorAgentId: null, body: "A human message", createdAt: "2026-10-09T20:42:00Z",
} as unknown as IssueChatComment));
const userProfileMap = new Map([
["alex", { label: "Alex Morgan", image: "/alex.png" }],
["sam", { label: "Sam Rivera", image: "/sam.png" }],
]);
expect(commentsToTaskChatItems(comments, { currentUserId: "alex", userProfileMap })).toMatchObject([
{ authorUserId: "alex", authorName: "Alex Morgan", authorAvatarUrl: "/alex.png", isCurrentUser: true },
{ authorUserId: "sam", authorName: "Sam Rivera", authorAvatarUrl: "/sam.png", isCurrentUser: false },
]);
expect(commentsToTaskChatItems(comments, { currentUserId: "sam", userProfileMap })).toMatchObject([
{ authorUserId: "alex", isCurrentUser: false },
{ authorUserId: "sam", isCurrentUser: true },
]);
expect(commentsToTaskChatItems(comments, { currentUserId: "alex" })).toMatchObject([
{ authorName: "You", isCurrentUser: true, authorAvatarUrl: null },
{ authorName: "User", isCurrentUser: false, authorAvatarUrl: null },
]);
// Directory hydration changes presentation without changing canonical message IDs.
expect(commentsToTaskChatItems(comments, { currentUserId: "alex", userProfileMap })[1]).toMatchObject({
id: "comment-sam", authorName: "Sam Rivera", authorAvatarUrl: "/sam.png",
});
});
it("does not infer ownership from matching names or an unresolved viewer", () => {
const comment = { id: "other", authorType: "user", authorUserId: "other", body: "Hello" } as IssueChatComment;
expect(commentsToTaskChatItems([comment], {
currentUserId: "me", userLabelMap: new Map([["me", "Alex"], ["other", "Alex"]]),
})[0]).toMatchObject({ authorName: "Alex", isCurrentUser: false });
expect(commentsToTaskChatItems([comment])[0]).toMatchObject({ authorName: "User", isCurrentUser: false });
});
it("carries inbound channel attribution into the human bubble", () => {
expect(commentsToTaskChatItems([{
id: "photon-comment",
body: "From my phone",
authorType: "user",
authorUserId: "local-board",
metadata: { version: 1, sourceChannel: "imessage-photon", sections: [] },
createdAt: "2026-09-12T12:00:00Z",
} as unknown as IssueChatComment])).toMatchObject([{
author: "human", sourceChannel: "imessage-photon", text: "From my phone",
}]);
});
it("classifies a recovered local-board comment as an agent bubble", () => {
const items = commentsToTaskChatItems([{
id: "c-recovered",
body: "Recovered agent reply.",
authorType: "user",
authorUserId: "local-board",
authorAgentId: null,
derivedAuthorAgentId: "agent-1",
createdAt: "2026-08-07T09:00:00.000Z",
} as unknown as IssueChatComment]);
expect(items).toHaveLength(1);
expect(items[0]).toMatchObject({ kind: "message", author: "agent" });
});
it("never tags posted comments interstitial — the run's final reply keeps its bubble", () => {
const comments = [
{
id: "c1",
body: "Here is the finished work.",
authorType: "agent",
authorAgentId: "agent-1",
runId: "run-1",
createdAt: "2026-07-31T12:00:10.000Z",
} as unknown as IssueChatComment,
];
const items = commentsToTaskChatItems(comments);
expect(items).toHaveLength(1);
const item = items[0];
expect(item.kind).toBe("message");
if (item.kind !== "message") return;
expect(item.author).toBe("agent");
expect(item.interstitial).toBeUndefined();
});
it("classifies system comments as system even with a derivable run→agent linkage (PAP-443)", () => {
const comments = [
{
id: "c-sys",
body: "Paperclip automatically retried dispatch, but it still has no live execution path.",
authorType: "system",
authorAgentId: null,
derivedAuthorAgentId: "agent-1",
createdAt: "2026-08-07T09:00:00.000Z",
} as unknown as IssueChatComment,
];
const items = commentsToTaskChatItems(comments);
expect(items).toHaveLength(1);
const item = items[0];
if (item.kind !== "message") throw new Error("expected message item");
expect(item.author).toBe("system");
});
it("attaches verification caveats using durable comment run provenance", () => {
const verificationCaveats = [{
commandOrCheck: "external-validator",
reasonCode: "tool_unavailable",
detail: "The optional validator is unavailable.",
}];
const comments = [{
id: "c-final",
body: "Implemented and locally verified.",
authorType: "agent",
authorAgentId: "agent-1",
createdByRunId: "run-1",
createdAt: "2026-08-22T09:00:00.000Z",
} as unknown as IssueChatComment];
const items = commentsToTaskChatItems(comments, {
verificationCaveatsByRunId: new Map([["run-1", verificationCaveats]]),
});
expect(items).toEqual([expect.objectContaining({
kind: "message",
verificationCaveats,
})]);
});
it("carries presentation, metadata, and the raw timestamp for system comments", () => {
const presentation = {
kind: "system_notice",
tone: "warning",
title: "Run recovery",
detailsDefaultOpen: true,
};
const metadata = {
version: 1,
sections: [{ rows: [{ type: "text", label: "Reason", text: "quota" }] }],
};
const comments = [
{
id: "c-sys",
body: "Recovery notice.",
authorType: "system",
presentation,
metadata,
runAgentId: "agent-1",
createdAt: new Date("2026-08-07T09:00:00.000Z"),
} as unknown as IssueChatComment,
{
id: "c-agent",
body: "Agent reply.",
authorType: "agent",
authorAgentId: "agent-1",
presentation: null,
metadata,
createdAt: "2026-08-07T09:01:00.000Z",
} as unknown as IssueChatComment,
];
const items = commentsToTaskChatItems(comments);
const [sys, agent] = items;
if (sys.kind !== "message" || agent.kind !== "message") throw new Error("expected messages");
expect(sys.presentation).toEqual(presentation);
expect(sys.metadata).toEqual(metadata);
expect(sys.runAgentId).toBe("agent-1");
expect(sys.createdAtIso).toBe("2026-08-07T09:00:00.000Z");
// Non-system authors keep the item lean — no structured notice fields.
expect(agent.presentation).toBeUndefined();
expect(agent.metadata).toBeUndefined();
expect(agent.runAgentId).toBeUndefined();
expect(agent.createdAtIso).toBeUndefined();
});
it("keeps the regular comment time for a causally repositioned steered follow-up", () => {
const createdAt = "2026-09-04T14:09:33.000Z";
const conversationAnchorAt = "2026-09-04T14:10:14.000Z";
const [item] = commentsToTaskChatItems([
{
id: "c-steered",
body: "Use three seconds instead.",
authorType: "user",
authorUserId: "user-1",
authorAgentId: null,
followUpRequested: true,
consumedByRunId: "run-1",
steeredIntoRunId: "run-1",
conversationAnchorAt,
createdAt,
} as unknown as IssueChatComment,
]);
expect(item).toMatchObject({
kind: "message",
timestamp: formatTaskChatTimestamp(createdAt),
});
});
it("preserves human identity for steered and ordinary messages", () => {
const comment = {
id: "steered-by-colleague", body: "Check the latest result.",
authorType: "user", authorUserId: "colleague", steeredIntoRunId: "run-1",
createdAt: "2026-09-04T14:09:33.000Z",
} as unknown as IssueChatComment;
const ctx = {
currentUserId: "viewer",
userProfileMap: new Map([["colleague", { label: "Sam Rivera", image: "/api/assets/sam/content" }]]),
};
for (const steeredIntoRunId of ["run-1", null]) {
expect(commentsToTaskChatItems([{ ...comment, steeredIntoRunId }], ctx)[0]).toMatchObject({
authorName: "Sam Rivera", authorAvatarUrl: "/api/assets/sam/content", isCurrentUser: false,
});
}
expect(commentsToTaskChatItems([{ ...comment, authorUserId: "viewer" }], ctx)[0]).toMatchObject({
isCurrentUser: true,
});
});
it("keeps the regular comment time for a successor-run follow-up", () => {
const createdAt = "2026-09-04T14:09:33.000Z";
const conversationAnchorAt = "2026-09-04T14:10:35.000Z";
const [item] = commentsToTaskChatItems([
{
id: "c-delivered",
body: "Use three seconds instead.",
authorType: "user",
authorUserId: "user-1",
authorAgentId: null,
followUpRequested: true,
consumedByRunId: "run-2",
conversationAnchorAt,
createdAt,
} as unknown as IssueChatComment,
]);
expect(item).toMatchObject({
kind: "message",
timestamp: formatTaskChatTimestamp(createdAt),
});
});
it("keeps an ordinary first message on the compact timestamp", () => {
const createdAt = "2026-09-04T14:09:33.000Z";
const [item] = commentsToTaskChatItems([
{
id: "c-initial",
body: "Start the task.",
authorType: "user",
authorUserId: "user-1",
authorAgentId: null,
consumedByRunId: "run-1",
conversationAnchorAt: "2026-09-04T14:10:35.000Z",
createdAt,
} as unknown as IssueChatComment,
]);
expect(item).toMatchObject({
kind: "message",
timestamp: formatTaskChatTimestamp(createdAt),
});
});
it("routes an agent-authored workspace-ready notice through the system renderer", () => {
const presentation = {
kind: "system_notice",
tone: "info",
title: "Workspace ready · fix/workspace-ready-notice",
detailsDefaultOpen: false,
density: "compact",
} as const;
const metadata = {
version: 1,
sections: [
{
title: "Workspace",
rows: [
{ type: "key_value", label: "Strategy", value: "git_worktree" },
{
type: "key_value",
label: "Branch",
value: "fix/workspace-ready-notice",
},
{ type: "key_value", label: "CWD", value: "/worktrees/workspace-ready-notice" },
],
},
],
} as const;
const items = commentsToTaskChatItems([
{
id: "workspace-ready",
body: "## Workspace Ready\n\n- Strategy: `git_worktree`",
authorType: "agent",
authorAgentId: "agent-1",
createdByRunId: "run-1",
presentation,
metadata,
createdAt: "2026-09-02T12:59:03.318Z",
} as unknown as IssueChatComment,
]);
expect(items).toHaveLength(1);
expect(items[0]).toMatchObject({
id: "workspace-ready",
kind: "message",
author: "system",
presentation,
metadata,
runAgentId: null,
createdAtIso: "2026-09-02T12:59:03.318Z",
});
expect(items[0]).toHaveProperty("authorName", undefined);
expect(items[0]).toHaveProperty("agentIcon", undefined);
});
it("keeps an agent comment with message presentation as an agent bubble", () => {
const [item] = commentsToTaskChatItems([
{
id: "agent-message",
body: "Implementation is complete.",
authorType: "agent",
authorAgentId: "agent-1",
presentation: { kind: "message" },
metadata: { version: 1, sections: [] },
createdAt: "2026-09-02T13:00:00.000Z",
} as unknown as IssueChatComment,
]);
expect(item).toMatchObject({ kind: "message", author: "agent" });
if (item.kind !== "message") throw new Error("expected message item");
expect(item.presentation).toBeUndefined();
expect(item.metadata).toBeUndefined();
});
});