8546748759
Dependencies
- Security: body-parser and multer DoS advisories fixed (npm audit: 0).
- Server: Express 5 (query parser kept "extended", req.body defaults to {},
SPA catch-all /{*splat}, send-file helper restores Express 4's dot-folder
rule for sendFile/download, alerts ack route without inline regex),
better-sqlite3 13, js-yaml 5 (server/lib/yaml.js keeps js-yaml 4's default
schema), uuid dropped for crypto.randomUUID, node-pty beta.18, multer 2.5.
- Client: React 19, xterm 6 (WebGL renderer replaces the removed canvas one,
DOM fallback on context loss; Alt+arrow word moves re-mapped), lucide-react 1,
TypeScript 7, i18next/react-i18next/codemirror minors.
Workspace rename step 4
- `agenthub lanes` and `--lane` exit 1 with "renamed: use …"; client api.ts,
the CLI and the MCP server call /api/workspaces; /api/lanes stays mounted.
Server mode (S3): members manage their own Claude Code config
- server/lib/cc-target.js resolves whose config a /cc-config request touches
and gates the router by allowlist; /profiles* and /plugins/update-all stay
admin-only. Member writes go through write-as, CLI calls through sudo as the
member (never -y), reads through openSafe; cc_config_changed reaches only
its owner. Not verified live on an S3 install yet.
Other
- Chat: an HTML file is an artifact only when written whole and not a project
page (index.html, or under src/, public/, templates/, …).
- OpenAPI covers every /api route; openapi-coverage.test.js guards it.
- read-pool test keeps its event loop alive (its worker is unref'd).
- Docs: plan statuses, checklists, CLI.md, LANES.md, SERVER-MODE.md, API.md.
113 lines
4.0 KiB
TypeScript
113 lines
4.0 KiB
TypeScript
/**
|
|
* @file artifactView.test.ts
|
|
* @description What counts as an artifact and how it is shown: kinds by
|
|
* name, a React file only when written whole, a file's versions rebuilt from
|
|
* the conversation's Write/Edit/MultiEdit inputs (a failed call skipped, an
|
|
* edit to unseen text unknown), CSV rows, and the sandboxed documents.
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
import { describe, expect, it } from "vitest";
|
|
import {
|
|
artifactDocument,
|
|
artifactKind,
|
|
artifactVersions,
|
|
frameSandbox,
|
|
isArtifact,
|
|
parseCsv,
|
|
} from "../artifactView";
|
|
import type { TranscriptMessage } from "../types";
|
|
|
|
const use = (id: string, name: string, input: Record<string, unknown>): TranscriptMessage => ({
|
|
type: "assistant",
|
|
timestamp: `2026-10-21T00:00:0${id}.000Z`,
|
|
content: [{ type: "tool_use", id, name, input } as TranscriptMessage["content"][number]],
|
|
});
|
|
const result = (id: string, isError = false): TranscriptMessage => ({
|
|
type: "user",
|
|
sender: "tool",
|
|
timestamp: null,
|
|
content: [
|
|
{
|
|
type: "tool_result",
|
|
id,
|
|
output: "",
|
|
is_error: isError,
|
|
} as TranscriptMessage["content"][number],
|
|
],
|
|
});
|
|
|
|
describe("artifactView", () => {
|
|
it("knows an artifact by its name", () => {
|
|
expect(artifactKind("/a/page.HTML")).toBe("html");
|
|
expect(artifactKind("d.mmd")).toBe("mermaid");
|
|
expect(artifactKind("App.tsx")).toBe("react");
|
|
expect(artifactKind("x.go")).toBe("code");
|
|
expect(isArtifact("report.md", false)).toBe(true);
|
|
expect(isArtifact("App.tsx", false)).toBe(false);
|
|
expect(isArtifact("App.tsx", true)).toBe(true);
|
|
expect(isArtifact("server.js", true)).toBe(false);
|
|
});
|
|
|
|
it("never takes the project's own HTML pages for artifacts", () => {
|
|
expect(isArtifact("/tmp/out/report.html", true)).toBe(true);
|
|
expect(isArtifact("/tmp/out/report.html", false)).toBe(false);
|
|
expect(isArtifact("/repo/index.html", true)).toBe(false);
|
|
expect(isArtifact("/repo/client/INDEX.HTM", true)).toBe(false);
|
|
expect(isArtifact("/repo/public/about.html", true)).toBe(false);
|
|
expect(isArtifact("C:\\repo\\templates\\mail.html", true)).toBe(false);
|
|
expect(isArtifact("/repo/docs/chart.html", true)).toBe(true);
|
|
});
|
|
|
|
it("rebuilds a file's versions from Write, Edit and MultiEdit", () => {
|
|
const msgs = [
|
|
use("1", "Write", { file_path: "/w/a.md", content: "# A\nold line\n" }),
|
|
result("1"),
|
|
use("2", "Edit", { file_path: "/w/a.md", old_string: "old", new_string: "new" }),
|
|
result("2"),
|
|
use("3", "Edit", { file_path: "/w/a.md", old_string: "missing", new_string: "x" }),
|
|
result("3", true),
|
|
use("4", "MultiEdit", {
|
|
file_path: "/w/a.md",
|
|
edits: [{ old_string: "# A", new_string: "# B" }],
|
|
}),
|
|
result("4"),
|
|
use("5", "Write", { file_path: "/w/other.md", content: "no" }),
|
|
];
|
|
const v = artifactVersions(msgs, "/w/a.md");
|
|
expect(v.map((x) => x.content)).toEqual([
|
|
"# A\nold line\n",
|
|
"# A\nnew line\n",
|
|
"# B\nnew line\n",
|
|
]);
|
|
});
|
|
|
|
it("says an edit to text it never saw is of unknown content", () => {
|
|
const v = artifactVersions(
|
|
[use("1", "Edit", { file_path: "a.md", old_string: "a", new_string: "b" })],
|
|
"a.md"
|
|
);
|
|
expect(v).toHaveLength(1);
|
|
expect(v[0]!.content).toBeNull();
|
|
});
|
|
|
|
it("reads CSV with quotes and newlines inside them", () => {
|
|
expect(parseCsv('a,b\n"x, y","say ""hi""\nthere"\n')).toEqual([
|
|
["a", "b"],
|
|
["x, y", 'say "hi"\nthere'],
|
|
]);
|
|
});
|
|
|
|
it("builds sandboxed documents that keep the source inert inside a script", () => {
|
|
const doc = artifactDocument("react", "export default () => <p>a</script>b</p>", false)!;
|
|
expect(doc).toContain("babel");
|
|
expect(doc).not.toContain("a</script>b");
|
|
expect(artifactDocument("mermaid", "graph TD; A-->B<img>", true)).toContain(
|
|
"A-->B<img>"
|
|
);
|
|
expect(artifactDocument("code", "x", false)).toBeNull();
|
|
expect(frameSandbox("svg")).toBe("");
|
|
expect(frameSandbox("html")).not.toContain("allow-same-origin");
|
|
});
|
|
});
|