Files
PaperClipAI/ui/src/lib/issue-chat-messages.test.ts
T
DottaandPaperclip 8d6232e7b0 feat: reuse provider sign-in across AI connection workflows (#13248)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Users connect provider accounts during onboarding and agent setup.
> - They should reuse and manage those accounts through the existing
Connectors interface.
> - A second login wizard would diverge from the established provider
workflows.
> - This pull request composes the existing sign-in components into
Connections and agent configuration.
> - Users can select accounts without changing their agent's harness or
model.

## Linked Issues or Issue Description

**Problem or motivation**
AI credentials are configured separately from Connections. Agents cannot
consistently reuse a responsible user's account or a permitted shared
account.

**Proposed solution**
Manage AI accounts with the existing Connections grants and permissions.
Keep model and harness selection independent from credential selection.
Preserve legacy authentication until validated adoption.

**Alternatives considered**
A separate credential registry would duplicate ownership and access
policy. Automatic fallback would risk using the wrong account.

**Roadmap alignment**
This extends the shipped Apps, multi-user, secrets, and agent-runtime
capabilities. The maintainer requested the feature and reviewed the UI.
Related groundwork: #11899 (connection permissions), #10910 (connection
wizard), #11692 (Claude subscription profiles), and #11854 (Codex
account rotation).

## What Changed

- Add compact AI-account management to the existing Connectors pages.
- Reuse AgentProviderConnection, AdapterLoginPanel, AdapterLoginChrome,
and authentication controllers.
- Add the shared connection picker to agent setup/settings and task
requests.
- Preserve onboarding's sequence and reuse existing accounts.
- Add local-login recovery, retry, cancellation, and React StrictMode
handling.
- Add interactive Storybook scenarios, design-guide examples, and app
acceptance checks.

This is part 2 of the AI Connections change. The runtime foundation in
#13247 is merged. This PR now targets master.

## Verification

- Updated against master `47ded8bf9`, including the landed runtime
foundation and upstream task-search changes.
- Full workspace typecheck, production build, Storybook build, and token
gates passed on the integrated branch. Final local-login changes passed
59 focused tests; new-agent and inbox regression suites passed 63 tests.
- Browser checks verified automatic local Claude account detection,
resumable Codex login commands, retry, focus restoration, and
desktop/phone layouts. Commands create their isolated directory before
invoking the CLI.
- All CI test, browser, build, packaging, and runner jobs passed on
final head `dd17d3211931dd70aaa6ea619d83a7f9966dd18e`. The fresh
Greptile review is 5/5, the security scan passed, and there are no
unresolved review threads. The final CI aggregate gates passed.
- Local general-server coverage passed 11,804 tests; three
port-collision failures passed in an isolated 25-test rerun. All 6,111
UI tests passed. CLI coverage passed 484 tests; its remaining doctor
test requires port 3199, which is occupied by an unrelated report server
on this Mac. The complete CLI suite passed in CI.
- Live browser testing verified Codex API-key reconnect inside a task
card on desktop and phone. Real provider runs resumed and completed with
unchanged connection/grant identity and agent routing.
- Tested opening, cancelling, reopening, and completing connection
creation. A regression confirms Connect another account cannot submit
the new-agent form or copy provider keys into agent settings.
- Added shared inline repair, automatic local sign-in checks, and
responsive connection dialogs. Standalone Daytona installation ignores
workspace configuration and suppresses dependency scripts. Its
standalone build also passed with CI's exact pnpm 9.15.4.
- Destructive live tests are excluded by default. Explicit opt-in, local
deployment checks, and matching disposable fixture identities are
required before any mutation.

## Risks

- Local Codex/Grok creation requires the connection-specific terminal
login command.
- Browser sign-in uses the existing supported-environment controllers.
- This update verifies live local Claude detection and Codex API-key
task repair. New subscription authorization/refresh and
independent-human/native-runner isolation were not reverified in this
update.
- No agent automatically adopts managed Connections.

## Model Used

OpenAI GPT-6 through Codex, with reasoning, repository tools, code
execution, and browser testing. The exact runtime model identifier and
context-window size are not exposed in this session.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-09-12 16:51:26 -05:00

1811 lines
54 KiB
TypeScript

import { describe, expect, it } from "vitest";
import type { Agent } from "@paperclipai/shared";
import {
buildAssistantPartsFromTranscript,
buildIssueChatMessages,
isCoTSegmentActive,
isRedundantAiRecoveryNotice,
preserveReadableStreamingRetraction,
stabilizeThreadMessages,
type IssueChatComment,
type IssueChatLinkedRun,
} from "./issue-chat-messages";
import type {
AskUserQuestionsInteraction,
AskUserQuestionsQuestion,
ConnectionIntentInteraction,
RequestConfirmationInteraction,
SuggestTasksInteraction,
} from "./issue-thread-interactions";
import { pendingConnectionIntentInteraction } from "../fixtures/issueThreadInteractionFixtures";
import type { IssueTimelineEvent } from "./issue-timeline-events";
import type { ActiveRunForIssue, LiveRunForIssue } from "../api/heartbeats";
import { registerUIAdapter, unregisterUIAdapter } from "../adapters/registry";
describe("connection intents in the task feed", () => {
it("orders the card chronologically and collapses live/polling snapshots into one updated message", () => {
const pending = {
...pendingConnectionIntentInteraction,
createdAt: new Date("2026-08-26T12:02:00.000Z"),
updatedAt: new Date("2026-08-26T12:02:00.000Z"),
};
const connected: ConnectionIntentInteraction = {
...pending,
status: "accepted",
updatedAt: new Date("2026-08-26T12:04:00.000Z"),
resolvedAt: new Date("2026-08-26T12:04:00.000Z"),
result: {
version: 1,
outcome: "connected",
connectionId: "connection-1",
},
};
const messages = buildIssueChatMessages({
comments: [
createComment({
id: "before",
body: "Before",
createdAt: new Date("2026-08-26T12:01:00.000Z"),
}),
createComment({
id: "after",
body: "After",
createdAt: new Date("2026-08-26T12:03:00.000Z"),
}),
],
// Models the brief overlap where live invalidation and polling each
// supplied a snapshot of the same interaction id.
interactions: [pending, connected],
timelineEvents: [],
linkedRuns: [],
liveRuns: [],
});
expect(messages.map((message) => message.id)).toEqual([
"before",
`interaction:${pending.id}`,
"after",
]);
const interactionMessages = messages.filter(
(message) => message.id === `interaction:${pending.id}`,
);
expect(interactionMessages).toHaveLength(1);
const custom = interactionMessages[0]?.metadata?.custom as {
interaction?: ConnectionIntentInteraction;
};
expect(custom.interaction?.status).toBe("accepted");
expect(custom.interaction?.result?.outcome).toBe("connected");
});
});
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 {
const authorAgentId = overrides.authorAgentId ?? null;
return {
id: "comment-1",
companyId: "company-1",
issueId: "issue-1",
authorAgentId: null,
authorUserId: "user-1",
body: "Hello",
authorType: authorAgentId ? "agent" : "user",
presentation: null,
metadata: null,
sourceTrust: null,
createdAt: new Date("2026-04-06T12:00:00.000Z"),
updatedAt: new Date("2026-04-06T12:00:00.000Z"),
...overrides,
};
}
function createInteraction(
overrides: Partial<SuggestTasksInteraction> = {},
): SuggestTasksInteraction {
return {
id: "interaction-1",
companyId: "company-1",
issueId: "issue-1",
kind: "suggest_tasks",
title: "Suggested follow-up work",
summary: "Preview the next issue tree before accepting it.",
status: "pending",
continuationPolicy: "wake_assignee",
createdByAgentId: "agent-1",
createdByUserId: null,
resolvedByAgentId: null,
resolvedByUserId: null,
createdAt: new Date("2026-04-06T12:02:00.000Z"),
updatedAt: new Date("2026-04-06T12:02:00.000Z"),
resolvedAt: null,
payload: {
version: 1,
tasks: [
{
clientKey: "task-1",
title: "Prototype the card",
},
],
},
result: null,
...overrides,
resolverPolicy: overrides.resolverPolicy ?? "anyone",
requestedResolverPolicy: overrides.requestedResolverPolicy ?? "anyone",
effectiveResolverPolicy: overrides.effectiveResolverPolicy ?? "anyone",
resolverPolicyProvenance: overrides.resolverPolicyProvenance ?? "inherited",
effectiveResolverPolicySource:
overrides.effectiveResolverPolicySource ?? "requested",
legacyResolverPolicyAliases: overrides.legacyResolverPolicyAliases ?? {
requested: "board_or_agents",
effective: "board_or_agents",
},
};
}
function createRequestConfirmation(
overrides: Partial<RequestConfirmationInteraction> = {},
): RequestConfirmationInteraction {
return {
id: "confirmation-1",
companyId: "company-1",
issueId: "issue-1",
kind: "request_confirmation",
title: "Approve the plan",
summary: "Review and approve the latest plan.",
status: "pending",
continuationPolicy: "wake_assignee",
createdByAgentId: "agent-1",
createdByUserId: null,
resolvedByAgentId: null,
resolvedByUserId: null,
createdAt: new Date("2026-04-06T12:01:00.000Z"),
updatedAt: new Date("2026-04-06T12:01:00.000Z"),
resolvedAt: null,
payload: {
version: 1,
prompt: "Approve the plan?",
},
result: null,
...overrides,
resolverPolicy: overrides.resolverPolicy ?? "anyone",
requestedResolverPolicy: overrides.requestedResolverPolicy ?? "anyone",
effectiveResolverPolicy: overrides.effectiveResolverPolicy ?? "anyone",
resolverPolicyProvenance: overrides.resolverPolicyProvenance ?? "inherited",
effectiveResolverPolicySource:
overrides.effectiveResolverPolicySource ?? "requested",
legacyResolverPolicyAliases: overrides.legacyResolverPolicyAliases ?? {
requested: "board_or_agents",
effective: "board_or_agents",
},
};
}
describe("buildAssistantPartsFromTranscript", () => {
it("keeps canonical provider activity structured for the task-thread widget", () => {
const result = buildAssistantPartsFromTranscript([{
kind: "provider_activity",
ts: "2026-08-21T12:00:00.000Z",
family: "plan",
eventType: "plan.updated",
status: "completed",
title: "Plan",
summary: "Plan synchronized",
payload: { steps: [{ stepId: "s1", body: "Validate schemas", status: "completed" }] },
}]);
expect(result.parts).toHaveLength(1);
expect(result.parts[0]).toMatchObject({
type: "tool-call",
toolName: "paperclip_provider_activity",
args: {
family: "plan",
eventType: "plan.updated",
payload: { steps: [{ body: "Validate schemas" }] },
},
result: { status: "completed" },
});
expect(JSON.stringify(result.parts)).not.toContain("reasoning");
});
it("maps assistant text, reasoning, and tool activity while omitting noisy stderr", () => {
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([]);
});
it("accumulates streamed tool output before the completed result", () => {
const result = buildAssistantPartsFromTranscript([
{ kind: "tool_call", ts: "2026-04-06T12:00:00.000Z", name: "shell", toolUseId: "tool-1", input: {} },
{ kind: "tool_result", ts: "2026-04-06T12:00:01.000Z", toolUseId: "tool-1", content: "first\n", delta: true },
{ kind: "tool_result", ts: "2026-04-06T12:00:02.000Z", toolUseId: "tool-1", content: "second\n", delta: true },
{ kind: "tool_result", ts: "2026-04-06T12:00:03.000Z", toolUseId: "tool-1", content: "", isError: false },
]);
expect(result.parts).toMatchObject([
{ type: "tool-call", toolCallId: "tool-1", result: "first\nsecond\n", isError: false },
]);
});
it("preserves transcript ordering when text and tool activity are interleaved", () => {
const result = buildAssistantPartsFromTranscript([
{ kind: "assistant", ts: "2026-04-06T12:00:00.000Z", text: "First." },
{
kind: "tool_call",
ts: "2026-04-06T12:00:01.000Z",
name: "read_file",
toolUseId: "tool-1",
input: { path: "ui/src/components/IssueChatThread.tsx" },
},
{ kind: "assistant", ts: "2026-04-06T12:00:02.000Z", text: "Second." },
{
kind: "tool_result",
ts: "2026-04-06T12:00:03.000Z",
toolUseId: "tool-1",
content: "ok",
isError: false,
},
{
kind: "thinking",
ts: "2026-04-06T12:00:04.000Z",
text: "Need one more check.",
},
{
kind: "tool_call",
ts: "2026-04-06T12:00:05.000Z",
name: "write_file",
toolUseId: "tool-2",
input: { path: "ui/src/lib/issue-chat-messages.ts" },
},
{
kind: "tool_result",
ts: "2026-04-06T12:00:06.000Z",
toolUseId: "tool-2",
content: "saved",
isError: false,
},
]);
expect(result.parts).toMatchObject([
{ type: "text", text: "First." },
{
type: "tool-call",
toolCallId: "tool-1",
toolName: "read_file",
result: "ok",
},
{ type: "text", text: "Second." },
{ type: "reasoning", text: "Need one more check." },
{
type: "tool-call",
toolCallId: "tool-2",
toolName: "write_file",
result: "saved",
},
]);
});
it("treats a completed tool-only segment as resolved once a tool_result arrives", () => {
const result = buildAssistantPartsFromTranscript([
{
kind: "thinking",
ts: "2026-04-06T12:00:00.000Z",
text: "Checking the task.",
},
{
kind: "tool_call",
ts: "2026-04-06T12:00:01.000Z",
name: "search",
toolUseId: "tool-1",
input: { query: "paperclip" },
},
{
kind: "tool_result",
ts: "2026-04-06T12:00:02.000Z",
toolUseId: "tool-1",
content: "search completed",
isError: false,
},
{
kind: "assistant",
ts: "2026-04-06T12:00:03.000Z",
text: "Found the relevant code.",
},
]);
expect(result.parts).toMatchObject([
{ type: "reasoning", text: "Checking the task." },
{
type: "tool-call",
toolCallId: "tool-1",
toolName: "search",
result: "search completed",
isError: false,
},
{ type: "text", text: "Found the relevant code." },
]);
expect(result.segments).toEqual([
{
startMs: new Date("2026-04-06T12:00:00.000Z").getTime(),
endMs: new Date("2026-04-06T12:00:02.000Z").getTime(),
},
]);
});
it("marks only the latest chain-of-thought segment active while a run is live", () => {
expect(
isCoTSegmentActive({
isMessageRunning: true,
segmentIndex: 0,
segmentCount: 2,
}),
).toBe(false);
expect(
isCoTSegmentActive({
isMessageRunning: true,
segmentIndex: 1,
segmentCount: 2,
}),
).toBe(true);
expect(
isCoTSegmentActive({
isMessageRunning: false,
segmentIndex: 1,
segmentCount: 2,
}),
).toBe(false);
});
it("keeps run errors while suppressing init and system transcript noise", () => {
const result = buildAssistantPartsFromTranscript([
{
kind: "init",
ts: "2026-04-06T12:00:00.000Z",
model: "gpt-5.4",
sessionId: "session-123",
},
{
kind: "system",
ts: "2026-04-06T12:00:01.000Z",
text: "item started: planning_step (id=step-1)",
},
{
kind: "system",
ts: "2026-04-06T12:00:02.000Z",
text: "item completed: planning_step (id=step-1)",
},
{
kind: "result",
ts: "2026-04-06T12:00:03.000Z",
text: "Tool crashed during execution",
inputTokens: 0,
outputTokens: 0,
cachedTokens: 0,
costUsd: 0,
subtype: "error",
isError: true,
errors: ["ENOENT: missing file"],
},
]);
expect(result.parts).toMatchObject([
{
type: "reasoning",
text: "Run error: ENOENT: missing file",
},
]);
expect(result.notices).toEqual([]);
});
it("preserves diff transcript output as a fenced diff block", () => {
const result = buildAssistantPartsFromTranscript([
{
kind: "assistant",
ts: "2026-04-06T12:00:00.000Z",
text: "Applied the patch.",
},
{
kind: "diff",
ts: "2026-04-06T12:00:01.000Z",
changeType: "file_header",
text: "ui/src/lib/issue-chat-messages.ts",
},
{
kind: "diff",
ts: "2026-04-06T12:00:02.000Z",
changeType: "add",
text: "+function formatDiffBlock(lines: string[]) {",
},
{
kind: "diff",
ts: "2026-04-06T12:00:03.000Z",
changeType: "add",
text: "+ return ````diff`;",
},
]);
expect(result.parts).toMatchObject([
{ type: "text", text: "Applied the patch." },
{
type: "text",
text: [
"```diff",
"ui/src/lib/issue-chat-messages.ts",
"+function formatDiffBlock(lines: string[]) {",
"+ return ````diff`;",
"```",
].join("\n"),
},
]);
});
});
describe("buildIssueChatMessages", () => {
it("uses the company user label for current-user comments instead of collapsing to You", () => {
const messages = buildIssueChatMessages({
comments: [createComment({ authorUserId: "user-1" })],
timelineEvents: [],
linkedRuns: [],
liveRuns: [],
currentUserId: "user-1",
userLabelMap: new Map([["user-1", "Dotta"]]),
});
expect(messages[0]).toMatchObject({
role: "user",
metadata: {
custom: {
authorName: "Dotta",
authorUserId: "user-1",
},
},
});
});
it("flags an operator-interrupted historical run so the timeline can read 'interrupted'", () => {
const messages = buildIssueChatMessages({
comments: [],
timelineEvents: [],
linkedRuns: [
{
runId: "run-int",
status: "cancelled",
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"),
resultJson: {
operatorInterrupted: true,
interruptionSource: "issue_comment_interrupt",
},
},
{
runId: "run-plain",
status: "cancelled",
agentId: "agent-1",
createdAt: new Date("2026-04-06T12:03:00.000Z"),
startedAt: new Date("2026-04-06T12:03:00.000Z"),
finishedAt: new Date("2026-04-06T12:04:00.000Z"),
resultJson: null,
},
],
liveRuns: [],
});
const interrupted = messages.find(
(message) => message.id === "run-assistant:run-int",
);
const plain = messages.find(
(message) => message.id === "run-assistant:run-plain",
);
expect(interrupted?.metadata?.custom).toMatchObject({
runOperatorInterrupted: true,
});
expect(plain?.metadata?.custom).toMatchObject({
runOperatorInterrupted: false,
});
});
it("redacts deleted comment bodies while preserving tombstone metadata", () => {
const messages = buildIssueChatMessages({
comments: [
createComment({
body: "Sensitive deleted body",
deletedAt: new Date("2026-04-06T12:05:00.000Z"),
deletedByType: "user",
deletedByUserId: "user-1",
}),
],
timelineEvents: [],
linkedRuns: [],
liveRuns: [],
currentUserId: "user-1",
userLabelMap: new Map([["user-1", "Dotta"]]),
});
expect(messages[0]?.content).toEqual([{ type: "text", text: "" }]);
expect(messages[0]?.metadata.custom).toMatchObject({
deletedAt: "2026-04-06T12:05:00.000Z",
deletedByType: "user",
deletedByUserId: "user-1",
});
expect(JSON.stringify(messages[0])).not.toContain("Sensitive deleted body");
});
it("preserves low-trust source metadata on comment messages", () => {
const messages = buildIssueChatMessages({
comments: [
createComment({
authorAgentId: "agent-1",
authorUserId: null,
sourceTrust: {
preset: "low_trust_review",
disposition: "quarantined",
sourceAgentId: "agent-1",
},
}),
],
timelineEvents: [],
linkedRuns: [],
liveRuns: [],
agentMap: new Map([
["agent-1", createAgent("agent-1", "Low Trust Reviewer")],
]),
});
expect(messages[0]?.metadata.custom.sourceTrust).toMatchObject({
preset: "low_trust_review",
disposition: "quarantined",
sourceAgentId: "agent-1",
});
});
it("prefers derived agent attribution when a board-authored comment is proven to come from a run", () => {
const agentMap = new Map<string, Agent>([
["agent-1", createAgent("agent-1", "Claude")],
]);
const messages = buildIssueChatMessages({
comments: [
createComment({
authorUserId: "user-1",
derivedAuthorAgentId: "agent-1",
derivedCreatedByRunId: "run-1",
}),
],
timelineEvents: [],
linkedRuns: [],
liveRuns: [],
agentMap,
currentUserId: "user-1",
userLabelMap: new Map([["user-1", "Dotta"]]),
});
expect(messages[0]).toMatchObject({
role: "assistant",
metadata: {
custom: {
authorName: "Claude",
authorType: "agent",
authorAgentId: "agent-1",
authorUserId: "user-1",
runId: "run-1",
runAgentId: "agent-1",
},
},
});
});
it("does not reattribute a genuine board/user comment that has no derived agent", () => {
const agentMap = new Map<string, Agent>([
["agent-1", createAgent("agent-1", "Claude")],
]);
const messages = buildIssueChatMessages({
comments: [
createComment({
authorUserId: "local-board",
authorType: "user",
// No agent ever resolved for this comment — a real board action.
derivedAuthorAgentId: null,
derivedCreatedByRunId: null,
runId: null,
runAgentId: null,
}),
],
timelineEvents: [],
linkedRuns: [],
liveRuns: [],
agentMap,
currentUserId: "user-1",
userLabelMap: new Map([["local-board", "Board"]]),
});
expect(messages[0]).toMatchObject({
role: "user",
metadata: {
custom: {
authorType: "user",
authorAgentId: null,
authorUserId: "local-board",
},
},
});
});
it("renders a comment as agent-authored when runAgentId is set from activity log", () => {
const agentMap = new Map<string, Agent>([
["agent-1", createAgent("agent-1", "Claude")],
]);
const messages = buildIssueChatMessages({
comments: [
createComment({
authorUserId: "user-1",
runId: "run-1",
runAgentId: "agent-1",
}),
],
timelineEvents: [],
linkedRuns: [],
liveRuns: [],
agentMap,
currentUserId: "user-1",
userLabelMap: new Map([["user-1", "Dotta"]]),
});
expect(messages[0]).toMatchObject({
role: "assistant",
metadata: {
custom: {
authorName: "Claude",
authorType: "agent",
authorAgentId: "agent-1",
authorUserId: "user-1",
runId: "run-1",
runAgentId: "agent-1",
},
},
});
});
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: (runId) => runId === "run-live-1",
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",
"assistant:comment-2",
"assistant:run-assistant: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",
});
});
it("suppresses live-run Working messages for terminal issues", () => {
const liveRun: LiveRunForIssue = {
id: "run-live-terminal",
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 terminalMessages = buildIssueChatMessages({
comments: [],
timelineEvents: [],
linkedRuns: [],
liveRuns: [liveRun],
issueStatus: "done",
currentUserId: "user-1",
});
const liveMessages = buildIssueChatMessages({
comments: [],
timelineEvents: [],
linkedRuns: [],
liveRuns: [liveRun],
issueStatus: "in_progress",
currentUserId: "user-1",
});
expect(
terminalMessages.find(
(message) => message.id === "run-assistant:run-live-terminal",
),
).toBeUndefined();
expect(
liveMessages.find(
(message) => message.id === "run-assistant:run-live-terminal",
),
).toMatchObject({
status: { type: "running" },
metadata: { custom: { waitingText: "Working..." } },
});
});
it("merges thread interactions into the same chronological feed as comments and runs", () => {
const messages = buildIssueChatMessages({
comments: [
createComment({
id: "comment-1",
createdAt: new Date("2026-04-06T12:01:00.000Z"),
updatedAt: new Date("2026-04-06T12:01:00.000Z"),
}),
],
interactions: [
createInteraction({
id: "interaction-2",
createdAt: new Date("2026-04-06T12:02:00.000Z"),
updatedAt: new Date("2026-04-06T12:02:00.000Z"),
}),
],
timelineEvents: [],
linkedRuns: [],
liveRuns: [
{
id: "run-live-1",
status: "running",
invocationSource: "manual",
triggerDetail: null,
startedAt: "2026-04-06T12:03:00.000Z",
finishedAt: null,
createdAt: "2026-04-06T12:03:00.000Z",
agentId: "agent-1",
agentName: "CodexCoder",
adapterType: "codex_local",
},
],
transcriptsByRunId: new Map([
[
"run-live-1",
[
{
kind: "assistant",
ts: "2026-04-06T12:03:01.000Z",
text: "Working on it.",
},
],
],
]),
hasOutputForRun: (runId) => runId === "run-live-1",
currentUserId: "user-1",
});
expect(messages.map((message) => `${message.role}:${message.id}`)).toEqual([
"user:comment-1",
"system:interaction:interaction-2",
"assistant:run-assistant:run-live-1",
]);
expect(messages[1]).toMatchObject({
metadata: {
custom: {
kind: "interaction",
anchorId: "interaction-interaction-2",
},
},
});
});
it("drops degenerate ask_user_questions interactions so they leave no empty slot (PAP-424)", () => {
function askInteraction(
id: string,
questions: AskUserQuestionsQuestion[],
): AskUserQuestionsInteraction {
return {
id,
companyId: "company-1",
issueId: "issue-1",
kind: "ask_user_questions",
title: null,
summary: null,
status: "pending",
continuationPolicy: "wake_assignee",
createdByAgentId: "agent-1",
createdByUserId: null,
resolvedByAgentId: null,
resolvedByUserId: null,
createdAt: new Date("2026-04-06T12:02:00.000Z"),
updatedAt: new Date("2026-04-06T12:02:00.000Z"),
resolvedAt: null,
resolverPolicy: "anyone",
requestedResolverPolicy: "anyone",
effectiveResolverPolicy: "anyone",
resolverPolicyProvenance: "inherited",
effectiveResolverPolicySource: "requested",
legacyResolverPolicyAliases: {
requested: "board_or_agents",
effective: "board_or_agents",
},
payload: { version: 1, questions },
result: null,
} as AskUserQuestionsInteraction;
}
const messages = buildIssueChatMessages({
comments: [
createComment({
id: "comment-1",
createdAt: new Date("2026-04-06T12:01:00.000Z"),
updatedAt: new Date("2026-04-06T12:01:00.000Z"),
}),
],
interactions: [
// A truly unanswerable card (no options, no free-text) — must be
// filtered out entirely.
askInteraction("interaction-degenerate", [
{
id: "q1",
prompt: "Anything?",
selectionMode: "single",
options: [],
},
]),
// A legitimate yes/no question survives.
askInteraction("interaction-legit", [
{
id: "q1",
prompt: "Ship it?",
selectionMode: "single",
options: [
{ id: "yes", label: "Yes" },
{ id: "no", label: "No" },
],
},
]),
],
timelineEvents: [],
linkedRuns: [],
liveRuns: [],
currentUserId: "user-1",
});
const ids = messages.map((message) => `${message.role}:${message.id}`);
// The legit card is present; the degenerate one leaves no message at all.
expect(ids).toEqual([
"user:comment-1",
"system:interaction:interaction-legit",
]);
expect(ids).not.toContain("system:interaction:interaction-degenerate");
});
it("preserves ephemeral active-run status metadata for rendering", () => {
const activeRun: ActiveRunForIssue = {
id: "run-active-1",
status: "running",
invocationSource: "manual",
triggerDetail: null,
startedAt: "2026-04-06T12:03:00.000Z",
finishedAt: null,
createdAt: "2026-04-06T12:03:00.000Z",
agentId: "agent-1",
agentName: "CodexCoder",
adapterType: "codex_local",
currentStatusMessage: "Syncing git worktree to environment",
currentStatusUpdatedAt: "2026-04-06T12:03:05.000Z",
currentToolName: "bash",
lastAssistantSnippet: "Checking repository status",
lastEventAt: "2026-04-06T12:03:08.000Z",
};
const messages = buildIssueChatMessages({
comments: [],
timelineEvents: [],
linkedRuns: [],
liveRuns: [],
activeRun,
currentUserId: "user-1",
});
expect(messages).toHaveLength(1);
expect(messages[0]).toMatchObject({
role: "assistant",
status: { type: "running" },
metadata: {
custom: {
kind: "live-run",
runId: "run-active-1",
currentStatusMessage: "Syncing git worktree to environment",
currentStatusUpdatedAt: "2026-04-06T12:03:05.000Z",
currentToolName: "bash",
lastAssistantSnippet: "Checking repository status",
lastEventAt: "2026-04-06T12:03:08.000Z",
},
},
});
});
it("places request confirmations after later same-run handoff status and comment", () => {
const messages = buildIssueChatMessages({
comments: [
createComment({
id: "comment-handoff",
authorAgentId: "agent-1",
authorUserId: null,
body: "Ready for approval.",
createdAt: new Date("2026-04-06T12:03:00.000Z"),
updatedAt: new Date("2026-04-06T12:03:00.000Z"),
runId: "run-1",
runAgentId: "agent-1",
}),
createComment({
id: "comment-user-reply",
body: "Approved.",
createdAt: new Date("2026-04-06T12:04:00.000Z"),
updatedAt: new Date("2026-04-06T12:04:00.000Z"),
}),
],
interactions: [
createRequestConfirmation({
id: "confirmation-1",
sourceRunId: "run-1",
status: "expired",
result: {
version: 1,
outcome: "superseded_by_comment",
commentId: "comment-user-reply",
},
}),
],
timelineEvents: [
{
id: "event-in-review",
actorType: "agent",
actorId: "agent-1",
createdAt: new Date("2026-04-06T12:02:00.000Z"),
runId: "run-1",
statusChange: {
from: "in_progress",
to: "in_review",
},
},
],
linkedRuns: [],
liveRuns: [],
currentUserId: "user-1",
});
expect(messages.map((message) => `${message.role}:${message.id}`)).toEqual([
"system:activity:event-in-review",
"assistant:comment-handoff",
"system:interaction:confirmation-1",
"user:comment-user-reply",
]);
});
it("keeps request confirmations chronological without later same-run handoff evidence", () => {
const messages = buildIssueChatMessages({
comments: [
createComment({
id: "comment-later",
createdAt: new Date("2026-04-06T12:02:00.000Z"),
updatedAt: new Date("2026-04-06T12:02:00.000Z"),
}),
],
interactions: [
createRequestConfirmation({
id: "confirmation-1",
sourceRunId: "run-1",
}),
],
timelineEvents: [],
linkedRuns: [],
liveRuns: [],
currentUserId: "user-1",
});
expect(messages.map((message) => `${message.role}:${message.id}`)).toEqual([
"system:interaction:confirmation-1",
"user:comment-later",
]);
});
it("does not move request confirmations past unrelated comments before same-run handoff", () => {
const messages = buildIssueChatMessages({
comments: [
createComment({
id: "comment-user-reply",
body: "I have a question first.",
createdAt: new Date("2026-04-06T12:02:00.000Z"),
updatedAt: new Date("2026-04-06T12:02:00.000Z"),
}),
createComment({
id: "comment-handoff",
authorAgentId: "agent-1",
authorUserId: null,
body: "Ready for approval.",
createdAt: new Date("2026-04-06T12:03:00.000Z"),
updatedAt: new Date("2026-04-06T12:03:00.000Z"),
runId: "run-1",
runAgentId: "agent-1",
}),
],
interactions: [
createRequestConfirmation({
id: "confirmation-1",
sourceRunId: "run-1",
}),
],
timelineEvents: [],
linkedRuns: [],
liveRuns: [],
currentUserId: "user-1",
});
expect(messages.map((message) => `${message.role}:${message.id}`)).toEqual([
"system:interaction:confirmation-1",
"user:comment-user-reply",
"assistant:comment-handoff",
]);
});
it("keeps succeeded runs as assistant messages when transcript output exists", () => {
const agentMap = new Map<string, Agent>([
["agent-1", createAgent("agent-1", "CodexCoder")],
]);
const messages = buildIssueChatMessages({
comments: [],
timelineEvents: [],
linkedRuns: [
{
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:03:00.000Z"),
},
],
liveRuns: [],
transcriptsByRunId: new Map([
[
"run-history-1",
[
{
kind: "thinking",
ts: "2026-04-06T12:01:10.000Z",
text: "Checking the current issue thread.",
},
{
kind: "assistant",
ts: "2026-04-06T12:02:30.000Z",
text: "Updated the thread renderer.",
},
],
],
]),
hasOutputForRun: (runId) => runId === "run-history-1",
agentMap,
currentUserId: "user-1",
});
expect(messages).toHaveLength(1);
expect(messages[0]).toMatchObject({
id: "run-assistant:run-history-1",
role: "assistant",
status: { type: "complete", reason: "stop" },
metadata: {
custom: {
kind: "historical-run",
runId: "run-history-1",
chainOfThoughtLabel: "Worked for 2 minutes",
},
},
});
expect(messages[0]?.content).toMatchObject([
{ type: "reasoning", text: "Checking the current issue thread." },
{ type: "text", text: "Updated the thread renderer." },
]);
});
it("compacts long run transcripts in issue chat while preserving matching tool context", () => {
const isoAt = (baseMs: number, offsetSeconds: number) =>
new Date(baseMs + offsetSeconds * 1000).toISOString();
const baseMs = Date.parse("2026-04-06T12:00:00.000Z");
const transcript = [
...Array.from({ length: 9 }, (_, index) => ({
kind: "assistant" as const,
ts: isoAt(baseMs, index),
text: `Older update ${index + 1}`,
})),
{
kind: "tool_call" as const,
ts: isoAt(baseMs, 9),
name: "search",
toolUseId: "tool-keep",
input: { query: "issue chat virtualization" },
},
...Array.from({ length: 79 }, (_, index) => ({
kind: "assistant" as const,
ts: isoAt(baseMs, 10 + index),
text: `Recent update ${index + 1}`,
})),
{
kind: "tool_result" as const,
ts: isoAt(baseMs, 89),
toolUseId: "tool-keep",
content: "search completed",
isError: false,
},
];
const messages = buildIssueChatMessages({
comments: [],
timelineEvents: [],
linkedRuns: [
{
runId: "run-history-3",
status: "succeeded",
agentId: "agent-1",
agentName: "CodexCoder",
createdAt: new Date("2026-04-06T12:00:00.000Z"),
startedAt: new Date("2026-04-06T12:00:00.000Z"),
finishedAt: new Date("2026-04-06T12:03:00.000Z"),
},
],
liveRuns: [],
transcriptsByRunId: new Map([["run-history-3", transcript]]),
hasOutputForRun: (runId) => runId === "run-history-3",
currentUserId: "user-1",
});
expect(messages).toHaveLength(1);
const textParts =
messages[0]?.content
.filter(
(part): part is { type: "text"; text: string } =>
part.type === "text",
)
.map((part) => part.text) ?? [];
expect(textParts.join("\n")).not.toContain("Older update 1");
expect(messages[0]?.content).toContainEqual(
expect.objectContaining({
type: "tool-call",
toolCallId: "tool-keep",
toolName: "search",
result: "search completed",
}),
);
});
it("honors a wider transcript window declared by an adapter's UI module", () => {
// Capability path: an adapter (built-in or plugin) declares
// transcriptPresentation on its UI module; shared code resolves it via the
// registry with no adapter identities. The test registers a synthetic
// verbose adapter — the test right above pins the default 30-entry window
// for adapters that declare nothing.
registerUIAdapter({
type: "verbose_test_local",
label: "Verbose Test",
parseStdoutLine: () => [],
ConfigFields: () => null,
buildAdapterConfig: () => ({}),
transcriptPresentation: {
maxVisibleEntries: 400,
liveReasoningView: "scrollLog",
},
});
try {
const isoAt = (baseMs: number, offsetSeconds: number) =>
new Date(baseMs + offsetSeconds * 1000).toISOString();
const baseMs = Date.parse("2026-04-06T12:00:00.000Z");
// 90 renderable entries: over the default 30-entry window, under the
// declared 400-entry window, so nothing is trimmed.
const transcript = [
...Array.from({ length: 9 }, (_, index) => ({
kind: "assistant" as const,
ts: isoAt(baseMs, index),
text: `Older update ${index + 1}`,
})),
{
kind: "tool_call" as const,
ts: isoAt(baseMs, 9),
name: "search",
toolUseId: "tool-keep",
input: { query: "issue chat virtualization" },
},
...Array.from({ length: 79 }, (_, index) => ({
kind: "assistant" as const,
ts: isoAt(baseMs, 10 + index),
text: `Recent update ${index + 1}`,
})),
{
kind: "tool_result" as const,
ts: isoAt(baseMs, 89),
toolUseId: "tool-keep",
content: "search completed",
isError: false,
},
];
const messages = buildIssueChatMessages({
comments: [],
timelineEvents: [],
linkedRuns: [
{
runId: "run-history-verbose",
status: "succeeded",
agentId: "agent-1",
agentName: "VerboseCoder",
adapterType: "verbose_test_local",
createdAt: new Date("2026-04-06T12:00:00.000Z"),
startedAt: new Date("2026-04-06T12:00:00.000Z"),
finishedAt: new Date("2026-04-06T12:03:00.000Z"),
},
],
liveRuns: [],
transcriptsByRunId: new Map([["run-history-verbose", transcript]]),
hasOutputForRun: (runId) => runId === "run-history-verbose",
currentUserId: "user-1",
});
expect(messages).toHaveLength(1);
const textParts =
messages[0]?.content
.filter(
(part): part is { type: "text"; text: string } =>
part.type === "text",
)
.map((part) => part.text) ?? [];
expect(textParts.join("\n")).toContain("Older update 1");
expect(textParts.join("\n")).toContain("Recent update 79");
} finally {
unregisterUIAdapter("verbose_test_local");
}
});
it("keeps the same assistant message id when a live run becomes a cancelled historical run", () => {
const liveMessages = buildIssueChatMessages({
comments: [],
timelineEvents: [],
linkedRuns: [],
liveRuns: [
{
id: "run-1",
status: "running",
invocationSource: "manual",
triggerDetail: null,
startedAt: "2026-04-06T12:01:00.000Z",
finishedAt: null,
createdAt: "2026-04-06T12:01:00.000Z",
agentId: "agent-1",
agentName: "CodexCoder",
adapterType: "codex_local",
},
],
transcriptsByRunId: new Map([
[
"run-1",
[
{
kind: "assistant",
ts: "2026-04-06T12:01:05.000Z",
text: "Working on it.",
},
],
],
]),
hasOutputForRun: (runId) => runId === "run-1",
currentUserId: "user-1",
});
const cancelledMessages = buildIssueChatMessages({
comments: [],
timelineEvents: [],
linkedRuns: [
{
runId: "run-1",
status: "cancelled",
agentId: "agent-1",
agentName: "CodexCoder",
createdAt: new Date("2026-04-06T12:01:00.000Z"),
startedAt: new Date("2026-04-06T12:01:00.000Z"),
finishedAt: new Date("2026-04-06T12:01:08.000Z"),
},
],
liveRuns: [],
transcriptsByRunId: new Map([
[
"run-1",
[
{
kind: "assistant",
ts: "2026-04-06T12:01:05.000Z",
text: "Working on it.",
},
],
],
]),
hasOutputForRun: (runId) => runId === "run-1",
currentUserId: "user-1",
});
expect(liveMessages).toHaveLength(1);
expect(cancelledMessages).toHaveLength(1);
expect(liveMessages[0]).toMatchObject({
id: "run-assistant:run-1",
status: { type: "running" },
});
expect(cancelledMessages[0]).toMatchObject({
id: "run-assistant:run-1",
status: { type: "complete", reason: "stop" },
metadata: { custom: { runStatus: "cancelled" } },
});
});
it("labels pause-caused cancelled runs as paused by board", () => {
const messages = buildIssueChatMessages({
comments: [],
timelineEvents: [],
linkedRuns: [
{
runId: "run-paused",
status: "cancelled",
agentId: "agent-1",
agentName: "CodexCoder",
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"),
resultJson: { stopReason: "paused" },
},
],
liveRuns: [],
transcriptsByRunId: new Map([
[
"run-paused",
[
{
kind: "assistant",
ts: "2026-04-06T12:01:05.000Z",
text: "Working on it.",
},
],
],
]),
hasOutputForRun: (runId) => runId === "run-paused",
currentUserId: "user-1",
});
expect(messages).toHaveLength(1);
expect(messages[0]?.metadata.custom).toMatchObject({
chainOfThoughtLabel: "Paused by board after 1 minute",
runStatus: "cancelled",
});
});
it("labels error-code-only operator interruptions as interrupted by board", () => {
const messages = buildIssueChatMessages({
comments: [],
timelineEvents: [],
linkedRuns: [
{
runId: "run-interrupted",
status: "cancelled",
agentId: "agent-1",
agentName: "CodexCoder",
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"),
errorCode: "operator_interrupted",
resultJson: null,
},
],
liveRuns: [],
transcriptsByRunId: new Map([
[
"run-interrupted",
[
{
kind: "assistant",
ts: "2026-04-06T12:01:05.000Z",
text: "Working on it.",
},
],
],
]),
hasOutputForRun: (runId) => runId === "run-interrupted",
currentUserId: "user-1",
});
expect(messages).toHaveLength(1);
expect(messages[0]?.metadata.custom).toMatchObject({
chainOfThoughtLabel: "Interrupted by board after 1 minute",
runOperatorInterrupted: true,
runStatus: "cancelled",
});
});
it("can keep succeeded runs without transcript output for embedded run feeds", () => {
const messages = buildIssueChatMessages({
comments: [],
timelineEvents: [],
linkedRuns: [
{
runId: "run-history-2",
status: "succeeded",
agentId: "agent-1",
agentName: "CodexCoder",
createdAt: new Date("2026-04-06T12:01:00.000Z"),
startedAt: new Date("2026-04-06T12:01:00.000Z"),
finishedAt: new Date("2026-04-06T12:03:00.000Z"),
},
],
liveRuns: [],
includeSucceededRunsWithoutOutput: true,
currentUserId: "user-1",
});
expect(messages).toHaveLength(1);
expect(messages[0]).toMatchObject({
id: "run:run-history-2",
role: "system",
metadata: {
custom: {
kind: "run",
runId: "run-history-2",
runAgentName: "CodexCoder",
runStatus: "succeeded",
},
},
});
});
});
describe("stabilizeThreadMessages", () => {
it("reveals live streamed additions at word boundaries instead of character boundaries", () => {
expect(
preserveReadableStreamingRetraction("Writing ", "Writing the pla"),
).toBe("Writing the ");
expect(
preserveReadableStreamingRetraction("Writing ", "Writing the plan "),
).toBe("Writing the plan ");
expect(
preserveReadableStreamingRetraction("Writing ", "Writing the plan."),
).toBe("Writing the plan.");
expect(
preserveReadableStreamingRetraction("Writing ", "Writing draft"),
).toBe("Writing draft");
});
it("holds sliding-window removals until an older paragraph or group boundary drops", () => {
expect(
preserveReadableStreamingRetraction(
"First sentence. Second sentence is visible",
"irst sentence. Second sentence is visible now ",
),
).toBe("irst sentence. Second sentence is visible now ");
expect(
preserveReadableStreamingRetraction(
"First sentence. Second sentence is visible",
"Second sentence is visible now ",
),
).toBe("Second sentence is visible now ");
expect(
preserveReadableStreamingRetraction(
"Paragraph one.\n\nParagraph two is visible",
"Paragraph two is visible now ",
),
).toBe("Paragraph two is visible now ");
expect(
preserveReadableStreamingRetraction(
"The answer is 42",
"42 is the answer",
),
).toBe("42 is the answer");
expect(
preserveReadableStreamingRetraction(
"The quick brown fox jumps over the lazy dog",
"quick brown fox jumps over the lazy dog near the river",
),
).toBe("quick brown fox jumps over the lazy dog near the river");
});
it("keeps live streamed retractions readable until a whole line disappears", () => {
expect(
preserveReadableStreamingRetraction(
"First line\nSecond line\nThird line is complete",
"First line\nSecond line\nThird line",
),
).toBe("First line\nSecond line\nThird line is complete");
expect(
preserveReadableStreamingRetraction(
"First line\nSecond line\nThird line is complete",
"First line\nSecond line",
),
).toBe("First line\nSecond line");
const liveRun: LiveRunForIssue = {
id: "run-live-retract",
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 buildLiveMessages = (text: string) =>
buildIssueChatMessages({
comments: [],
timelineEvents: [],
linkedRuns: [],
liveRuns: [liveRun],
transcriptsByRunId: new Map([
[
"run-live-retract",
[{ kind: "assistant", ts: "2026-04-06T12:04:01.000Z", text }],
],
]),
hasOutputForRun: (runId) => runId === "run-live-retract",
currentUserId: "user-1",
});
const fullText = "First line\nSecond line\nThird line is complete";
const firstStable = stabilizeThreadMessages(
buildLiveMessages(fullText),
[],
new Map(),
);
const partialRetractionStable = stabilizeThreadMessages(
buildLiveMessages("First line\nSecond line\nThird line"),
firstStable.messages,
firstStable.cache,
);
expect(partialRetractionStable.messages).toBe(firstStable.messages);
expect(partialRetractionStable.messages[0]?.content[0]).toMatchObject({
type: "text",
text: fullText,
});
const wholeLineRetractionStable = stabilizeThreadMessages(
buildLiveMessages("First line\nSecond line"),
partialRetractionStable.messages,
partialRetractionStable.cache,
);
expect(wholeLineRetractionStable.messages[0]?.content[0]).toMatchObject({
type: "text",
text: "First line\nSecond line",
});
});
it("reuses unchanged message objects across rebuilds", () => {
const firstPass = buildIssueChatMessages({
comments: [createComment()],
timelineEvents: [],
linkedRuns: [],
liveRuns: [],
currentUserId: "user-1",
});
const firstStable = stabilizeThreadMessages(firstPass, [], new Map());
const secondPass = buildIssueChatMessages({
comments: [
createComment(),
createComment({
id: "comment-2",
body: "New message",
createdAt: new Date("2026-04-06T12:01:00.000Z"),
updatedAt: new Date("2026-04-06T12:01:00.000Z"),
}),
],
timelineEvents: [],
linkedRuns: [],
liveRuns: [],
currentUserId: "user-1",
});
const secondStable = stabilizeThreadMessages(
secondPass,
firstStable.messages,
firstStable.cache,
);
expect(secondStable.messages).toHaveLength(2);
expect(secondStable.messages[0]).toBe(firstStable.messages[0]);
expect(secondStable.messages[1]?.id).toBe("comment-2");
});
it("reuses the previous array when nothing semantically changed", () => {
const firstPass = buildIssueChatMessages({
comments: [createComment()],
timelineEvents: [],
linkedRuns: [],
liveRuns: [],
currentUserId: "user-1",
});
const firstStable = stabilizeThreadMessages(firstPass, [], new Map());
const secondPass = buildIssueChatMessages({
comments: [createComment()],
timelineEvents: [],
linkedRuns: [],
liveRuns: [],
currentUserId: "user-1",
});
const secondStable = stabilizeThreadMessages(
secondPass,
firstStable.messages,
firstStable.cache,
);
expect(secondStable.messages).toBe(firstStable.messages);
});
});
describe("AI recovery presentation", () => {
it.each(["pending", "accepted", "rejected", "expired"] as const)("replaces diagnostic notices with the same-run %s connection card", (status) => {
const interaction: ConnectionIntentInteraction = { ...pendingConnectionIntentInteraction, status, sourceRunId: "failed-run", payload: { ...pendingConnectionIntentInteraction.payload, purpose: "ai" } };
const notice = createComment({ authorType: "system", presentation: { kind: "system_notice", title: "AI connection needs attention", tone: "danger", detailsDefaultOpen: false }, metadata: { version: 1, sourceRunId: "failed-run", sections: [] } });
expect(isRedundantAiRecoveryNotice(notice, [interaction])).toBe(true);
expect(isRedundantAiRecoveryNotice(notice, [{ ...interaction, sourceRunId: "other-run" }])).toBe(false);
expect(isRedundantAiRecoveryNotice(notice, [])).toBe(false);
expect(isRedundantAiRecoveryNotice(notice, [{ ...interaction, payload: { ...interaction.payload, purpose: undefined } }])).toBe(false);
const messages = buildIssueChatMessages({ comments: [notice], interactions: [interaction], timelineEvents: [], linkedRuns: [], liveRuns: [] });
expect(messages).toHaveLength(1);
expect(messages[0]?.metadata.custom).toMatchObject({ kind: "interaction" });
});
});