From e9736468cb117dceb3732ab4a4643565101c9fea Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 2 Jun 2026 10:04:09 +0000 Subject: [PATCH 1/2] test(common): clear_cache refreshes case-insensitive path lookup Co-authored-by: PuritanWizard --- .../PyKotor/tests/common/test_case_aware_path.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/Libraries/PyKotor/tests/common/test_case_aware_path.py b/Libraries/PyKotor/tests/common/test_case_aware_path.py index 24f0ba06e..1c7a63743 100644 --- a/Libraries/PyKotor/tests/common/test_case_aware_path.py +++ b/Libraries/PyKotor/tests/common/test_case_aware_path.py @@ -30,7 +30,7 @@ def add_sys_path(p: pathlib.Path): from typing import TYPE_CHECKING -from pykotor.tools.path import CaseAwarePath # noqa: E402 # pyright: ignore[reportMissingImports] +from pykotor.tools.path import CaseAwarePath, clear_cache # noqa: E402 # pyright: ignore[reportMissingImports] if TYPE_CHECKING: from collections.abc import Generator @@ -137,6 +137,20 @@ def test_fix_path_formatting(self): assert CaseAwarePath.str_norm("/path//to/dir/", slash="\\") == "\\path\\to\\dir" assert CaseAwarePath.str_norm("/path//to/dir/", slash="/") == "/path/to/dir" + @unittest.skipIf(sys.platform == "win32", "POSIX-specific case resolution behavior") + def test_clear_cache_refreshes_case_insensitive_directory_lookup(self): + with tempfile.TemporaryDirectory() as tmpdir: + root = pathlib.Path(tmpdir) / "GameRoot" + root.mkdir() + (root / "dialog.tlk").write_bytes(b"tlk") + wrong_case = CaseAwarePath(root.parent / "gameroot" / "DIALOG.TLK") + assert wrong_case.name == "dialog.tlk" + (root / "dialog.tlk").unlink() + (root / "streamsounds").mkdir() + clear_cache() + nested = CaseAwarePath(root.parent / "GameRoot" / "StreamSounds") + assert nested.is_dir() + @unittest.skipIf(sys.platform == "win32", "POSIX-specific case resolution behavior") def test_virtual_archive_segment_does_not_scan_file_path(self): with tempfile.TemporaryDirectory() as tmpdir: From 31039d6a271bd488aafedf026f9c8001220110d1 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 2 Jun 2026 10:04:11 +0000 Subject: [PATCH 2/2] test(cli): cover installation json export progress for CI vs TTY Co-authored-by: PuritanWizard --- .../PyKotor/tests/cli/test_json_commands.py | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/Libraries/PyKotor/tests/cli/test_json_commands.py b/Libraries/PyKotor/tests/cli/test_json_commands.py index 67a02752f..72b9437db 100644 --- a/Libraries/PyKotor/tests/cli/test_json_commands.py +++ b/Libraries/PyKotor/tests/cli/test_json_commands.py @@ -27,7 +27,9 @@ from pykotor.resource.formats.tpc import TPC, TPCTextureFormat, bytes_tpc, read_tpc from pykotor.resource.type import ResourceType from pykotor.tools.resource_json import ( + _ExportProgressReporter, _serialize_mdl_face, + _supports_live_progress, export_installation_to_json_tree, iter_installation_resource_documents, serialize_file_resource_document, @@ -328,6 +330,90 @@ def test_export_installation_to_json_tree_logs_percentage_progress( ) +def test_supports_live_progress_false_when_ci_env_truthy( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Automation must not rely on carriage-return stderr updates; log milestones instead.""" + monkeypatch.setenv("CI", "true") + + class FakeTTY: + def isatty(self) -> bool: + return True + + assert _supports_live_progress(FakeTTY()) is False + + +def test_supports_live_progress_false_when_github_actions_truthy( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.delenv("CI", raising=False) + monkeypatch.setenv("GITHUB_ACTIONS", "true") + + class FakeTTY: + def isatty(self) -> bool: + return True + + assert _supports_live_progress(FakeTTY()) is False + + +def test_export_progress_reporter_logs_to_logger_when_ci_disables_live_updates( + monkeypatch: pytest.MonkeyPatch, + caplog: pytest.LogCaptureFixture, +) -> None: + monkeypatch.setenv("CI", "true") + + class FakeTTY: + def isatty(self) -> bool: + return True + + def write(self, _s: str) -> None: + raise AssertionError("stderr live progress must not be used when CI is set") + + def flush(self) -> None: + raise AssertionError("stderr live progress must not be used when CI is set") + + log = logging.getLogger("pykotor.tests.resource_json_export_progress") + log.setLevel(logging.INFO) + with caplog.at_level(logging.INFO, logger=log.name): + progress = _ExportProgressReporter(log, total_resources=4, stream=FakeTTY()) + assert progress.live_updates is False + progress.update(1, "dialog.tlk") + progress.finish() + messages = [record.getMessage() for record in caplog.records if record.name == log.name] + assert any("25.00% Writing dialog.tlk" in m for m in messages) + + +def test_export_progress_reporter_writes_carriage_returns_when_not_ci_and_stream_is_tty( + monkeypatch: pytest.MonkeyPatch, + caplog: pytest.LogCaptureFixture, +) -> None: + monkeypatch.delenv("CI", raising=False) + monkeypatch.delenv("GITHUB_ACTIONS", raising=False) + + writes: list[str] = [] + + class FakeTTY: + def isatty(self) -> bool: + return True + + def write(self, s: str) -> None: + writes.append(s) + + def flush(self) -> None: + pass + + log = logging.getLogger("pykotor.tests.resource_json_export_progress_tty") + log.setLevel(logging.INFO) + with caplog.at_level(logging.INFO, logger=log.name): + progress = _ExportProgressReporter(log, total_resources=4, stream=FakeTTY()) + assert progress.live_updates is True + progress.update(1, "dialog.tlk") + progress.finish() + assert any(w.startswith("\r") for w in writes) + assert not [r for r in caplog.records if r.name == log.name] + assert writes[-1].startswith("\r") + + def test_iter_installation_resource_documents_reuses_installation_export_shape( tmp_path: Path, ) -> None: