fix: code review — breaking change docs, version marker migration, checksum tests

- Document playwright→patchright breaking change in CHANGELOG
- Document default viewport change (1920x955) in CHANGELOG
- Add legacy latest_version marker fallback for <0.3.0 upgrades
- Add checksum parsing/verification tests (Python + JS)
- Document CLOAKBROWSER_SKIP_CHECKSUM env var in README
- Fix case-insensitive SHA256SUMS regex in JS
- Replace page.wait_for_timeout() with time.sleep() in example
This commit is contained in:
CloakHQ
2026-03-02 09:03:57 +01:00
parent fc10bdf13e
commit a4b6caff47
8 changed files with 124 additions and 26 deletions
+5
View File
@@ -10,6 +10,11 @@ Changes are tagged: **[wrapper]** for Python/JS wrapper, **[binary]** for Chromi
Chromium v145 upgrade. 26 fingerprint patches (up from 16). New download verification and fallback system. Pending: macOS v145 binary builds. Chromium v145 upgrade. 26 fingerprint patches (up from 16). New download verification and fallback system. Pending: macOS v145 binary builds.
### Breaking
- **[wrapper]** Python dependency changed from `playwright` to `patchright` (CDP stealth fork). Patchright is API-compatible, but if you import `playwright` directly elsewhere, add it as a separate dependency. Replace `from playwright.sync_api` with `from patchright.sync_api` (or keep using `cloakbrowser.launch()` which handles this automatically).
- **[wrapper]** `launch_context()` / `launchContext()` now defaults viewport to 1920x955 (realistic maximized Chrome on 1080p Windows) instead of Playwright's default 1280x720. Pass `viewport={"width": 1280, "height": 720}` explicitly to restore old behavior.
### 2026-03-01 ### 2026-03-01
- **[wrapper]** Upgrade wrapper to Chromium v145.0.7632.109 - **[wrapper]** Upgrade wrapper to Chromium v145.0.7632.109
+1
View File
@@ -324,6 +324,7 @@ clearCache();
| `CLOAKBROWSER_CACHE_DIR` | `~/.cloakbrowser` | Binary cache directory | | `CLOAKBROWSER_CACHE_DIR` | `~/.cloakbrowser` | Binary cache directory |
| `CLOAKBROWSER_DOWNLOAD_URL` | `cloakbrowser.dev` | Custom download URL for binary | | `CLOAKBROWSER_DOWNLOAD_URL` | `cloakbrowser.dev` | Custom download URL for binary |
| `CLOAKBROWSER_AUTO_UPDATE` | `true` | Set to `false` to disable background update checks | | `CLOAKBROWSER_AUTO_UPDATE` | `true` | Set to `false` to disable background update checks |
| `CLOAKBROWSER_SKIP_CHECKSUM` | `false` | Set to `true` to skip SHA-256 verification after download |
## Fingerprint Management ## Fingerprint Management
+13 -11
View File
@@ -162,17 +162,19 @@ def get_effective_version() -> str:
Returns the platform's hardcoded version if no update has been downloaded. Returns the platform's hardcoded version if no update has been downloaded.
""" """
base = get_chromium_version() base = get_chromium_version()
marker = get_cache_dir() / f"latest_version_{get_platform_tag()}" # Try platform-scoped marker first, fall back to legacy marker for upgrades from <0.3.0
if marker.exists(): cache = get_cache_dir()
try: for name in (f"latest_version_{get_platform_tag()}", "latest_version"):
version = marker.read_text().strip() marker = cache / name
if version and _version_newer(version, base): if marker.exists():
# Verify the binary actually exists try:
binary = get_binary_path(version) version = marker.read_text().strip()
if binary.exists(): if version and _version_newer(version, base):
return version binary = get_binary_path(version)
except (ValueError, OSError): if binary.exists():
pass return version
except (ValueError, OSError):
pass
return base return base
+3 -2
View File
@@ -13,6 +13,7 @@ Usage:
""" """
import sys import sys
import time
from cloakbrowser import launch_context from cloakbrowser import launch_context
@@ -27,7 +28,7 @@ def test_fingerprint_scan(page):
"""fingerprint-scan.com — bot risk score + headless detection signals.""" """fingerprint-scan.com — bot risk score + headless detection signals."""
print("=== fingerprint-scan.com ===") print("=== fingerprint-scan.com ===")
page.goto("https://fingerprint-scan.com/", wait_until="domcontentloaded", timeout=30000) page.goto("https://fingerprint-scan.com/", wait_until="domcontentloaded", timeout=30000)
page.wait_for_timeout(20000) # Castle.js needs time to compute score time.sleep(20) # Castle.js needs time to compute score
# Check bot risk score # Check bot risk score
score = page.evaluate( score = page.evaluate(
@@ -92,7 +93,7 @@ def test_creepjs(page):
"https://abrahamjuliot.github.io/creepjs/", wait_until="domcontentloaded", timeout=30000 "https://abrahamjuliot.github.io/creepjs/", wait_until="domcontentloaded", timeout=30000
) )
print("Waiting 30s for CreepJS analysis...") print("Waiting 30s for CreepJS analysis...")
page.wait_for_timeout(30000) time.sleep(30)
# Extract % scores from page text (matches test-infra/matrix_tests/group3_bot_detection.py) # Extract % scores from page text (matches test-infra/matrix_tests/group3_bot_detection.py)
scores = page.evaluate("""() => { scores = page.evaluate("""() => {
+14 -10
View File
@@ -122,19 +122,23 @@ export function getFallbackDownloadUrl(version?: string): string {
export function getEffectiveVersion(): string { export function getEffectiveVersion(): string {
const base = getChromiumVersion(); const base = getChromiumVersion();
const marker = path.join(getCacheDir(), `latest_version_${getPlatformTag()}`); const cacheDir = getCacheDir();
try { // Try platform-scoped marker first, fall back to legacy marker for upgrades from <0.3.0
if (fs.existsSync(marker)) { for (const name of [`latest_version_${getPlatformTag()}`, "latest_version"]) {
const version = fs.readFileSync(marker, "utf-8").trim(); const marker = path.join(cacheDir, name);
if (version && versionNewer(version, base)) { try {
const binary = getBinaryPath(version); if (fs.existsSync(marker)) {
if (fs.existsSync(binary)) { const version = fs.readFileSync(marker, "utf-8").trim();
return version; if (version && versionNewer(version, base)) {
const binary = getBinaryPath(version);
if (fs.existsSync(binary)) {
return version;
}
} }
} }
} catch {
// Marker unreadable — try next
} }
} catch {
// Marker unreadable — fall back to hardcoded
} }
return base; return base;
} }
+3 -2
View File
@@ -234,12 +234,13 @@ async function fetchChecksums(version?: string): Promise<Map<string, string> | n
return null; return null;
} }
function parseChecksums(text: string): Map<string, string> { /** @internal Exported for testing only. */
export function parseChecksums(text: string): Map<string, string> {
const result = new Map<string, string>(); const result = new Map<string, string>();
for (const line of text.trim().split("\n")) { for (const line of text.trim().split("\n")) {
const trimmed = line.trim(); const trimmed = line.trim();
if (!trimmed) continue; if (!trimmed) continue;
const match = trimmed.match(/^([a-f0-9]{64})\s+\*?(.+)$/); const match = trimmed.match(/^([a-f0-9]{64})\s+\*?(.+)$/i);
if (match) { if (match) {
result.set(match[2]!, match[1]!.toLowerCase()); result.set(match[2]!, match[1]!.toLowerCase());
} }
+33 -1
View File
@@ -8,7 +8,7 @@ import {
parseVersion, parseVersion,
versionNewer, versionNewer,
} from "../src/config.js"; } from "../src/config.js";
import { getLatestChromiumVersion } from "../src/download.js"; import { getLatestChromiumVersion, parseChecksums } from "../src/download.js";
describe("version comparison", () => { describe("version comparison", () => {
it("parseVersion handles 4-part versions", () => { it("parseVersion handles 4-part versions", () => {
@@ -149,6 +149,38 @@ describe("latest version (platform-aware)", () => {
}); });
}); });
describe("parseChecksums", () => {
// Valid 64-char hex strings for testing
const HASH_A = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
const HASH_B = "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2";
it("parses standard SHA256SUMS format", () => {
const text = [
`${HASH_A} cloakbrowser-linux-x64.tar.gz`,
`${HASH_B} cloakbrowser-darwin-arm64.tar.gz`,
].join("\n");
const result = parseChecksums(text);
expect(result.get("cloakbrowser-linux-x64.tar.gz")).toBe(HASH_A);
expect(result.get("cloakbrowser-darwin-arm64.tar.gz")).toBe(HASH_B);
});
it("handles binary-mode asterisk prefix", () => {
const text = `${HASH_A} *cloakbrowser-linux-x64.tar.gz`;
const result = parseChecksums(text);
expect(result.has("cloakbrowser-linux-x64.tar.gz")).toBe(true);
});
it("skips empty lines", () => {
const text = `\n\n${HASH_A} file.tar.gz\n\n`;
expect(parseChecksums(text).size).toBe(1);
});
it("returns empty map for empty input", () => {
expect(parseChecksums("").size).toBe(0);
expect(parseChecksums(" \n \n").size).toBe(0);
});
});
describe("effective version", () => { describe("effective version", () => {
it("returns platform version when no marker exists", () => { it("returns platform version when no marker exists", () => {
// Default behavior — no marker file in test environment // Default behavior — no marker file in test environment
+52
View File
@@ -2,6 +2,7 @@
from __future__ import annotations from __future__ import annotations
import hashlib
import os import os
from pathlib import Path from pathlib import Path
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
@@ -19,7 +20,9 @@ from cloakbrowser.config import (
) )
from cloakbrowser.download import ( from cloakbrowser.download import (
_get_latest_chromium_version, _get_latest_chromium_version,
_parse_checksums,
_should_check_for_update, _should_check_for_update,
_verify_checksum,
) )
@@ -242,3 +245,52 @@ class TestGetLatestVersion:
with patch("cloakbrowser.download.httpx.get", side_effect=Exception("timeout")): with patch("cloakbrowser.download.httpx.get", side_effect=Exception("timeout")):
result = _get_latest_chromium_version() result = _get_latest_chromium_version()
assert result is None assert result is None
class TestParseChecksums:
HASH_A = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
HASH_B = "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"
def test_standard_format(self):
text = (
f"{self.HASH_A} cloakbrowser-linux-x64.tar.gz\n"
f"{self.HASH_B} cloakbrowser-darwin-arm64.tar.gz\n"
)
result = _parse_checksums(text)
assert result["cloakbrowser-linux-x64.tar.gz"] == self.HASH_A
assert result["cloakbrowser-darwin-arm64.tar.gz"] == self.HASH_B
def test_binary_mode_asterisk(self):
text = f"{self.HASH_A} *cloakbrowser-linux-x64.tar.gz\n"
result = _parse_checksums(text)
assert "cloakbrowser-linux-x64.tar.gz" in result
def test_empty_lines_skipped(self):
text = f"\n\n{self.HASH_A} file.tar.gz\n\n"
result = _parse_checksums(text)
assert len(result) == 1
def test_uppercase_lowered(self):
text = f"{self.HASH_A.upper()} file.tar.gz\n"
result = _parse_checksums(text)
assert result["file.tar.gz"] == self.HASH_A
def test_empty_input(self):
assert _parse_checksums("") == {}
assert _parse_checksums(" \n \n") == {}
class TestVerifyChecksum:
def test_matching_checksum(self, tmp_path):
content = b"test binary content"
file = tmp_path / "test.tar.gz"
file.write_bytes(content)
expected = hashlib.sha256(content).hexdigest()
# Should not raise
_verify_checksum(file, expected)
def test_mismatched_checksum(self, tmp_path):
file = tmp_path / "test.tar.gz"
file.write_bytes(b"real content")
with pytest.raises(RuntimeError, match="Checksum verification failed"):
_verify_checksum(file, "0" * 64)