2026-04-02 11:51:40 -05:00
|
|
|
// @vitest-environment jsdom
|
|
|
|
|
|
|
|
|
|
import { act } from "react";
|
|
|
|
|
import type { ReactNode } from "react";
|
|
|
|
|
import { createRoot } from "react-dom/client";
|
|
|
|
|
import { MemoryRouter } from "react-router-dom";
|
2026-04-06 10:36:31 -05:00
|
|
|
import type { Agent, Approval } from "@paperclipai/shared";
|
2026-04-02 11:51:40 -05:00
|
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
|
import { CommentThread } from "./CommentThread";
|
|
|
|
|
|
|
|
|
|
vi.mock("./MarkdownBody", () => ({
|
|
|
|
|
MarkdownBody: ({ children, className }: { children: ReactNode; className?: string }) => (
|
|
|
|
|
<div className={className}>{children}</div>
|
|
|
|
|
),
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
vi.mock("./MarkdownEditor", () => ({
|
|
|
|
|
MarkdownEditor: ({ value, onChange, placeholder }: {
|
|
|
|
|
value: string;
|
|
|
|
|
onChange: (value: string) => void;
|
|
|
|
|
placeholder?: string;
|
|
|
|
|
}) => (
|
|
|
|
|
<textarea
|
|
|
|
|
aria-label="Comment editor"
|
|
|
|
|
value={value}
|
|
|
|
|
placeholder={placeholder}
|
|
|
|
|
onChange={(event) => onChange(event.target.value)}
|
|
|
|
|
/>
|
|
|
|
|
),
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
vi.mock("./InlineEntitySelector", () => ({
|
|
|
|
|
InlineEntitySelector: () => null,
|
|
|
|
|
}));
|
|
|
|
|
|
2026-04-06 10:36:31 -05:00
|
|
|
vi.mock("./ApprovalCard", () => ({
|
|
|
|
|
ApprovalCard: ({
|
|
|
|
|
approval,
|
|
|
|
|
onApprove,
|
|
|
|
|
onReject,
|
|
|
|
|
}: {
|
|
|
|
|
approval: Approval;
|
|
|
|
|
onApprove?: () => void;
|
|
|
|
|
onReject?: () => void;
|
|
|
|
|
}) => (
|
|
|
|
|
<div>
|
|
|
|
|
<div>{approval.type}</div>
|
|
|
|
|
<div>{String(approval.payload.title ?? "")}</div>
|
|
|
|
|
{onApprove ? <button type="button" onClick={onApprove}>Approve</button> : null}
|
|
|
|
|
{onReject ? <button type="button" onClick={onReject}>Reject</button> : null}
|
|
|
|
|
</div>
|
|
|
|
|
),
|
|
|
|
|
}));
|
|
|
|
|
|
2026-04-02 11:51:40 -05:00
|
|
|
vi.mock("@/plugins/slots", () => ({
|
|
|
|
|
PluginSlotOutlet: () => null,
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
|
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
|
|
|
|
|
|
|
|
|
|
describe("CommentThread", () => {
|
|
|
|
|
let container: HTMLDivElement;
|
2026-04-07 19:16:24 -05:00
|
|
|
let writeTextMock: ReturnType<typeof vi.fn>;
|
|
|
|
|
let execCommandMock: ReturnType<typeof vi.fn>;
|
2026-04-02 11:51:40 -05:00
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
container = document.createElement("div");
|
|
|
|
|
document.body.appendChild(container);
|
|
|
|
|
vi.useFakeTimers();
|
|
|
|
|
vi.setSystemTime(new Date("2026-03-11T12:00:00.000Z"));
|
2026-04-07 19:16:24 -05:00
|
|
|
writeTextMock = vi.fn(async () => {});
|
|
|
|
|
execCommandMock = vi.fn(() => true);
|
|
|
|
|
Object.assign(navigator, {
|
|
|
|
|
clipboard: {
|
|
|
|
|
writeText: writeTextMock,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
Object.defineProperty(window, "isSecureContext", {
|
|
|
|
|
value: true,
|
|
|
|
|
configurable: true,
|
|
|
|
|
});
|
|
|
|
|
document.execCommand = execCommandMock;
|
2026-04-02 11:51:40 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
|
vi.useRealTimers();
|
|
|
|
|
container.remove();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("renders historical runs as timeline rows using the finished time", () => {
|
|
|
|
|
const root = createRoot(container);
|
|
|
|
|
const agent: Agent = {
|
|
|
|
|
id: "agent-1",
|
|
|
|
|
companyId: "company-1",
|
|
|
|
|
name: "CodexCoder",
|
|
|
|
|
urlKey: "codexcoder",
|
|
|
|
|
role: "engineer",
|
|
|
|
|
title: null,
|
|
|
|
|
icon: "code",
|
|
|
|
|
status: "active",
|
|
|
|
|
reportsTo: null,
|
|
|
|
|
capabilities: null,
|
|
|
|
|
adapterType: "process",
|
|
|
|
|
adapterConfig: {},
|
|
|
|
|
runtimeConfig: {},
|
|
|
|
|
budgetMonthlyCents: 0,
|
|
|
|
|
spentMonthlyCents: 0,
|
|
|
|
|
pauseReason: null,
|
|
|
|
|
pausedAt: null,
|
|
|
|
|
permissions: { canCreateAgents: false },
|
|
|
|
|
lastHeartbeatAt: null,
|
|
|
|
|
metadata: null,
|
|
|
|
|
createdAt: new Date("2026-03-11T00:00:00.000Z"),
|
|
|
|
|
updatedAt: new Date("2026-03-11T00:00:00.000Z"),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
act(() => {
|
|
|
|
|
root.render(
|
|
|
|
|
<MemoryRouter>
|
|
|
|
|
<CommentThread
|
|
|
|
|
comments={[]}
|
|
|
|
|
linkedRuns={[{
|
|
|
|
|
runId: "run-12345678abcd",
|
|
|
|
|
status: "succeeded",
|
|
|
|
|
agentId: "agent-1",
|
|
|
|
|
createdAt: "2026-03-11T07:00:00.000Z",
|
|
|
|
|
startedAt: "2026-03-11T08:00:00.000Z",
|
|
|
|
|
finishedAt: "2026-03-11T10:00:00.000Z",
|
|
|
|
|
}]}
|
|
|
|
|
agentMap={new Map([["agent-1", agent]])}
|
|
|
|
|
onAdd={async () => {}}
|
|
|
|
|
/>
|
|
|
|
|
</MemoryRouter>,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const runRow = container.querySelector("#run-run-12345678abcd") as HTMLDivElement | null;
|
|
|
|
|
expect(runRow).not.toBeNull();
|
|
|
|
|
expect(runRow?.className).toContain("py-1.5");
|
|
|
|
|
expect(runRow?.className).toContain("items-center");
|
|
|
|
|
expect(runRow?.className).not.toContain("border");
|
|
|
|
|
expect(container.textContent).toContain("CodexCoder");
|
|
|
|
|
expect(container.textContent).toContain("succeeded");
|
|
|
|
|
expect(container.textContent).toContain("2h ago");
|
|
|
|
|
expect(container.textContent).not.toContain("4h ago");
|
|
|
|
|
const runLink = container.querySelector('a[href="/agents/agent-1/runs/run-12345678abcd"]') as HTMLAnchorElement | null;
|
|
|
|
|
expect(runLink?.textContent).toContain("run-1234");
|
|
|
|
|
expect(runLink?.className).toContain("rounded-md");
|
|
|
|
|
expect(runLink?.className).toContain("px-2");
|
|
|
|
|
|
|
|
|
|
act(() => {
|
|
|
|
|
root.unmount();
|
|
|
|
|
});
|
|
|
|
|
});
|
2026-04-04 13:04:34 -05:00
|
|
|
|
|
|
|
|
it("replaces the composer with a warning when comments are disabled", () => {
|
|
|
|
|
const root = createRoot(container);
|
|
|
|
|
|
|
|
|
|
act(() => {
|
|
|
|
|
root.render(
|
|
|
|
|
<MemoryRouter>
|
|
|
|
|
<CommentThread
|
|
|
|
|
comments={[]}
|
|
|
|
|
composerDisabledReason="Workspace is closed."
|
|
|
|
|
onAdd={async () => {}}
|
|
|
|
|
/>
|
|
|
|
|
</MemoryRouter>,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(container.textContent).toContain("Workspace is closed.");
|
|
|
|
|
expect(container.querySelector('textarea[aria-label="Comment editor"]')).toBeNull();
|
|
|
|
|
expect(container.textContent).not.toContain("Comment");
|
|
|
|
|
|
|
|
|
|
act(() => {
|
2026-04-24 15:50:32 -05:00
|
|
|
root.unmount();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("shows follow-up badges on explicit follow-up comments and timeline rows", () => {
|
|
|
|
|
const root = createRoot(container);
|
|
|
|
|
|
|
|
|
|
act(() => {
|
|
|
|
|
root.render(
|
|
|
|
|
<MemoryRouter>
|
|
|
|
|
<CommentThread
|
|
|
|
|
comments={[{
|
|
|
|
|
id: "comment-1",
|
|
|
|
|
companyId: "company-1",
|
|
|
|
|
issueId: "issue-1",
|
|
|
|
|
authorAgentId: null,
|
|
|
|
|
authorUserId: "local-board",
|
|
|
|
|
body: "Please continue validation.",
|
Add recovery handoff system notices (#5289)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies.
> - Agent runs can end productively while the source issue still lacks a
durable final disposition.
> - That leaves the control plane unsure whether to resume, escalate, or
close the work.
> - Issue comments also need a presentation contract so system-authored
recovery notices can render as first-class thread messages without
overloading normal comments.
> - This pull request adds successful-run handoff recovery, comment
presentation metadata, and system notice rendering.
> - The benefit is stricter task liveness with clearer operator-facing
recovery state.
## What Changed
- Added successful-run handoff decisions, wake payloads, escalation
behavior, and recovery tests.
- Added issue comment presentation metadata with migration
`0078_white_darwin.sql` and shared/server/company portability support.
- Rendered recovery/system notices in issue chat with dedicated UI
components, fixtures, tests, and storybook/lab coverage.
- Included the current recovery model-profile hint patch so automatic
recovery follow-ups use the cheap profile.
## Verification
- `pnpm install --frozen-lockfile`
- `pnpm exec vitest run
server/src/services/recovery/successful-run-handoff.test.ts
ui/src/components/SystemNotice.test.tsx
ui/src/lib/system-notice-comment.test.ts
ui/src/components/IssueChatThreadSystemNotice.test.tsx`
## Risks
- Migration-bearing PR: merge this before any other branch that might
later add a migration.
- The branch touches both recovery services and issue-thread rendering,
so review should pay attention to recovery wake idempotency and comment
metadata compatibility.
## Model Used
- OpenAI GPT-5 Codex via Paperclip `codex_local` adapter, with
shell/git/GitHub CLI tool use.
## 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 run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-05-06 06:05:58 -05:00
|
|
|
authorType: "user",
|
|
|
|
|
presentation: null,
|
|
|
|
|
metadata: null,
|
2026-04-24 15:50:32 -05:00
|
|
|
followUpRequested: true,
|
|
|
|
|
createdAt: new Date("2026-03-11T10:00:00.000Z"),
|
|
|
|
|
updatedAt: new Date("2026-03-11T10:00:00.000Z"),
|
|
|
|
|
}]}
|
|
|
|
|
timelineEvents={[{
|
|
|
|
|
id: "event-1",
|
|
|
|
|
actorType: "agent",
|
|
|
|
|
actorId: "agent-1",
|
|
|
|
|
createdAt: new Date("2026-03-11T10:00:00.000Z"),
|
|
|
|
|
commentId: "comment-1",
|
|
|
|
|
followUpRequested: true,
|
|
|
|
|
}]}
|
|
|
|
|
onAdd={async () => {}}
|
|
|
|
|
/>
|
|
|
|
|
</MemoryRouter>,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(container.textContent).toContain("Follow-up");
|
|
|
|
|
expect(container.textContent).toContain("requested follow-up");
|
|
|
|
|
|
|
|
|
|
act(() => {
|
2026-04-04 13:04:34 -05:00
|
|
|
root.unmount();
|
|
|
|
|
});
|
|
|
|
|
});
|
2026-04-06 10:36:31 -05:00
|
|
|
|
[codex] Improve issue detail and issue-list UX (#3678)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - A core part of that is the operator experience around reading issue
state, agent chat, and sub-task structure
> - The current branch had a long run of issue-detail and issue-list UX
fixes that all improve how humans follow and steer active work
> - Those changes mostly live in the UI/chat surface and should be
reviewed together instead of mixed with workspace/runtime work
> - This pull request packages the issue-detail, chat, markdown, and
sub-issue list improvements into one standalone change
> - The benefit is a cleaner, less jumpy, more reliable issue workflow
on desktop and mobile without coupling it to unrelated server/runtime
refactors
## What Changed
- Stabilized issue chat runtime wiring, optimistic comment handling,
queued-comment cancellation, and composer anchoring during live updates
- Fixed several issue-detail rendering and navigation regressions
including placeholder bleed, local polling scope, mobile inbox-to-issue
transitions, and visible refresh resets
- Improved markdown and rich-content handling with advisory image
normalization, editor fallback behavior, touch mention recovery, and
`issue:` quicklook links
- Refined sub-issue behavior with parent-derived defaults, current-user
inheritance fixes, empty-state cleanup, and a reusable issue-list
presentation for sub-issues
- Added targeted UI tests for the new issue-detail, chat scroll/message,
placeholder-data, markdown, and issue-list behaviors
## Verification
- `pnpm vitest run ui/src/components/IssueChatThread.test.tsx
ui/src/components/MarkdownEditor.test.tsx
ui/src/components/IssuesList.test.tsx
ui/src/context/LiveUpdatesProvider.test.tsx
ui/src/lib/issue-chat-messages.test.ts
ui/src/lib/issue-chat-scroll.test.ts
ui/src/lib/issue-detail-subissues.test.ts
ui/src/lib/query-placeholder-data.test.tsx
ui/src/hooks/usePaperclipIssueRuntime.test.tsx`
## Risks
- Medium: this branch touches the highest-traffic issue-detail UI paths,
so regressions would show up as chat/thread or sub-issue UX glitches
- The changes are UI-heavy and would benefit from reviewer screenshots
or a quick manual browser pass before merge
## Model Used
- OpenAI Codex coding agent (GPT-5-class runtime in Codex CLI; exact
deployed model ID is not exposed in this environment), reasoning
enabled, tool use and local code execution enabled
## 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 run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots
- [ ] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-14 12:50:48 -05:00
|
|
|
it("hides the reopen control and infers reopen for closed agent-assigned issues", async () => {
|
|
|
|
|
const root = createRoot(container);
|
|
|
|
|
const onAdd = vi.fn(async () => {});
|
|
|
|
|
|
|
|
|
|
act(() => {
|
|
|
|
|
root.render(
|
|
|
|
|
<MemoryRouter>
|
|
|
|
|
<CommentThread
|
|
|
|
|
comments={[]}
|
|
|
|
|
issueStatus="done"
|
|
|
|
|
currentAssigneeValue="agent:agent-1"
|
|
|
|
|
onAdd={onAdd}
|
|
|
|
|
/>
|
|
|
|
|
</MemoryRouter>,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(container.textContent).not.toContain("Re-open");
|
|
|
|
|
|
|
|
|
|
const editor = container.querySelector('textarea[aria-label="Comment editor"]') as HTMLTextAreaElement | null;
|
|
|
|
|
const submitButton = Array.from(container.querySelectorAll("button")).find(
|
|
|
|
|
(element) => element.textContent === "Comment",
|
|
|
|
|
) as HTMLButtonElement | undefined;
|
|
|
|
|
expect(editor).not.toBeNull();
|
|
|
|
|
expect(submitButton).toBeDefined();
|
|
|
|
|
|
|
|
|
|
act(() => {
|
|
|
|
|
const valueSetter = Object.getOwnPropertyDescriptor(
|
|
|
|
|
window.HTMLTextAreaElement.prototype,
|
|
|
|
|
"value",
|
|
|
|
|
)?.set;
|
|
|
|
|
valueSetter?.call(editor, "Please pick this back up");
|
|
|
|
|
editor?.dispatchEvent(new Event("input", { bubbles: true }));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await act(async () => {
|
|
|
|
|
submitButton?.click();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(onAdd).toHaveBeenCalledWith("Please pick this back up", true, undefined);
|
|
|
|
|
|
|
|
|
|
act(() => {
|
|
|
|
|
root.unmount();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-06 10:36:31 -05:00
|
|
|
it("renders linked approvals inline in the timeline", () => {
|
|
|
|
|
const root = createRoot(container);
|
|
|
|
|
const agent: Agent = {
|
|
|
|
|
id: "agent-1",
|
|
|
|
|
companyId: "company-1",
|
|
|
|
|
name: "CodexCoder",
|
|
|
|
|
urlKey: "codexcoder",
|
|
|
|
|
role: "engineer",
|
|
|
|
|
title: null,
|
|
|
|
|
icon: "code",
|
|
|
|
|
status: "active",
|
|
|
|
|
reportsTo: null,
|
|
|
|
|
capabilities: null,
|
|
|
|
|
adapterType: "process",
|
|
|
|
|
adapterConfig: {},
|
|
|
|
|
runtimeConfig: {},
|
|
|
|
|
budgetMonthlyCents: 0,
|
|
|
|
|
spentMonthlyCents: 0,
|
|
|
|
|
pauseReason: null,
|
|
|
|
|
pausedAt: null,
|
|
|
|
|
permissions: { canCreateAgents: false },
|
|
|
|
|
lastHeartbeatAt: null,
|
|
|
|
|
metadata: null,
|
|
|
|
|
createdAt: new Date("2026-03-11T00:00:00.000Z"),
|
|
|
|
|
updatedAt: new Date("2026-03-11T00:00:00.000Z"),
|
|
|
|
|
};
|
|
|
|
|
const approval: Approval = {
|
|
|
|
|
id: "approval-1",
|
|
|
|
|
companyId: "company-1",
|
|
|
|
|
type: "request_board_approval",
|
|
|
|
|
requestedByAgentId: "agent-1",
|
|
|
|
|
requestedByUserId: null,
|
|
|
|
|
status: "pending",
|
|
|
|
|
payload: {
|
|
|
|
|
title: "Approve hosting spend",
|
|
|
|
|
text: "Estimated monthly cost is $42.",
|
|
|
|
|
},
|
|
|
|
|
decisionNote: null,
|
|
|
|
|
decidedByUserId: null,
|
|
|
|
|
decidedAt: null,
|
|
|
|
|
createdAt: new Date("2026-03-11T09:00:00.000Z"),
|
|
|
|
|
updatedAt: new Date("2026-03-11T09:00:00.000Z"),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
act(() => {
|
|
|
|
|
root.render(
|
|
|
|
|
<MemoryRouter>
|
|
|
|
|
<CommentThread
|
|
|
|
|
comments={[]}
|
|
|
|
|
linkedApprovals={[approval]}
|
|
|
|
|
agentMap={new Map([["agent-1", agent]])}
|
|
|
|
|
onAdd={async () => {}}
|
|
|
|
|
onApproveApproval={async () => {}}
|
|
|
|
|
onRejectApproval={async () => {}}
|
|
|
|
|
/>
|
|
|
|
|
</MemoryRouter>,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const approvalRow = container.querySelector("#approval-approval-1") as HTMLDivElement | null;
|
|
|
|
|
expect(approvalRow).not.toBeNull();
|
|
|
|
|
expect(container.textContent).toContain("request_board_approval");
|
|
|
|
|
expect(container.textContent).toContain("Approve hosting spend");
|
|
|
|
|
expect(container.textContent).toContain("Approve");
|
|
|
|
|
expect(container.textContent).toContain("Reject");
|
|
|
|
|
|
|
|
|
|
act(() => {
|
|
|
|
|
root.unmount();
|
|
|
|
|
});
|
|
|
|
|
});
|
2026-04-07 19:16:24 -05:00
|
|
|
|
|
|
|
|
it("uses a larger copy control with feedback and a clipboard fallback", async () => {
|
|
|
|
|
const root = createRoot(container);
|
|
|
|
|
|
|
|
|
|
act(() => {
|
|
|
|
|
root.render(
|
|
|
|
|
<MemoryRouter>
|
|
|
|
|
<CommentThread
|
|
|
|
|
comments={[{
|
|
|
|
|
id: "comment-1",
|
|
|
|
|
companyId: "company-1",
|
|
|
|
|
issueId: "issue-1",
|
|
|
|
|
authorAgentId: null,
|
|
|
|
|
authorUserId: "user-1",
|
|
|
|
|
body: "Hello from the comment body",
|
Add recovery handoff system notices (#5289)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies.
> - Agent runs can end productively while the source issue still lacks a
durable final disposition.
> - That leaves the control plane unsure whether to resume, escalate, or
close the work.
> - Issue comments also need a presentation contract so system-authored
recovery notices can render as first-class thread messages without
overloading normal comments.
> - This pull request adds successful-run handoff recovery, comment
presentation metadata, and system notice rendering.
> - The benefit is stricter task liveness with clearer operator-facing
recovery state.
## What Changed
- Added successful-run handoff decisions, wake payloads, escalation
behavior, and recovery tests.
- Added issue comment presentation metadata with migration
`0078_white_darwin.sql` and shared/server/company portability support.
- Rendered recovery/system notices in issue chat with dedicated UI
components, fixtures, tests, and storybook/lab coverage.
- Included the current recovery model-profile hint patch so automatic
recovery follow-ups use the cheap profile.
## Verification
- `pnpm install --frozen-lockfile`
- `pnpm exec vitest run
server/src/services/recovery/successful-run-handoff.test.ts
ui/src/components/SystemNotice.test.tsx
ui/src/lib/system-notice-comment.test.ts
ui/src/components/IssueChatThreadSystemNotice.test.tsx`
## Risks
- Migration-bearing PR: merge this before any other branch that might
later add a migration.
- The branch touches both recovery services and issue-thread rendering,
so review should pay attention to recovery wake idempotency and comment
metadata compatibility.
## Model Used
- OpenAI GPT-5 Codex via Paperclip `codex_local` adapter, with
shell/git/GitHub CLI tool use.
## 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 run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-05-06 06:05:58 -05:00
|
|
|
authorType: "user",
|
|
|
|
|
presentation: null,
|
|
|
|
|
metadata: null,
|
2026-04-07 19:16:24 -05:00
|
|
|
createdAt: new Date("2026-03-11T11:00:00.000Z"),
|
|
|
|
|
updatedAt: new Date("2026-03-11T11:00:00.000Z"),
|
|
|
|
|
}]}
|
|
|
|
|
onAdd={async () => {}}
|
|
|
|
|
/>
|
|
|
|
|
</MemoryRouter>,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const copyButton = Array.from(container.querySelectorAll("button")).find(
|
|
|
|
|
(element) => element.getAttribute("aria-label") === "Copy comment as markdown",
|
|
|
|
|
) as HTMLButtonElement | undefined;
|
|
|
|
|
|
|
|
|
|
expect(copyButton).toBeDefined();
|
|
|
|
|
expect(copyButton?.className).toContain("min-h-8");
|
|
|
|
|
expect(copyButton?.textContent).toContain("Copy");
|
|
|
|
|
|
|
|
|
|
Object.defineProperty(window, "isSecureContext", {
|
|
|
|
|
value: false,
|
|
|
|
|
configurable: true,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await act(async () => {
|
|
|
|
|
copyButton?.click();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(writeTextMock).not.toHaveBeenCalled();
|
|
|
|
|
expect(execCommandMock).toHaveBeenCalledWith("copy");
|
|
|
|
|
expect(copyButton?.textContent).toContain("Copied");
|
|
|
|
|
|
|
|
|
|
act(() => {
|
|
|
|
|
vi.advanceTimersByTime(1500);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(copyButton?.textContent).toContain("Copy");
|
|
|
|
|
|
|
|
|
|
act(() => {
|
|
|
|
|
root.unmount();
|
|
|
|
|
});
|
|
|
|
|
});
|
2026-04-02 11:51:40 -05:00
|
|
|
});
|