Files
PaperClipAI/ui/src/components/MarkdownBody.test.tsx
T
DottaandPaperclip dabb8c5daf test: remove repeated setup and strengthen regression proof (#16022)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Its tests protect company boundaries, approvals, durable tasks,
credentials and the board UI.
> - Several owner suites repeat large database fixtures and render
frames.
> - Some tests repeat stronger proof or pass when the behavior they
describe is broken.
> - This pull request shares exact setup, removes proven duplicate cases
and repairs weak observations.
> - It preserves distinct security, lifecycle, concurrency and
user-facing contracts.
> - The benefit is 5,027 fewer test lines and stronger regression
checks.

## Linked Issues or Issue Description

Refs #15978. This continues the merged test-audit cleanup in [PR
#15978](https://github.com/paperclipai/paperclip/pull/15978), which
removed 11,976 test/support lines. The savings in this pull request are
additional.

Related open work: [PR
#13460](https://github.com/paperclipai/paperclip/pull/13460) changes
legacy OAuth parent/version lock ordering. This pull request retains
those production paths and the existing lock-order test. Its secrets
changes compact fixtures and repair the two AWS wrapper exclusion
observations.

Scoped title searches found no open duplicate audit or
fixture-consolidation PR and no matching audit issue. The problem is
described here using the enhancement template.

**What existing behavior does this improve?**

The maintenance and regression value of the existing server, UI, CLI,
shared and wiki-plugin test suites.

**Subsystem affected**

Cross-cutting test code. Two UI modules also lose unused test inspection
exports.

**Current behavior**

Owner tests repeat database rows, service inputs, HTTP setup and render
frames. Some cases repeat stronger assertions. Two AWS lock tests record
only cleanup events, so an AWS provider write can happen inside the held
lock without failing those observations. Several other negative tests
accept or do not observe the wrong result. Worktree fixtures also use a
temporary path alias that conflicts with the existing canonical-path
checks on macOS.

**Proposed behavior**

Use small local helpers that preserve exact inputs, fresh allocations,
callbacks, query order and await positions. Remove only cases with named
stronger keepers. Tighten negative assertions and observe actual AWS
provider entry while a confirmed lock holder remains parked. Resolve
temporary fixture roots to their canonical paths.

**Reason and benefit**

Remove 5,027 net test lines and 15 redundant registered executions.
Remove 11 lines of unused UI inspection exports. Keep every distinct
company, actor, approval, credential, persistence, stale-write and
concurrency contract. No wall-time speedup is claimed for setup
compaction.

**Breaking changes**

None for application behavior, APIs or stored data. The removed exports
expose private UI test state and have no remaining callers.

## What Changed

- Consolidate repeated runtime, workspace, portability, route,
tool-access, policy, gateway and state-service fixtures.
- Share repeated UI render frames and CLI/wiki-plugin setup while
retaining the original actions and expected results.
- Remove 15 repeated case executions only where the audit records an
exact stronger keeper.
- Repair weak HTTP/UI negative assertions and two AWS account-home lock
observations. Keep the existing durable results and cleanup behavior.
- Remove the unused `__inflightGetCount` and `__secondTickInternals`
inspection exports. Keep observable request coalescing and ticker
lifecycle checks.
- Canonicalize temporary worktree fixture paths without weakening
production path checks.
- Record the owner decisions, savings, independent preservation reviews
and validation in [the dated audit
report](https://github.com/paperclipai/paperclip/blob/58c5c4aa241c85f4665a3159c192888bbb2a5cfa/doc/plans/2026-10-12-test-audit-rounds.md)
and [the compact control
evidence](https://github.com/paperclipai/paperclip/blob/58c5c4aa241c85f4665a3159c192888bbb2a5cfa/doc/plans/2026-10-12-test-audit-controls.json).

## Verification

- Complete owner and retained-sibling baselines passed before each audit
cutover. Independent reviewers checked the full original tests, actual
helper bodies, exact inputs, receipt provenance, async order, fresh
allocations and case registrations.
- All 14 UI/route/wiki mutations failed at their intended assertions:
ten UI/API/ticker controls, one route control and three wiki
persistence/receipt controls. Each mutated production source was
restored to its exact original bytes.
- Both old AWS lock tests passed with their respective AWS-only lock
bypass. Both repaired tests failed at the intended assertions with the
same bypasses. These give 16 intended failures and two passing weak
controls across 18 executions. Each source was restored exactly.
- Final complete state-owner/sibling run: 329/329 passed at code
revision `64370831ebe191e5ce092f85f79b6fb661df7e16`.
- Final `pnpm -r typecheck`, `pnpm check:token-gates` and `pnpm build`:
passed at the same code revision.
- Delivery head `58c5c4aa241c85f4665a3159c192888bbb2a5cfa` includes the
evidence commit and master `4b766b872993045a2e787404f0475503683e5ded`.
All 27 audited test files and both application files match the locally
verified code revision byte-for-byte. Current-head CI validates the
integrated branch, including the incoming master changes.
- All eight production-owner SHA guards for the 16 deliberate failing
mutation controls still match the integrated delivery checkout. The
mutation receipts therefore apply to those current owners.
- Full default `pnpm test:run`: **failed** with exit 1 at frozen code
revision `64370831ebe191e5ce092f85f79b6fb661df7e16`; HEAD and tracked
source bytes remained unchanged. The first general-server group reported
878 passing files, four failing files and six skipped files; 17,846
passing cases, 24 failing cases and 105 skipped cases. Later groups did
not run. Failures were `http-log-redaction` (socket hangup),
`public-mcp` (initial 15-second timeout followed by hosted token
“Refresh before subscribing” failures), `remove-test-home` (macOS
`ENOTEMPTY` where permission errors were expected, plus cleanup
failure), and `workspace-git-snapshot-streaming` (300-second timeout).
All four failing test files and the relevant cleanup/MCP production
owners are unchanged by this audit. Full local success is not claimed.
- Focused complete-owner diagnosis at the same frozen revision: 149/152
passed, three failed, zero skipped. `public-mcp` passed 82/82 and
`http-log-redaction` passed 66/66 unchanged. The two `remove-test-home`
cases reproduced macOS `ENOTEMPTY` against an expected `EACCES`/`EPERM`
contract; the 32 MiB workspace snapshot case reproduced its existing
300,000 ms timeout. HEAD and tracked files stayed clean. Exact blob
comparison confirmed all four test files and the relevant cleanup/MCP
owners match both inspected master
`4b766b872993045a2e787404f0475503683e5ded` and the frozen local
revision. The three remaining local failures are unresolved; no timeout,
retry or assertion was weakened.
- Current-head CI: [workflow
38299262149](https://github.com/paperclipai/paperclip/actions/runs/38299262149)
has passed runner selection and created all downstream
test/typecheck/build gates; those gates are queued, with no failures
reported. Greptile completed successfully at
`58c5c4aa241c85f4665a3159c192888bbb2a5cfa` with [5/5 and zero
comments](https://github.com/paperclipai/paperclip/pull/16022#issuecomment-6131457592).
All review threads are resolved or absent. This pull request will remain
open after verification.

Reviewers can reproduce the required workspace checks:

```sh
pnpm -r typecheck
pnpm check:token-gates
pnpm test:run
pnpm build
```

Browser suites were not run. Their sources and user journeys are
unchanged by this audit.

## Risks

- A fixture helper can hide a specialized input or change object
freshness. Full-program expansion checks and independent reviews
preserve the exact old inputs and execution order.
- Removing duplicate proof can remove a distinct contract by mistake.
The audit records the keeper for each deletion; all distinct security
and lifecycle cases remain.
- Concurrency checks can miss progress during a fixed delay. The AWS
repairs record actual provider entry, confirm holder entry, use measured
entry windows, require exact final event order and drain both operations
in `finally`.
- The full local default run failed. Focused diagnosis reproduced two
macOS cleanup failures and one existing snapshot timeout in unchanged
owners; these remain unresolved. Current-head CI is pending. No test
deadline, retry policy, skip or CI shard was weakened to obtain a pass.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

OpenAI Codex with GPT-6, reasoning, repository inspection, code
execution and delegated audit/review. The runtime does not expose the
exact served model identifier or context-window size; those values are
unavailable and are not inferred from the model catalog.

## 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
- [ ] 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
- [ ] 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-10-12 22:23:11 +00:00

744 lines
30 KiB
TypeScript

// @vitest-environment node
import type { ComponentProps, ReactNode } from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { afterEach, describe, expect, it, vi } from "vitest";
import { renderToStaticMarkup } from "react-dom/server";
import {
buildAgentMentionHref,
buildIssueReferenceHref,
buildProjectMentionHref,
buildRoutineMentionHref,
buildSkillMentionHref,
buildUserMentionHref,
appearanceForPalette,
} from "@paperclipai/shared";
import { ThemeProvider } from "../context/ThemeContext";
import { MarkdownBody } from "./MarkdownBody";
import { queryKeys } from "../lib/queryKeys";
import type { WorkspaceFileAvailabilityTarget } from "../lib/workspace-file-availability";
/** Stands in for a server-confirmed openable reference in the issue's workspace. */
const OPENABLE_AUTO_TARGET: WorkspaceFileAvailabilityTarget = {
workspace: "auto",
projectId: null,
workspaceId: null,
projectName: null,
};
const mockIssuesApi = vi.hoisted(() => ({
get: vi.fn(),
}));
vi.mock("@/lib/router", () => ({
Link: ({
children,
to,
...props
}: { children: ReactNode; to: string } & React.ComponentProps<"a">) => (
<a href={to} {...props}>{children}</a>
),
useLocation: () => ({
pathname: "/PAP/issues/PAP-10306",
search: "",
hash: "",
state: null,
}),
}));
vi.mock("../api/issues", () => ({
issuesApi: mockIssuesApi,
}));
// Defaults to null (no provider) so the existing suite exercises the permissive
// path unchanged. Gating tests override the return value per-case.
const mockUseOptionalCompany = vi.hoisted(() => vi.fn<() => { companies: Array<{ issuePrefix: string }>; selectedCompanyId?: string } | null>(() => null));
vi.mock("../context/CompanyContext", () => ({
useOptionalCompany: mockUseOptionalCompany,
}));
afterEach(() => {
mockUseOptionalCompany.mockReturnValue(null);
});
function renderMarkdown(
children: string,
seededIssues: Array<{ identifier: string; status: string; title?: string; cacheKey?: string }> = [],
props: Partial<ComponentProps<typeof MarkdownBody>> = {},
) {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
});
for (const issue of seededIssues) {
queryClient.setQueryData(queryKeys.issues.detail(issue.cacheKey ?? issue.identifier), {
id: issue.identifier,
identifier: issue.identifier,
status: issue.status,
title: issue.title,
});
}
return renderToStaticMarkup(
<QueryClientProvider client={queryClient}>
<ThemeProvider>
<MarkdownBody {...props}>{children}</MarkdownBody>
</ThemeProvider>
</QueryClientProvider>,
);
}
describe("MarkdownBody", () => {
it("preserves a saved document anchor in an explicit task link", () => {
// Hover/focus can resolve the issue to its plain identifier. Both the old
// fragment-bearing cache key and the corrected plain key model that load.
const html = renderMarkdown("[Saved document](/PAP/issues/PAP-1271#document-output)", [
{ identifier: "PAP-1271", status: "done" },
{ identifier: "PAP-1271", status: "done", cacheKey: "PAP-1271#document-output" },
], { linkIssueReferences: true });
expect(html).toContain('href="/issues/PAP-1271#document-output"');
expect(html).not.toContain("PAP-1271%23document-output");
});
it("renders markdown images without a resolver", () => {
const html = renderToStaticMarkup(
<QueryClientProvider client={new QueryClient()}>
<ThemeProvider>
<MarkdownBody>{"![](/api/attachments/test/content)"}</MarkdownBody>
</ThemeProvider>
</QueryClientProvider>,
);
expect(html).toContain('<img src="/api/attachments/test/content" alt=""/>');
});
it("resolves relative image paths when a resolver is provided", () => {
const html = renderToStaticMarkup(
<QueryClientProvider client={new QueryClient()}>
<ThemeProvider>
<MarkdownBody resolveImageSrc={(src) => `/resolved/${src}`}>
{"![Org chart](images/org-chart.png)"}
</MarkdownBody>
</ThemeProvider>
</QueryClientProvider>,
);
expect(html).toContain('src="/resolved/images/org-chart.png"');
expect(html).toContain('alt="Org chart"');
});
it("renders decision images as inert references without resolving or preloading them", () => {
const resolveImageSrc = vi.fn((src: string) => `https://resolver.invalid/${src}`);
const html = renderMarkdown(
'# Plan\n\n**Keep this step.**\n\n![Evidence](https://provider.invalid/track.png "Provider image")\n\n![Relative](images/proof.png)\n\n![Protocol relative](//provider.invalid/other.png)',
[], { mediaMode: "reference", resolveImageSrc },
);
expect(html).toContain("<h1>Plan</h1>");
expect(html).toContain("<strong>Keep this step.</strong>");
expect(html).toContain("Image: Evidence (https://provider.invalid/track.png)");
expect(html).toContain('title="Provider image"');
expect(html).toContain("Image: Relative (images/proof.png)");
expect(html).toContain("Image: Protocol relative (//provider.invalid/other.png)");
expect(html).not.toMatch(/<(?:img|link|iframe|video|audio)\b/);
expect(resolveImageSrc).not.toHaveBeenCalled();
});
it("keeps diagram source inert in decision media mode", () => {
const source = 'flowchart LR\n A@{ img: "https://provider.invalid/diagram.png" }';
const html = renderMarkdown(`\`\`\`mermaid\n${source}\n\`\`\``, [], { mediaMode: "reference" });
expect(html).toContain('class="language-mermaid"');
expect(html).toContain("https://provider.invalid/diagram.png");
expect(html).not.toContain('class="paperclip-mermaid"');
expect(html).not.toContain("Rendering Mermaid diagram");
});
it("renders user, agent, project, skill, and routine mentions as chips", () => {
const html = renderToStaticMarkup(
<QueryClientProvider client={new QueryClient()}>
<ThemeProvider>
<MarkdownBody>
{`[@Taylor](${buildUserMentionHref("user-123")}) [@CodexCoder](${buildAgentMentionHref("agent-123", "code")}) [@Paperclip App](${buildProjectMentionHref("project-456", "#336699")}) [/release-changelog](${buildSkillMentionHref("skill-789", "release-changelog")}) [/routine:Weekly review](${buildRoutineMentionHref("routine-123")})`}
</MarkdownBody>
</ThemeProvider>
</QueryClientProvider>,
);
expect(html).toContain('href="/company/settings/access"');
expect(html).toContain('data-mention-kind="user"');
expect(html).toContain('href="/agents/agent-123"');
expect(html).toContain('data-mention-kind="agent"');
expect(html).toContain("--paperclip-mention-avatar-image");
expect(html).not.toContain("--paperclip-mention-icon-mask");
expect(html).toContain('href="/projects/project-456"');
expect(html).toContain('data-mention-kind="project"');
expect(html).toContain("--paperclip-mention-project-color:#336699");
expect(html).toContain('href="/skills/skill-789"');
expect(html).toContain('data-mention-kind="skill"');
expect(html).toContain('href="/routines/routine-123"');
expect(html).toContain('data-mention-kind="routine"');
});
it("sanitizes unsafe javascript markdown links", () => {
const html = renderMarkdown("[click me](javascript:alert(document.cookie))");
expect(html).toContain('<a href="" rel="noreferrer"');
expect(html).toContain(">click me</a>");
expect(html).not.toContain("javascript:");
});
it("uses the current company agent appearance for an old saved mention", () => {
mockUseOptionalCompany.mockReturnValue({ companies: [{ issuePrefix: "PAP" }], selectedCompanyId: "company-1" });
const client = new QueryClient();
client.setQueryData(queryKeys.agents.list("company-1"), [
{ id: "agent-123", appearance: appearanceForPalette("electric-grove") },
]);
const html = renderToStaticMarkup(
<QueryClientProvider client={client}>
<ThemeProvider>
<MarkdownBody>{`[@CodexCoder](${buildAgentMentionHref("agent-123", "code")})`}</MarkdownBody>
</ThemeProvider>
</QueryClientProvider>,
);
expect(html).toContain("/cap-v1/electric-grove/rest.png");
expect(html).not.toContain("mention-icon-mask");
});
it("renders raw HTML tags as escaped text", () => {
const html = renderMarkdown(
'<script>fetch("/api/secrets")</script>\n<iframe src="https://example.com"></iframe>\n<p onclick="steal()">Plain text</p>',
);
expect(html).not.toContain("<script>");
expect(html).not.toContain("<iframe");
expect(html).not.toContain("<p onclick");
expect(html).not.toContain('onclick="steal()"');
expect(html).toContain("&lt;script&gt;");
expect(html).toContain("onclick=&quot;steal()&quot;");
expect(html).toContain("Plain text");
});
it("hides markdown HTML comments instead of rendering placeholder text", () => {
const html = renderMarkdown("Before\n\n<!-- -->\n\nAfter");
expect(html).toContain("Before");
expect(html).toContain("After");
expect(html).not.toContain("&lt;!--");
expect(html).not.toContain("--&gt;");
});
it("hides escaped HTML comment placeholders before attachment images", () => {
const html = renderMarkdown("\\<!-- --> ![](/api/attachments/57d0805a-1b95-4fa5-abb4-d0c33e2e649c/content)");
expect(html).toContain('<img src="/api/attachments/57d0805a-1b95-4fa5-abb4-d0c33e2e649c/content" alt=""/>');
expect(html).not.toContain("&lt;!--");
expect(html).not.toContain("--&gt;");
});
it("hides incomplete streamed HTML comment placeholders before attachment images", () => {
const html = renderMarkdown("\\<!-- ![](/api/attachments/57d0805a-1b95-4fa5-abb4-d0c33e2e649c/content)");
expect(html).toContain('<img src="/api/attachments/57d0805a-1b95-4fa5-abb4-d0c33e2e649c/content" alt=""/>');
expect(html).not.toContain("&lt;!--");
});
it("hides incomplete encoded HTML comment placeholders", () => {
const html = renderMarkdown("&lt;!-- -- ![](/api/attachments/57d0805a-1b95-4fa5-abb4-d0c33e2e649c/content)");
expect(html).toContain('<img src="/api/attachments/57d0805a-1b95-4fa5-abb4-d0c33e2e649c/content" alt=""/>');
expect(html).not.toContain("&lt;!--");
expect(html).not.toContain("&amp;lt;!--");
});
it("keeps HTML comment markers when they are literal code content", () => {
const inlineHtml = renderMarkdown("Use `<!-- -->` as a literal.");
const blockHtml = renderMarkdown("```html\n<!-- keep this example -->\n```");
expect(inlineHtml).toContain("&lt;!-- --&gt;");
expect(blockHtml).toContain("&lt;!-- keep this example --&gt;");
});
it("uses soft-break styling by default", () => {
const html = renderMarkdown("First line\nSecond line");
expect(html).toContain("First line<br/>");
expect(html).toContain("Second line");
});
it("can opt out of soft-break styling", () => {
const html = renderToStaticMarkup(
<QueryClientProvider client={new QueryClient()}>
<ThemeProvider>
<MarkdownBody softBreaks={false}>
{"First line\nSecond line"}
</MarkdownBody>
</ThemeProvider>
</QueryClientProvider>,
);
expect(html).not.toContain("<br/>");
});
it("does not inject extra line-break nodes into nested lists", () => {
const html = renderMarkdown("1. Parent item\n - child a\n - child b\n\n2. Second item");
expect(html).not.toContain("[&amp;_p]:whitespace-pre-line");
expect(html).not.toContain("Parent item<br/>");
expect(html).toContain("<ol>");
expect(html).toContain("<ul>");
});
it("linkifies bare issue identifiers in markdown text", () => {
const html = renderMarkdown("Depends on PAP-1271 for the hover state.", [
{ identifier: "PAP-1271", status: "done" },
]);
expect(html).toContain('href="/issues/PAP-1271"');
expect(html).toContain("var(--status-task-icon-done)");
expect(html).toContain(">PAP-1271<");
expect(html).toContain('data-mention-kind="issue"');
expect(html).toContain("paperclip-markdown-issue-ref");
expect(html).not.toContain("paperclip-mention-chip--issue");
});
it("holds the quicklook preview until an issue mention is confirmed readable (PAP-16070)", () => {
// While the fetch is in flight we don't know if the viewer can read the
// issue. The mention stays a clickable link, but the IssueLinkQuicklook
// hover preview (a Radix Popover portal) must NOT mount yet: a private
// mention 404s straight to the locked chip, and mounting then tearing down
// that portal to swap in the chip is the element churn that crashed the
// chat transcript renderer.
const html = renderMarkdown("Depends on PAP-1271 for the hover state.");
expect(html).toContain('data-mention-pending="true"');
expect(html).toContain('data-mention-kind="issue"');
expect(html).toContain("paperclip-markdown-issue-ref");
// Clickability is preserved through the loading state.
expect(html).toContain('href="/issues/PAP-1271"');
expect(html).toContain(">PAP-1271<");
});
it("drops the pending marker once a seeded (readable) issue mention resolves", () => {
const html = renderMarkdown("Depends on PAP-1271 for the hover state.", [
{ identifier: "PAP-1271", status: "done" },
]);
expect(html).toContain('href="/issues/PAP-1271"');
expect(html).not.toContain('data-mention-pending="true"');
});
it("uses concise issue aria labels until a distinct title is available", () => {
const html = renderMarkdown("Depends on PAP-1271 and PAP-1272.", [
{ identifier: "PAP-1271", status: "done" },
{ identifier: "PAP-1272", status: "blocked", title: "Fix hover state" },
]);
expect(html).toContain('aria-label="Issue PAP-1271"');
expect(html).toContain('aria-label="Issue PAP-1272: Fix hover state"');
expect(html).not.toContain('aria-label="Issue PAP-1271: PAP-1271"');
});
it("preserves absolute issue URLs as external links", () => {
const url = "http://remote.example.test:3103/PAPA/issues/PAPA-115#comment-850083f3-24de-43e7-a8cd-bc01f7cc9f0d";
const html = renderMarkdown(`See ${url}.`, [
{ identifier: "PAPA-115", status: "blocked" },
]);
expect(html).toContain(`href="${url}"`);
expect(html).toContain('target="_blank"');
expect(html).toContain("lucide-external-link");
expect(html).not.toContain('href="/issues/PAPA-115"');
expect(html).not.toContain("paperclip-markdown-issue-ref");
});
it("linkifies plain internal issue paths in markdown text", () => {
const html = renderMarkdown("See /issues/PAP-1179 and /PAP/issues/pap-1180 for context.", [
{ identifier: "PAP-1179", status: "blocked" },
{ identifier: "PAP-1180", status: "done" },
]);
expect(html).toContain('href="/issues/PAP-1179"');
expect(html).toContain('href="/issues/PAP-1180"');
expect(html).toContain(">/issues/PAP-1179<");
expect(html).toContain(">/PAP/issues/pap-1180<");
expect(html).toContain("var(--status-task-icon-blocked)");
expect(html).toContain("var(--status-task-icon-done)");
});
it("does not auto-link non-issue internal route paths", () => {
const html = renderMarkdown("Use /issues/new for the creation form, /issues/PAP-42extra as text, and /api/issues for data.");
expect(html).toContain("Use /issues/new for the creation form, /issues/PAP-42extra as text, and /api/issues for data.");
expect(html).not.toContain('href="/issues/new"');
expect(html).not.toContain('href="/issues/PAP-42"');
expect(html).not.toContain('data-mention-kind="issue"');
});
it("rewrites issue scheme links to internal issue links", () => {
const html = renderMarkdown("See issue://PAP-1310 and issue://:PAP-1311.", [
{ identifier: "PAP-1310", status: "done" },
{ identifier: "PAP-1311", status: "blocked" },
]);
expect(html).toContain('href="/issues/PAP-1310"');
expect(html).toContain('href="/issues/PAP-1311"');
expect(html).toContain(">issue://PAP-1310<");
expect(html).toContain(">issue://:PAP-1311<");
expect(html).toContain("var(--status-task-icon-done)");
expect(html).toContain("var(--status-task-icon-blocked)");
});
it("linkifies issue identifiers inside inline code spans", () => {
const html = renderMarkdown("Reference `PAP-1271` here.", [
{ identifier: "PAP-1271", status: "done" },
]);
expect(html).toContain('href="/issues/PAP-1271"');
expect(html).toContain('<code style="overflow-wrap:anywhere;word-break:break-word">PAP-1271</code>');
expect(html).toContain("var(--status-task-icon-done)");
expect(html).toContain("paperclip-markdown-issue-ref");
});
it("renders linked inline-code workspace paths as file viewer links before issue links", () => {
const html = renderMarkdown(
"- **MP4**: [`videos/90-days-paperclip/out/90-days-paperclip-1x1.mp4`](/PAP/issues/PAP-10306 \"Publish handoff\")",
[{ identifier: "PAP-10306", status: "in_review", title: "Publish handoff" }],
{ resolveWorkspaceFileRef: () => OPENABLE_AUTO_TARGET },
);
expect(html).toContain('data-workspace-file-link="true"');
expect(html).toContain('data-workspace-file-path="videos/90-days-paperclip/out/90-days-paperclip-1x1.mp4"');
expect(html).toContain("videos/90-days-paperclip/out/90-days-paperclip-1x1.mp4");
expect(html).not.toContain("max-w-(--sz-38ch)");
expect(html).not.toContain("paperclip-markdown-issue-ref");
expect(html).not.toContain('href="/issues/PAP-10306"');
});
it("renders auto-detected workspace paths as plain code without an availability resolver", () => {
const html = renderMarkdown("Check `ui/src/pages/IssueDetail.tsx:42` please.");
expect(html).not.toContain("data-workspace-file-link");
expect(html).not.toContain("paperclip-workspace-file-link");
expect(html).toContain("ui/src/pages/IssueDetail.tsx:42");
});
it("keeps a non-openable auto-detected path as plain code with no chip affordances", () => {
const html = renderMarkdown(
"Check `ui/src/pages/IssueDetail.tsx:42` please.",
[],
{ resolveWorkspaceFileRef: () => null },
);
expect(html).not.toContain("data-workspace-file-link");
expect(html).not.toContain('role="button"');
expect(html).not.toContain("paperclip-workspace-file-link");
expect(html).toContain("<code");
});
it("keeps an explicit markdown link ordinary when its path is not openable", () => {
const html = renderMarkdown(
"See [`ui/src/a.ts:1`](/PAP/issues/PAP-10306)",
[{ identifier: "PAP-10306", status: "todo" }],
{ resolveWorkspaceFileRef: () => null },
);
expect(html).not.toContain("data-workspace-file-link");
expect(html).toContain('href="/issues/PAP-10306"');
});
it("promotes an openable auto-detected path to a workspace file chip", () => {
const html = renderMarkdown(
"Check `ui/src/pages/IssueDetail.tsx:42` please.",
[],
{ resolveWorkspaceFileRef: () => OPENABLE_AUTO_TARGET },
);
expect(html).toContain('data-workspace-file-link="true"');
expect(html).toContain('data-workspace-file-path="ui/src/pages/IssueDetail.tsx"');
});
it("keeps trailing punctuation outside auto-linked issue references", () => {
const html = renderMarkdown("See PAP-1271: /issues/PAP-1272] and issue://PAP-1273.", [
{ identifier: "PAP-1271", status: "done" },
{ identifier: "PAP-1272", status: "blocked" },
{ identifier: "PAP-1273", status: "todo" },
]);
expect(html).toContain('<a href="/issues/PAP-1271"');
expect(html).toContain('>PAP-1271</a>:');
expect(html).toContain('<a href="/issues/PAP-1272"');
expect(html).toContain('>/issues/PAP-1272</a>]');
expect(html).toContain('<a href="/issues/PAP-1273"');
expect(html).toContain('>issue://PAP-1273</a>.');
});
it("can opt out of issue reference linkification for offline previews", () => {
const html = renderToStaticMarkup(
<QueryClientProvider client={new QueryClient()}>
<ThemeProvider>
<MarkdownBody linkIssueReferences={false}>
{"Depends on PAP-1271 and [manual link](PAP-1271)."}
</MarkdownBody>
</ThemeProvider>
</QueryClientProvider>,
);
expect(html).not.toContain('href="/issues/PAP-1271"');
expect(html).toContain("Depends on PAP-1271");
expect(html).toContain('href="PAP-1271"');
});
it("leaves wiki links as text unless explicitly enabled", () => {
const html = renderMarkdown("See [[wiki/entities/paperclip]].");
expect(html).toContain("[[wiki/entities/paperclip]]");
expect(html).not.toContain('href="/wiki/page/wiki/entities/paperclip.md"');
});
it("renders wiki links with a custom resolver when enabled", () => {
const html = renderMarkdown(
"See [[wiki/entities/paperclip|Paperclip]] and [[wiki/entities/dotta-b]].",
[],
{
enableWikiLinks: true,
resolveWikiLinkHref: (target) => `/wiki/page/${target.endsWith(".md") ? target : `${target}.md`}`,
},
);
expect(html).toContain('href="/wiki/page/wiki/entities/paperclip.md"');
expect(html).toContain('data-paperclip-wiki-link="true"');
expect(html).toContain('data-paperclip-wiki-target="wiki/entities/paperclip"');
expect(html).toContain(">Paperclip</a>");
expect(html).toContain('href="/wiki/page/wiki/entities/dotta-b.md"');
expect(html).toContain(">wiki/entities/dotta-b</a>");
expect(html).not.toContain("[[wiki/entities/paperclip");
});
it("keeps wiki links as text when the custom resolver rejects them", () => {
const html = renderMarkdown(
"See [[wiki/entities/paperclip]].",
[],
{
enableWikiLinks: true,
wikiLinkRoot: "/wiki/page",
resolveWikiLinkHref: () => null,
},
);
expect(html).toContain("[[wiki/entities/paperclip]]");
expect(html).not.toContain('data-paperclip-wiki-link="true"');
expect(html).not.toContain('href="/wiki/page/wiki/entities/paperclip"');
});
it("does not render wiki links inside code spans or code blocks", () => {
const html = renderMarkdown(
"Inline `[[wiki/entities/paperclip]]`.\n\n```md\n[[wiki/entities/dotta-b]]\n```",
[],
{
enableWikiLinks: true,
wikiLinkRoot: "/wiki/page",
},
);
expect(html).toContain("[[wiki/entities/paperclip]]");
expect(html).toContain("[[wiki/entities/dotta-b]]");
expect(html).not.toContain('href="/wiki/page/wiki/entities/paperclip"');
expect(html).not.toContain('href="/wiki/page/wiki/entities/dotta-b"');
});
it("applies wrap-friendly styles to long inline content", () => {
const html = renderMarkdown("averyveryveryveryveryveryveryveryveryverylongtoken");
expect(html).toContain('class="paperclip-markdown prose prose-sm min-w-0 max-w-full break-words overflow-hidden');
expect(html).toContain('style="overflow-wrap:anywhere;word-break:break-word"');
expect(html).toContain("<p");
});
it("applies wrap-friendly styles to long links", () => {
const html = renderMarkdown("[link](https://example.com/reallyreallyreallyreallyreallyreallyreallyreallylong)");
expect(html).toContain('<a href="https://example.com/reallyreallyreallyreallyreallyreallyreallyreallylong"');
expect(html).toContain('style="overflow-wrap:anywhere;word-break:break-word"');
});
it("renders markdown tables in a horizontally scrollable region", () => {
const html = renderMarkdown([
"| Time UTC | Source | Finding | Stalled leaf | Escalation |",
"| --- | --- | --- | --- | --- |",
"| 2026-04-30T14:31:35Z | PAP-2505 | in_review_without_action_path | PAP-2779 | PAP-2910 |",
].join("\n"));
expect(html).toContain('class="paperclip-markdown-table-scroll"');
expect(html).toContain('aria-label="Scrollable table"');
expect(html).toContain('tabindex="0"');
expect(html).toContain("<table>");
expect(html).toContain('style="overflow-wrap:anywhere;word-break:normal"');
});
it("opens external links in a new tab with safe rel attributes", () => {
const html = renderMarkdown("[docs](https://example.com/docs)");
expect(html).toContain("<a href=\"https://example.com/docs\"");
expect(html).toContain('target="_blank"');
expect(html).toContain('rel="noopener noreferrer"');
expect(html).not.toContain("lucide-github");
expect(html).toContain("lucide-external-link");
expect(html).toMatch(/<span style="white-space:nowrap">s<svg[^>]*lucide-external-link/);
});
it("opens GitHub links in a new tab", () => {
const html = renderMarkdown("[pr](https://github.com/paperclipai/paperclip/pull/4099)");
expect(html).toContain('target="_blank"');
expect(html).toContain('rel="noopener noreferrer"');
});
it("does not set target on relative internal links", () => {
const html = renderMarkdown("[settings](/company/settings)");
expect(html).toContain('href="/company/settings"');
expect(html).not.toContain('target="_blank"');
expect(html).toContain('rel="noreferrer"');
});
it("prefixes GitHub markdown links with the GitHub icon glued to the first character", () => {
const html = renderMarkdown("[https://github.com/paperclipai/paperclip/pull/4099](https://github.com/paperclipai/paperclip/pull/4099)");
expect(html).toContain('<a href="https://github.com/paperclipai/paperclip/pull/4099"');
expect(html).toContain('class="lucide lucide-github mr-1 inline h-3.5 w-3.5 align-(--va-0_125em)"');
// The icon and first character "h" must sit in a no-wrap span so the
// icon can never be orphaned on the previous line from the URL text.
expect(html).toMatch(/<span style="white-space:nowrap">.*lucide-github.*?<\/svg>h<\/span>/);
expect(html).toContain("ttps://github.com/paperclipai/paperclip/pull/4099");
expect(html).not.toContain("lucide-external-link");
});
it("prefixes GitHub autolinks with the GitHub icon", () => {
const html = renderMarkdown("See https://github.com/paperclipai/paperclip/issues/1778");
expect(html).toContain('<a href="https://github.com/paperclipai/paperclip/issues/1778"');
expect(html).toContain('class="lucide lucide-github mr-1 inline h-3.5 w-3.5 align-(--va-0_125em)"');
});
;
;
it("does not render the new-tab icon on internal links", () => {
const html = renderMarkdown("[settings](/company/settings)");
expect(html).not.toContain("lucide-external-link");
});
it("keeps fenced code blocks width-bounded and horizontally scrollable", () => {
const html = renderMarkdown("```text\nGET /heartbeat-runs/ca5d23fc-c15b-4826-8ff1-2b6dd11be096/log?offset=2062357&limitBytes=256000\n```");
expect(html).toContain("<pre");
expect(html).toContain('style="max-width:100%;overflow-x:auto"');
});
it("renders a copy button alongside fenced code blocks", () => {
const html = renderMarkdown("```ts\nconst a = 1;\n```");
expect(html).toContain("paperclip-markdown-codeblock");
expect(html).toContain("paperclip-markdown-codeblock-actions");
expect(html).toContain("position:absolute;top:0.4rem;right:0.4rem;display:inline-flex");
expect(html).toContain("paperclip-markdown-codeblock-wrap");
expect(html).toContain('aria-label="Wrap lines"');
expect(html).toContain("position:static;opacity:1;display:inline-flex");
expect(html).toContain("paperclip-markdown-codeblock-copy");
expect(html).toContain('aria-label="Copy code"');
expect(html).toContain("lucide-copy");
});
it("renders code block actions for indented preformatted markdown blocks", () => {
const html = renderMarkdown("Plan:\n\n source fetch/sync -> signal inbox");
expect(html).toContain("paperclip-markdown-codeblock");
expect(html).toContain("paperclip-markdown-codeblock-wrap");
expect(html).toContain('aria-label="Wrap lines"');
expect(html).toContain("paperclip-markdown-codeblock-copy");
});
it("does not render a copy button on inline code", () => {
const html = renderMarkdown("Reference `inline-code` here.");
expect(html).not.toContain("paperclip-markdown-codeblock-copy");
});
it("renders internal issue links and bare identifiers as inline issue refs", () => {
const html = renderMarkdown(`See PAP-42 and [linked task](${buildIssueReferenceHref("PAP-77")}) for follow-up.`, [
{ identifier: "PAP-42", status: "done" },
{ identifier: "PAP-77", status: "blocked" },
]);
expect(html).toContain('href="/issues/PAP-42"');
expect(html).toContain('href="/issues/PAP-77"');
expect(html).toContain('data-mention-kind="issue"');
expect(html).toContain("paperclip-markdown-issue-ref");
expect(html).not.toContain("paperclip-mention-chip--issue");
});
it("gates bare-identifier auto-linking to known company prefixes", () => {
mockUseOptionalCompany.mockReturnValue({ companies: [{ issuePrefix: "PAP" }] });
const html = renderMarkdown("Depends on PAP-1271 and blocked by JIRA-2.", [
{ identifier: "PAP-1271", status: "done" },
{ identifier: "JIRA-2", status: "done" },
]);
// Known prefix links; foreign tracker key stays as plain text.
expect(html).toContain('href="/issues/PAP-1271"');
expect(html).not.toContain('href="/issues/JIRA-2"');
expect(html).toContain("blocked by JIRA-2.");
});
it("stays permissive when companies are loaded but the list is empty", () => {
mockUseOptionalCompany.mockReturnValue({ companies: [] });
const html = renderMarkdown("See JIRA-2 for context.", [
{ identifier: "JIRA-2", status: "done" },
]);
expect(html).toContain('href="/issues/JIRA-2"');
});
it("renders the inline mention status glyph at md (16px / h-4 w-4)", () => {
const html = renderMarkdown("See PAP-1271 for context.", [
{ identifier: "PAP-1271", status: "in_progress" },
]);
// Unified glyph at 16px (PAP-349 round 4: stepped down from lg), with the
// h-4 w-4 class override so the Tailwind sizing matches the intrinsic SVG
// size.
expect(html).toContain('viewBox="0 0 24 24"');
expect(html).toContain('width="16"');
expect(html).toContain('height="16"');
expect(html).toContain("h-4");
expect(html).toContain("w-4");
// PAP-243b: the glyph is optically centered to the body text
// (vertical-align: middle + a 1px lift), not floating off the baseline.
expect(html).toContain("align-middle");
expect(html).not.toContain("align-(--va-0_125em)");
// Legacy h-3 w-3 sizing is gone.
expect(html).not.toContain("mr-1 h-3 w-3");
});
it("never gates explicit internal issue paths, even for unknown prefixes", () => {
mockUseOptionalCompany.mockReturnValue({ companies: [{ issuePrefix: "PAP" }] });
const html = renderMarkdown("See /ACME/issues/ACME-1 for the writeup.", [
{ identifier: "ACME-1", status: "done" },
]);
expect(html).toContain('href="/issues/ACME-1"');
});
});