From b43753f4df7be406d887548ffae51063bc996ed8 Mon Sep 17 00:00:00 2001 From: seanbetts Date: Wed, 29 Apr 2026 20:36:02 +0100 Subject: [PATCH] Add SteamDB browser fallback --- README.md | 61 ++++++++++- SKILL.md | 9 ++ scripts/bootstrap_steamdb.sh | 62 +++++++++++ scripts/check_steamdb.sh | 71 +++++++++++- scripts/close_steamdb.sh | 34 ++++++ scripts/fetch_steamdb_with_playwright.js | 116 ++++++++++++++++++++ scripts/run_watch.sh | 12 ++- status/current.md | 22 ++-- status/evidence.jsonl | 4 + tests/test_check_steamdb.py | 131 +++++++++++++++++++++++ 10 files changed, 505 insertions(+), 17 deletions(-) create mode 100755 scripts/bootstrap_steamdb.sh create mode 100755 scripts/close_steamdb.sh create mode 100755 scripts/fetch_steamdb_with_playwright.js create mode 100644 tests/test_check_steamdb.py diff --git a/README.md b/README.md index a2241f2..553e3ed 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,16 @@ scripts/close_komodo.sh That keeps Komodo access isolated from your normal Chrome profile and gives the agent a reusable trusted browser session when Komodo blocks normal automation. +SteamDB can require the same live-browser treatment: + +```sh +scripts/bootstrap_steamdb.sh +scripts/run_watch.sh 2026-04-25 ~/steam_hardware_watch /tmp/SteamTracking-master +scripts/close_steamdb.sh +``` + +If both sources are blocked, bootstrap both before the run. + ## Output Each run writes: @@ -292,12 +302,60 @@ The helper scripts now support that same live-session route with: - `PLAYWRIGHT_CDP_ENDPOINT` - automatic local env loading from `.local/komodo-env.sh` +## Optional SteamDB Browser Bootstrap + +SteamDB may return a Cloudflare browser challenge to `curl` and to fresh automated browser contexts. The SteamDB helper uses a realistic Chrome user agent by default and rejects challenge HTML instead of saving it as a successful page. When that is not enough, use a dedicated live Chrome session. + +Use the bundled helper: + +```sh +scripts/bootstrap_steamdb.sh +``` + +This: + +- uses a dedicated profile under `.local/` +- launches a separate Chrome instance in the background +- exposes a stable CDP endpoint +- writes `.local/steamdb-env.sh` + +If SteamDB needs manual interaction, switch to that dedicated Chrome window, complete the challenge, and leave the browser running. + +Then run: + +```sh +scripts/run_watch.sh 2026-04-25 ~/steam_hardware_watch /tmp/SteamTracking-master +``` + +`check_steamdb.sh` automatically picks up `.local/steamdb-env.sh` when present. `run_watch.sh` auto-closes the dedicated SteamDB browser at the end unless you set: + +```sh +STEAMDB_KEEP_BROWSER_OPEN=1 +``` + +When finished: + +```sh +scripts/close_steamdb.sh +``` + +Manual equivalent: + +```sh +STEAMDB_PLAYWRIGHT_FALLBACK=1 +STEAMDB_PROFILE_DIR=~/.steam-hardware-watch/playwright-steamdb-profile +STEAMDB_CDP_ENDPOINT=http://127.0.0.1:PORT +STEAMDB_BROWSER_CHANNEL=chrome +``` + ## Current Known Limits - `Komodo` can block both `curl` and fresh browser automation. +- `SteamDB` can return a Cloudflare browser challenge to curl and fresh automated browser contexts. - We can currently discover and report blocked media URLs even when we cannot download them automatically. - Exported storage state may still be insufficient for Cloudflare-protected Komodo assets. - Live trusted browser attach is currently the strongest repeatable fallback for protected Komodo media. +- Live trusted browser attach is also the strongest SteamDB fallback when browser-style curl is blocked. - The fully non-visual path is not reliable yet; after bootstrap, the working fallback is still a live dedicated browser running in the background. ## Recommended Workflow @@ -313,4 +371,5 @@ Escalation: 1. if Komodo blocks, continue the run 2. use `manual-asset-urls.txt` for manual follow-up -3. only use the Playwright bootstrap if protected Komodo media is important to the run +3. if SteamDB blocks, use `scripts/bootstrap_steamdb.sh` and rerun when SteamDB verification is important +4. only use the Playwright bootstrap if protected Komodo media or SteamDB verification is important to the run diff --git a/SKILL.md b/SKILL.md index 692f8eb..50b77fe 100644 --- a/SKILL.md +++ b/SKILL.md @@ -137,6 +137,15 @@ Known IDs and starting points are in [references/sources.md](references/sources. The helper script saves the raw HTML pages. Summarize only material changes. +If SteamDB returns a Cloudflare challenge, `check_steamdb.sh` should record the source as blocked instead of saving the challenge HTML as a successful page. To retry with a trusted local browser session: + +1. run `scripts/bootstrap_steamdb.sh` +2. complete any manual challenge in the dedicated Chrome window +3. leave the browser running +4. rerun the watch, or run `check_steamdb.sh` directly + +The helper loads `.local/steamdb-env.sh` automatically and reuses the live browser over CDP when `STEAMDB_PLAYWRIGHT_FALLBACK=1`. + ### 3. SteamTracking / GameTracking Look for: diff --git a/scripts/bootstrap_steamdb.sh b/scripts/bootstrap_steamdb.sh new file mode 100755 index 0000000..8acfac5 --- /dev/null +++ b/scripts/bootstrap_steamdb.sh @@ -0,0 +1,62 @@ +#!/bin/sh +set -eu + +SCRIPT_DIR="$(CDPATH= cd -- "$(dirname "$0")" && pwd)" +REPO_DIR="$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)" +LOCAL_DIR="$REPO_DIR/.local" + +PROFILE_DIR="${1:-${STEAMDB_PROFILE_DIR:-$LOCAL_DIR/playwright-steamdb-profile}}" +PORT="${2:-${STEAMDB_CDP_PORT:-55685}}" +URL="${STEAMDB_BOOTSTRAP_URL:-https://steamdb.info/app/4165870/}" +ENV_FILE="${STEAMDB_ENV_FILE:-$LOCAL_DIR/steamdb-env.sh}" + +mkdir -p "$PROFILE_DIR" "$LOCAL_DIR" + +existing_pid="$(ps aux | awk -v profile="$PROFILE_DIR" -v port="$PORT" ' + index($0, "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome") && + index($0, profile) && + index($0, "--remote-debugging-port=" port) { + print $2 + exit + } +')" + +if [ -z "$existing_pid" ]; then + open -g -na "Google Chrome" --args \ + "--user-data-dir=$PROFILE_DIR" \ + "--remote-debugging-port=$PORT" \ + --no-first-run \ + --disable-sync \ + --new-window \ + "$URL" + + attempts=0 + until curl -fsS "http://127.0.0.1:$PORT/json/version" >/dev/null 2>&1; do + attempts=$((attempts + 1)) + if [ "$attempts" -ge 30 ]; then + echo "failed to observe a Chrome CDP endpoint on port $PORT" >&2 + exit 1 + fi + sleep 1 + done +else + echo "SteamDB bootstrap browser already running on port $PORT (pid $existing_pid)." +fi + +cat > "$ENV_FILE" < "$ERROR_FILE" : > "$REPORT_DIR/steamdb-key-lines.txt" +load_local_env() { + env_file="${STEAMDB_ENV_FILE:-$REPO_DIR/.local/steamdb-env.sh}" + if [ -f "$env_file" ]; then + # shellcheck disable=SC1090 + . "$env_file" + fi +} + +maybe_set_cdp_endpoint() { + if [ -n "${STEAMDB_CDP_ENDPOINT:-}" ] || [ -z "${STEAMDB_PROFILE_DIR:-}" ]; then + return 0 + fi + + port="$(ps aux | awk -v profile="$STEAMDB_PROFILE_DIR" ' + index($0, profile) && match($0, /--remote-debugging-port=([0-9]+)/) { + value = substr($0, RSTART, RLENGTH) + sub(/^--remote-debugging-port=/, "", value) + print value + exit + } + ' 2>/dev/null || true)" + + if [ -n "$port" ]; then + STEAMDB_CDP_ENDPOINT="http://127.0.0.1:$port" + export STEAMDB_CDP_ENDPOINT + fi +} + +is_challenge_page() { + path="$1" + rg -qi "Checking your browser|Just a moment|Cloudflare|cf-chl|cf-browser-verification" "$path" +} + +load_local_env + +STEAMDB_USER_AGENT="${STEAMDB_USER_AGENT:-Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36}" +export STEAMDB_USER_AGENT + fetch_html() { name="$1" url="$2" - if ! curl -A 'Mozilla/5.0' --retry 2 --retry-delay 2 --max-time 45 -fsSL "$url" > "$OUT_DIR/$name.html" 2>/dev/null; then - printf '%s\n' "failed to fetch $url" >> "$ERROR_FILE" - rm -f "$OUT_DIR/$name.html" - return 1 + tmp="$OUT_DIR/$name.html.tmp" + out="$OUT_DIR/$name.html" + + if curl -A "$STEAMDB_USER_AGENT" \ + -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8' \ + -H 'Accept-Language: en-US,en;q=0.9' \ + -H 'Upgrade-Insecure-Requests: 1' \ + --compressed \ + --retry 2 --retry-delay 2 --max-time 45 -fsSL "$url" > "$tmp" 2>/dev/null; then + if ! is_challenge_page "$tmp"; then + mv "$tmp" "$out" + return 0 + fi fi + + if [ "${STEAMDB_PLAYWRIGHT_FALLBACK:-0}" = "1" ] && command -v node >/dev/null 2>&1; then + maybe_set_cdp_endpoint + if node "$SCRIPT_DIR/fetch_steamdb_with_playwright.js" "$url" "$tmp" >/dev/null 2>&1 \ + && [ -s "$tmp" ] \ + && ! is_challenge_page "$tmp"; then + mv "$tmp" "$out" + return 0 + fi + fi + + rm -f "$tmp" "$out" + printf '%s\n' "failed to fetch $url: blocked or challenge page" >> "$ERROR_FILE" + return 1 } fetch_html "controller-app" "https://steamdb.info/app/4165870/" || true diff --git a/scripts/close_steamdb.sh b/scripts/close_steamdb.sh new file mode 100755 index 0000000..e077ce3 --- /dev/null +++ b/scripts/close_steamdb.sh @@ -0,0 +1,34 @@ +#!/bin/sh +set -eu + +SCRIPT_DIR="$(CDPATH= cd -- "$(dirname "$0")" && pwd)" +REPO_DIR="$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)" +LOCAL_DIR="$REPO_DIR/.local" + +PROFILE_DIR="${1:-${STEAMDB_PROFILE_DIR:-$LOCAL_DIR/playwright-steamdb-profile}}" +ENV_FILE="${STEAMDB_ENV_FILE:-$LOCAL_DIR/steamdb-env.sh}" + +pids="$(ps aux | awk -v profile="$PROFILE_DIR" ' + index($0, profile) && !index($0, "awk -v profile=") { + print $2 + } +')" + +if [ -n "$pids" ]; then + for pid in $pids; do + kill "$pid" 2>/dev/null || true + done + printf '%s\n' "Closed dedicated SteamDB browser/profile processes for $PROFILE_DIR." +else + printf '%s\n' "No dedicated SteamDB browser/profile processes found for $PROFILE_DIR." +fi + +rm -f \ + "$PROFILE_DIR/SingletonLock" \ + "$PROFILE_DIR/SingletonSocket" \ + "$PROFILE_DIR/SingletonCookie" + +if [ -f "$ENV_FILE" ]; then + rm -f "$ENV_FILE" + printf '%s\n' "Removed $ENV_FILE" +fi diff --git a/scripts/fetch_steamdb_with_playwright.js b/scripts/fetch_steamdb_with_playwright.js new file mode 100755 index 0000000..61c8c99 --- /dev/null +++ b/scripts/fetch_steamdb_with_playwright.js @@ -0,0 +1,116 @@ +#!/usr/bin/env node +const fs = require("fs"); +const { URL } = require("url"); + +function loadPlaywright() { + const candidates = []; + if (process.env.PLAYWRIGHT_CORE_PATH) candidates.push(process.env.PLAYWRIGHT_CORE_PATH); + candidates.push("playwright-core"); + for (const candidate of candidates) { + try { + return require(candidate); + } catch (err) { + } + } + throw new Error( + "Unable to load playwright-core. Set PLAYWRIGHT_CORE_PATH or install playwright-core." + ); +} + +function launchOptions(headless) { + const options = { headless }; + const channel = process.env.STEAMDB_BROWSER_CHANNEL || process.env.PLAYWRIGHT_BROWSER_CHANNEL; + if (channel) { + options.channel = channel; + } + return options; +} + +function userAgent() { + return process.env.STEAMDB_USER_AGENT || + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36"; +} + +function findPage(context, origin) { + const pages = context.pages(); + return pages.find((candidate) => candidate.url().startsWith(origin)) || pages[0] || null; +} + +async function main() { + const [, , targetUrl, outputPath] = process.argv; + if (!targetUrl || !outputPath) { + console.error("usage: fetch_steamdb_with_playwright.js URL OUTPUT_PATH"); + process.exit(1); + } + + const { chromium } = loadPlaywright(); + const headless = process.env.STEAMDB_PLAYWRIGHT_HEADLESS !== "0" && + process.env.PLAYWRIGHT_HEADLESS !== "0"; + const profileDir = process.env.STEAMDB_PROFILE_DIR || process.env.PLAYWRIGHT_PROFILE_DIR; + const storageState = process.env.STEAMDB_STORAGE_STATE || process.env.PLAYWRIGHT_STORAGE_STATE; + const referer = process.env.STEAMDB_REFERER || process.env.PLAYWRIGHT_REFERER; + const cdpEndpoint = process.env.STEAMDB_CDP_ENDPOINT || process.env.PLAYWRIGHT_CDP_ENDPOINT; + const settleMs = Number(process.env.STEAMDB_PLAYWRIGHT_SETTLE_MS || 8000); + + let browser = null; + let context = null; + let page = null; + let shouldCloseBrowser = false; + let shouldCloseContext = false; + let shouldClosePage = false; + + if (cdpEndpoint) { + browser = await chromium.connectOverCDP(cdpEndpoint); + shouldCloseBrowser = true; + context = browser.contexts()[0]; + if (!context) { + throw new Error(`No browser context available via CDP endpoint ${cdpEndpoint}`); + } + const origin = new URL(targetUrl).origin + "/"; + page = findPage(context, origin); + if (!page) { + page = await context.newPage(); + shouldClosePage = true; + } + } else if (profileDir) { + context = await chromium.launchPersistentContext(profileDir, { + ...launchOptions(headless), + userAgent: userAgent(), + }); + shouldCloseContext = true; + page = context.pages()[0] || await context.newPage(); + } else { + browser = await chromium.launch(launchOptions(headless)); + shouldCloseBrowser = true; + context = await browser.newContext({ + userAgent: userAgent(), + storageState: storageState || undefined, + extraHTTPHeaders: referer ? { Referer: referer } : undefined, + }); + shouldCloseContext = true; + page = await context.newPage(); + } + + try { + await page.goto(targetUrl, { waitUntil: "domcontentloaded", timeout: 45000 }); + if (settleMs > 0) { + await page.waitForTimeout(settleMs); + } + fs.writeFileSync(outputPath, await page.content()); + } finally { + if (shouldClosePage && page && !page.isClosed()) { + await page.close(); + } + if (shouldCloseContext && context) { + await context.close(); + } + if (shouldCloseBrowser && browser) { + await browser.close(); + } + } +} + +main().catch((err) => { + console.error(err && err.stack ? err.stack : String(err)); + process.exit(1); +}); diff --git a/scripts/run_watch.sh b/scripts/run_watch.sh index 038dbd5..2f58045 100755 --- a/scripts/run_watch.sh +++ b/scripts/run_watch.sh @@ -13,14 +13,20 @@ TRACKING_DIR="${3:-/tmp/SteamTracking-master}" SCRIPT_DIR="$(CDPATH= cd -- "$(dirname "$0")" && pwd)" REPO_DIR="$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)" KOMODO_ENV_FILE="${KOMODO_ENV_FILE:-$REPO_DIR/.local/komodo-env.sh}" +STEAMDB_ENV_FILE="${STEAMDB_ENV_FILE:-$REPO_DIR/.local/steamdb-env.sh}" cleanup() { if [ "${KOMODO_KEEP_BROWSER_OPEN:-0}" = "1" ]; then - return 0 - fi - if [ -f "$KOMODO_ENV_FILE" ] && [ -x "$SCRIPT_DIR/close_komodo.sh" ]; then + : + elif [ -f "$KOMODO_ENV_FILE" ] && [ -x "$SCRIPT_DIR/close_komodo.sh" ]; then "$SCRIPT_DIR/close_komodo.sh" >/dev/null 2>&1 || true fi + + if [ "${STEAMDB_KEEP_BROWSER_OPEN:-0}" = "1" ]; then + : + elif [ -f "$STEAMDB_ENV_FILE" ] && [ -x "$SCRIPT_DIR/close_steamdb.sh" ]; then + "$SCRIPT_DIR/close_steamdb.sh" >/dev/null 2>&1 || true + fi } trap cleanup EXIT INT TERM diff --git a/status/current.md b/status/current.md index 42f6f65..6f928dc 100644 --- a/status/current.md +++ b/status/current.md @@ -1,12 +1,12 @@ # Steam Hardware Watch Status -Last updated: `2026-04-26` +Last updated: `2026-04-28` ## Best Current Answer - `Price`: no confirmed public price leak for the new Steam Controller, Steam Machine, or Steam Frame. -- `Release date`: official Valve store pages now support a broad `early 2026` / `coming in 2026` window, but no exact preorder, ship, or launch date was found. -- `Same-time launch`: stronger than the seeded baseline for a shared public rollout window because Valve's official Steam Hardware page groups Controller, Machine, and Frame together. Exact same-day availability is still unconfirmed, and the controller remains furthest along. +- `Release date`: official Valve store pages still support a broad `early 2026` / `coming in 2026` window, but no exact preorder, ship, or launch date was found. +- `Same-time launch`: stronger than the seeded baseline for a shared public rollout window because Valve's official Steam Hardware page groups Controller, Machine, and Frame together. Exact same-day availability is still unconfirmed, and the controller remains furthest along because the fresh Komodo movement is still controller-only. ## Source Snapshot @@ -20,26 +20,30 @@ Last updated: `2026-04-26` - `wishlist-only` - On `2026-04-24`, Komodo bulk-updated product records for all three hardware lines across currencies. - Also on `2026-04-24`, Komodo exposed a fresh controller-only section and media rollout with banner, videos, feature art, controls imagery, and a spec image. -- Rechecked on `2026-04-26`: product modified timestamps remain `2026-04-24T15:12:12` for Controller, `2026-04-24T14:32:54` for Machine, and `2026-04-24T14:33:36` for Frame. +- Rechecked on `2026-04-28`: the Controller product modified timestamp moved to `2026-04-28T18:32:11`; Machine and Frame remain at `2026-04-24T14:32:54` and `2026-04-24T14:33:36`. - No comparable fresh Machine or Frame section and media rollout was found. Machine and Frame still only show older wishlist sections in the current Komodo search output. +- Komodo added a new Controller section, `Steam Controller - 10 Specs`, modified `2026-04-27T09:41:01`. +- Komodo updated the Controller `09 Spec image` section on `2026-04-28T19:05:51`, adding two new AVIF media records plus their generated sizes. +- Visual asset retrieval is now complete for the current Komodo set: `130` discovered and `130` retrieved. The browsable library contains `11` videos, `11` images, `3` logos, `3` poster frames, `42` thumbnails, and `60` variants. ### SteamDB - Browser-verified SteamDB state on `2026-04-26`: - `Steam Controller` app `4165870` remains `prerelease` with store release date `Coming soon`. - `steam_controller_unboxing_2026` app `4653940` remains a released free video app, first seen on `2026-04-20`, with package `1620489` available as free on demand and showing `No Price`. - - The standard curl helper was blocked by SteamDB, so browser-style verification is the current usable path. +- The standard curl helper was still blocked by SteamDB on `2026-04-28`, so browser-style verification remains the current usable path. ### SteamTracking / GameTracking -- A fresh SteamTracking checkout on `2026-04-26` still includes controller onboarding, firmware update flow, puck handling, and pairing logic for the new controller ecosystem. +- A fresh SteamTracking checkout on `2026-04-28` still includes controller onboarding, firmware update flow, puck handling, and pairing logic for the new controller ecosystem. - Pairing-focused hits include `ibex_internal`, `ibex_external`, `PairDongleTritonConnected`, `PairDongleTritonDocked`, and `ShouldTritonPairInOobe`. ### Valve Support / CDN -- Official Valve store pages were reachable and saved: - - `/sale/hardware` says the Steam Hardware family expands in early 2026, lists Steam Controller, Steam Machine, and Steam Frame, and was modified at `2026-04-26T06:06:01Z` in the embedded event metadata. - - `/sale/steamcontroller` has a dedicated Steam Controller page and was modified at `2026-04-26T06:05:47Z` in the embedded event metadata. +- Official Valve store pages were reachable and saved on `2026-04-28`: + - `/sale/hardware` still says the Steam Hardware family expands in early 2026, lists Steam Controller, Steam Machine, and Steam Frame, and its embedded event metadata now shows `rtime32_last_modified` `2026-04-28T10:09:22Z`. + - `/sale/steamcontroller` has a dedicated Steam Controller page and its embedded event metadata now shows `rtime32_last_modified` `2026-04-28T18:31:12Z`. + - The embedded Steam Hardware submenu now points to cleaner `/hardware`, `/hardware/steamcontroller`, `/hardware/steammachine`, and `/hardware/steamframe` paths instead of the older `/sale/...` paths. - No official Valve price, exact release date, or preorder date was found. ### Customs / Regulatory diff --git a/status/evidence.jsonl b/status/evidence.jsonl index 320d794..43493a2 100644 --- a/status/evidence.jsonl +++ b/status/evidence.jsonl @@ -12,3 +12,7 @@ {"date_checked": "2026-04-26", "source": "steamdb", "entity": "steam-controller", "claim": "SteamDB still shows steam_controller_unboxing_2026 as a released free video app with an available free-on-demand package.", "evidence_type": "hidden_media_app", "confidence": "medium", "ref": "https://steamdb.info/app/4653940/", "notes": "Browser fetch succeeded; app first seen 2026-04-20 and last record update 2026-04-21T22:16:14Z; package 1620489 lists No Price."} {"date_checked": "2026-04-26", "source": "steamtracking", "entity": "steam-controller", "claim": "Fresh SteamTracking checkout still contains Ibex dongles, Triton pairing, and controller OOBE strings.", "evidence_type": "client_strings", "confidence": "medium", "ref": "/Users/sean/steam_hardware_watch/2026-04-26/reports/steamtracking-pairing-focus.txt", "notes": "Includes ibex_internal, ibex_external, PairDongleTritonConnected, PairDongleTritonDocked, and ShouldTritonPairInOobe in service_steaminputmanager.proto."} {"date_checked": "2026-04-26", "source": "komodo", "entity": "steam-controller,steam-machine,steam-frame", "claim": "Komodo API access was restored after using a realistic browser user agent; product records remain last modified on 2026-04-24 and no newer Machine or Frame rollout was found.", "evidence_type": "api_recheck", "confidence": "high", "ref": "/Users/sean/steam_hardware_watch/2026-04-26/reports/komodo-product-modified.tsv", "notes": "Root cause of earlier block was helper user-agent Mozilla/5.0 receiving HTTP 403; default curl and realistic Chrome/Safari UAs returned JSON."} +{"date_checked":"2026-04-28","source":"komodo","entity":"steam-controller","claim":"Komodo moved the Steam Controller product record forward again while Machine and Frame remained on their 2026-04-24 modified timestamps.","evidence_type":"product_metadata_delta","confidence":"high","ref":"/Users/sean/steam_hardware_watch/2026-04-28/reports/komodo-product-modified.tsv","notes":"Controller modified 2026-04-28T18:32:11; Machine 2026-04-24T14:32:54; Frame 2026-04-24T14:33:36."} +{"date_checked":"2026-04-28","source":"komodo","entity":"steam-controller","claim":"Komodo added a new Controller specs section and updated the Controller spec-image section with two new AVIF media records.","evidence_type":"sections_media_rollout","confidence":"high","ref":"/Users/sean/steam_hardware_watch/2026-04-28/reports/komodo-controller-sections.tsv","notes":"New section 433617 / 433487 modified 2026-04-27T09:41:01; section 433484 / 433473 modified 2026-04-28T19:05:51 with new media 433664 and 433679."} +{"date_checked":"2026-04-28","source":"komodo","entity":"steam-controller","claim":"The current Komodo visual asset set is fully retrieved and organized into the media-style library.","evidence_type":"asset_retrieval","confidence":"high","ref":"/Users/sean/steam_hardware_watch/2026-04-28/assets/manifests/asset-library.json","notes":"130 discovered and 130 retrieved; library has 11 videos, 11 images, 3 logos, 3 poster frames, 42 thumbnails, and 60 variants, all classified as steam-controller."} +{"date_checked":"2026-04-28","source":"valve-store","entity":"steam-controller,steam-machine,steam-frame","claim":"Official Valve store metadata changed again, including cleaner Steam Hardware submenu URLs under /hardware while still showing only broad 2026 timing.","evidence_type":"official_store_metadata","confidence":"high","ref":"/Users/sean/steam_hardware_watch/2026-04-28/api/valve/steam-hardware-root.html","notes":"Steam Hardware event modified 2026-04-28T10:09:22Z; Controller event modified 2026-04-28T18:31:12Z; no price, preorder date, or exact launch date found."} diff --git a/tests/test_check_steamdb.py b/tests/test_check_steamdb.py new file mode 100644 index 0000000..cf52540 --- /dev/null +++ b/tests/test_check_steamdb.py @@ -0,0 +1,131 @@ +import os +import stat +import subprocess +import textwrap +import unittest +from pathlib import Path +from tempfile import TemporaryDirectory + + +ROOT = Path(__file__).resolve().parents[1] + + +def write_executable(path, content): + path.write_text(textwrap.dedent(content).lstrip(), encoding="utf-8") + path.chmod(path.stat().st_mode | stat.S_IXUSR) + + +class CheckSteamDBTests(unittest.TestCase): + def run_check(self, curl_mode, node_mode="success"): + tmp = TemporaryDirectory() + self.addCleanup(tmp.cleanup) + tmp_path = Path(tmp.name) + run_dir = tmp_path / "run" + fake_bin = tmp_path / "bin" + fake_bin.mkdir() + log_dir = tmp_path / "logs" + log_dir.mkdir() + + write_executable( + fake_bin / "curl", + """ + #!/bin/sh + printf '%s\\n' "$*" >> "$FAKE_LOG_DIR/curl-args.log" + case "$FAKE_CURL_MODE" in + success) + printf '%s\\n' 'Steam Controller AppID: 4165870Coming soon prerelease' + ;; + challenge) + printf '%s\\n' 'Just a moment...

Checking your browser...

Cloudflare' + ;; + fail) + exit 22 + ;; + *) + echo "unknown FAKE_CURL_MODE: $FAKE_CURL_MODE" >&2 + exit 2 + ;; + esac + """, + ) + write_executable( + fake_bin / "node", + """ + #!/bin/sh + printf '%s\\n' "$*" >> "$FAKE_LOG_DIR/node-args.log" + output="" + for arg in "$@"; do + output="$arg" + done + case "$FAKE_NODE_MODE" in + success) + printf '%s\\n' 'Steam Controller AppID: 4165870Coming soon prerelease' > "$output" + ;; + challenge) + printf '%s\\n' 'Just a moment...Checking your browser... Cloudflare' > "$output" + ;; + fail) + exit 1 + ;; + *) + echo "unknown FAKE_NODE_MODE: $FAKE_NODE_MODE" >&2 + exit 2 + ;; + esac + """, + ) + + env = os.environ.copy() + env.update( + { + "PATH": f"{fake_bin}:{env['PATH']}", + "FAKE_CURL_MODE": curl_mode, + "FAKE_NODE_MODE": node_mode, + "FAKE_LOG_DIR": str(log_dir), + "STEAMDB_PLAYWRIGHT_FALLBACK": "1", + } + ) + + result = subprocess.run( + [str(ROOT / "scripts" / "check_steamdb.sh"), str(run_dir)], + cwd=ROOT, + env=env, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + ) + return result, run_dir, log_dir + + def test_uses_realistic_chrome_user_agent_by_default(self): + result, run_dir, log_dir = self.run_check("success") + + self.assertEqual("", result.stderr) + self.assertEqual(0, result.returncode) + curl_args = (log_dir / "curl-args.log").read_text(encoding="utf-8") + self.assertIn("Chrome/123.0.0.0", curl_args) + self.assertNotIn("-A Mozilla/5.0 --retry", curl_args) + self.assertTrue((run_dir / "api" / "steamdb" / "controller-app.html").exists()) + + def test_treats_cloudflare_challenge_html_as_blocked(self): + result, run_dir, _ = self.run_check("challenge", node_mode="fail") + + self.assertEqual("", result.stderr) + self.assertEqual(0, result.returncode) + self.assertFalse((run_dir / "api" / "steamdb" / "controller-app.html").exists()) + errors = (run_dir / "reports" / "steamdb-errors.txt").read_text(encoding="utf-8") + self.assertIn("blocked or challenge page", errors) + + def test_uses_playwright_fallback_after_challenge_page(self): + result, run_dir, log_dir = self.run_check("challenge", node_mode="success") + + self.assertEqual("", result.stderr) + self.assertEqual(0, result.returncode) + self.assertTrue((log_dir / "node-args.log").exists()) + html = (run_dir / "api" / "steamdb" / "controller-app.html").read_text( + encoding="utf-8" + ) + self.assertIn("Steam Controller", html) + self.assertIn("Coming soon", html) + errors = (run_dir / "reports" / "steamdb-errors.txt").read_text(encoding="utf-8") + self.assertEqual("", errors)