from __future__ import annotations import json import os import shutil import tempfile import unittest from dataclasses import replace from datetime import date from pathlib import Path from company_reports.publishing import ArtifactToolBuilder, AtomicReportPublisher from company_reports.service import CompanyReportService, RunRequest from tests.test_company_reports_service import FakeRepository, synthetic_snapshot PROJECT_ROOT = Path(__file__).resolve().parents[1] BUILDER_SCRIPT = PROJECT_ROOT / "company_reports" / "xlsx" / "build_workbook.mjs" ARTIFACT_PACKAGE = ( PROJECT_ROOT / "company_reports" / "xlsx" / "node_modules" / "@oai" / "artifact-tool" ) def artifact_tool_available() -> bool: configured = os.environ.get("COMPANY_REPORT_ARTIFACT_TOOL_MODULE", "").strip() return ARTIFACT_PACKAGE.exists() or bool(configured and Path(configured).is_file()) def node_binary() -> str: configured = os.environ.get("COMPANY_REPORT_NODE_BINARY", "").strip() return configured or shutil.which("node") or "" @unittest.skipUnless(artifact_tool_available(), "artifact-tool dependency is not installed") class CompanyReportIntegrationTests(unittest.TestCase): def test_missing_and_unmatched_group_codes_export_blank_booking_room(self): node = node_binary() if not node: self.skipTest("Node.js is unavailable") class CapturingBuilder: def __init__(self, delegate): self.delegate = delegate self.reports = [] def build(self, report, work_dir): self.reports.append(report) return self.delegate.build(report, work_dir) with tempfile.TemporaryDirectory(prefix="company-report-integration-") as temp_dir: project_root = Path(temp_dir) / "project" output_root = project_root / "outputs" / "company_reports" project_root.mkdir(parents=True) repository = FakeRepository( replace( synthetic_snapshot(include_invalid_hana=True), room_items=(), group_parse_versions={}, ) ) builder = CapturingBuilder( ArtifactToolBuilder(BUILDER_SCRIPT, node_binary=node, timeout_seconds=90) ) publisher = AtomicReportPublisher(project_root, output_root) service = CompanyReportService( repository, builder, publisher, output_root / ".staging", ) result = service.run( RunRequest(2026, 7, date(2026, 7, 10), ("QBD", "HanaTour")) ) self.assertEqual(result.status, "success") self.assertEqual([item.row_count for item in result.companies], [1, 1]) self.assertTrue(all(item.errors == () for item in result.companies)) self.assertEqual(len(builder.reports), 2) qbd_report, hana_report = builder.reports self.assertEqual(qbd_report.periods[0].rows[0].res_comment, "SYN-GROUP-A") self.assertEqual(qbd_report.periods[0].rows[0].booking_room, "") self.assertEqual(hana_report.periods[0].rows[0].res_comment, "") self.assertEqual(hana_report.periods[0].rows[0].booking_room, "") self.assertTrue( (output_root / "2026" / "07" / "QBD-July-2026.xlsx").is_file() ) self.assertTrue( (output_root / "2026" / "07" / "HanaTour-July-2026.xlsx").is_file() ) def test_same_snapshot_rerun_rebuilds_full_month_with_equal_semantics(self): node = node_binary() if not node: self.skipTest("Node.js is unavailable") with tempfile.TemporaryDirectory(prefix="company-report-integration-") as temp_dir: project_root = Path(temp_dir) / "project" output_root = project_root / "outputs" / "company_reports" project_root.mkdir(parents=True) repository = FakeRepository(synthetic_snapshot()) builder = ArtifactToolBuilder(BUILDER_SCRIPT, node_binary=node, timeout_seconds=90) publisher = AtomicReportPublisher(project_root, output_root) service = CompanyReportService( repository, builder, publisher, output_root / ".staging", ) request = RunRequest(2026, 7, date(2026, 7, 10), ("QBD",)) first = service.run(request) second = service.run(request) self.assertEqual(first.status, "success") self.assertEqual(second.status, "success") self.assertEqual(first.companies[0].row_count, 1) self.assertEqual(second.companies[0].row_count, 1) first_semantic = first.companies[0].artifact["semantic_sha256"] second_semantic = second.companies[0].artifact["semantic_sha256"] self.assertEqual(len(first_semantic), 64) self.assertEqual(first_semantic, second_semantic) self.assertEqual( first.companies[0].period_row_counts, second.companies[0].period_row_counts, ) self.assertEqual(first.companies[0].version_no, 1) self.assertEqual(second.companies[0].version_no, 2) month_root = output_root / "2026" / "07" filename = "QBD-July-2026.xlsx" current = month_root / filename first_archive = month_root / "archive" / "v0001" / filename second_archive = month_root / "archive" / "v0002" / filename self.assertTrue(first_archive.is_file()) self.assertTrue(second_archive.is_file()) self.assertEqual(current.read_bytes(), second_archive.read_bytes()) self.assertEqual(current.stat().st_mode & 0o777, 0o600) self.assertEqual(len(repository.activated), 2) first_result = json.loads( (month_root / "archive" / "v0001" / "QBD-v0001.result.json").read_text( encoding="utf-8" ) ) second_result = json.loads( (month_root / "archive" / "v0002" / "QBD-v0002.result.json").read_text( encoding="utf-8" ) ) self.assertEqual( first_result["artifact"]["semantic_sha256"], second_result["artifact"]["semantic_sha256"], ) serialized = json.dumps(second.to_dict(), ensure_ascii=False) self.assertNotIn("SYN-GROUP-A", serialized) self.assertNotIn("SYN-BLOCK-A", serialized) if __name__ == "__main__": unittest.main()