294 lines
12 KiB
TypeScript
294 lines
12 KiB
TypeScript
import { describe, expect, it } from "bun:test";
|
|
import type { UIMessage } from "ai";
|
|
|
|
import {
|
|
deriveOpenTargets,
|
|
isCollectibleArtifactTarget,
|
|
selectAutoOpenTarget,
|
|
} from "../src/react-app/domains/session/artifacts/open-target";
|
|
|
|
function message(id: string, role: "user" | "assistant", text: string): UIMessage {
|
|
return { id, role, parts: [{ type: "text", text, state: "done" }] };
|
|
}
|
|
|
|
function toolMessage(id: string, toolName: string, input: Record<string, unknown>, output: unknown) {
|
|
return {
|
|
id,
|
|
role: "assistant",
|
|
parts: [{
|
|
type: "dynamic-tool",
|
|
toolName,
|
|
toolCallId: `${id}_tool`,
|
|
state: "output-available",
|
|
input,
|
|
output,
|
|
}],
|
|
};
|
|
}
|
|
|
|
describe("deriveOpenTargets", () => {
|
|
it("extracts file and localhost URL targets from recent assistant output", () => {
|
|
const targets = deriveOpenTargets([
|
|
toolMessage("msg_tool", "write", { filePath: "reports/revenue.xlsx" }, { filePath: "reports/revenue.xlsx" }),
|
|
message("msg_1", "assistant", "Created reports/revenue.xlsx and started http://localhost:5173 for preview."),
|
|
]);
|
|
|
|
expect(targets.map((target) => target.value)).toContain("reports/revenue.xlsx");
|
|
expect(targets.map((target) => target.value)).toContain("http://localhost:5173");
|
|
expect(targets.find((target) => target.value === "reports/revenue.xlsx")?.preview).toBe("sheet");
|
|
});
|
|
|
|
it("extracts websocket URLs so local socket/dev-server hints stay visible", () => {
|
|
const targets = deriveOpenTargets([
|
|
toolMessage("msg_tool", "write", { filePath: "dist/index.html" }, { filePath: "dist/index.html" }),
|
|
message("msg_1", "assistant", "Socket open at ws://localhost:5173/socket and preview at dist/index.html"),
|
|
]);
|
|
|
|
expect(targets.map((target) => target.value)).toContain("ws://localhost:5173/socket");
|
|
expect(targets.map((target) => target.value)).toContain("dist/index.html");
|
|
});
|
|
|
|
it("normalizes Workspace/<id>/ prefixes from artifact paths", () => {
|
|
const targets = deriveOpenTargets([
|
|
toolMessage("msg_tool_1", "write", { filePath: "Workspace/32423/reports/artifact-eval.md" }, { filePath: "Workspace/32423/reports/artifact-eval.md" }),
|
|
toolMessage("msg_tool_2", "write", { filePath: "Workspace/32423/reports/artifact-eval.csv" }, { filePath: "Workspace/32423/reports/artifact-eval.csv" }),
|
|
message("msg_1", "assistant", "See Workspace/32423/reports/artifact-eval.md and Workspace/32423/reports/artifact-eval.csv"),
|
|
]);
|
|
|
|
expect(targets.map((target) => target.value)).toContain("reports/artifact-eval.md");
|
|
expect(targets.map((target) => target.value)).toContain("reports/artifact-eval.csv");
|
|
});
|
|
|
|
it("prefers explicit dynamic tool metadata over prose guesses", () => {
|
|
const targets = deriveOpenTargets([
|
|
toolMessage("msg_tool", "write", { path: "summary.md" }, { path: "summary.md" }),
|
|
]);
|
|
|
|
expect(targets[0]).toMatchObject({ value: "summary.md", preview: "markdown", confidence: 95 });
|
|
});
|
|
|
|
it("opens standalone Mermaid files through the Markdown preview", () => {
|
|
const targets = deriveOpenTargets([
|
|
toolMessage("msg_tool", "write", { path: "diagrams/architecture.mmd" }, { path: "diagrams/architecture.mmd" }),
|
|
]);
|
|
|
|
expect(targets[0]).toMatchObject({ value: "diagrams/architecture.mmd", preview: "markdown", confidence: 95 });
|
|
});
|
|
|
|
it("extracts filePath metadata from write tools", () => {
|
|
const targets = deriveOpenTargets([
|
|
toolMessage("msg_tool", "write", { filePath: "reports/summary.md" }, { filePath: "reports/summary.md" }),
|
|
]);
|
|
|
|
expect(targets[0]).toMatchObject({ value: "reports/summary.md", preview: "markdown", confidence: 95 });
|
|
});
|
|
|
|
it("collects written code files for the artifact editor", () => {
|
|
const targets = deriveOpenTargets([
|
|
toolMessage("msg_tool", "write", { filePath: "src/widget.tsx" }, { filePath: "src/widget.tsx" }),
|
|
]);
|
|
|
|
const target = targets[0];
|
|
|
|
expect(target).toMatchObject({ value: "src/widget.tsx", preview: "code", confidence: 95 });
|
|
expect(target ? isCollectibleArtifactTarget({ ...target, exists: true }) : false).toBe(true);
|
|
});
|
|
|
|
it("uses markdown link href once when the label is the href basename", () => {
|
|
const targets = deriveOpenTargets([
|
|
message("msg_1", "assistant", "I generated the file [native-link.txt](reports/native-link.txt)."),
|
|
]);
|
|
|
|
expect(targets.map((target) => target.value)).toEqual(["reports/native-link.txt"]);
|
|
});
|
|
|
|
it("keeps distinct markdown link labels as normal file mentions", () => {
|
|
const targets = deriveOpenTargets([
|
|
message("msg_1", "assistant", "I generated the file [summary.md](reports/native-link.txt)."),
|
|
]);
|
|
|
|
expect(targets.map((target) => target.value).sort()).toEqual(["reports/native-link.txt", "summary.md"]);
|
|
});
|
|
|
|
it("extracts PowerPoint decks from assistant artifact summaries", () => {
|
|
const targets = deriveOpenTargets([
|
|
message("msg_1", "assistant", "Updated file: decks/openwork-vertebrae-deck.pptx"),
|
|
]);
|
|
const deck = targets.find((target) => target.value === "decks/openwork-vertebrae-deck.pptx");
|
|
|
|
expect(deck).toMatchObject({ preview: "slides", confidence: 65 });
|
|
expect(deck ? isCollectibleArtifactTarget({ ...deck, exists: true }) : false).toBe(true);
|
|
});
|
|
|
|
it("extracts artifact paths from OpenWork extension call metadata", () => {
|
|
const targets = deriveOpenTargets([
|
|
toolMessage("msg_tool", "openwork_extension_call", {
|
|
extensionId: "openai-image-generation",
|
|
action: "image_generate",
|
|
}, {
|
|
ok: true,
|
|
extensionId: "openai-image-generation",
|
|
action: "image_generate",
|
|
path: "artifacts/potato.png",
|
|
result: {
|
|
path: "artifacts/potato.png",
|
|
bytes: 12345,
|
|
model: "gpt-image-2",
|
|
},
|
|
}),
|
|
]);
|
|
|
|
expect(targets[0]).toMatchObject({ value: "artifacts/potato.png", preview: "image", confidence: 95 });
|
|
});
|
|
|
|
it("extracts artifact targets from attachment sources", () => {
|
|
const targets = deriveOpenTargets([
|
|
{
|
|
id: "msg_attachment",
|
|
role: "assistant",
|
|
parts: [{
|
|
type: "source-document",
|
|
sourceId: "attachment-source",
|
|
mediaType: "text/csv",
|
|
title: "customers.csv",
|
|
filename: "reports/customers.csv",
|
|
}],
|
|
},
|
|
]);
|
|
|
|
expect(targets[0]).toMatchObject({ value: "reports/customers.csv", preview: "sheet", confidence: 95 });
|
|
});
|
|
|
|
it("keeps URI-backed source documents as URL targets when filename is missing", () => {
|
|
const targets = deriveOpenTargets([
|
|
{
|
|
id: "msg_source",
|
|
role: "assistant",
|
|
parts: [{
|
|
type: "source-document",
|
|
sourceId: "url-source",
|
|
mediaType: "text/html",
|
|
title: "https://example.com/docs/report.html",
|
|
}],
|
|
},
|
|
]);
|
|
|
|
expect(targets[0]).toMatchObject({ kind: "url", value: "https://example.com/docs/report.html", preview: "browser" });
|
|
});
|
|
|
|
it("does not extract file artifacts from read tool metadata or output", () => {
|
|
const targets = deriveOpenTargets([
|
|
toolMessage(
|
|
"msg_tool",
|
|
"read",
|
|
{ filePath: "reports/source.md" },
|
|
{ content: "Reviewed reports/source.md and referenced reports/source.csv" },
|
|
),
|
|
message("msg_2", "assistant", "Reviewed reports/source.md and reports/source.csv."),
|
|
]);
|
|
|
|
expect(targets.map((target) => target.value)).not.toContain("reports/source.md");
|
|
expect(targets.map((target) => target.value)).not.toContain("reports/source.csv");
|
|
});
|
|
|
|
it("extracts paths written by apply_patch metadata", () => {
|
|
const targets = deriveOpenTargets([
|
|
toolMessage("msg_tool", "apply_patch", {
|
|
patchText: "*** Begin Patch\n*** Add File: reports/new-report.md\n+hello\n*** Update File: reports/existing-report.csv\n@@\n-old\n+new\n*** End Patch",
|
|
}, "Success. Updated files."),
|
|
]);
|
|
|
|
expect(targets.map((target) => target.value)).toContain("reports/new-report.md");
|
|
expect(targets.map((target) => target.value)).toContain("reports/existing-report.csv");
|
|
});
|
|
|
|
it("does not turn package search results into artifacts", () => {
|
|
const targets = deriveOpenTargets([
|
|
toolMessage("msg_tool", "glob", { pattern: "**/package.json" }, {
|
|
files: [
|
|
"package.json",
|
|
"apps/app/package.json",
|
|
"packages/ui/package.json",
|
|
"reports/revenue.csv",
|
|
],
|
|
}),
|
|
message("msg_2", "assistant", "Found package.json, apps/app/package.json, and reports/revenue.csv"),
|
|
]);
|
|
|
|
expect(targets.map((target) => target.value)).not.toContain("package.json");
|
|
expect(targets.map((target) => target.value)).not.toContain("apps/app/package.json");
|
|
expect(targets.map((target) => target.value)).not.toContain("packages/ui/package.json");
|
|
expect(targets.map((target) => target.value)).not.toContain("reports/revenue.csv");
|
|
});
|
|
|
|
it("does not turn discovery tool markdown listings into artifacts", () => {
|
|
const targets = deriveOpenTargets([
|
|
toolMessage("msg_write", "write", { filePath: "reports/created-report.md" }, { filePath: "reports/created-report.md" }),
|
|
toolMessage("msg_tool", "glob", { pattern: "**/*.md" }, {
|
|
files: [
|
|
"README.md",
|
|
".opencode/skills/example/SKILL.md",
|
|
"reports/created-report.md",
|
|
],
|
|
}),
|
|
message("msg_2", "assistant", "Created reports/created-report.md as the deliverable."),
|
|
]);
|
|
|
|
expect(targets.map((target) => target.value)).toContain("reports/created-report.md");
|
|
expect(targets.map((target) => target.value)).not.toContain("README.md");
|
|
expect(targets.map((target) => target.value)).not.toContain(".opencode/skills/example/SKILL.md");
|
|
});
|
|
|
|
it("does not collect server-verified missing file targets", () => {
|
|
const target = deriveOpenTargets([
|
|
toolMessage("msg_tool", "write", { filePath: "index.html" }, { filePath: "index.html" }),
|
|
message("msg_1", "assistant", "Preview file: index.html"),
|
|
])[0];
|
|
|
|
expect(target).toMatchObject({ value: "index.html", preview: "html" });
|
|
expect(isCollectibleArtifactTarget({ ...target, exists: false })).toBe(false);
|
|
expect(isCollectibleArtifactTarget({ ...target, exists: true })).toBe(true);
|
|
});
|
|
|
|
it("does not auto-open generated html files or localhost browser previews", () => {
|
|
const targets = deriveOpenTargets([
|
|
toolMessage("msg_tool", "write", { filePath: "public/index.html" }, { filePath: "public/index.html" }),
|
|
message("msg_1", "assistant", "Created public/index.html. API: `http://localhost:3000/api/info`. App: `http://localhost:3000`."),
|
|
]).map((target) => ({ ...target, exists: target.kind === "url" || target.value === "public/index.html" }));
|
|
|
|
expect(targets.map((target) => target.value)).toContain("http://localhost:3000/api/info");
|
|
expect(targets.map((target) => target.value)).toContain("http://localhost:3000");
|
|
expect(selectAutoOpenTarget(targets)).toBeNull();
|
|
});
|
|
|
|
it("normalizes escaped localhost root URL variants into one target", () => {
|
|
const targets = deriveOpenTargets([
|
|
message("msg_1", "assistant", "App: `http://localhost:3000/\\` and also http://localhost:3000//"),
|
|
]);
|
|
|
|
expect(targets.filter((target) => target.value === "http://localhost:3000")).toHaveLength(1);
|
|
expect(targets.map((target) => target.name)).not.toContain("\\");
|
|
});
|
|
|
|
it("keeps accessible targets from earlier session messages", () => {
|
|
const targets = deriveOpenTargets([
|
|
toolMessage("msg_tool", "write", { filePath: "reports/earlier.csv" }, { filePath: "reports/earlier.csv" }),
|
|
message("msg_1", "assistant", "Created reports/earlier.csv"),
|
|
...Array.from({ length: 12 }, (_, index) => message(`msg_noise_${index}`, "assistant", `Status update ${index + 1}`)),
|
|
message("msg_last", "assistant", "Server running at http://localhost:3000"),
|
|
]);
|
|
|
|
expect(targets.map((target) => target.value)).toContain("reports/earlier.csv");
|
|
expect(targets.map((target) => target.value)).toContain("http://localhost:3000");
|
|
});
|
|
|
|
it("does not auto-open high-confidence deliverables or browser previews", () => {
|
|
const targets = deriveOpenTargets([
|
|
toolMessage("msg_tool", "write", { filePath: "data/customers.csv" }, { filePath: "data/customers.csv" }),
|
|
message("msg_1", "assistant", "Created data/customers.csv and see https://example.com for docs."),
|
|
]).map((target) => ({ ...target, exists: target.kind === "file" }));
|
|
|
|
expect(selectAutoOpenTarget(targets)).toBeNull();
|
|
});
|
|
});
|