diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4a2a1e2..90634bc 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -72,16 +72,17 @@ jobs: <(grep -E '^[A-Za-z0-9_.-]+==' /tmp/requirements-lock.txt | sort) # ── Unit tests: matrix across OS and Python version ─────────────────────── - # Closes #13. The unittest suite is the merge gate. Multi-OS catches the - # rare path / line-ending issue that a single-OS run hides; multi-Python - # catches API drift across LTS / current / latest interpreters. + # Closes #13 and #44. Multi-OS catches path-normalisation drift and + # line-ending issues that a single-OS run hides; multi-Python catches API + # drift across LTS / current / latest interpreters. Matrix parallelism + # keeps wall-clock under ~15 min (slowest runner wins, not the sum). unittest: name: Unit tests (${{ matrix.os }} / Python ${{ matrix.python-version }}) runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: - os: [ubuntu-latest] + os: [ubuntu-latest, windows-latest, macos-latest] python-version: ["3.10", "3.11", "3.12", "3.13"] steps: # Pinned to immutable commit SHAs (not @v4 / @v5) so a compromised tag @@ -117,6 +118,21 @@ jobs: # ~2× the CI minutes for zero extra signal. run: python -m pytest tests/test_api_endpoints.py -v --tb=short + # ── PyInstaller desktop build (Windows only, once per workflow) ──────── + # Closes #44. Builds the onedir bundle and smoke-tests --help so the + # desktop entry point is verified without launching the GUI window. + - name: Install PyInstaller + if: matrix.os == 'windows-latest' && matrix.python-version == '3.12' + run: python -m pip install 'pyinstaller>=6,<7' + + - name: Build PyInstaller bundle + if: matrix.os == 'windows-latest' && matrix.python-version == '3.12' + run: pyinstaller cursor-browser.spec --noconfirm + + - name: Smoke-test PyInstaller exe (--help) + if: matrix.os == 'windows-latest' && matrix.python-version == '3.12' + run: dist\CursorChatBrowser\CursorChatBrowser.exe --help + # ── Typecheck: mypy ─────────────────────────────────────────────────────── # Codebase already has type hints across most of the surface (~70+ typed # functions). Mypy runs in lenient mode (--ignore-missing-imports for diff --git a/launcher.py b/launcher.py index fbfefac..8c70c12 100644 --- a/launcher.py +++ b/launcher.py @@ -5,10 +5,24 @@ directly in-process. """ +from __future__ import annotations + +import argparse +import sys + from app import create_app -def main(): +def main(argv: list[str] | None = None) -> None: + parser = argparse.ArgumentParser( + prog="cursor-chat-browser", + description=( + "Cursor Chat Browser - opens the Flask app in a native OS window " + "via pywebview (no HTTP server or port)." + ), + ) + parser.parse_args(argv) + try: import webview except ImportError: @@ -28,4 +42,4 @@ def main(): if __name__ == "__main__": - main() + main(sys.argv[1:]) diff --git a/tests/test_normalize_file_path.py b/tests/test_normalize_file_path.py index b3531fa..90d9274 100644 --- a/tests/test_normalize_file_path.py +++ b/tests/test_normalize_file_path.py @@ -1,13 +1,15 @@ -"""Tests for utils.path_helpers path/timestamp helpers (closes #46). +"""Tests for utils.path_helpers path/timestamp helpers (closes #44, #46). Covers ``normalize_file_path`` and ``to_epoch_ms``, both previously duplicated in scripts/export.py. All call-sites in the web app and CLI export script now use the shared implementations in utils.path_helpers. -Test inventory (this module only): 21 cases — 12 ``normalize_file_path``, +Test inventory (this module only): 23 cases — 14 ``normalize_file_path``, 9 ``to_epoch_ms``. On win32, 2 cases skip (POSIX passthrough in -``TestNormalizeFilePathPosixPassthrough`` only). A full-suite run may report -more skips (e.g. ``skipped=4``) from other test modules, not this file. +``TestNormalizeFilePathPosixPassthrough`` only). On non-win32, 2 cases skip +(``TestNormalizeFilePathWindowsNative`` — exercised on windows-latest CI). +A full-suite run may report more skips (e.g. ``skipped=4``) from other test +modules, not this file. """ import sys @@ -91,6 +93,20 @@ def test_mixed_case_drive_lowercased(self) -> None: self.assertEqual(out, r"e:\mixed\case\path") +class TestNormalizeFilePathWindowsNative(unittest.TestCase): + """Win32-only edge cases — run on actual Windows runners in CI (#44).""" + + @unittest.skipUnless(sys.platform == "win32", "native win32 path semantics") + def test_leading_backslash_before_drive_stripped(self) -> None: + out = normalize_file_path(r"\C:\Users\Dev\project") + self.assertEqual(out, r"c:\users\dev\project") + + @unittest.skipUnless(sys.platform == "win32", "native win32 path semantics") + def test_file_uri_backslashes_normalised(self) -> None: + out = normalize_file_path(r"file:///C:\Users\Dev\project") + self.assertEqual(out, r"c:\users\dev\project") + + class TestNormalizeFilePathPosixPassthrough(unittest.TestCase): def test_plain_posix_path_unchanged_on_non_windows(self) -> None: if sys.platform == "win32":