部署优化

This commit is contained in:
andy
2026-08-03 20:59:59 +08:00
parent 5294edab89
commit 727643f1f2
18 changed files with 358 additions and 130 deletions

View File

@@ -1,7 +1,5 @@
from __future__ import annotations
import os
import shutil
import tempfile
import unittest
from pathlib import Path
@@ -19,14 +17,6 @@ from tests.company_reports_acceptance_fixture import (
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 acceptance_reports():
@@ -38,16 +28,6 @@ def acceptance_reports():
)
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 ""
class CompanyReportAcceptanceCoreTests(unittest.TestCase):
def test_all_five_companies_have_contract_complete_month_end_reports(self):
reports = acceptance_reports()
@@ -106,15 +86,10 @@ class CompanyReportAcceptanceCoreTests(unittest.TestCase):
self.assertEqual(dy_row.departure.day, 20)
@unittest.skipUnless(artifact_tool_available(), "artifact-tool dependency is not installed")
class CompanyReportAcceptanceWorkbookTests(unittest.TestCase):
def test_real_builder_exports_reopens_and_renders_all_five_workbooks(self):
node = node_binary()
if not node:
self.skipTest("Node.js is unavailable")
def test_real_builder_exports_and_reopens_all_five_workbooks(self):
builder = ArtifactToolBuilder(
BUILDER_SCRIPT,
node_binary=node,
timeout_seconds=180,
)
with tempfile.TemporaryDirectory(prefix="company-report-acceptance-") as temp_dir:
@@ -125,15 +100,12 @@ class CompanyReportAcceptanceWorkbookTests(unittest.TestCase):
self.assertTrue(built.path.is_file())
self.assertEqual(built.path.name, report.filename)
self.assertEqual(built.summary["formula_count"], 0)
self.assertEqual(built.summary["preview_count"], 3)
self.assertEqual(built.summary["preview_count"], 0)
self.assertEqual(
built.summary["row_counts"],
[len(period.rows) for period in report.periods],
)
self.assertEqual(
len(list((root / report.company / "previews").glob("sheet-*.png"))),
3,
)
self.assertEqual(len(list((root / report.company / "previews").glob("*"))), 0)
if __name__ == "__main__":

View File

@@ -2,7 +2,6 @@ from __future__ import annotations
import json
import os
import shutil
import tempfile
import unittest
from dataclasses import replace
@@ -22,33 +21,10 @@ from tests.test_company_reports_service import FakeRepository, synthetic_snapsho
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
@@ -70,7 +46,7 @@ class CompanyReportIntegrationTests(unittest.TestCase):
)
)
builder = CapturingBuilder(
ArtifactToolBuilder(BUILDER_SCRIPT, node_binary=node, timeout_seconds=90)
ArtifactToolBuilder(BUILDER_SCRIPT, timeout_seconds=90)
)
publisher = AtomicReportPublisher(project_root, output_root)
service = CompanyReportService(
@@ -101,10 +77,6 @@ class CompanyReportIntegrationTests(unittest.TestCase):
)
def test_same_snapshot_rerun_reuses_publication_when_binary_changes(self):
node = node_binary()
if not node:
self.skipTest("Node.js is unavailable")
class StableVersionRepository(FakeRepository):
def reserve_report(self, report):
reservation = ReservedReport(9001, 1, report.company)
@@ -142,7 +114,7 @@ class CompanyReportIntegrationTests(unittest.TestCase):
project_root.mkdir(parents=True)
repository = StableVersionRepository(synthetic_snapshot())
builder = RebuildingBuilder(
ArtifactToolBuilder(BUILDER_SCRIPT, node_binary=node, timeout_seconds=90)
ArtifactToolBuilder(BUILDER_SCRIPT, timeout_seconds=90)
)
publisher = AtomicReportPublisher(project_root, output_root)
service = CompanyReportService(

View File

@@ -7,9 +7,19 @@ from datetime import date
from pathlib import Path
from typing import Optional
from company_reports.contracts import BatchSnapshot, ErrorCode
from openpyxl import load_workbook
from company_reports.contracts import (
BatchSnapshot,
CompanyReport,
DailyVersionPin,
ErrorCode,
PeriodReport,
ReportRow,
)
from company_reports.core import build_company_report
from company_reports.publishing import (
ArtifactToolBuilder,
AtomicReportPublisher,
BuiltWorkbook,
PublicationError,
@@ -53,6 +63,38 @@ def empty_report():
)
def workbook_report() -> CompanyReport:
row = ReportRow(
arrival=date(2026, 7, 8),
departure=date(2026, 7, 10),
nights=2,
block_code="=BLOCK-A",
res_comment="SYN-GROUP-A",
booking_room="【DBL】1",
total_booking_price="+900",
normalized_group_code="SYN-GROUP-A",
record_ids=(1,),
duplicate_group=True,
multi_price_review=True,
)
return CompanyReport(
company="QBD",
report_year=2026,
report_month=7,
as_of_date=date(2026, 7, 10),
filename="QBD-July-2026.xlsx",
periods=(
PeriodReport(1, 10, True, "QBD 01-10 Jul 2026", (row,)),
PeriodReport(11, 20, False, "QBD 11-20 Jul 2026", ()),
PeriodReport(21, 31, False, "QBD 21-31 Jul 2026", ()),
),
warnings=(),
errors=(),
daily_versions=(DailyVersionPin(date(2026, 7, 8), 100),),
booking_versions={"SYN-GROUP-A": 700},
)
def synthetic_built_workbook(
work_dir: Path,
filename: str,
@@ -70,6 +112,41 @@ def synthetic_built_workbook(
class CompanyReportPublishingTests(unittest.TestCase):
def test_builder_creates_xlsx_without_private_artifact_tool_runtime(self):
with tempfile.TemporaryDirectory(prefix="company-report-builder-test-") as temp_dir:
root = Path(temp_dir)
report = workbook_report()
builder = ArtifactToolBuilder(
Path("/private/artifact-tool/build_workbook.mjs"),
node_binary="/definitely/not/node",
)
built = builder.build(report, root)
self.assertTrue(built.path.is_file())
self.assertEqual(built.path.stat().st_mode & 0o777, 0o600)
self.assertEqual(built.summary["status"], "success")
self.assertEqual(built.summary["row_counts"], [1, 0, 0])
self.assertEqual(built.summary["formula_count"], 0)
self.assertEqual(len(built.summary["semantic_sha256"]), 64)
workbook = load_workbook(built.path)
self.assertEqual(workbook.sheetnames, [period.sheet_name for period in report.periods])
sheet = workbook[report.periods[0].sheet_name]
self.assertEqual(
[cell.value for cell in sheet[1]],
[
"ARRIVAL",
"DEPARTURE",
"NIGHTS",
"BLOCK_CODE",
"RES_COMMENT",
"Booking Room",
"Total Booking Price",
],
)
self.assertEqual(sheet["D2"].value, "'=BLOCK-A")
self.assertEqual(sheet["G2"].value, "'+900")
def test_publish_archives_prior_current_and_activates_new_version(self):
with tempfile.TemporaryDirectory(prefix="company-report-publish-test-") as temp_dir:
project_root = Path(temp_dir) / "project"

View File

@@ -33,8 +33,12 @@ class DeploymentEntrypointTests(unittest.TestCase):
root_requirements = (PROJECT_ROOT / "requirements.txt").read_text(
encoding="utf-8"
)
company_requirements = (
PROJECT_ROOT / "requirements-company-reports.txt"
).read_text(encoding="utf-8")
self.assertIn("--enable-processing", compose)
self.assertIn("--enable-company-reports", compose)
self.assertNotIn("\n mcp:", compose)
self.assertNotIn("DEERFLOW", compose)
self.assertNotIn("ARR_MCP", compose)
@@ -47,6 +51,13 @@ class DeploymentEntrypointTests(unittest.TestCase):
self.assertNotIn("basic_auth", caddy)
self.assertNotIn("requirements-agent-integration", root_requirements)
self.assertNotIn("requirements-arr-mcp", root_requirements)
self.assertIn("openpyxl", company_requirements)
company_package = json.loads(
(PROJECT_ROOT / "company_reports" / "xlsx" / "package.json").read_text(
encoding="utf-8"
)
)
self.assertNotIn("@oai/artifact-tool", json.dumps(company_package))
if __name__ == "__main__":