forked from EduCraft/curriculum-project-hub
feat(hub): switch capability provider to Aliyun Doc Mind (ADR-0027) (#6)
Co-authored-by: Hong Jiarong <me@jrhim.com> Co-committed-by: Hong Jiarong <me@jrhim.com>
This commit is contained in:
@@ -4,11 +4,11 @@ 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 { MineruClientError, type MineruClient, type MineruParseResult } from "../../src/capability/mineruClient.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 = "mineru";
|
||||
const PROVIDER_ID = "aliyun_docmind";
|
||||
|
||||
/**
|
||||
* ADR-0027: pdf_to_md_bundle adapter contract tests. Proves:
|
||||
@@ -16,8 +16,8 @@ const PROVIDER_ID = "mineru";
|
||||
* - fail-closed credential resolution (no ACTIVE connection → error)
|
||||
* - UsageFact attribution with non-token meter (pages), costSource rules
|
||||
* - outputs (md + images) written into workspace
|
||||
* - MinerU client errors propagate
|
||||
* The MineruClient is mocked; the prisma + envelope + filesystem are real.
|
||||
* - 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;
|
||||
@@ -58,9 +58,9 @@ describe("pdf_to_md_bundle capability adapter (ADR-0027)", () => {
|
||||
async function seedActiveCapabilityConnection(): Promise<void> {
|
||||
const payload: CapabilitySecretPayload = {
|
||||
schemaVersion: 1,
|
||||
baseUrl: "https://mineru.test/api",
|
||||
apiToken: "mineru-secret-token",
|
||||
projectId: null,
|
||||
accessKeyId: "LTAI-test-key-id",
|
||||
accessKeySecret: "test-secret-never-log",
|
||||
endpoint: "docmind-api.cn-hangzhou.aliyuncs.com",
|
||||
};
|
||||
const connection = await prisma.organizationCapabilityConnection.create({
|
||||
data: {
|
||||
@@ -95,23 +95,23 @@ describe("pdf_to_md_bundle capability adapter (ADR-0027)", () => {
|
||||
});
|
||||
}
|
||||
|
||||
function mockMineruClient(result: Partial<MineruParseResult> = {}): MineruClient {
|
||||
const full: MineruParseResult = {
|
||||
markdown: "# Parsed Document\n\nHello world.\n\n\n",
|
||||
function mockDocmindClient(result: Partial<DocmindParseResult> = {}): CapabilityProviderClient {
|
||||
const full: DocmindParseResult = {
|
||||
markdown: "# Parsed Document\n\nHello world.\n\n\n",
|
||||
images: [
|
||||
{ filename: "page_1_fig_0.jpg", data: new Uint8Array([0xff, 0xd8, 0xff, 0xe0]) },
|
||||
{ filename: "page_1.jpg", data: new Uint8Array([0xff, 0xd8, 0xff, 0xe0]) },
|
||||
],
|
||||
pageCount: 3,
|
||||
costUsd: 0.015,
|
||||
requestId: "mineru-task-abc",
|
||||
costUsd: 0.0168,
|
||||
requestId: "docmind-job-abc",
|
||||
...result,
|
||||
};
|
||||
return {
|
||||
parse: vi.fn(async (): Promise<MineruParseResult> => full),
|
||||
parse: vi.fn(async (): Promise<DocmindParseResult> => full),
|
||||
};
|
||||
}
|
||||
|
||||
function makeAdapter(client: MineruClient) {
|
||||
function makeAdapter(client: CapabilityProviderClient) {
|
||||
return createPdfToMdBundleAdapter({
|
||||
secrets: testSecretEnvelope,
|
||||
client,
|
||||
@@ -130,7 +130,7 @@ describe("pdf_to_md_bundle capability adapter (ADR-0027)", () => {
|
||||
it("writes md + images into the workspace and records a UsageFact", async () => {
|
||||
await seedActiveCapabilityConnection();
|
||||
const inputPath = await seedInputPdf();
|
||||
const client = mockMineruClient();
|
||||
const client = mockDocmindClient();
|
||||
const adapter = makeAdapter(client);
|
||||
|
||||
const result = await adapter.invoke({
|
||||
@@ -147,12 +147,12 @@ describe("pdf_to_md_bundle capability adapter (ADR-0027)", () => {
|
||||
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_fig_0.jpg")).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<typeof vi.fn>).mock.calls[0]?.[0] as CapabilitySecretPayload;
|
||||
expect(call.apiToken).toBe("mineru-secret-token");
|
||||
expect(call.baseUrl).toBe("https://mineru.test/api");
|
||||
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 } });
|
||||
@@ -163,15 +163,15 @@ describe("pdf_to_md_bundle capability adapter (ADR-0027)", () => {
|
||||
expect(fact.capabilityId).toBe(CAPABILITY_ID);
|
||||
expect(Number(fact.quantity)).toBe(3);
|
||||
expect(fact.unit).toBe("pages");
|
||||
expect(Number(fact.costUsd!)).toBeCloseTo(0.015, 8);
|
||||
expect(Number(fact.costUsd!)).toBeCloseTo(0.0168, 8);
|
||||
expect(fact.costSource).toBe("provider_reported");
|
||||
expect(fact.correlationId).toBe("mineru-task-abc");
|
||||
expect(fact.correlationId).toBe("docmind-job-abc");
|
||||
});
|
||||
|
||||
it("writes UsageFact with costSource=unknown when MinerU reports no cost", async () => {
|
||||
it("writes UsageFact with costSource=unknown when docmind reports no cost", async () => {
|
||||
await seedActiveCapabilityConnection();
|
||||
const inputPath = await seedInputPdf();
|
||||
const client = mockMineruClient({ costUsd: null, requestId: null });
|
||||
const client = mockDocmindClient({ costUsd: null, requestId: null });
|
||||
const adapter = makeAdapter(client);
|
||||
|
||||
await adapter.invoke({
|
||||
@@ -194,7 +194,7 @@ describe("pdf_to_md_bundle capability adapter (ADR-0027)", () => {
|
||||
it("fails closed when the org has no ACTIVE capability connection", async () => {
|
||||
// No connection seeded.
|
||||
const inputPath = await seedInputPdf();
|
||||
const adapter = makeAdapter(mockMineruClient());
|
||||
const adapter = makeAdapter(mockDocmindClient());
|
||||
|
||||
await expect(adapter.invoke({
|
||||
runId,
|
||||
@@ -213,7 +213,7 @@ describe("pdf_to_md_bundle capability adapter (ADR-0027)", () => {
|
||||
|
||||
it("rejects an input path that escapes the workspace", async () => {
|
||||
await seedActiveCapabilityConnection();
|
||||
const adapter = makeAdapter(mockMineruClient());
|
||||
const adapter = makeAdapter(mockDocmindClient());
|
||||
|
||||
await expect(adapter.invoke({
|
||||
runId,
|
||||
@@ -229,7 +229,7 @@ describe("pdf_to_md_bundle capability adapter (ADR-0027)", () => {
|
||||
it("rejects an output path that escapes the workspace", async () => {
|
||||
await seedActiveCapabilityConnection();
|
||||
const inputPath = await seedInputPdf();
|
||||
const adapter = makeAdapter(mockMineruClient());
|
||||
const adapter = makeAdapter(mockDocmindClient());
|
||||
|
||||
await expect(adapter.invoke({
|
||||
runId,
|
||||
@@ -242,12 +242,12 @@ describe("pdf_to_md_bundle capability adapter (ADR-0027)", () => {
|
||||
})).rejects.toBeInstanceOf(CapabilityPathEscape);
|
||||
});
|
||||
|
||||
it("propagates MinerU client errors without writing a UsageFact", async () => {
|
||||
it("propagates docmind client errors without writing a UsageFact", async () => {
|
||||
await seedActiveCapabilityConnection();
|
||||
const inputPath = await seedInputPdf();
|
||||
const client: MineruClient = {
|
||||
parse: vi.fn(async (): Promise<MineruParseResult> => {
|
||||
throw new MineruClientError("upstream 502", "mineru_rejected", 502);
|
||||
const client: CapabilityProviderClient = {
|
||||
parse: vi.fn(async (): Promise<DocmindParseResult> => {
|
||||
throw new DocmindClientError("upstream 502", "docmind_rejected", 502);
|
||||
}),
|
||||
};
|
||||
const adapter = makeAdapter(client);
|
||||
@@ -269,7 +269,7 @@ describe("pdf_to_md_bundle capability adapter (ADR-0027)", () => {
|
||||
it("rejects empty markdown output as a parse failure", async () => {
|
||||
await seedActiveCapabilityConnection();
|
||||
const inputPath = await seedInputPdf();
|
||||
const client = mockMineruClient({ markdown: "" });
|
||||
const client = mockDocmindClient({ markdown: "" });
|
||||
const adapter = makeAdapter(client);
|
||||
|
||||
await expect(adapter.invoke({
|
||||
|
||||
Reference in New Issue
Block a user