diff --git a/config/skill-overrides/xlsx/recalc.py b/config/skill-overrides/xlsx/recalc.py index 74dc430b..6a6f6030 100644 --- a/config/skill-overrides/xlsx/recalc.py +++ b/config/skill-overrides/xlsx/recalc.py @@ -30,6 +30,8 @@ def inspect_workbook(path): cached = values[sheet.title][cell.coordinate] if cached.data_type == "e": errors.setdefault(str(cached.value), []).append(location) + elif cell.data_type == "f" and cached.value is None: + errors.setdefault("MISSING_FORMULA_CACHE", []).append(location) return formula_cells, errors finally: formulas.close() diff --git a/plugins/codex/plugins/xlsx/skills/xlsx/scripts/recalc.py b/plugins/codex/plugins/xlsx/skills/xlsx/scripts/recalc.py index 74dc430b..6a6f6030 100644 --- a/plugins/codex/plugins/xlsx/skills/xlsx/scripts/recalc.py +++ b/plugins/codex/plugins/xlsx/skills/xlsx/scripts/recalc.py @@ -30,6 +30,8 @@ def inspect_workbook(path): cached = values[sheet.title][cell.coordinate] if cached.data_type == "e": errors.setdefault(str(cached.value), []).append(location) + elif cell.data_type == "f" and cached.value is None: + errors.setdefault("MISSING_FORMULA_CACHE", []).append(location) return formula_cells, errors finally: formulas.close() diff --git a/scripts/tests/test_document_portability.py b/scripts/tests/test_document_portability.py index 8f0a64ab..6eb8c066 100644 --- a/scripts/tests/test_document_portability.py +++ b/scripts/tests/test_document_portability.py @@ -1,11 +1,15 @@ import importlib.util import subprocess +import sys import tempfile +import types import unittest import zipfile from pathlib import Path from unittest.mock import patch +from openpyxl import Workbook + ROOT = Path(__file__).resolve().parents[2] COMMON = ROOT / "config/skill-overrides/common" @@ -17,6 +21,16 @@ def load_module(name): return module +def load_recalc(): + office = types.ModuleType("office.soffice") + office.run_soffice = lambda *args, **kwargs: None + spec = importlib.util.spec_from_file_location("market_recalc", ROOT / "config/skill-overrides/xlsx/recalc.py") + module = importlib.util.module_from_spec(spec) + with patch.dict(sys.modules, {"office.soffice": office}): + spec.loader.exec_module(module) + return module + + class OfficePortabilityTests(unittest.TestCase): def test_windows_install_path_and_arguments_with_spaces(self): office = load_module("soffice") @@ -72,6 +86,41 @@ class OfficePortabilityTests(unittest.TestCase): package.unpack(source, root / "unpacked") self.assertFalse((root / "escape.txt").exists()) + def test_formula_without_cache_is_not_success(self): + recalc = load_recalc() + with tempfile.TemporaryDirectory() as temporary: + source = Path(temporary) / "中文.xlsx" + workbook = Workbook() + workbook.active["A1"] = "=1+1" + workbook.save(source) + formulas, errors = recalc.inspect_workbook(source) + self.assertEqual(formulas, {"Sheet!A1"}) + self.assertEqual(errors, {"MISSING_FORMULA_CACHE": ["Sheet!A1"]}) + + def test_recalc_timeout_preserves_original(self): + recalc = load_recalc() + with tempfile.TemporaryDirectory() as temporary: + source = Path(temporary) / "中文.xlsx" + Workbook().save(source) + before = source.read_bytes() + with patch.object(recalc, "run_soffice", side_effect=subprocess.TimeoutExpired("soffice", 1)): + report = recalc.recalc(source, 1) + self.assertIn("error", report) + self.assertEqual(source.read_bytes(), before) + + def test_recalc_external_links_refused_without_conversion(self): + recalc = load_recalc() + with tempfile.TemporaryDirectory() as temporary: + source = Path(temporary) / "linked.xlsx" + with zipfile.ZipFile(source, "w") as archive: + archive.writestr("xl/externalLinks/externalLink1.xml", "") + before = source.read_bytes() + with patch.object(recalc, "run_soffice") as convert: + report = recalc.recalc(source) + self.assertIn("error", report) + convert.assert_not_called() + self.assertEqual(source.read_bytes(), before) + if __name__ == "__main__": unittest.main()