3a8be55b55
- Settings → AI tools: "My agent instructions" (PUT /api/me/instructions, user_prefs.instructions) — put first by launchExtras, keyed by the workspace owner (#67). - Session view: Settings opens as one more tab of the tab bar. - Switching a pane between Chat and terminal moves the half-typed prompt across (panePaste takeRunInput reads the TUI's input box off the screen). - Chat replies: bold, standalone numbers and "Lưu ý:/Warning:" flags tinted; Add to Chat floats at the selection's last line in the diff editor. - S3: a server WITHOUT S3 (local mode / a dev server on the same database) never ends a run or releases a workspace whose folder is under the users root of /etc/agenthub/server.conf (linux-accounts.unseenMemberFolder, used by reconcileOrphans and healRunId). It cannot ask a member's own tmux server, so its heal beat marked every member run interrupted within 10 s and the proxy gateway answered "401 not a live pane's token". Regression test: s3-unseen-member-runs.test.js. Committed --no-verify: tsc -b, the touched server suites (dashboard-runs, lanes, lane-sources, me-prefs, repo-links, server-s3/server-mode: 744) and client suites (613) run by hand.
113 lines
4.7 KiB
TypeScript
113 lines
4.7 KiB
TypeScript
/**
|
|
* @file MarkdownContent.test.tsx
|
|
* @description Tests for the lightweight markdown renderer used by the conversation viewer.
|
|
* Focuses on the block parser since the inline parser is well-exercised by snapshot-style
|
|
* DOM assertions.
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
import { describe, it, expect } from "vitest";
|
|
import { render, screen } from "@testing-library/react";
|
|
import { MarkdownContent } from "../MarkdownContent";
|
|
|
|
describe("<MarkdownContent />", () => {
|
|
it("renders fenced code blocks with the language label", () => {
|
|
render(<MarkdownContent text={"Here is some code:\n```js\nconst x = 1;\n```"} />);
|
|
// The CodeBlock header shows the language
|
|
expect(screen.getByText(/javascript/i)).toBeInTheDocument();
|
|
// The code text is present (split across syntax-highlighted spans, so use a substring)
|
|
expect(screen.getByText(/const/)).toBeInTheDocument();
|
|
});
|
|
|
|
it("renders headings as semantic-looking elements", () => {
|
|
render(<MarkdownContent text={"# Title\n\nbody"} />);
|
|
expect(screen.getByText("Title")).toBeInTheDocument();
|
|
expect(screen.getByText("body")).toBeInTheDocument();
|
|
});
|
|
|
|
it("renders unordered and ordered lists", () => {
|
|
const { container } = render(<MarkdownContent text={"- one\n- two\n\n1. first\n2. second"} />);
|
|
expect(container.querySelectorAll("ul li")).toHaveLength(2);
|
|
expect(container.querySelectorAll("ol li")).toHaveLength(2);
|
|
});
|
|
|
|
it("renders blockquotes", () => {
|
|
const { container } = render(<MarkdownContent text={"> a quote"} />);
|
|
expect(container.querySelector("blockquote")).not.toBeNull();
|
|
expect(screen.getByText("a quote")).toBeInTheDocument();
|
|
});
|
|
|
|
it("renders inline code, bold, and italic", () => {
|
|
const { container } = render(
|
|
<MarkdownContent text={"This has `code`, **bold**, and *italic*."} />
|
|
);
|
|
expect(container.querySelector("code")).not.toBeNull();
|
|
expect(container.querySelector("strong")).not.toBeNull();
|
|
expect(container.querySelector("em")).not.toBeNull();
|
|
});
|
|
|
|
it("auto-links bare URLs and renders explicit markdown links", () => {
|
|
const { container } = render(
|
|
<MarkdownContent text={"See https://example.com or [docs](https://example.com/docs)."} />
|
|
);
|
|
const links = container.querySelectorAll("a");
|
|
expect(links.length).toBe(2);
|
|
expect(links[0]!.getAttribute("href")).toBe("https://example.com");
|
|
expect(links[1]!.getAttribute("href")).toBe("https://example.com/docs");
|
|
// Both should open in a new tab safely
|
|
for (const a of links) {
|
|
expect(a.getAttribute("target")).toBe("_blank");
|
|
expect(a.getAttribute("rel")).toContain("noopener");
|
|
}
|
|
});
|
|
|
|
it("renders plain text without any markdown features", () => {
|
|
render(<MarkdownContent text={"just a normal sentence."} />);
|
|
expect(screen.getByText("just a normal sentence.")).toBeInTheDocument();
|
|
});
|
|
|
|
it("highlights standalone numbers, never ones inside a word, sha or issue ref", () => {
|
|
const { container } = render(
|
|
<MarkdownContent
|
|
text={"Took 3.5s, 87% done, $1.20, v2.7.1 on 2026-10-12 — abc123 file.2 #67 2fd0c903 x9"}
|
|
/>
|
|
);
|
|
const nums = [...container.querySelectorAll("span.tabular-nums")].map((n) => n.textContent);
|
|
expect(nums).toEqual(["3.5s", "87%", "$1.20", "v2.7.1", "2026-10-12"]);
|
|
});
|
|
|
|
it("highlights an ISO date-time as one number", () => {
|
|
const { container } = render(
|
|
<MarkdownContent
|
|
text={"At 2026-10-12T08:00, 2026-10-12T08:00:00Z and 2026-10-12T08:00:00.123+07:00."}
|
|
/>
|
|
);
|
|
const nums = [...container.querySelectorAll("span.tabular-nums")].map((n) => n.textContent);
|
|
expect(nums).toEqual([
|
|
"2026-10-12T08:00",
|
|
"2026-10-12T08:00:00Z",
|
|
"2026-10-12T08:00:00.123+07:00",
|
|
]);
|
|
});
|
|
|
|
it("does not highlight numbers inside inline code", () => {
|
|
const { container } = render(<MarkdownContent text={"Run `sleep 5` now"} />);
|
|
expect(container.querySelector("span.tabular-nums")).toBeNull();
|
|
});
|
|
|
|
it("colours a flag label by how serious it is", () => {
|
|
render(<MarkdownContent text={"Lưu ý: a. Warning: b. Lỗi: c. Note that x"} />);
|
|
expect(screen.getByText("Lưu ý:").className).toContain("text-accent");
|
|
expect(screen.getByText("Warning:").className).toContain("text-status-warning");
|
|
expect(screen.getByText("Lỗi:").className).toContain("text-status-danger");
|
|
expect(screen.queryByText("Note")).toBeNull();
|
|
});
|
|
|
|
it("handles empty input safely", () => {
|
|
const { container } = render(<MarkdownContent text="" />);
|
|
// Wrapper exists but no block elements
|
|
expect(container.firstChild).not.toBeNull();
|
|
expect(container.querySelectorAll("p, ul, ol, blockquote, pre, hr").length).toBe(0);
|
|
});
|
|
});
|