diff --git a/.github/workflows/windows-realprofile-e2e.yml b/.github/workflows/windows-realprofile-e2e.yml deleted file mode 100644 index e6d3955f99..0000000000 --- a/.github/workflows/windows-realprofile-e2e.yml +++ /dev/null @@ -1,72 +0,0 @@ -name: Windows real-profile live E2E - -# ON-DEMAND ONLY (real-profile browsing, PR #95620 — Windows locked-DB copy). -# -# Proves on a REAL windows-latest runner that the real-profile snapshot copies -# Chrome's cookie/login SQLite DBs via the online-backup API while a running -# Chrome holds them with a Windows OS-level share lock — the exact "file in use -# by another application" failure the copy approach had to solve. This cannot be -# exercised on the Linux lanes (a Linux write-txn only reproduces SQLite's -# internal lock, not the Windows filesystem share lock). -# -# Fires only on pushes to this feature branch, so it costs nothing on normal -# PRs. This is a PROOF workflow — delete it (and the test) before/at merge; it -# must not land on main. - -on: - push: - branches: - - "feat/real-profile-cdp" - -permissions: - contents: read - -concurrency: - group: windows-realprofile-e2e-${{ github.sha }} - cancel-in-progress: false - -jobs: - real-profile-e2e: - name: real-profile locked-DB live E2E (windows-latest) - runs-on: windows-latest - timeout-minutes: 12 - steps: - - name: Checkout code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Install Google Chrome - shell: pwsh - run: choco install googlechrome --no-progress -y --ignore-checksums - - - name: Install uv - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # 8.2.0 - with: - version: "0.9.28" - enable-cache: true - cache-dependency-glob: | - pyproject.toml - uv.lock - - - name: Set up Python 3.11 - uses: ./.github/actions/retry - with: - command: uv python install 3.11 - - - name: Install dependencies - uses: ./.github/actions/retry - with: - command: uv sync --locked --python 3.11 --extra dev - - - name: Run real-profile locked-DB diagnostic + live E2E - shell: bash - run: | - set -uo pipefail - # Diagnostic FIRST — each strategy is internally bounded (sqlite - # timeout=3, quick win32 open), so it reports fast even if the - # product path would hang. Its output is the answer we need. - uv run --no-sync python -m pytest \ - tests/hermes_cli/test_real_profile_windows_diag.py \ - -o addopts= -v -s -p no:cacheprovider - # Live contract test second, hard-bounded by the OS so a product-path - # hang can't burn the job. faulthandler dumps a traceback at 150s. - uv run --no-sync python -X faulthandler -c "import faulthandler,sys,subprocess; faulthandler.dump_traceback_later(150, exit=True); sys.exit(subprocess.call([sys.executable,'-m','pytest','tests/hermes_cli/test_real_profile_windows_live.py','-o','addopts=','-v','-s','-p','no:cacheprovider']))" || true diff --git a/tests/hermes_cli/test_real_profile_windows_diag.py b/tests/hermes_cli/test_real_profile_windows_diag.py deleted file mode 100644 index 61ae1749ad..0000000000 --- a/tests/hermes_cli/test_real_profile_windows_diag.py +++ /dev/null @@ -1,113 +0,0 @@ -"""DIAGNOSTIC (Windows live): which read strategy can open Chrome's locked DB? - -Not a pass/fail test — it prints, for a cookie DB held open by a running -Chrome, which of several open strategies SUCCEED. This tells us empirically -whether ANY in-process read path exists (immutable=1, nolock, raw win32 share -flags, shutil) before we reach for VSS/admin. Runs on windows-latest only. -""" -from __future__ import annotations - -import os -import shutil -import sqlite3 -import subprocess -import sys -import time -from pathlib import Path - -import pytest - -pytestmark = pytest.mark.skipif(sys.platform != "win32", reason="Windows-only diagnostic") - -_CHROME = ( - r"C:\Program Files\Google\Chrome\Application\chrome.exe", - r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe", -) - - -def _chrome(): - for p in _CHROME: - if os.path.isfile(p): - return p - return shutil.which("chrome") or shutil.which("chrome.exe") - - -def test_diagnose_locked_db_read_strategies(tmp_path, capsys): - chrome = _chrome() - if not chrome: - pytest.skip("no chrome") - ud = tmp_path / "ud"; ud.mkdir() - proc = subprocess.Popen( - [chrome, "--headless=new", "--disable-gpu", "--no-first-run", - "--no-default-browser-check", f"--user-data-dir={ud}", - "--remote-debugging-port=0", "about:blank"], - stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, - ) - results = [] - try: - ck = None - deadline = time.time() + 60 - while time.time() < deadline and not ck: - for rel in (r"Default\Network\Cookies", r"Default\Cookies"): - c = ud / rel - if c.is_file() and c.stat().st_size > 0: - ck = c; break - time.sleep(1) - if not ck: - pytest.skip("no cookie db materialized") - - def rec(name, fn): - try: - fn(); results.append((name, "OK")) - except Exception as e: - results.append((name, f"{type(e).__name__}: {str(e)[:80]}")) - - # 1. plain shutil copy (the original failing path) - rec("shutil.copy2", lambda: shutil.copy2(str(ck), str(tmp_path / "c1"))) - # 2. open() read binary - rec("open-rb", lambda: open(str(ck), "rb").read(64)) - # 3. sqlite mode=ro - rec("sqlite mode=ro backup", lambda: _bk(f"file:{ck}?mode=ro", tmp_path / "c3")) - # 4. sqlite immutable=1 (tells sqlite the file won't change; skips locking) - rec("sqlite immutable=1 backup", lambda: _bk(f"file:{ck}?immutable=1", tmp_path / "c4")) - # 5. sqlite mode=ro&nolock=1 - rec("sqlite ro+nolock backup", lambda: _bk(f"file:{ck}?mode=ro&nolock=1", tmp_path / "c5")) - # 6. raw win32 CreateFile with full share flags, then read bytes - rec("win32 share-all read", lambda: _win32_read(str(ck))) - - print("\n=== LOCKED-DB READ STRATEGY RESULTS ===") - for name, outcome in results: - print(f" {name:32} -> {outcome}") - # Surface in the CI log regardless of capture. - sys.stderr.write("\n".join(f"{n} -> {o}" for n, o in results) + "\n") - finally: - proc.terminate() - try: proc.wait(timeout=15) - except subprocess.TimeoutExpired: proc.kill() - - -def _bk(uri, dst): - src = sqlite3.connect(uri, uri=True, timeout=3) - try: - out = sqlite3.connect(str(dst)) - try: - with out: - src.backup(out) - finally: - out.close() - finally: - src.close() - - -def _win32_read(path): - import ctypes - from ctypes import wintypes - GENERIC_READ = 0x80000000 - FILE_SHARE_ALL = 0x1 | 0x2 | 0x4 # READ|WRITE|DELETE - OPEN_EXISTING = 3 - CreateFileW = ctypes.windll.kernel32.CreateFileW - CreateFileW.restype = wintypes.HANDLE - h = CreateFileW(path, GENERIC_READ, FILE_SHARE_ALL, None, OPEN_EXISTING, 0, None) - if h == wintypes.HANDLE(-1).value or h is None: - raise OSError(f"CreateFile failed err={ctypes.get_last_error()}") - ctypes.windll.kernel32.CloseHandle(h) diff --git a/tests/hermes_cli/test_real_profile_windows_live.py b/tests/hermes_cli/test_real_profile_windows_live.py deleted file mode 100644 index 675e1f06eb..0000000000 --- a/tests/hermes_cli/test_real_profile_windows_live.py +++ /dev/null @@ -1,168 +0,0 @@ -"""LIVE Windows E2E: real-profile auth-DB copy under a real Chrome share-lock. - -Runs ONLY on a windows-latest GitHub runner (see .github/workflows/ -windows-realprofile-e2e.yml). It proves the thing the Linux lanes cannot: -that copying the SQLite auth DBs via the online-backup API succeeds while a -REAL Chrome process holds the cookie DB with a Windows OS-level share lock — -the exact "file in use by another application" failure the copy approach had -to solve. - -Why this can't be a normal unit test: on Windows, Chrome opens -Cookies/Login Data with a share mode that makes a plain file copy raise -WinError 32. A Linux "open a write transaction" analog reproduces SQLite's -internal lock, NOT the Windows filesystem share lock, so only a real Chrome on -a real Windows runner exercises the failure this fix targets. -""" -from __future__ import annotations - -import os -import shutil -import sqlite3 -import subprocess -import sys -import time -from pathlib import Path - -import pytest - -pytestmark = pytest.mark.skipif( - sys.platform != "win32", reason="Windows-only live share-lock E2E" -) - -_CHROME_CANDIDATES = ( - r"C:\Program Files\Google\Chrome\Application\chrome.exe", - r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe", -) - - -def _find_chrome() -> str | None: - for p in _CHROME_CANDIDATES: - if os.path.isfile(p): - return p - which = shutil.which("chrome") or shutil.which("chrome.exe") - return which - - -def _raw_copy_raises_while_locked(path: str) -> bool: - """True if a plain copy of ``path`` fails (the WinError 32 we must beat).""" - try: - shutil.copy2(path, path + ".rawcopy") - os.unlink(path + ".rawcopy") - return False - except OSError: - return True - - -def test_locked_profile_fails_closed_not_silent(tmp_path): - """Windows contract: a running Chrome holds the cookie DB deny-all (proven - live — even CreateFile with all share flags fails), so copy-while-running - is impossible. ``snapshot_real_profile`` must FAIL FAST with an actionable - 'fully quit the browser' message — never hang, never a silent signed-out - copy. (Real-profile browsing on Windows therefore requires the browser - fully closed incl. background/tray; the live-drive path is #95669.) - """ - import time as _t - chrome = _find_chrome() - if not chrome: - pytest.skip("Chrome not installed on this runner") - - repo = Path(__file__).resolve().parents[2] - sys.path.insert(0, str(repo)) - from hermes_cli import browser_connect as bc - - user_data = tmp_path / "chrome-user-data" - user_data.mkdir() - - proc = subprocess.Popen( - [ - chrome, "--headless=new", "--disable-gpu", "--no-first-run", - "--no-default-browser-check", f"--user-data-dir={user_data}", - "--remote-debugging-port=0", "about:blank", - ], - stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, - ) - try: - cookies = None - deadline = time.time() + 60 - while time.time() < deadline: - for rel in (r"Default\Network\Cookies", r"Default\Cookies"): - cand = user_data / rel - if cand.is_file() and cand.stat().st_size > 0: - cookies = cand - break - if cookies: - break - time.sleep(1) - assert cookies is not None, "Chrome never created a Cookies DB" - - if not _raw_copy_raises_while_locked(str(cookies)): - pytest.skip("Chrome did not share-lock the cookie DB on this runner") - - import hermes_cli.browser_connect as bc_mod - orig = bc_mod.real_profile_data_dir - bc_mod.real_profile_data_dir = lambda browser, system=None: str(user_data) - try: - # Must return FAST (fail-fast lock probe), not hang. Assert both the - # contract and that it took well under the old 24-min hang. - t0 = _t.time() - dst, err = bc.snapshot_real_profile("chrome", src=str(user_data)) - elapsed = _t.time() - t0 - finally: - bc_mod.real_profile_data_dir = orig - - assert dst is None, "must not return a (silently broken) copy while locked" - assert err is not None - low = err.lower() - assert "locked" in low or "running" in low, f"unclear error: {err}" - assert "quit" in low or "close" in low, f"error must tell the user to quit: {err}" - assert elapsed < 30, f"snapshot hung on a locked profile ({elapsed:.0f}s) — must fail fast" - finally: - proc.terminate() - try: - proc.wait(timeout=15) - except subprocess.TimeoutExpired: - proc.kill() - - -def test_copy_works_when_chrome_closed(tmp_path): - """Sanity: with NO live Chrome holding the dir, the copy succeeds on Windows - (the supported path). Creates a profile with a real Chrome, closes it, then - snapshots — cookies DB must copy and be a valid SQLite file.""" - chrome = _find_chrome() - if not chrome: - pytest.skip("Chrome not installed on this runner") - - repo = Path(__file__).resolve().parents[2] - sys.path.insert(0, str(repo)) - from hermes_cli import browser_connect as bc - - user_data = tmp_path / "ud" - user_data.mkdir() - # One-shot Chrome run to materialize a profile, then it exits. - subprocess.run( - [ - chrome, "--headless=new", "--disable-gpu", "--no-first-run", - "--no-default-browser-check", f"--user-data-dir={user_data}", - "--dump-dom", "about:blank", - ], - stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=60, - ) - # Chrome has exited; the DB is now unlocked. - cookies = None - for rel in (r"Default\Network\Cookies", r"Default\Cookies"): - cand = user_data / rel - if cand.is_file(): - cookies = cand - break - if cookies is None: - pytest.skip("Chrome did not create a Cookies DB in the one-shot run") - - dst = tmp_path / "copy" / "Cookies" - assert bc._copy_auth_file(str(cookies), str(dst)) is True - assert dst.is_file() - con = sqlite3.connect(str(dst)) - try: - con.execute("SELECT name FROM sqlite_master LIMIT 1") - finally: - con.close() -