FN-7764: add cross-type artifact create/list/view test coverage

Extend artifact test coverage to pin creation, listing, and viewing across every artifact type and payload variant on both the agent/dashboard-chat tool surface and the dashboard artifacts route.

- Add a route-level integration test covering list/serve for all artifact types (document, image, video, audio, other) across inline content, uri reference, and binary data payloads, including task-scoped filtering, registry-level (task-less) artifacts, and 404 behavior for uri-only artifacts requested via /media.
- Add an engine-level real-TaskStore test exercising fn_artifact_register/list/view (agent tools) and the dashboard-chat artifact tool for every artifact type and content/uri/dataBase64 variant, asserting list and view output correctness.
- Factor out shared PNG_IMAGE_BYTES fixture and per-type MIME/binary fixtures to keep new assertions concise.

Files changed:
 .../__tests__/artifacts-route-integration.test.ts  | 150 ++++++++++++++++++++-
 .../src/__tests__/agent-artifact-tools.test.ts     | 142 ++++++++++++++++++-
 2 files changed, 287 insertions(+), 5 deletions(-)

Fusion-Task-Id: FN-7764

Fusion-Task-Lineage: 187b3f0f-d1b4-42fe-9658-1ee67870b524

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-09 23:48:45 -07:00
parent cc901c2b9d
commit 587bdfd394
2 changed files with 287 additions and 5 deletions

View File

@@ -6,10 +6,25 @@ import { mkdtempSync, rmSync } from "node:fs";
import http from "node:http";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { TaskStore, type ArtifactWithTask } from "@fusion/core";
import { TaskStore, type ArtifactType, type ArtifactWithTask } from "@fusion/core";
import { createApiRoutes } from "../../routes.js";
import { request as REQUEST } from "../../test-request.js";
const ARTIFACT_TYPES: ArtifactType[] = ["document", "image", "video", "audio", "other"];
const PNG_IMAGE_BYTES = Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII=", "base64");
const BINARY_MEDIA_BY_TYPE: Record<ArtifactType, { mimeType: string; bytes: Buffer }> = {
document: { mimeType: "application/pdf", bytes: Buffer.from("%PDF-1.4\n% FN-7764 document bytes\n") },
image: { mimeType: "image/png", bytes: PNG_IMAGE_BYTES },
video: { mimeType: "video/mp4", bytes: Buffer.from("\x00\x00\x00\x18ftypmp42FN7764-video") },
audio: { mimeType: "audio/mpeg", bytes: Buffer.from("ID3\x03\x00\x00\x00\x00\x00\x0fFN7764-audio") },
other: { mimeType: "application/octet-stream", bytes: Buffer.from("FN-7764 other binary payload") },
};
function contentMimeFor(type: ArtifactType): string {
return type === "document" ? "text/markdown" : "text/plain";
}
/*
* FNXC:ArtifactRegistry 2026-06-27-00:00:
* The mocked artifacts route tests skip the real listArtifacts LEFT JOIN and hand-write media files. This integration test uses a real TaskStore so the Documents Artifacts view contract is pinned end-to-end: registered image artifacts list with task metadata and stream from the real disk write path.
@@ -42,7 +57,7 @@ describe("artifacts route integration", () => {
title: "Render screenshot",
description: "Capture dashboard artifact rendering evidence",
});
const imageBytes = Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII=", "base64");
const imageBytes = PNG_IMAGE_BYTES;
const artifact = await store.registerArtifact({
type: "image",
title: "Dashboard screenshot",
@@ -116,7 +131,7 @@ describe("artifacts route integration", () => {
});
it("a global image artifact still streams from the managed global artifacts directory", async () => {
const imageBytes = Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII=", "base64");
const imageBytes = PNG_IMAGE_BYTES;
const artifact = await store.registerArtifact({
type: "image",
title: "Global screenshot",
@@ -231,6 +246,135 @@ describe("artifacts route integration", () => {
expect(ids).not.toContain(registryArtifact.id);
});
/*
* FNXC:ArtifactRegistry 2026-07-09-17:35:
* FN-7764 pins the route/media side of the same operator-facing invariant as the agent tools: every artifact type and payload class must be discoverable through GET /api/artifacts with task context, and binary or inline media must be viewable without broken image-only assumptions.
*/
it("lists and serves every artifact type across inline, uri, binary, task, and registry-level states", async () => {
const task = await store.createTask({
title: "FN-7764 route artifact matrix",
description: "Capture route-level artifact evidence",
});
const otherTask = await store.createTask({ title: "Other artifact task", description: "Must not leak through taskId filter" });
const created: ArtifactWithTask[] = [];
for (const type of ARTIFACT_TYPES) {
created.push(await store.registerArtifact({
type,
title: `route ${type} inline content`,
description: `inline ${type} route evidence`,
mimeType: contentMimeFor(type),
content: `Inline ${type} route evidence for FN-7764`,
authorId: "agent-route-content",
authorType: "agent",
taskId: task.id,
}) as ArtifactWithTask);
created.push(await store.registerArtifact({
type,
title: `route ${type} uri reference`,
description: `uri ${type} route evidence`,
mimeType: BINARY_MEDIA_BY_TYPE[type].mimeType,
uri: `artifacts/route-${type}-external.bin`,
authorId: "agent-route-uri",
authorType: "agent",
taskId: task.id,
}) as ArtifactWithTask);
created.push(await store.registerArtifact({
type,
title: `route ${type} binary media`,
description: `binary ${type} route evidence`,
mimeType: BINARY_MEDIA_BY_TYPE[type].mimeType,
data: BINARY_MEDIA_BY_TYPE[type].bytes,
authorId: "agent-route-binary",
authorType: "agent",
taskId: task.id,
}) as ArtifactWithTask);
}
const registryOnly = await store.registerArtifact({
type: "other",
title: "route registry-level other binary",
description: "task-less registry evidence",
mimeType: "application/octet-stream",
data: Buffer.from("registry-level-other-bytes"),
authorId: "agent-route-registry",
authorType: "agent",
});
const otherTaskArtifact = await store.registerArtifact({
type: "document",
title: "other task hidden artifact",
content: "This belongs to another task",
authorId: "agent-route-content",
authorType: "agent",
taskId: otherTask.id,
});
const allRes = await REQUEST(app, "GET", "/api/artifacts?limit=100");
expect(allRes.status).toBe(200);
const allArtifacts = allRes.body as ArtifactWithTask[];
for (const artifact of created) {
const listed = allArtifacts.find((candidate) => candidate.id === artifact.id);
expect(listed).toMatchObject({
id: artifact.id,
type: artifact.type,
title: artifact.title,
taskId: task.id,
taskTitle: "route artifact matrix",
});
expect(listed?.taskColumn).toBeTruthy();
expect(listed?.content).toBeUndefined();
}
const registryListed = allArtifacts.find((candidate) => candidate.id === registryOnly.id);
expect(registryListed?.id).toBe(registryOnly.id);
expect(registryListed?.taskId).toBeUndefined();
expect(registryListed?.taskTitle).toBeUndefined();
expect(registryListed?.taskColumn).toBeUndefined();
const taskScopedRes = await REQUEST(app, "GET", `/api/artifacts?taskId=${encodeURIComponent(task.id)}&limit=100`);
expect(taskScopedRes.status).toBe(200);
const taskScopedIds = (taskScopedRes.body as ArtifactWithTask[]).map((artifact) => artifact.id);
expect(taskScopedIds).toEqual(expect.arrayContaining(created.map((artifact) => artifact.id)));
expect(taskScopedIds).not.toContain(registryOnly.id);
expect(taskScopedIds).not.toContain(otherTaskArtifact.id);
const audioFilter = await REQUEST(app, "GET", `/api/artifacts?taskId=${encodeURIComponent(task.id)}&type=audio&authorId=agent-route-binary&q=binary&limit=2&offset=0`);
expect(audioFilter.status).toBe(200);
expect(audioFilter.body).toHaveLength(1);
expect((audioFilter.body as ArtifactWithTask[])[0]).toMatchObject({ type: "audio", authorId: "agent-route-binary", title: "route audio binary media" });
const paged = await REQUEST(app, "GET", `/api/artifacts?taskId=${encodeURIComponent(task.id)}&limit=1&offset=1`);
expect(paged.status).toBe(200);
expect(paged.body).toHaveLength(1);
for (const type of ARTIFACT_TYPES) {
const inline = created.find((artifact) => artifact.type === type && artifact.title.endsWith("inline content"));
expect(inline).toBeDefined();
const inlineMedia = await requestRawBuffer(app, `/api/artifacts/${inline!.id}/media`);
expect(inlineMedia.status).toBe(200);
expect(inlineMedia.headers["content-type"]).toContain(contentMimeFor(type));
expect(inlineMedia.body.toString()).toBe(`Inline ${type} route evidence for FN-7764`);
const binary = created.find((artifact) => artifact.type === type && artifact.title.endsWith("binary media"));
expect(binary).toBeDefined();
const binaryMedia = await requestRawBuffer(app, `/api/artifacts/${binary!.id}/media`);
expect(binaryMedia.status).toBe(200);
expect(binaryMedia.headers["content-type"]).toBe(BINARY_MEDIA_BY_TYPE[type].mimeType);
expect(binaryMedia.body).toEqual(BINARY_MEDIA_BY_TYPE[type].bytes);
const uri = created.find((artifact) => artifact.type === type && artifact.title.endsWith("uri reference"));
expect(uri).toBeDefined();
const missingUriMedia = await REQUEST(app, "GET", `/api/artifacts/${uri!.id}/media`);
expect(missingUriMedia.status).toBe(404);
}
const registryMedia = await requestRawBuffer(app, `/api/artifacts/${registryOnly.id}/media`);
expect(registryMedia.status).toBe(200);
expect(registryMedia.headers["content-type"]).toBe("application/octet-stream");
expect(registryMedia.body).toEqual(Buffer.from("registry-level-other-bytes"));
});
/*
* FNXC:ArtifactRegistry 2026-07-04-20:10:
* FN-7544: a SECOND TaskStore instance against the same DB (mirroring the dashboard-vs-engine or

View File

@@ -1,5 +1,8 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { Artifact, ArtifactWithTask, MessageStore, TaskStore } from "@fusion/core";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import type { Artifact, ArtifactType, ArtifactWithTask, MessageStore, TaskStore } from "@fusion/core";
import { DASHBOARD_USER_ID } from "@fusion/core";
import {
createArtifactListTool,
@@ -79,6 +82,39 @@ function getText(result: any): string {
return first?.type === "text" ? first.text : "";
}
type RealTaskStoreModule = typeof import("@fusion/core");
async function createRealTaskStore() {
const { TaskStore: RealTaskStore } = await vi.importActual<RealTaskStoreModule>("@fusion/core");
const rootDir = mkdtempSync(join(tmpdir(), "agent-artifact-tools-root-"));
const globalDir = mkdtempSync(join(tmpdir(), "agent-artifact-tools-global-"));
const store = new RealTaskStore(rootDir, globalDir, { inMemoryDb: true });
await store.init();
return { store, rootDir, globalDir };
}
function getArtifactId(result: any): string {
const artifactId = result?.details?.artifactId;
expect(typeof artifactId).toBe("string");
return artifactId;
}
const ARTIFACT_TYPES: ArtifactType[] = ["document", "image", "video", "audio", "other"];
function mimeFor(type: ArtifactType, variant: "content" | "uri" | "dataBase64"): string {
if (variant === "dataBase64") return "image/png";
if (variant === "content") return type === "document" ? "text/markdown" : "text/plain";
switch (type) {
case "document": return "application/pdf";
case "image": return "image/png";
case "video": return "video/mp4";
case "audio": return "audio/mpeg";
case "other": return "application/octet-stream";
}
}
function findChatTool(name: "fn_artifact_register" | "fn_artifact_list" | "fn_artifact_view", store: TaskStore, messageStore?: MessageStore) {
const tool = createChatArtifactTools(store, messageStore).find((candidate) => candidate.name === name);
expect(tool).toBeDefined();
@@ -587,3 +623,105 @@ describe("artifact tool factory integration", () => {
expect(getArtifact).toHaveBeenCalledTimes(1);
});
});
describe("artifact tools real-store create/list/view invariant", () => {
let realStore: TaskStore | null = null;
let rootDir: string | null = null;
let globalDir: string | null = null;
afterEach(() => {
realStore?.close();
if (rootDir) rmSync(rootDir, { recursive: true, force: true });
if (globalDir) rmSync(globalDir, { recursive: true, force: true });
realStore = null;
rootDir = null;
globalDir = null;
});
/*
* FNXC:ArtifactRegistry 2026-07-09-17:35:
* FN-7764 exists because operators need concrete confidence that artifact creation and viewing work for every supported type and payload variant, not only the previously verified image path. This real-store matrix pins the agent and dashboard-chat tool contract from registration through list/view output so future changes cannot silently break non-image artifact discovery.
*/
it("creates every artifact type and supported variant through agent and chat tools, then lists and views them", async () => {
const created = await createRealTaskStore();
realStore = created.store as unknown as TaskStore;
rootDir = created.rootDir;
globalDir = created.globalDir;
const task = await realStore.createTask({ title: "FN-7764 artifact matrix", description: "Exercise artifact tool invariant" });
const agentRegister = createArtifactRegisterTool(realStore, AUTHOR_ID);
const agentList = createArtifactListTool(realStore);
const agentView = createArtifactViewTool(realStore);
const chatRegister = findChatTool("fn_artifact_register", realStore);
const expected: Array<{ id: string; type: ArtifactType; title: string; variant: "content" | "uri" | "dataBase64"; authorId: string }> = [];
for (const surface of ["agent", "chat"] as const) {
for (const type of ARTIFACT_TYPES) {
const contentTitle = `${surface} ${type} inline content`;
const contentParams = {
type,
title: contentTitle,
description: `${type} inline artifact created by ${surface}`,
mimeType: mimeFor(type, "content"),
content: `# ${contentTitle}\nInline ${type} evidence for FN-7764.`,
...(surface === "agent" ? { taskId: task.id } : { task_id: task.id }),
};
const contentResult = await runTool(surface === "agent" ? agentRegister : chatRegister, `${surface}-${type}-content`, contentParams);
expect(getText(contentResult)).toContain("Registered artifact");
expected.push({ id: getArtifactId(contentResult), type, title: contentTitle, variant: "content", authorId: surface === "agent" ? AUTHOR_ID : "dashboard-chat" });
const uriTitle = `${surface} ${type} uri reference`;
const uriParams = {
type,
title: uriTitle,
description: `${type} uri artifact created by ${surface}`,
mimeType: mimeFor(type, "uri"),
uri: `artifacts/${surface}-${type}-reference.bin`,
...(surface === "agent" ? { taskId: task.id } : { task_id: task.id }),
};
const uriResult = await runTool(surface === "agent" ? agentRegister : chatRegister, `${surface}-${type}-uri`, uriParams);
expect(getText(uriResult)).toContain("Registered artifact");
expected.push({ id: getArtifactId(uriResult), type, title: uriTitle, variant: "uri", authorId: surface === "agent" ? AUTHOR_ID : "dashboard-chat" });
}
const imageTitle = `${surface} image dataBase64 bytes`;
const dataResult = await runTool(surface === "agent" ? agentRegister : chatRegister, `${surface}-image-dataBase64`, {
type: "image",
title: imageTitle,
description: `PNG bytes created by ${surface}`,
mimeType: "image/png",
dataBase64: PNG_IMAGE_BYTES.toString("base64"),
...(surface === "agent" ? { taskId: task.id } : { task_id: task.id }),
});
expect(getText(dataResult)).toContain("Registered artifact");
expected.push({ id: getArtifactId(dataResult), type: "image", title: imageTitle, variant: "dataBase64", authorId: surface === "agent" ? AUTHOR_ID : "dashboard-chat" });
}
const listResult = await runTool(agentList, "matrix-list-all", { taskId: task.id, limit: 50 });
const listText = getText(listResult);
for (const item of expected) {
expect(listText).toContain(`${item.id} [${item.type}] ${item.title}`);
const viewResult = await runTool(agentView, `view-${item.id}`, { id: item.id });
const viewText = getText(viewResult);
expect(viewText).toContain(`Artifact: ${item.title}`);
expect(viewText).toContain(`Type: ${item.type}`);
expect(viewText).toContain(`Author: ${item.authorId} (agent)`);
expect(viewText).toContain(`Task: ${task.id}`);
if (item.variant === "content") {
expect(viewText).toContain(`Inline ${item.type} evidence for FN-7764.`);
} else {
expect(viewText).toContain("URI: artifacts/");
expect(viewText).not.toContain("Inline ");
}
}
const imageFilter = await runTool(agentList, "matrix-list-image", { taskId: task.id, type: "image", limit: 20 });
const imageText = getText(imageFilter);
expect(imageText).toContain("[image]");
expect(imageText).not.toContain("[audio]");
const chatFilter = await runTool(agentList, "matrix-list-chat", { taskId: task.id, authorId: "dashboard-chat", search: "dataBase64", limit: 10 });
expect(getText(chatFilter)).toContain("dashboard-chat");
expect(getText(chatFilter)).toContain("image dataBase64 bytes");
});
});