feat: per-platform Chromium versioning and build number support

- Add PLATFORM_CHROMIUM_VERSIONS map (Linux=v145, macOS=v142)
- Add get_chromium_version()/getChromiumVersion() for platform-specific version
- Make auto-update check release assets before offering updates
- Scope version markers per-platform (latest_version_linux-x64)
- Support 5th version segment for hotfix builds (e.g. 145.0.7632.109.2)
- Derive AVAILABLE_PLATFORMS from version map
This commit is contained in:
CloakHQ
2026-03-02 08:23:44 +01:00
parent 4c2e06682b
commit fc10bdf13e
9 changed files with 296 additions and 75 deletions
+29 -13
View File
@@ -10,10 +10,20 @@ from pathlib import Path
from ._version import __version__ from ._version import __version__
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Chromium version shipped with this release # Chromium version shipped with this release.
# Different platforms may ship different versions (e.g. Linux gets v145 first,
# macOS stays on v142 until Mac builds are ready).
# CHROMIUM_VERSION is the latest across all platforms (for display/reference).
# Use get_chromium_version() for the current platform's actual version.
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
CHROMIUM_VERSION = "145.0.7632.109" CHROMIUM_VERSION = "145.0.7632.109"
PLATFORM_CHROMIUM_VERSIONS: dict[str, str] = {
"linux-x64": "145.0.7632.109",
"darwin-arm64": "142.0.7444.175",
"darwin-x64": "142.0.7444.175",
}
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Default stealth arguments passed to the patched Chromium binary. # Default stealth arguments passed to the patched Chromium binary.
# These activate source-level fingerprint patches compiled into the binary. # These activate source-level fingerprint patches compiled into the binary.
@@ -70,9 +80,14 @@ SUPPORTED_PLATFORMS: dict[tuple[str, str], str] = {
("Darwin", "x86_64"): "darwin-x64", ("Darwin", "x86_64"): "darwin-x64",
} }
# Platforms with pre-built binaries available for download. # Platforms with pre-built binaries available for download (derived from version map).
# Update this set as new platform builds are released. AVAILABLE_PLATFORMS: set[str] = set(PLATFORM_CHROMIUM_VERSIONS.keys())
AVAILABLE_PLATFORMS: set[str] = {"linux-x64", "darwin-arm64", "darwin-x64"}
def get_chromium_version() -> str:
"""Return the Chromium version for the current platform."""
tag = get_platform_tag()
return PLATFORM_CHROMIUM_VERSIONS.get(tag, CHROMIUM_VERSION)
def get_platform_tag() -> str: def get_platform_tag() -> str:
@@ -105,7 +120,7 @@ def get_cache_dir() -> Path:
def get_binary_dir(version: str | None = None) -> Path: def get_binary_dir(version: str | None = None) -> Path:
"""Return the directory for a Chromium version binary.""" """Return the directory for a Chromium version binary."""
v = version or CHROMIUM_VERSION v = version or get_chromium_version()
return get_cache_dir() / f"chromium-{v}" return get_cache_dir() / f"chromium-{v}"
@@ -141,23 +156,24 @@ def check_platform_available() -> None:
def get_effective_version() -> str: def get_effective_version() -> str:
"""Return the best available version: auto-updated if available, else hardcoded. """Return the best available version: auto-updated if available, else platform default.
Reads the latest_version marker file from the cache directory. Reads a platform-scoped marker file from the cache directory.
Returns CHROMIUM_VERSION if no update has been downloaded. Returns the platform's hardcoded version if no update has been downloaded.
""" """
marker = get_cache_dir() / "latest_version" base = get_chromium_version()
marker = get_cache_dir() / f"latest_version_{get_platform_tag()}"
if marker.exists(): if marker.exists():
try: try:
version = marker.read_text().strip() version = marker.read_text().strip()
if version and _version_newer(version, CHROMIUM_VERSION): if version and _version_newer(version, base):
# Verify the binary actually exists # Verify the binary actually exists
binary = get_binary_path(version) binary = get_binary_path(version)
if binary.exists(): if binary.exists():
return version return version
except (ValueError, OSError): except (ValueError, OSError):
pass pass
return CHROMIUM_VERSION return base
def _version_tuple(v: str) -> tuple[int, ...]: def _version_tuple(v: str) -> tuple[int, ...]:
@@ -187,14 +203,14 @@ GITHUB_DOWNLOAD_BASE_URL = (
def get_download_url(version: str | None = None) -> str: def get_download_url(version: str | None = None) -> str:
"""Return the full download URL for the current platform's binary archive.""" """Return the full download URL for the current platform's binary archive."""
v = version or CHROMIUM_VERSION v = version or get_chromium_version()
tag = get_platform_tag() tag = get_platform_tag()
return f"{DOWNLOAD_BASE_URL}/chromium-v{v}/cloakbrowser-{tag}.tar.gz" return f"{DOWNLOAD_BASE_URL}/chromium-v{v}/cloakbrowser-{tag}.tar.gz"
def get_fallback_download_url(version: str | None = None) -> str: def get_fallback_download_url(version: str | None = None) -> str:
"""Return the GitHub Releases fallback URL for the binary archive.""" """Return the GitHub Releases fallback URL for the binary archive."""
v = version or CHROMIUM_VERSION v = version or get_chromium_version()
tag = get_platform_tag() tag = get_platform_tag()
return f"{GITHUB_DOWNLOAD_BASE_URL}/chromium-v{v}/cloakbrowser-{tag}.tar.gz" return f"{GITHUB_DOWNLOAD_BASE_URL}/chromium-v{v}/cloakbrowser-{tag}.tar.gz"
+21 -11
View File
@@ -30,6 +30,7 @@ from .config import (
get_binary_dir, get_binary_dir,
get_binary_path, get_binary_path,
get_cache_dir, get_cache_dir,
get_chromium_version,
get_download_url, get_download_url,
get_effective_version, get_effective_version,
get_fallback_download_url, get_fallback_download_url,
@@ -76,18 +77,19 @@ def ensure_binary() -> str:
_maybe_trigger_update_check() _maybe_trigger_update_check()
return str(binary_path) return str(binary_path)
# Fall back to hardcoded version if effective version binary doesn't exist # Fall back to platform's hardcoded version if effective version binary doesn't exist
if effective != CHROMIUM_VERSION: platform_version = get_chromium_version()
if effective != platform_version:
fallback_path = get_binary_path() fallback_path = get_binary_path()
if fallback_path.exists() and _is_executable(fallback_path): if fallback_path.exists() and _is_executable(fallback_path):
logger.debug("Binary found in cache: %s", fallback_path) logger.debug("Binary found in cache: %s", fallback_path)
_maybe_trigger_update_check() _maybe_trigger_update_check()
return str(fallback_path) return str(fallback_path)
# Download hardcoded version # Download platform's hardcoded version
logger.info( logger.info(
"Stealth Chromium %s not found. Downloading for %s...", "Stealth Chromium %s not found. Downloading for %s...",
CHROMIUM_VERSION, platform_version,
get_platform_tag(), get_platform_tag(),
) )
_download_and_extract() _download_and_extract()
@@ -168,7 +170,7 @@ def _verify_download_checksum(file_path: Path, version: str | None = None) -> No
def _fetch_checksums(version: str | None = None) -> dict[str, str] | None: def _fetch_checksums(version: str | None = None) -> dict[str, str] | None:
"""Fetch SHA256SUMS file for a version. Returns {filename: hash} or None.""" """Fetch SHA256SUMS file for a version. Returns {filename: hash} or None."""
v = version or CHROMIUM_VERSION v = version or get_chromium_version()
has_custom_url = os.environ.get("CLOAKBROWSER_DOWNLOAD_URL") has_custom_url = os.environ.get("CLOAKBROWSER_DOWNLOAD_URL")
# Build URL list — respect custom URL contract (no GitHub fallback) # Build URL list — respect custom URL contract (no GitHub fallback)
@@ -384,7 +386,7 @@ def check_for_update() -> str | None:
latest = _get_latest_chromium_version() latest = _get_latest_chromium_version()
if latest is None: if latest is None:
return None return None
if not _version_newer(latest, CHROMIUM_VERSION): if not _version_newer(latest, get_chromium_version()):
return None return None
binary_dir = get_binary_dir(latest) binary_dir = get_binary_dir(latest)
@@ -420,15 +422,22 @@ def _should_check_for_update() -> bool:
def _get_latest_chromium_version() -> str | None: def _get_latest_chromium_version() -> str | None:
"""Hit GitHub Releases API, return latest chromium-v* version string or None.""" """Hit GitHub Releases API, return latest chromium-v* version for this platform.
Checks that the release has a binary asset for the current platform,
so Linux-only releases won't be offered to macOS users.
"""
try: try:
resp = httpx.get( resp = httpx.get(
GITHUB_API_URL, params={"per_page": 10}, timeout=10.0 GITHUB_API_URL, params={"per_page": 10}, timeout=10.0
) )
resp.raise_for_status() resp.raise_for_status()
platform_tarball = f"cloakbrowser-{get_platform_tag()}.tar.gz"
for release in resp.json(): for release in resp.json():
tag = release.get("tag_name", "") tag = release.get("tag_name", "")
if tag.startswith("chromium-v") and not release.get("draft"): if tag.startswith("chromium-v") and not release.get("draft"):
asset_names = {a["name"] for a in release.get("assets", [])}
if platform_tarball in asset_names:
return tag.removeprefix("chromium-v") return tag.removeprefix("chromium-v")
return None return None
except Exception: except Exception:
@@ -437,10 +446,10 @@ def _get_latest_chromium_version() -> str | None:
def _write_version_marker(version: str) -> None: def _write_version_marker(version: str) -> None:
"""Write the latest version marker to cache dir.""" """Write the latest version marker for this platform to cache dir."""
cache_dir = get_cache_dir() cache_dir = get_cache_dir()
cache_dir.mkdir(parents=True, exist_ok=True) cache_dir.mkdir(parents=True, exist_ok=True)
marker = cache_dir / "latest_version" marker = cache_dir / f"latest_version_{get_platform_tag()}"
# Write to temp file then rename for atomicity # Write to temp file then rename for atomicity
tmp = marker.with_suffix(".tmp") tmp = marker.with_suffix(".tmp")
tmp.write_text(version) tmp.write_text(version)
@@ -455,10 +464,11 @@ def _check_and_download_update() -> None:
check_file.parent.mkdir(parents=True, exist_ok=True) check_file.parent.mkdir(parents=True, exist_ok=True)
check_file.write_text(str(time.time())) check_file.write_text(str(time.time()))
platform_version = get_chromium_version()
latest = _get_latest_chromium_version() latest = _get_latest_chromium_version()
if latest is None: if latest is None:
return return
if not _version_newer(latest, CHROMIUM_VERSION): if not _version_newer(latest, platform_version):
return return
# Already downloaded? # Already downloaded?
@@ -469,7 +479,7 @@ def _check_and_download_update() -> None:
logger.info( logger.info(
"Newer Chromium available: %s (current: %s). Downloading in background...", "Newer Chromium available: %s (current: %s). Downloading in background...",
latest, latest,
CHROMIUM_VERSION, platform_version,
) )
_download_and_extract(version=latest) _download_and_extract(version=latest)
_write_version_marker(latest) _write_version_marker(latest)
+25 -10
View File
@@ -8,10 +8,20 @@ import os from "node:os";
import path from "node:path"; import path from "node:path";
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Chromium version shipped with this release // Chromium version shipped with this release.
// Different platforms may ship different versions (e.g. Linux gets v145 first,
// macOS stays on v142 until Mac builds are ready).
// CHROMIUM_VERSION is the latest across all platforms (for display/reference).
// Use getChromiumVersion() for the current platform's actual version.
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export const CHROMIUM_VERSION = "145.0.7632.109"; export const CHROMIUM_VERSION = "145.0.7632.109";
export const PLATFORM_CHROMIUM_VERSIONS: Record<string, string> = {
"linux-x64": "145.0.7632.109",
"darwin-arm64": "142.0.7444.175",
"darwin-x64": "142.0.7444.175",
};
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Platform detection // Platform detection
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -22,9 +32,13 @@ const SUPPORTED_PLATFORMS: Record<string, string> = {
"darwin-x64": "darwin-x64", "darwin-x64": "darwin-x64",
}; };
// Platforms with pre-built binaries available for download. // Platforms with pre-built binaries available for download (derived from version map).
// Update this set as new platform builds are released. const AVAILABLE_PLATFORMS = new Set(Object.keys(PLATFORM_CHROMIUM_VERSIONS));
const AVAILABLE_PLATFORMS = new Set(["linux-x64", "darwin-arm64", "darwin-x64"]);
export function getChromiumVersion(): string {
const tag = getPlatformTag();
return PLATFORM_CHROMIUM_VERSIONS[tag] ?? CHROMIUM_VERSION;
}
export function getPlatformTag(): string { export function getPlatformTag(): string {
const platform = process.platform; const platform = process.platform;
@@ -56,7 +70,7 @@ export function getCacheDir(): string {
} }
export function getBinaryDir(version?: string): string { export function getBinaryDir(version?: string): string {
return path.join(getCacheDir(), `chromium-${version || CHROMIUM_VERSION}`); return path.join(getCacheDir(), `chromium-${version || getChromiumVersion()}`);
} }
export function getBinaryPath(version?: string): string { export function getBinaryPath(version?: string): string {
@@ -95,23 +109,24 @@ export const GITHUB_DOWNLOAD_BASE_URL =
"https://github.com/CloakHQ/cloakbrowser/releases/download"; "https://github.com/CloakHQ/cloakbrowser/releases/download";
export function getDownloadUrl(version?: string): string { export function getDownloadUrl(version?: string): string {
const v = version || CHROMIUM_VERSION; const v = version || getChromiumVersion();
const tag = getPlatformTag(); const tag = getPlatformTag();
return `${DOWNLOAD_BASE_URL}/chromium-v${v}/cloakbrowser-${tag}.tar.gz`; return `${DOWNLOAD_BASE_URL}/chromium-v${v}/cloakbrowser-${tag}.tar.gz`;
} }
export function getFallbackDownloadUrl(version?: string): string { export function getFallbackDownloadUrl(version?: string): string {
const v = version || CHROMIUM_VERSION; const v = version || getChromiumVersion();
const tag = getPlatformTag(); const tag = getPlatformTag();
return `${GITHUB_DOWNLOAD_BASE_URL}/chromium-v${v}/cloakbrowser-${tag}.tar.gz`; return `${GITHUB_DOWNLOAD_BASE_URL}/chromium-v${v}/cloakbrowser-${tag}.tar.gz`;
} }
export function getEffectiveVersion(): string { export function getEffectiveVersion(): string {
const marker = path.join(getCacheDir(), "latest_version"); const base = getChromiumVersion();
const marker = path.join(getCacheDir(), `latest_version_${getPlatformTag()}`);
try { try {
if (fs.existsSync(marker)) { if (fs.existsSync(marker)) {
const version = fs.readFileSync(marker, "utf-8").trim(); const version = fs.readFileSync(marker, "utf-8").trim();
if (version && versionNewer(version, CHROMIUM_VERSION)) { if (version && versionNewer(version, base)) {
const binary = getBinaryPath(version); const binary = getBinaryPath(version);
if (fs.existsSync(binary)) { if (fs.existsSync(binary)) {
return version; return version;
@@ -121,7 +136,7 @@ export function getEffectiveVersion(): string {
} catch { } catch {
// Marker unreadable — fall back to hardcoded // Marker unreadable — fall back to hardcoded
} }
return CHROMIUM_VERSION; return base;
} }
export function parseVersion(v: string): number[] { export function parseVersion(v: string): number[] {
+22 -12
View File
@@ -14,7 +14,6 @@ import { extract as tarExtract } from "tar";
import type { BinaryInfo } from "./types.js"; import type { BinaryInfo } from "./types.js";
import { import {
CHROMIUM_VERSION,
DOWNLOAD_BASE_URL, DOWNLOAD_BASE_URL,
GITHUB_API_URL, GITHUB_API_URL,
GITHUB_DOWNLOAD_BASE_URL, GITHUB_DOWNLOAD_BASE_URL,
@@ -22,6 +21,7 @@ import {
getBinaryDir, getBinaryDir,
getBinaryPath, getBinaryPath,
getCacheDir, getCacheDir,
getChromiumVersion,
getDownloadUrl, getDownloadUrl,
getEffectiveVersion, getEffectiveVersion,
getFallbackDownloadUrl, getFallbackDownloadUrl,
@@ -66,8 +66,9 @@ export async function ensureBinary(): Promise<string> {
return binaryPath; return binaryPath;
} }
// Fall back to hardcoded version if effective version binary doesn't exist // Fall back to platform's hardcoded version if effective version binary doesn't exist
if (effective !== CHROMIUM_VERSION) { const platformVersion = getChromiumVersion();
if (effective !== platformVersion) {
const fallbackPath = getBinaryPath(); const fallbackPath = getBinaryPath();
if (fs.existsSync(fallbackPath) && isExecutable(fallbackPath)) { if (fs.existsSync(fallbackPath) && isExecutable(fallbackPath)) {
maybeTriggerUpdateCheck(); maybeTriggerUpdateCheck();
@@ -75,9 +76,9 @@ export async function ensureBinary(): Promise<string> {
} }
} }
// Download hardcoded version // Download platform's hardcoded version
console.log( console.log(
`[cloakbrowser] Stealth Chromium ${CHROMIUM_VERSION} not found. Downloading for ${getPlatformTag()}...` `[cloakbrowser] Stealth Chromium ${platformVersion} not found. Downloading for ${getPlatformTag()}...`
); );
await downloadAndExtract(); await downloadAndExtract();
@@ -120,7 +121,7 @@ export function binaryInfo(): BinaryInfo {
/** Manually check for a newer Chromium version. Returns new version or null. */ /** Manually check for a newer Chromium version. Returns new version or null. */
export async function checkForUpdate(): Promise<string | null> { export async function checkForUpdate(): Promise<string | null> {
const latest = await getLatestChromiumVersion(); const latest = await getLatestChromiumVersion();
if (!latest || !versionNewer(latest, CHROMIUM_VERSION)) return null; if (!latest || !versionNewer(latest, getChromiumVersion())) return null;
const binaryDir = getBinaryDir(latest); const binaryDir = getBinaryDir(latest);
if (fs.existsSync(binaryDir)) { if (fs.existsSync(binaryDir)) {
@@ -209,7 +210,7 @@ async function verifyDownloadChecksum(filePath: string, version?: string): Promi
} }
async function fetchChecksums(version?: string): Promise<Map<string, string> | null> { async function fetchChecksums(version?: string): Promise<Map<string, string> | null> {
const v = version || CHROMIUM_VERSION; const v = version || getChromiumVersion();
const hasCustomUrl = !!process.env.CLOAKBROWSER_DOWNLOAD_URL; const hasCustomUrl = !!process.env.CLOAKBROWSER_DOWNLOAD_URL;
// Respect custom URL contract — no GitHub fallback when custom URL is set // Respect custom URL contract — no GitHub fallback when custom URL is set
@@ -437,7 +438,8 @@ function shouldCheckForUpdate(): boolean {
return true; return true;
} }
async function getLatestChromiumVersion(): Promise<string | null> { /** @internal Exported for testing only. */
export async function getLatestChromiumVersion(): Promise<string | null> {
try { try {
const resp = await fetch(`${GITHUB_API_URL}?per_page=10`, { const resp = await fetch(`${GITHUB_API_URL}?per_page=10`, {
signal: AbortSignal.timeout(10_000), signal: AbortSignal.timeout(10_000),
@@ -446,10 +448,17 @@ async function getLatestChromiumVersion(): Promise<string | null> {
const releases = (await resp.json()) as Array<{ const releases = (await resp.json()) as Array<{
tag_name: string; tag_name: string;
draft: boolean; draft: boolean;
assets: Array<{ name: string }>;
}>; }>;
const platformTarball = `cloakbrowser-${getPlatformTag()}.tar.gz`;
for (const release of releases) { for (const release of releases) {
if (release.tag_name.startsWith("chromium-v") && !release.draft) { if (release.tag_name.startsWith("chromium-v") && !release.draft) {
return release.tag_name.replace("chromium-v", ""); const assetNames = new Set(
(release.assets ?? []).map((a) => a.name)
);
if (assetNames.has(platformTarball)) {
return release.tag_name.replace(/^chromium-v/, "");
}
} }
} }
return null; return null;
@@ -461,7 +470,7 @@ async function getLatestChromiumVersion(): Promise<string | null> {
function writeVersionMarker(version: string): void { function writeVersionMarker(version: string): void {
const cacheDir = getCacheDir(); const cacheDir = getCacheDir();
fs.mkdirSync(cacheDir, { recursive: true }); fs.mkdirSync(cacheDir, { recursive: true });
const marker = path.join(cacheDir, "latest_version"); const marker = path.join(cacheDir, `latest_version_${getPlatformTag()}`);
const tmp = `${marker}.tmp`; const tmp = `${marker}.tmp`;
fs.writeFileSync(tmp, version); fs.writeFileSync(tmp, version);
fs.renameSync(tmp, marker); fs.renameSync(tmp, marker);
@@ -477,8 +486,9 @@ async function checkAndDownloadUpdate(): Promise<void> {
String(Date.now()) String(Date.now())
); );
const platformVersion = getChromiumVersion();
const latest = await getLatestChromiumVersion(); const latest = await getLatestChromiumVersion();
if (!latest || !versionNewer(latest, CHROMIUM_VERSION)) return; if (!latest || !versionNewer(latest, platformVersion)) return;
// Already downloaded? // Already downloaded?
if (fs.existsSync(getBinaryDir(latest))) { if (fs.existsSync(getBinaryDir(latest))) {
@@ -487,7 +497,7 @@ async function checkAndDownloadUpdate(): Promise<void> {
} }
console.log( console.log(
`[cloakbrowser] Newer Chromium available: ${latest} (current: ${CHROMIUM_VERSION}). Downloading in background...` `[cloakbrowser] Newer Chromium available: ${latest} (current: ${platformVersion}). Downloading in background...`
); );
await downloadAndExtract(latest); await downloadAndExtract(latest);
writeVersionMarker(latest); writeVersionMarker(latest);
+6 -5
View File
@@ -1,6 +1,7 @@
import { describe, it, expect } from "vitest"; import { describe, it, expect } from "vitest";
import { import {
CHROMIUM_VERSION, CHROMIUM_VERSION,
getChromiumVersion,
getDefaultStealthArgs, getDefaultStealthArgs,
getCacheDir, getCacheDir,
getBinaryDir, getBinaryDir,
@@ -10,7 +11,7 @@ import { _buildArgsForTest } from "../src/playwright.js";
describe("config", () => { describe("config", () => {
it("CHROMIUM_VERSION matches expected format", () => { it("CHROMIUM_VERSION matches expected format", () => {
expect(CHROMIUM_VERSION).toMatch(/^\d+\.\d+\.\d+\.\d+$/); expect(CHROMIUM_VERSION).toMatch(/^\d+\.\d+\.\d+\.\d+(\.\d+)?$/);
}); });
it("getDefaultStealthArgs returns expected flags", () => { it("getDefaultStealthArgs returns expected flags", () => {
@@ -53,14 +54,14 @@ describe("config", () => {
expect(dir).toContain(".cloakbrowser"); expect(dir).toContain(".cloakbrowser");
}); });
it("getBinaryDir includes version", () => { it("getBinaryDir includes platform version", () => {
const dir = getBinaryDir(); const dir = getBinaryDir();
expect(dir).toContain(`chromium-${CHROMIUM_VERSION}`); expect(dir).toContain(`chromium-${getChromiumVersion()}`);
}); });
it("getDownloadUrl contains version and platform tag", () => { it("getDownloadUrl contains platform version and platform tag", () => {
const url = getDownloadUrl(); const url = getDownloadUrl();
expect(url).toContain(CHROMIUM_VERSION); expect(url).toContain(getChromiumVersion());
expect(url).toContain("cloakbrowser-"); expect(url).toContain("cloakbrowser-");
expect(url).toContain(".tar.gz"); expect(url).toContain(".tar.gz");
expect(url).toContain("cloakbrowser.dev"); expect(url).toContain("cloakbrowser.dev");
+2 -2
View File
@@ -1,12 +1,12 @@
import { describe, it, expect } from "vitest"; import { describe, it, expect } from "vitest";
import { binaryInfo } from "../src/download.js"; import { binaryInfo } from "../src/download.js";
import { CHROMIUM_VERSION } from "../src/config.js"; import { getChromiumVersion } from "../src/config.js";
describe("binaryInfo", () => { describe("binaryInfo", () => {
it("returns correct structure", () => { it("returns correct structure", () => {
const info = binaryInfo(); const info = binaryInfo();
expect(info.version).toBe(CHROMIUM_VERSION); expect(info.version).toBe(getChromiumVersion());
expect(info.platform).toMatch(/^(linux|darwin)-(x64|arm64)$/); expect(info.platform).toMatch(/^(linux|darwin)-(x64|arm64)$/);
expect(info.binaryPath).toBeTruthy(); expect(info.binaryPath).toBeTruthy();
expect(typeof info.installed).toBe("boolean"); expect(typeof info.installed).toBe("boolean");
+100 -4
View File
@@ -1,11 +1,14 @@
import { describe, it, expect } from "vitest"; import { describe, it, expect, vi, afterEach } from "vitest";
import { import {
CHROMIUM_VERSION, CHROMIUM_VERSION,
getChromiumVersion,
getDownloadUrl, getDownloadUrl,
getEffectiveVersion, getEffectiveVersion,
getPlatformTag,
parseVersion, parseVersion,
versionNewer, versionNewer,
} from "../src/config.js"; } from "../src/config.js";
import { getLatestChromiumVersion } from "../src/download.js";
describe("version comparison", () => { describe("version comparison", () => {
it("parseVersion handles 4-part versions", () => { it("parseVersion handles 4-part versions", () => {
@@ -32,13 +35,29 @@ describe("version comparison", () => {
it("major bump wins over minor", () => { it("major bump wins over minor", () => {
expect(versionNewer("143.0.0.0", "142.9.9999.999")).toBe(true); expect(versionNewer("143.0.0.0", "142.9.9999.999")).toBe(true);
}); });
it("parseVersion handles 5-part build numbers", () => {
expect(parseVersion("145.0.7632.109.2")).toEqual([145, 0, 7632, 109, 2]);
});
it("build bump detected", () => {
expect(versionNewer("145.0.7632.109.3", "145.0.7632.109.2")).toBe(true);
});
it("build suffix newer than no suffix", () => {
expect(versionNewer("145.0.7632.109.2", "145.0.7632.109")).toBe(true);
});
it("no suffix older than build suffix", () => {
expect(versionNewer("145.0.7632.109", "145.0.7632.109.2")).toBe(false);
});
}); });
describe("download URL", () => { describe("download URL", () => {
it("uses chromium-v prefix and cloakbrowser repo", () => { it("uses chromium-v prefix and cloakbrowser repo", () => {
const url = getDownloadUrl(); const url = getDownloadUrl();
expect(url).toContain("cloakbrowser.dev"); expect(url).toContain("cloakbrowser.dev");
expect(url).toContain(`chromium-v${CHROMIUM_VERSION}`); expect(url).toContain(`chromium-v${getChromiumVersion()}`);
expect(url.endsWith(".tar.gz")).toBe(true); expect(url.endsWith(".tar.gz")).toBe(true);
}); });
@@ -53,9 +72,86 @@ describe("download URL", () => {
}); });
}); });
describe("latest version (platform-aware)", () => {
const platformTarball = `cloakbrowser-${getPlatformTag()}.tar.gz`;
function makeAssets(platforms: string[]) {
return platforms.map((p) => ({ name: `cloakbrowser-${p}.tar.gz` }));
}
function mockFetch(releases: Array<Record<string, unknown>>) {
return vi.spyOn(globalThis, "fetch").mockResolvedValue({
ok: true,
json: async () => releases,
} as Response);
}
afterEach(() => {
vi.restoreAllMocks();
});
it("returns version when release has platform asset", async () => {
mockFetch([
{
tag_name: "chromium-v145.0.7718.0",
draft: false,
assets: makeAssets(["linux-x64", "darwin-arm64", "darwin-x64"]),
},
]);
expect(await getLatestChromiumVersion()).toBe("145.0.7718.0");
});
it("skips release without platform asset", async () => {
const spy = mockFetch([
{
tag_name: "chromium-v145.0.7718.0",
draft: false,
assets: makeAssets(["linux-x64"]), // Linux only
},
{
tag_name: "chromium-v142.0.7444.175",
draft: false,
assets: makeAssets(["linux-x64", "darwin-arm64", "darwin-x64"]),
},
]);
const result = await getLatestChromiumVersion();
const tag = getPlatformTag();
if (tag === "linux-x64") {
expect(result).toBe("145.0.7718.0");
} else {
expect(result).toBe("142.0.7444.175");
}
});
it("returns null when no release has platform asset", async () => {
mockFetch([
{
tag_name: "chromium-v145.0.7718.0",
draft: false,
assets: [{ name: "cloakbrowser-windows-x64.tar.gz" }],
},
]);
expect(await getLatestChromiumVersion()).toBeNull();
});
it("skips draft releases", async () => {
const all = ["linux-x64", "darwin-arm64", "darwin-x64"];
mockFetch([
{ tag_name: "chromium-v999.0.0.0", draft: true, assets: makeAssets(all) },
{ tag_name: "chromium-v145.0.7718.0", draft: false, assets: makeAssets(all) },
]);
expect(await getLatestChromiumVersion()).toBe("145.0.7718.0");
});
it("returns null on network error", async () => {
vi.spyOn(globalThis, "fetch").mockRejectedValue(new Error("timeout"));
expect(await getLatestChromiumVersion()).toBeNull();
});
});
describe("effective version", () => { describe("effective version", () => {
it("returns CHROMIUM_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
expect(getEffectiveVersion()).toBe(CHROMIUM_VERSION); expect(getEffectiveVersion()).toBe(getChromiumVersion());
}); });
}); });
+3 -2
View File
@@ -1,7 +1,8 @@
"""Basic launch tests for cloakbrowser.""" """Basic launch tests for cloakbrowser."""
import pytest import pytest
from cloakbrowser import launch, launch_async, binary_info, CHROMIUM_VERSION from cloakbrowser import launch, launch_async, binary_info
from cloakbrowser.config import get_chromium_version
def test_binary_info(): def test_binary_info():
@@ -11,7 +12,7 @@ def test_binary_info():
assert "platform" in info assert "platform" in info
assert "binary_path" in info assert "binary_path" in info
assert "installed" in info assert "installed" in info
assert info["version"] == CHROMIUM_VERSION assert info["version"] == get_chromium_version()
def test_launch_and_close(): def test_launch_and_close():
+87 -15
View File
@@ -12,8 +12,10 @@ from cloakbrowser.config import (
CHROMIUM_VERSION, CHROMIUM_VERSION,
_version_newer, _version_newer,
_version_tuple, _version_tuple,
get_chromium_version,
get_download_url, get_download_url,
get_effective_version, get_effective_version,
get_platform_tag,
) )
from cloakbrowser.download import ( from cloakbrowser.download import (
_get_latest_chromium_version, _get_latest_chromium_version,
@@ -41,12 +43,27 @@ class TestVersionComparison:
def test_major_bump(self): def test_major_bump(self):
assert _version_newer("143.0.0.0", "142.9.9999.999") is True assert _version_newer("143.0.0.0", "142.9.9999.999") is True
def test_5th_segment_parsing(self):
assert _version_tuple("145.0.7632.109.2") == (145, 0, 7632, 109, 2)
def test_build_bump(self):
assert _version_newer("145.0.7632.109.3", "145.0.7632.109.2") is True
def test_build_suffix_newer_than_no_suffix(self):
assert _version_newer("145.0.7632.109.2", "145.0.7632.109") is True
def test_no_suffix_older_than_build_suffix(self):
assert _version_newer("145.0.7632.109", "145.0.7632.109.2") is False
def test_new_chromium_beats_old_build(self):
assert _version_newer("146.0.0.0", "145.0.7632.109.2") is True
class TestDownloadUrl: class TestDownloadUrl:
def test_default_url_format(self): def test_default_url_format(self):
url = get_download_url() url = get_download_url()
assert "cloakbrowser.dev" in url assert "cloakbrowser.dev" in url
assert f"chromium-v{CHROMIUM_VERSION}" in url assert f"chromium-v{get_chromium_version()}" in url
assert url.endswith(".tar.gz") assert url.endswith(".tar.gz")
def test_custom_version_url(self): def test_custom_version_url(self):
@@ -111,30 +128,42 @@ class TestShouldCheckForUpdate:
class TestEffectiveVersion: class TestEffectiveVersion:
def test_no_marker_returns_hardcoded(self, tmp_path): def test_no_marker_returns_platform_version(self, tmp_path):
with patch.dict(os.environ, {"CLOAKBROWSER_CACHE_DIR": str(tmp_path)}): with patch.dict(os.environ, {"CLOAKBROWSER_CACHE_DIR": str(tmp_path)}):
assert get_effective_version() == CHROMIUM_VERSION assert get_effective_version() == get_chromium_version()
def test_marker_with_newer_version(self, tmp_path): def test_marker_with_newer_version(self, tmp_path):
with patch.dict(os.environ, {"CLOAKBROWSER_CACHE_DIR": str(tmp_path)}): with patch.dict(os.environ, {"CLOAKBROWSER_CACHE_DIR": str(tmp_path)}):
marker = tmp_path / "latest_version" marker = tmp_path / f"latest_version_{get_platform_tag()}"
marker.write_text("999.0.0.0") marker.write_text("999.0.0.0")
# Binary doesn't exist, so should fall back # Binary doesn't exist, so should fall back
assert get_effective_version() == CHROMIUM_VERSION assert get_effective_version() == get_chromium_version()
def test_marker_with_older_version_ignored(self, tmp_path): def test_marker_with_older_version_ignored(self, tmp_path):
with patch.dict(os.environ, {"CLOAKBROWSER_CACHE_DIR": str(tmp_path)}): with patch.dict(os.environ, {"CLOAKBROWSER_CACHE_DIR": str(tmp_path)}):
marker = tmp_path / "latest_version" marker = tmp_path / f"latest_version_{get_platform_tag()}"
marker.write_text("100.0.0.0") marker.write_text("100.0.0.0")
assert get_effective_version() == CHROMIUM_VERSION assert get_effective_version() == get_chromium_version()
class TestGetLatestVersion: class TestGetLatestVersion:
def test_parses_chromium_tag(self): """Tests for _get_latest_chromium_version with platform-aware asset checking."""
def _make_assets(self, platforms: list[str]) -> list[dict]:
"""Helper to build asset list from platform tags."""
return [{"name": f"cloakbrowser-{p}.tar.gz"} for p in platforms]
def _platform_tarball(self) -> str:
return f"cloakbrowser-{get_platform_tag()}.tar.gz"
def test_parses_chromium_tag_with_platform_asset(self):
mock_response = MagicMock() mock_response = MagicMock()
mock_response.json.return_value = [ mock_response.json.return_value = [
{"tag_name": "chromium-v145.0.7718.0", "draft": False}, {
{"tag_name": "chromium-v142.0.7444.175", "draft": False}, "tag_name": "chromium-v145.0.7718.0",
"draft": False,
"assets": self._make_assets(["linux-x64", "darwin-arm64", "darwin-x64"]),
},
] ]
mock_response.raise_for_status = MagicMock() mock_response.raise_for_status = MagicMock()
@@ -142,11 +171,37 @@ class TestGetLatestVersion:
result = _get_latest_chromium_version() result = _get_latest_chromium_version()
assert result == "145.0.7718.0" assert result == "145.0.7718.0"
def test_skips_draft_releases(self): def test_skips_release_without_platform_asset(self):
"""If latest release has no asset for our platform, fall back to older release."""
mock_response = MagicMock() mock_response = MagicMock()
mock_response.json.return_value = [ mock_response.json.return_value = [
{"tag_name": "chromium-v999.0.0.0", "draft": True}, {
{"tag_name": "chromium-v145.0.7718.0", "draft": False}, "tag_name": "chromium-v145.0.7718.0",
"draft": False,
"assets": self._make_assets(["linux-x64"]), # Linux only
},
{
"tag_name": "chromium-v142.0.7444.175",
"draft": False,
"assets": self._make_assets(["linux-x64", "darwin-arm64", "darwin-x64"]),
},
]
mock_response.raise_for_status = MagicMock()
with patch("cloakbrowser.download.httpx.get", return_value=mock_response):
result = _get_latest_chromium_version()
tag = get_platform_tag()
if tag == "linux-x64":
assert result == "145.0.7718.0"
else:
assert result == "142.0.7444.175"
def test_skips_draft_releases(self):
mock_response = MagicMock()
all_platforms = ["linux-x64", "darwin-arm64", "darwin-x64"]
mock_response.json.return_value = [
{"tag_name": "chromium-v999.0.0.0", "draft": True, "assets": self._make_assets(all_platforms)},
{"tag_name": "chromium-v145.0.7718.0", "draft": False, "assets": self._make_assets(all_platforms)},
] ]
mock_response.raise_for_status = MagicMock() mock_response.raise_for_status = MagicMock()
@@ -156,9 +211,10 @@ class TestGetLatestVersion:
def test_skips_non_chromium_tags(self): def test_skips_non_chromium_tags(self):
mock_response = MagicMock() mock_response = MagicMock()
all_platforms = ["linux-x64", "darwin-arm64", "darwin-x64"]
mock_response.json.return_value = [ mock_response.json.return_value = [
{"tag_name": "v0.2.0", "draft": False}, {"tag_name": "v0.2.0", "draft": False, "assets": self._make_assets(all_platforms)},
{"tag_name": "chromium-v145.0.7718.0", "draft": False}, {"tag_name": "chromium-v145.0.7718.0", "draft": False, "assets": self._make_assets(all_platforms)},
] ]
mock_response.raise_for_status = MagicMock() mock_response.raise_for_status = MagicMock()
@@ -166,6 +222,22 @@ class TestGetLatestVersion:
result = _get_latest_chromium_version() result = _get_latest_chromium_version()
assert result == "145.0.7718.0" assert result == "145.0.7718.0"
def test_returns_none_when_no_platform_assets(self):
"""If no release has our platform, return None."""
mock_response = MagicMock()
mock_response.json.return_value = [
{
"tag_name": "chromium-v145.0.7718.0",
"draft": False,
"assets": [{"name": "cloakbrowser-windows-x64.tar.gz"}],
},
]
mock_response.raise_for_status = MagicMock()
with patch("cloakbrowser.download.httpx.get", return_value=mock_response):
result = _get_latest_chromium_version()
assert result is None
def test_network_error_returns_none(self): def test_network_error_returns_none(self):
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()