import asyncio import base64 import json from io import BytesIO from zipfile import ZipFile from pathlib import Path import pytest from PIL import Image from app.contracts import ExportAsset, ExportRequest, AgentBenchmarkRequest, BenchmarkStatus from app.export import service as exports from app.export.assets import validate_assets, source_hash from app.errors import ApiError def asset(source='flowchart LR\n A --> B'): buf=BytesIO(); Image.new('RGB',(60,40),'blue').save(buf,'PNG') return ExportAsset(kind='mermaid', source_hash=source_hash(source), png_base64=base64.b64encode(buf.getvalue()).decode()) @pytest.mark.parametrize('format',['html','pdf','docx']) def test_static_mermaid_in_export(format): async def run(): job=await exports.create_export(ExportRequest(source={'type':'markdown','markdown':'```mermaid\nflowchart LR\n A --> B\n```'},format=format,assets=[asset()],title='snapshot')) finished=await exports.wait_for_export(job.job_id) assert finished.status.value=='completed' assert not any('mermaid' in w for w in finished.warnings) data=exports.get_export_file(job.job_id).read_bytes() if format=='html': assert b'data:image/png;base64,' in data elif format=='pdf': assert b'/Subtype /Image' in data else: with ZipFile(BytesIO(data)) as archive: assert any(n.startswith('word/media/') for n in archive.namelist()) asyncio.run(run()) def test_asset_invalid_and_duplicate(): with pytest.raises(ApiError): validate_assets([asset().model_copy(update={'png_base64':'not png'})]) with pytest.raises(ApiError): validate_assets([asset(),asset()]) def test_stale_asset_does_not_replace_source(): from app.export.assets import attach_assets from app.export.markdown import parse_document document=parse_document('```mermaid\nflowchart LR\n X --> Y\n```') attach_assets(document,validate_assets([asset()])) assert 'static_png' not in document.children[0].attributes @pytest.mark.parametrize('format',['html','pdf','docx']) def test_math_and_local_image_export(format): from app.config import get_settings vault=get_settings().vault_path; vault.mkdir(parents=True) Image.new('RGB',(100,50),'green').save(vault/'figure.png') async def run(): job=await exports.create_export(ExportRequest(source={'type':'markdown','file_path':'demo.md', 'markdown':'Formula $\\frac{x^2}{2}$\n\n![figure](figure.png)'},format=format)) done=await exports.wait_for_export(job.job_id) assert done.status.value=='completed' assert not any('公式' in w or '图片' in w for w in done.warnings) data=exports.get_export_file(job.job_id).read_bytes() if format=='html': assert data.count(b'data:image/png;base64,')==2 if format=='docx': with ZipFile(BytesIO(data)) as archive: assert len([n for n in archive.namelist() if n.startswith('word/media/')])==2 asyncio.run(run()) def test_local_image_path_escape_and_tex_fallback(): from app.export.assets import enrich_document from app.export.markdown import parse_document document=parse_document('![no](../outside.png)\n\n$\\unknownmacro{x}$') warnings=enrich_document(document,'demo.md') assert len(warnings)==2 @pytest.mark.parametrize('theme',['light','dark','sepia','paper-moments','ocean-blue','midnight-purple']) def test_function_preview_theme_and_parser(theme): from app.plot_routes import PlotRequest, preview result=preview(PlotRequest(source='y = x^2\ny = sin(x)',theme_id=theme)) assert ' B\n```\n\n')*65) attach_assets(document, validate_assets([asset()])) warnings = enrich_document(document) assert sum(bool(node.attributes.get('static_png')) for node in document.children) == 64 assert any('预算' in warning for warning in warnings)