import { describe, expect, it, vi } from "vitest"; import { defaultExportConfig, type DocxCapability, type PreparedDocxMedia, type ThemeManifest } from "@md-to-pdf/core"; import type { PandocDocxConversionInput, PandocDocxConversionResult, PandocRuntimeResolution } from "@md-to-pdf/docx-engine"; import type { DocxThemeTokenSet } from "@md-to-pdf/docx-theme-engine"; import { DocxExportService, readDocxExportRuntimeLimits, type DocxMediaCaptureAdapter, type PreparedDocxExport } from "../src/index.js"; function theme(): ThemeManifest { return { manifestVersion: 1, id: "test-theme", name: "测试主题", version: "1.0.0", description: "测试", author: "测试", license: "内部许可", entry: "theme.css", domPreset: "generic", defaultFontSize: "16px", supportedFeatures: [], category: "general", compatibleProfiles: [], docxStyle: { preset: "technical" }, bundled: true }; } const prepared: PreparedDocxExport = { request: { markdown: "# 测试", fileName: "报告?.md", language: "zh-CN", resources: [], exportConfig: { ...defaultExportConfig, themeId: "test-theme" } }, document: { rendererVersion: 1, articleHtml: '

测试

', bodyHtml: "

测试

", metadata: { title: "测试", author: "测试人", subject: "", keywords: [], language: "zh-CN" }, semanticDocument: { schemaVersion: 1, titlePolicy: { metadataTitle: "suppress", firstBodyHeading: "keep" }, regions: [] }, features: [], warnings: [] }, theme: { manifest: theme(), source: "bundled", css: "" } }; const availableCapability: DocxCapability = { format: "docx", status: "available", expectedVersion: "3.9.0.2", detectedVersion: "3.9.0.2" }; const themeTokens: DocxThemeTokenSet = { schemaVersion: 1, themeId: "test-theme", themeFingerprint: "c".repeat(64), mode: "auto-with-overrides", basePreset: "technical", slots: [], diagnostics: [] }; function resolution( capability: DocxCapability = availableCapability ): PandocRuntimeResolution { return capability.status === "available" ? { capability, executablePath: "pandoc" } : { capability }; } const emptyAdapter: DocxMediaCaptureAdapter = { async capture() { return { plan: { targets: [], echartsErrors: [], mermaidErrors: [] }, captures: [] }; } }; function conversionResult(): PandocDocxConversionResult { return { docx: new Uint8Array([1, 2, 3]), templateFingerprint: "a".repeat(64), templateCacheKey: "b".repeat(64), validation: { partCount: 10, xmlPartCount: 9, relationshipCount: 3, headerCount: 0, footerCount: 1 }, timings: { referenceMs: 3, pandocMs: 4, validationMs: 5 } }; } function createService(options: { convert?: ( input: PandocDocxConversionInput, signal?: AbortSignal ) => Promise; capability?: DocxCapability; limits?: { concurrency: number; maxQueue: number; timeoutMs: number; }; }) { return new DocxExportService({ application: { prepareDocxExport: vi.fn().mockResolvedValue(prepared) }, runtime: { probe: vi .fn() .mockResolvedValue(resolution(options.capability)) }, converter: { convert: options.convert ?? vi.fn().mockResolvedValue(conversionResult()) }, themeTokens: { prepare: vi.fn().mockResolvedValue({ tokens: themeTokens, warnings: [] }) }, limits: options.limits }); } describe("DOCX 共享导出服务", () => { it("串联准备、媒体和转换并返回完整耗时", async () => { const convert = vi .fn() .mockResolvedValue(conversionResult()); const service = createService({ convert }); const result = await service.generate( { markdown: "# 测试" }, { mediaAdapter: emptyAdapter } ); expect(result.docx).toEqual(new Uint8Array([1, 2, 3])); expect(result.fileName).toBe("报告_.docx"); expect(convert).toHaveBeenCalledWith( expect.objectContaining({ semanticDocument: prepared.document.semanticDocument }), expect.any(AbortSignal) ); expect(result.diagnostics).toEqual({ warnings: [], echartsErrors: [], mermaidErrors: [] }); expect(convert).toHaveBeenCalledWith( expect.objectContaining({ themeTokens }), expect.any(AbortSignal) ); expect(result.timings).toMatchObject({ themeStyleMs: expect.any(Number), referenceMs: 3, pandocMs: 4, validationMs: 5 }); for (const value of Object.values(result.timings)) { expect(value).toBeGreaterThanOrEqual(0); } await service.close(); }); it("限制并发和排队长度", async () => { let releaseFirst!: () => void; const firstBlocked = new Promise((resolve) => { releaseFirst = resolve; }); let active = 0; let maximumActive = 0; let calls = 0; const service = createService({ limits: { concurrency: 1, maxQueue: 1, timeoutMs: 5_000 }, convert: async () => { calls += 1; active += 1; maximumActive = Math.max(maximumActive, active); if (calls === 1) { await firstBlocked; } active -= 1; return conversionResult(); } }); const first = service.generate({}, { mediaAdapter: emptyAdapter }); await vi.waitFor(() => expect(calls).toBe(1)); const second = service.generate({}, { mediaAdapter: emptyAdapter }); const third = service.generate({}, { mediaAdapter: emptyAdapter }); await expect(third).rejects.toMatchObject({ code: "DOCX_QUEUE_FULL", retryable: true }); releaseFirst(); await Promise.all([first, second]); expect(maximumActive).toBe(1); expect(calls).toBe(2); await service.close(); }); it("总超时会传播取消信号并释放任务", async () => { const abortAwareAdapter: DocxMediaCaptureAdapter = { capture(_request, signal) { return new Promise((_resolve, reject) => { signal?.addEventListener( "abort", () => reject(signal.reason), { once: true } ); }); } }; const service = createService({ limits: { concurrency: 1, maxQueue: 0, timeoutMs: 30 } }); await expect( service.generate({}, { mediaAdapter: abortAwareAdapter }) ).rejects.toMatchObject({ code: "DOCX_RENDER_TIMEOUT", retryable: true }); await service.close(); }); it("映射 capability 错误并解析运行限制", async () => { const unavailable: DocxCapability = { format: "docx", status: "version-mismatch", expectedVersion: "3.9.0.2", detectedVersion: "3.9.0.1", message: "版本不匹配" }; const service = createService({ capability: unavailable }); await expect( service.generate({}, { mediaAdapter: emptyAdapter }) ).rejects.toMatchObject({ statusCode: 503, code: "DOCX_RUNTIME_VERSION_MISMATCH" }); await service.close(); expect( readDocxExportRuntimeLimits({ DOCX_CONCURRENCY: "2", DOCX_MAX_QUEUE: "6", DOCX_TIMEOUT_MS: "120000" }) ).toEqual({ concurrency: 2, maxQueue: 6, timeoutMs: 120_000 }); expect(() => readDocxExportRuntimeLimits({ DOCX_CONCURRENCY: "0" }) ).toThrow("不能小于 1"); }); });