From 587bdfd3946826c466b69907a5e19d33a03776df Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Thu, 9 Jul 2026 23:48:45 -0700 Subject: [PATCH] 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) --- .../artifacts-route-integration.test.ts | 150 +++++++++++++++++- .../__tests__/agent-artifact-tools.test.ts | 142 ++++++++++++++++- 2 files changed, 287 insertions(+), 5 deletions(-) diff --git a/packages/dashboard/src/routes/__tests__/artifacts-route-integration.test.ts b/packages/dashboard/src/routes/__tests__/artifacts-route-integration.test.ts index af501504b1..0d703d3fbe 100644 --- a/packages/dashboard/src/routes/__tests__/artifacts-route-integration.test.ts +++ b/packages/dashboard/src/routes/__tests__/artifacts-route-integration.test.ts @@ -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 = { + 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 diff --git a/packages/engine/src/__tests__/agent-artifact-tools.test.ts b/packages/engine/src/__tests__/agent-artifact-tools.test.ts index 9188b864d9..ad0686719a 100644 --- a/packages/engine/src/__tests__/agent-artifact-tools.test.ts +++ b/packages/engine/src/__tests__/agent-artifact-tools.test.ts @@ -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("@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"); + }); +});