chore: remove Windows real-profile PROOF workflow + live tests
The windows-latest proof E2E and its live/diagnostic tests were branch-only evidence (they proved the deny-all lock + fast-fail contract on a real runner). Per policy proof workflows never land on main. The product fix (fast lock probe + fail-fast message) and its portable unit tests remain in tests/tools/test_browser_real_profile.py.
This commit is contained in:
@@ -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
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user