import { mkdtemp, mkdir, writeFile, readFile, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { prisma, resetDb, seedTestOrganization, testSecretEnvelope, DEFAULT_ORG_ID } from "./helpers.js"; import { createPdfToMdBundleAdapter, CapabilityPathEscape } from "../../src/capability/pdfToMdBundle.js"; import { DocmindClientError, type CapabilityProviderClient, type DocmindParseResult } from "../../src/capability/docmindClient.js"; import type { CapabilitySecretPayload } from "../../src/capability/types.js"; const CAPABILITY_ID = "pdf_to_md_bundle"; const PROVIDER_ID = "aliyun_docmind"; /** * ADR-0027: pdf_to_md_bundle adapter contract tests. Proves: * - workspace containment (input + output confined to run workspace) * - fail-closed credential resolution (no ACTIVE connection → error) * - UsageFact attribution with non-token meter (pages), costSource rules * - outputs (md + images) written into workspace * - docmind client errors propagate * The client is mocked; the prisma + envelope + filesystem are real. */ describe("pdf_to_md_bundle capability adapter (ADR-0027)", () => { let workspaceRoot: string; let runId: string; beforeEach(async () => { await resetDb(); await seedTestOrganization(); workspaceRoot = await mkdtemp(join(tmpdir(), "cph-cap-")); runId = "run-cap-test"; // AgentRun is required for the UsageFact FK. await prisma.project.create({ data: { id: "proj-cap", organizationId: DEFAULT_ORG_ID, name: "Cap Test", workspaceDir: workspaceRoot, }, }); await prisma.agentRun.create({ data: { id: runId, projectId: "proj-cap", entrypoint: "FEISHU", provider: "openrouter", model: "mock-model", status: "ACTIVE", prompt: "convert this pdf", metadata: {}, }, }); }); afterEach(async () => { await rm(workspaceRoot, { recursive: true, force: true }).catch(() => {}); }); async function seedActiveCapabilityConnection(): Promise { const payload: CapabilitySecretPayload = { schemaVersion: 1, accessKeyId: "LTAI-test-key-id", accessKeySecret: "test-secret-never-log", endpoint: "docmind-api.cn-hangzhou.aliyuncs.com", }; const connection = await prisma.organizationCapabilityConnection.create({ data: { id: "cap-conn-1", organizationId: DEFAULT_ORG_ID, capabilityId: CAPABILITY_ID, status: "ACTIVE", activatedAt: new Date(), }, }); const envelope = testSecretEnvelope.encryptJson( { purpose: "capability", organizationId: DEFAULT_ORG_ID, connectionId: connection.id, secretVersionId: "cap-sv-1", }, payload, ); await prisma.capabilityCredentialVersion.create({ data: { id: "cap-sv-1", connectionId: connection.id, version: 1, envelope: envelope as object, keyId: envelope.keyId, }, }); await prisma.organizationCapabilityConnection.update({ where: { id: connection.id }, data: { activeSecretVersionId: "cap-sv-1" }, }); } function mockDocmindClient(result: Partial = {}): CapabilityProviderClient { const full: DocmindParseResult = { markdown: "# Parsed Document\n\nHello world.\n\n![fig](page_1.jpg)\n", images: [ { filename: "page_1.jpg", data: new Uint8Array([0xff, 0xd8, 0xff, 0xe0]) }, ], pageCount: 3, costUsd: 0.0168, requestId: "docmind-job-abc", ...result, }; return { parse: vi.fn(async (): Promise => full), }; } function makeAdapter(client: CapabilityProviderClient) { return createPdfToMdBundleAdapter({ secrets: testSecretEnvelope, client, prisma, }); } async function seedInputPdf(name: string = "input.pdf"): Promise { const dir = join(workspaceRoot, "sources"); await mkdir(dir, { recursive: true }); const path = join(dir, name); await writeFile(path, "%PDF-1.4 fake pdf bytes"); return join("sources", name); } it("writes md + images into the workspace and records a UsageFact", async () => { await seedActiveCapabilityConnection(); const inputPath = await seedInputPdf(); const client = mockDocmindClient(); const adapter = makeAdapter(client); const result = await adapter.invoke({ runId, organizationId: DEFAULT_ORG_ID, projectId: "proj-cap", workspaceDir: workspaceRoot, inputPath, outputDir: "output", prisma, }); // Outputs landed in workspace. const md = await readFile(join(workspaceRoot, "output", "document.md"), "utf8"); expect(md).toContain("# Parsed Document"); expect(result.artifacts.some((a) => a.kind === "markdown" && a.path === "output/document.md")).toBe(true); expect(result.artifacts.some((a) => a.kind === "image" && a.path === "output/page_1.jpg")).toBe(true); // Client received the decrypted credential, not the env. const call = (client.parse as ReturnType).mock.calls[0]?.[0] as CapabilitySecretPayload; expect(call.accessKeyId).toBe("LTAI-test-key-id"); expect(call.endpoint).toBe("docmind-api.cn-hangzhou.aliyuncs.com"); // UsageFact written with non-token meter and provider_reported cost. const facts = await prisma.usageFact.findMany({ where: { runId } }); expect(facts).toHaveLength(1); const fact = facts[0]!; expect(fact.kind).toBe("external_capability"); expect(fact.provider).toBe(PROVIDER_ID); expect(fact.capabilityId).toBe(CAPABILITY_ID); expect(Number(fact.quantity)).toBe(3); expect(fact.unit).toBe("pages"); expect(Number(fact.costUsd!)).toBeCloseTo(0.0168, 8); expect(fact.costSource).toBe("provider_reported"); expect(fact.correlationId).toBe("docmind-job-abc"); }); it("writes UsageFact with costSource=unknown when docmind reports no cost", async () => { await seedActiveCapabilityConnection(); const inputPath = await seedInputPdf(); const client = mockDocmindClient({ costUsd: null, requestId: null }); const adapter = makeAdapter(client); await adapter.invoke({ runId, organizationId: DEFAULT_ORG_ID, projectId: "proj-cap", workspaceDir: workspaceRoot, inputPath, outputDir: "output", prisma, }); const fact = (await prisma.usageFact.findFirstOrThrow({ where: { runId } })); expect(fact.costUsd).toBeNull(); expect(fact.costSource).toBe("unknown"); // quantity still recorded — missing cost ≠ zero consumption (ADR-0022). expect(Number(fact.quantity)).toBe(3); }); it("fails closed when the org has no ACTIVE capability connection", async () => { // No connection seeded. const inputPath = await seedInputPdf(); const adapter = makeAdapter(mockDocmindClient()); await expect(adapter.invoke({ runId, organizationId: DEFAULT_ORG_ID, projectId: "proj-cap", workspaceDir: workspaceRoot, inputPath, outputDir: "output", prisma, })).rejects.toThrow(/no ACTIVE capability connection/); // No UsageFact written for a failed resolution. const facts = await prisma.usageFact.findMany({ where: { runId } }); expect(facts).toHaveLength(0); }); it("rejects an input path that escapes the workspace", async () => { await seedActiveCapabilityConnection(); const adapter = makeAdapter(mockDocmindClient()); await expect(adapter.invoke({ runId, organizationId: DEFAULT_ORG_ID, projectId: "proj-cap", workspaceDir: workspaceRoot, inputPath: "../../../etc/passwd", outputDir: "output", prisma, })).rejects.toBeInstanceOf(CapabilityPathEscape); }); it("rejects an output path that escapes the workspace", async () => { await seedActiveCapabilityConnection(); const inputPath = await seedInputPdf(); const adapter = makeAdapter(mockDocmindClient()); await expect(adapter.invoke({ runId, organizationId: DEFAULT_ORG_ID, projectId: "proj-cap", workspaceDir: workspaceRoot, inputPath, outputDir: "../../outside", prisma, })).rejects.toBeInstanceOf(CapabilityPathEscape); }); it("propagates docmind client errors without writing a UsageFact", async () => { await seedActiveCapabilityConnection(); const inputPath = await seedInputPdf(); const client: CapabilityProviderClient = { parse: vi.fn(async (): Promise => { throw new DocmindClientError("upstream 502", "docmind_rejected", 502); }), }; const adapter = makeAdapter(client); await expect(adapter.invoke({ runId, organizationId: DEFAULT_ORG_ID, projectId: "proj-cap", workspaceDir: workspaceRoot, inputPath, outputDir: "output", prisma, })).rejects.toThrow(/upstream 502/); const facts = await prisma.usageFact.findMany({ where: { runId } }); expect(facts).toHaveLength(0); }); it("rejects empty markdown output as a parse failure", async () => { await seedActiveCapabilityConnection(); const inputPath = await seedInputPdf(); const client = mockDocmindClient({ markdown: "" }); const adapter = makeAdapter(client); await expect(adapter.invoke({ runId, organizationId: DEFAULT_ORG_ID, projectId: "proj-cap", workspaceDir: workspaceRoot, inputPath, outputDir: "output", prisma, })).rejects.toThrow(/empty markdown/); }); });