mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-16 10:50:38 +09:00
180 lines
5.3 KiB
TypeScript
180 lines
5.3 KiB
TypeScript
|
|
import { describe, expect, it } from "vitest";
|
||
|
|
import type { Agent } from "@paperclipai/shared";
|
||
|
|
import {
|
||
|
|
buildAssistantPartsFromTranscript,
|
||
|
|
buildIssueChatMessages,
|
||
|
|
type IssueChatComment,
|
||
|
|
type IssueChatLinkedRun,
|
||
|
|
} from "./issue-chat-messages";
|
||
|
|
import type { IssueTimelineEvent } from "./issue-timeline-events";
|
||
|
|
import type { LiveRunForIssue } from "../api/heartbeats";
|
||
|
|
|
||
|
|
function createAgent(id: string, name: string): Agent {
|
||
|
|
return {
|
||
|
|
id,
|
||
|
|
companyId: "company-1",
|
||
|
|
name,
|
||
|
|
role: "engineer",
|
||
|
|
title: null,
|
||
|
|
icon: "code",
|
||
|
|
status: "active",
|
||
|
|
reportsTo: null,
|
||
|
|
capabilities: null,
|
||
|
|
adapterType: "codex_local",
|
||
|
|
adapterConfig: {},
|
||
|
|
runtimeConfig: {},
|
||
|
|
budgetMonthlyCents: 0,
|
||
|
|
spentMonthlyCents: 0,
|
||
|
|
lastHeartbeatAt: null,
|
||
|
|
metadata: {},
|
||
|
|
createdAt: new Date(),
|
||
|
|
updatedAt: new Date(),
|
||
|
|
pauseReason: null,
|
||
|
|
pausedAt: null,
|
||
|
|
urlKey: "codexcoder",
|
||
|
|
permissions: { canCreateAgents: false },
|
||
|
|
} as Agent;
|
||
|
|
}
|
||
|
|
|
||
|
|
function createComment(overrides: Partial<IssueChatComment> = {}): IssueChatComment {
|
||
|
|
return {
|
||
|
|
id: "comment-1",
|
||
|
|
companyId: "company-1",
|
||
|
|
issueId: "issue-1",
|
||
|
|
authorAgentId: null,
|
||
|
|
authorUserId: "user-1",
|
||
|
|
body: "Hello",
|
||
|
|
createdAt: new Date("2026-04-06T12:00:00.000Z"),
|
||
|
|
updatedAt: new Date("2026-04-06T12:00:00.000Z"),
|
||
|
|
...overrides,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
describe("buildAssistantPartsFromTranscript", () => {
|
||
|
|
it("maps assistant text, reasoning, tool calls, and tool results", () => {
|
||
|
|
const result = buildAssistantPartsFromTranscript([
|
||
|
|
{ kind: "assistant", ts: "2026-04-06T12:00:00.000Z", text: "Working on it. " },
|
||
|
|
{ kind: "assistant", ts: "2026-04-06T12:00:01.000Z", text: "Done." },
|
||
|
|
{ kind: "thinking", ts: "2026-04-06T12:00:02.000Z", text: "Need to inspect files." },
|
||
|
|
{
|
||
|
|
kind: "tool_call",
|
||
|
|
ts: "2026-04-06T12:00:03.000Z",
|
||
|
|
name: "read_file",
|
||
|
|
toolUseId: "tool-1",
|
||
|
|
input: { path: "ui/src/pages/IssueDetail.tsx" },
|
||
|
|
},
|
||
|
|
{
|
||
|
|
kind: "tool_result",
|
||
|
|
ts: "2026-04-06T12:00:04.000Z",
|
||
|
|
toolUseId: "tool-1",
|
||
|
|
content: "file contents",
|
||
|
|
isError: false,
|
||
|
|
},
|
||
|
|
{ kind: "stderr", ts: "2026-04-06T12:00:05.000Z", text: "warn: noisy setup output" },
|
||
|
|
]);
|
||
|
|
|
||
|
|
expect(result.parts).toHaveLength(3);
|
||
|
|
expect(result.parts[0]).toMatchObject({ type: "text", text: "Working on it. Done." });
|
||
|
|
expect(result.parts[1]).toMatchObject({ type: "reasoning", text: "Need to inspect files." });
|
||
|
|
expect(result.parts[2]).toMatchObject({
|
||
|
|
type: "tool-call",
|
||
|
|
toolCallId: "tool-1",
|
||
|
|
toolName: "read_file",
|
||
|
|
result: "file contents",
|
||
|
|
isError: false,
|
||
|
|
});
|
||
|
|
expect(result.notices).toEqual(["warn: noisy setup output"]);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe("buildIssueChatMessages", () => {
|
||
|
|
it("orders events before comments and appends active live runs as running assistant messages", () => {
|
||
|
|
const agentMap = new Map<string, Agent>([["agent-1", createAgent("agent-1", "CodexCoder")]]);
|
||
|
|
const comments = [
|
||
|
|
createComment(),
|
||
|
|
createComment({
|
||
|
|
id: "comment-2",
|
||
|
|
authorAgentId: "agent-1",
|
||
|
|
authorUserId: null,
|
||
|
|
body: "I made the change.",
|
||
|
|
createdAt: new Date("2026-04-06T12:03:00.000Z"),
|
||
|
|
updatedAt: new Date("2026-04-06T12:03:00.000Z"),
|
||
|
|
runId: "run-1",
|
||
|
|
runAgentId: "agent-1",
|
||
|
|
}),
|
||
|
|
];
|
||
|
|
const timelineEvents: IssueTimelineEvent[] = [
|
||
|
|
{
|
||
|
|
id: "event-1",
|
||
|
|
createdAt: new Date("2026-04-06T11:59:00.000Z"),
|
||
|
|
actorType: "user",
|
||
|
|
actorId: "user-1",
|
||
|
|
statusChange: {
|
||
|
|
from: "done",
|
||
|
|
to: "todo",
|
||
|
|
},
|
||
|
|
},
|
||
|
|
];
|
||
|
|
const linkedRuns: IssueChatLinkedRun[] = [
|
||
|
|
{
|
||
|
|
runId: "run-history-1",
|
||
|
|
status: "succeeded",
|
||
|
|
agentId: "agent-1",
|
||
|
|
createdAt: new Date("2026-04-06T12:01:00.000Z"),
|
||
|
|
startedAt: new Date("2026-04-06T12:01:00.000Z"),
|
||
|
|
finishedAt: new Date("2026-04-06T12:02:00.000Z"),
|
||
|
|
},
|
||
|
|
];
|
||
|
|
const liveRuns: LiveRunForIssue[] = [
|
||
|
|
{
|
||
|
|
id: "run-live-1",
|
||
|
|
status: "running",
|
||
|
|
invocationSource: "manual",
|
||
|
|
triggerDetail: null,
|
||
|
|
startedAt: "2026-04-06T12:04:00.000Z",
|
||
|
|
finishedAt: null,
|
||
|
|
createdAt: "2026-04-06T12:04:00.000Z",
|
||
|
|
agentId: "agent-1",
|
||
|
|
agentName: "CodexCoder",
|
||
|
|
adapterType: "codex_local",
|
||
|
|
},
|
||
|
|
];
|
||
|
|
|
||
|
|
const messages = buildIssueChatMessages({
|
||
|
|
comments,
|
||
|
|
timelineEvents,
|
||
|
|
linkedRuns,
|
||
|
|
liveRuns,
|
||
|
|
transcriptsByRunId: new Map([
|
||
|
|
[
|
||
|
|
"run-live-1",
|
||
|
|
[{ kind: "assistant", ts: "2026-04-06T12:04:01.000Z", text: "Streaming reply" }],
|
||
|
|
],
|
||
|
|
]),
|
||
|
|
hasOutputForRun: () => true,
|
||
|
|
companyId: "company-1",
|
||
|
|
projectId: "project-1",
|
||
|
|
agentMap,
|
||
|
|
currentUserId: "user-1",
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(messages.map((message) => `${message.role}:${message.id}`)).toEqual([
|
||
|
|
"system:activity:event-1",
|
||
|
|
"user:comment-1",
|
||
|
|
"system:run:run-history-1",
|
||
|
|
"assistant:comment-2",
|
||
|
|
"assistant:live-run:run-live-1",
|
||
|
|
]);
|
||
|
|
|
||
|
|
const liveRunMessage = messages.at(-1);
|
||
|
|
expect(liveRunMessage).toMatchObject({
|
||
|
|
role: "assistant",
|
||
|
|
status: { type: "running" },
|
||
|
|
});
|
||
|
|
expect(liveRunMessage?.content[0]).toMatchObject({
|
||
|
|
type: "text",
|
||
|
|
text: "Streaming reply",
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|