from __future__ import annotations import json import os import shutil import tempfile import unittest 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_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()