//! Integration tests for the bundle build path (ADR-0030): compiling a bundle //! target's template (`exports/.typ` under a `bundle.toml` root) as //! main, injecting the augmented **bundle** manifest, against the real //! `render/` package. use std::path::PathBuf; use cph_diag::Severity; use cph_typst::{build_augmented_bundle_manifest, Engine}; fn fixture_root() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/bundle") } fn real_render_dir() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("..") .join("..") .join("render") } fn load_bundle() -> cph_model::Bundle { let (bundle, diags) = cph_model::load_bundle(&fixture_root()); let bundle = bundle.expect("bundle fixture loads into a Bundle"); let errors: Vec<_> = diags .iter() .filter(|d| d.severity == Severity::Error) .collect(); assert!(errors.is_empty(), "fixture has loader errors: {errors:?}"); bundle } /// PURE UNIT TEST (no fonts, no render package): the augmented bundle manifest /// carries the bundle's own `[info]` and an ordered `[[lessons]]`, each with /// that member's own `info`/`target` and a `path`-prefixed outline (ADR-0030). #[test] fn augmented_bundle_manifest_prefixes_member_paths() { let bundle = load_bundle(); let src = build_augmented_bundle_manifest(&bundle); assert!( src.contains("测试合集"), "bundle info.title present:\n{src}" ); let doc: toml::Value = toml::from_str(&src).expect("augmented bundle manifest is valid TOML"); let lessons = doc .get("lessons") .and_then(|l| l.as_array()) .expect("lessons array present"); assert_eq!(lessons.len(), 2, "two member lessons:\n{src}"); // lesson-a: target defaults to its own first declared target ("student"), // outline paths are prefixed with "lesson-a/". let a_target = lessons[0].get("target").unwrap().as_str().unwrap(); assert_eq!(a_target, "student"); let a_outline = lessons[0].get("outline").unwrap().as_array().unwrap(); // segment + section heading + lemma = 3 outline entries. assert_eq!(a_outline.len(), 3); let a_seg_path = a_outline[0].get("path").unwrap().as_str().unwrap(); assert_eq!(a_seg_path, "lesson-a/segments/a"); let a_section_path = a_outline[1].get("path").unwrap().as_str().unwrap(); assert_eq!(a_section_path, "lesson-a/小节"); let a_lemma_path = a_outline[2].get("path").unwrap().as_str().unwrap(); assert_eq!(a_lemma_path, "lesson-a/小节/lemmas/引理甲"); // lesson-b: bundle.toml overrides `target = "teacher"`. let b_target = lessons[1].get("target").unwrap().as_str().unwrap(); assert_eq!(b_target, "teacher"); let b_outline = lessons[1].get("outline").unwrap().as_array().unwrap(); assert_eq!(b_outline.len(), 1); let b_seg_path = b_outline[0].get("path").unwrap().as_str().unwrap(); assert_eq!(b_seg_path, "lesson-b/segments/b"); } /// THROUGH-TEMPLATE compile-check against the REAL render package: compiling /// the bundle's `merged` target as main with the injected augmented bundle /// manifest is clean. #[test] fn compile_check_clean_through_bundle_template() { let bundle = load_bundle(); let engine = Engine::with_render_dir(real_render_dir()); let diags = engine.compile_check_bundle(&bundle, "merged"); let errors: Vec<_> = diags .iter() .filter(|d| d.severity == Severity::Error) .collect(); assert!(errors.is_empty(), "unexpected compile errors: {errors:#?}"); } /// THROUGH-TEMPLATE PDF export, fully offline: a non-trivial combined PDF is /// produced from the two member lessons through the real bundle template. #[test] fn build_bundle_pdf_through_template_offline() { let bundle = load_bundle(); let engine = Engine::with_render_dir(real_render_dir()); let pdf = engine .build_bundle_pdf(&bundle, "merged") .unwrap_or_else(|d| panic!("bundle PDF build failed: {d:#?}")); assert!(pdf.starts_with(b"%PDF"), "output is a PDF"); assert!( pdf.len() > 1024, "bundle PDF is non-trivial (got {} bytes)", pdf.len() ); } /// An undeclared bundle target name is a blocking `SchemaViolation`, exactly /// like a lesson's own unknown-target path. #[test] fn unknown_bundle_target_is_blocking() { let bundle = load_bundle(); let engine = Engine::with_render_dir(real_render_dir()); let diags = engine.compile_check_bundle(&bundle, "nonexistent"); assert_eq!(diags.len(), 1, "one blocking diagnostic: {diags:#?}"); assert_eq!(diags[0].severity, Severity::Error); assert!( diags[0].message.contains("not declared"), "expected an undeclared-target error: {diags:#?}" ); }