diff --git a/SKILL.md b/SKILL.md index 4dd413d..92a1a06 100644 --- a/SKILL.md +++ b/SKILL.md @@ -1,16 +1,23 @@ --- name: steam-hardware-watch -description: Investigate rumored or upcoming Valve hardware launches, especially Steam Controller, Steam Machine, and Steam Frame. Use when the user wants recurring checks across Komodo, SteamKit/PICS, SteamDB, SteamTracking or GameTracking, Valve CDN or support endpoints, and customs or regulatory sources, with saved artifacts and an updated status ledger. +description: Investigate Steam Frame launch readiness, using Steam Controller and Steam Machine as baselines after their public launches. Use when the user wants recurring checks across SteamKit/PICS, Valve store/support endpoints, SteamDB, Komodo, SteamTracking or GameTracking, SteamVR, SteamOS, and customs or regulatory sources, with saved artifacts and an updated status ledger. --- # Steam Hardware Watch -Use this skill for recurring evidence-gathering on Valve hardware launch status. Default focus is: +Use this skill for recurring evidence-gathering on Valve hardware launch status. Current default focus is Steam Frame launch readiness: -- `price` -- `release date` -- `same-time launch vs staggered launch` -- `which product is furthest along` +- `Frame price` +- `Frame release date` +- `Frame package or reservation visibility` +- `Frame staging on Valve, SteamDB, Komodo, SteamVR, SteamOS, and logistics sources` + +Post-Steam Machine launch mode: + +- treat Steam Frame as the active target +- keep Steam Machine and Steam Controller in the watcher as controls, precedent, and shared-infrastructure context +- do not elevate Machine post-launch churn as a Frame signal unless it mentions Frame, changes shared reservation infrastructure, or provides a direct launch-sequence comparator +- answer daily check requests from a Frame-first perspective by default Keep the workflow tiered. Do the high-yield checks every run. Do slower or noisier checks when core sources move. @@ -46,6 +53,7 @@ Use [scripts/init_run.sh](scripts/init_run.sh) to scaffold a run folder and run Use the source helpers when available: - [scripts/run_watch.sh](scripts/run_watch.sh) +- [scripts/run_frame_watch.sh](scripts/run_frame_watch.sh) - [scripts/check_komodo.sh](scripts/check_komodo.sh) - [scripts/check_steamkit_pics.sh](scripts/check_steamkit_pics.sh) - [scripts/check_steamdb.sh](scripts/check_steamdb.sh) @@ -68,9 +76,10 @@ Asset discovery should be dynamic: Default agent entry point: -- use [scripts/run_watch.sh](scripts/run_watch.sh) unless there is a specific reason to run a source helper directly +- use [scripts/run_frame_watch.sh](scripts/run_frame_watch.sh) for normal Frame-focused runs +- use [scripts/run_watch.sh](scripts/run_watch.sh) when the user asks for the broader all-hardware view or when debugging a source helper directly - treat the helper scripts as agent-owned implementation details -- summarize results for the human from `run-summary.md`, `status-draft.md`, and the comparison report +- summarize results for the human from `frame-focus.md`, `run-summary.md`, `status-draft.md`, and the comparison report ## Evidence Priority @@ -78,17 +87,38 @@ Apply the rubric in [references/evidence-rubric.md](references/evidence-rubric.m Default source order: -1. `Komodo` -2. `SteamKit / PICS` +1. `SteamKit / PICS` +2. `Valve support / store / CDN` 3. `SteamDB` -4. `SteamTracking / GameTracking` -5. `SteamVR depots` -6. `SteamOS package mirror` -7. `Valve support / store / CDN` +4. `Komodo` +5. `SteamTracking / GameTracking` +6. `SteamVR depots` +7. `SteamOS package mirror` 8. `customs / regulatory` Treat price and exact release date as unconfirmed unless directly exposed by a primary source. +## Frame-Critical Signals + +Prioritize these before interpreting lower-level churn: + +- SteamKit/PICS movement for Frame app `4165890` or packages `1629484`, `1629486` +- Valve `appdetails` showing Frame packages, package groups, price, purchase or reservation state, or an exact release date +- Valve `packagedetails` for Frame packages moving from `success:false` to public data +- SteamDB package pages for `1629484` or `1629486` moving from existence-only records to details, new `Last Record Update`, or new changenumber +- Komodo Frame product timestamp changes, new Frame sections, media, manuals, videos, or support docs +- SteamTracking/GameTracking additions that name Frame setup, pairing, verification, OOBE, guided tours, Steam Link VR, wireless adapters, or Frame package IDs +- SteamVR or SteamOS additions that explicitly mention Frame, Deckard, Roy, XR, VR, ARM64, wireless adapter, or controller support in a Frame context +- shipment or filing rows explicitly labelled VR, headset, XR, wireless adapter, or known Frame supplier/importer patterns + +Interpretation rules: + +- SteamKit/PICS and Valve store/package APIs are the primary readiness sources. +- SteamDB is corroborating and useful for human-readable package history. +- Komodo is high-signal for public site staging, but Cloudflare blocks should not stop the rest of a run. +- SteamTracking, SteamVR, and SteamOS changes are context until they connect to Frame package/API movement, public pages, or shipment evidence. +- Machine and Controller changes are comparators after launch, not active launch targets. + ## Core Workflow ### 1. Komodo @@ -383,7 +413,8 @@ Use the seeded status files as the starting baseline rather than rediscovering t ## Wrapper Script -Use [scripts/run_watch.sh](scripts/run_watch.sh) for the normal path. +Use [scripts/run_frame_watch.sh](scripts/run_frame_watch.sh) for the normal Frame-focused path. +Use [scripts/run_watch.sh](scripts/run_watch.sh) when the user explicitly wants a broader all-hardware run. It will: @@ -392,10 +423,11 @@ It will: 3. auto-detect the previous run folder when possible 4. write a comparison report into the current run folder 5. save discovered visual assets into the current run folder -6. generate a draft status update into the current run folder +6. generate a Frame-focused report into the current run folder +7. generate a draft status update into the current run folder Example: ```sh -~/.codex/skills/steam-hardware-watch/scripts/run_watch.sh 2026-04-25 +scripts/run_frame_watch.sh 2026-06-24 ``` diff --git a/scripts/draft_status_update.py b/scripts/draft_status_update.py index d920480..080d9c0 100755 --- a/scripts/draft_status_update.py +++ b/scripts/draft_status_update.py @@ -45,6 +45,7 @@ def build_draft(run_dir: Path): if compare_file: compare_lines = [line for line in read_lines(compare_file) if line.startswith("- ")][:12] + frame_focus_lines = first_n_nonempty(reports / "frame-focus.md", 12) komodo_mods = first_n_nonempty(reports / "komodo-product-modified.tsv", 10) steamkit_lines = first_n_nonempty(reports / "steamkit-pics-key-lines.txt", 10) steamdb_lines = first_n_nonempty(reports / "steamdb-key-lines.txt", 8) @@ -104,6 +105,17 @@ def build_draft(run_dir: Path): [ "", "### Source Snapshot Draft", + "", + "#### Steam Frame Focus", + ] + ) + if frame_focus_lines: + lines.extend([f"- `{line}`" for line in frame_focus_lines]) + else: + lines.append("- No Steam Frame focused report found.") + + lines.extend( + [ "", "#### Komodo", ] diff --git a/scripts/run_frame_watch.sh b/scripts/run_frame_watch.sh new file mode 100755 index 0000000..bd47a60 --- /dev/null +++ b/scripts/run_frame_watch.sh @@ -0,0 +1,31 @@ +#!/bin/sh +set -eu + +if [ "$#" -lt 1 ]; then + echo "usage: $0 YYYY-MM-DD [base_dir] [tracking_dir]" >&2 + exit 1 +fi + +RUN_DATE="$1" + +SCRIPT_DIR="$(CDPATH= cd -- "$(dirname "$0")" && pwd)" +REPO_DIR="$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)" +BASE_DIR="${2:-$REPO_DIR/runs}" +TRACKING_DIR="${3:-/tmp/SteamTracking-master}" + +if [ "$#" -ge 3 ]; then + "$SCRIPT_DIR/run_watch.sh" "$RUN_DATE" "$BASE_DIR" "$TRACKING_DIR" +else + "$SCRIPT_DIR/run_watch.sh" "$RUN_DATE" "$BASE_DIR" +fi + +RUN_DIR="$BASE_DIR/$RUN_DATE" +FRAME_FOCUS_OUT="$RUN_DIR/reports/frame-focus.md" + +if [ ! -f "$FRAME_FOCUS_OUT" ]; then + python3 "$SCRIPT_DIR/write_frame_focus_report.py" \ + --run-dir "$RUN_DIR" \ + --output "$FRAME_FOCUS_OUT" +fi + +printf '%s\n' "Frame focus report: $FRAME_FOCUS_OUT" diff --git a/scripts/run_watch.sh b/scripts/run_watch.sh index b951975..c908457 100755 --- a/scripts/run_watch.sh +++ b/scripts/run_watch.sh @@ -79,6 +79,12 @@ else printf '%s\n' "No previous run folder found under $BASE_DIR" fi +FRAME_FOCUS_OUT="$RUN_DIR/reports/frame-focus.md" +python3 "$SCRIPT_DIR/write_frame_focus_report.py" \ + --run-dir "$RUN_DIR" \ + --output "$FRAME_FOCUS_OUT" +printf '%s\n' "Frame focus report: $FRAME_FOCUS_OUT" + STATUS_DRAFT_OUT="$RUN_DIR/reports/status-draft.md" python3 "$SCRIPT_DIR/draft_status_update.py" \ --run-dir "$RUN_DIR" \ diff --git a/scripts/write_frame_focus_report.py b/scripts/write_frame_focus_report.py new file mode 100755 index 0000000..d7783fb --- /dev/null +++ b/scripts/write_frame_focus_report.py @@ -0,0 +1,295 @@ +#!/usr/bin/env python3 +import argparse +import csv +from pathlib import Path + +FRAME_APP_ID = "4165890" +FRAME_PACKAGE_IDS = {"1629484", "1629486"} +FRAME_KOMODO_BASELINE = "2026-05-27T16:53:46" +FRAME_STEAMDB_BASELINE_UPDATE = "5 May 2026 - 18:50:54 UTC" +MAX_LINE_LEN = 220 + + +def read_text(path: Path): + if not path.exists(): + return "" + return path.read_text(encoding="utf-8", errors="ignore") + + +def read_lines(path: Path): + return [line.rstrip("\n") for line in read_text(path).splitlines() if line.strip()] + + +def read_tsv(path: Path): + if not path.exists(): + return [] + with path.open("r", encoding="utf-8", errors="ignore", newline="") as handle: + return list(csv.DictReader(handle, delimiter="\t")) + + +def read_komodo_product_modified(path: Path): + rows = [] + for line in read_lines(path): + parts = line.split("\t") + if parts[:2] == ["product", "modified"]: + continue + if len(parts) >= 2: + rows.append({"product": parts[0], "modified": parts[1]}) + return rows + + +def concise(value: str): + compact = " ".join((value or "").split()) + if len(compact) > MAX_LINE_LEN: + return compact[: MAX_LINE_LEN - 3] + "..." + return compact + + +def bullet_table_row(row, columns): + values = [] + for column in columns: + value = concise(row.get(column, "")) + if value: + values.append(f"{column}={value}") + return "; ".join(values) + + +def frame_rows(rows, id_key="id"): + return [ + row + for row in rows + if row.get("product") == "Steam Frame" + or row.get(id_key) == FRAME_APP_ID + or row.get(id_key) in FRAME_PACKAGE_IDS + or row.get("package_id") in FRAME_PACKAGE_IDS + ] + + +def machine_rows(rows, id_key="id"): + return [row for row in rows if row.get("product") == "Steam Machine"] + + +def valve_frame_ready(rows): + for row in frame_rows(rows): + row_type = row.get("type") + row_id = row.get("id") + status = row.get("status") + details = row.get("details", "") + if row_type == "package" and row_id in FRAME_PACKAGE_IDS and status == "public": + return True + if row_type == "app" and row_id == FRAME_APP_ID: + has_packages = "packages=" in details and "packages=;" not in details + has_package_groups = "package_groups=0" not in details and "package_groups=" in details + has_price = "price=" in details and not details.endswith("price=") + if has_packages or has_package_groups or has_price: + return True + return False + + +def steamkit_frame_moved(rows): + for row in frame_rows(rows): + if row.get("changed_since_previous") == "yes": + return True + if row.get("type") == "package" and row.get("id") in FRAME_PACKAGE_IDS and row.get("status") == "available": + return True + return False + + +def steamdb_frame_moved(rows): + for row in frame_rows(rows, id_key="package_id"): + if row.get("status") and row.get("status") != "private_exists_only": + return True + if row.get("last_record_update") and row.get("last_record_update") != FRAME_STEAMDB_BASELINE_UPDATE: + return True + return False + + +def komodo_frame_moved(rows): + for row in rows: + if row.get("product") == "frame" and row.get("modified") and row.get("modified") != FRAME_KOMODO_BASELINE: + return True + return False + + +def key_lines(path: Path, terms, limit): + selected = [] + for line in read_lines(path): + lowered = line.lower() + if any(term in lowered for term in terms): + selected.append(concise(line)) + if len(selected) >= limit: + break + return selected + + +def customs_vr_lines(path: Path, limit): + selected = [] + for line in read_lines(path): + lowered = line.lower() + if line.startswith("|") and any(term in lowered for term in ("virtual reality", "vr", "headset", "xr")): + selected.append(concise(line)) + if len(selected) >= limit: + break + return selected + + +def latest_compare_file(reports: Path): + compare_files = sorted(reports.glob("compare-vs-*.md")) + if not compare_files: + return None + return compare_files[-1] + + +def build(run_dir: Path): + reports = run_dir / "reports" + valve_rows = read_tsv(reports / "valve-reservation-packages.tsv") + steamkit_rows = read_tsv(reports / "steamkit-pics-packages.tsv") + steamdb_rows = read_tsv(reports / "steamdb-reservation-packages.tsv") + komodo_rows = read_komodo_product_modified(reports / "komodo-product-modified.tsv") + + valve_ready = valve_frame_ready(valve_rows) + steamkit_moved = steamkit_frame_moved(steamkit_rows) + steamdb_moved = steamdb_frame_moved(steamdb_rows) + komodo_moved = komodo_frame_moved(komodo_rows) + + if valve_ready: + verdict = "Steam Frame public readiness detected." + reason = "Valve store/package data now exposes Frame package, package group, price, or purchase/reservation readiness." + elif steamkit_moved or steamdb_moved: + verdict = "Steam Frame backend movement detected, but public store readiness is not confirmed." + reason = "Frame app/package metadata moved in SteamKit/PICS or SteamDB, while Valve public endpoints still need confirmation." + elif komodo_moved: + verdict = "Steam Frame site staging detected, but store/package readiness is not confirmed." + reason = "Komodo Frame records moved without matching Valve package readiness." + else: + verdict = "No public Steam Frame readiness change detected." + reason = "Frame app/package, public Valve endpoint, SteamDB, and Komodo checks do not show a launch-readiness move." + + lines = [ + f"# Steam Frame Focus: {run_dir.name}", + "", + "## Verdict", + "", + f"- {verdict}", + f"- {reason}", + "", + "## Frame Critical Signals", + "", + f"- Frame app: `{FRAME_APP_ID}`", + "- Frame packages: `1629484`, `1629486`", + "", + "### Valve Store / Package API", + ] + + frame_valve_rows = frame_rows(valve_rows) + if frame_valve_rows: + for row in frame_valve_rows: + lines.append( + f"- `{bullet_table_row(row, ('type', 'product', 'id', 'status', 'name', 'release_or_price', 'details'))}`" + ) + else: + lines.append("- No Frame rows found in `valve-reservation-packages.tsv`.") + + lines.extend(["", "### SteamKit / PICS"]) + frame_steamkit_rows = frame_rows(steamkit_rows) + if frame_steamkit_rows: + for row in frame_steamkit_rows: + lines.append( + f"- `{bullet_table_row(row, ('type', 'product', 'id', 'status', 'changenumber', 'previous_changenumber', 'changed_since_previous', 'name', 'details'))}`" + ) + else: + lines.append("- No Frame rows found in `steamkit-pics-packages.tsv`.") + + lines.extend(["", "### SteamDB"]) + frame_steamdb_rows = frame_rows(steamdb_rows, id_key="package_id") + if frame_steamdb_rows: + for row in frame_steamdb_rows: + lines.append( + f"- `{bullet_table_row(row, ('product', 'package_id', 'last_record_update', 'last_changenumber', 'possible_apps', 'status'))}`" + ) + else: + lines.append("- No Frame rows found in `steamdb-reservation-packages.tsv`.") + + lines.extend(["", "### Komodo"]) + frame_komodo = [row for row in komodo_rows if row.get("product") == "frame"] + if frame_komodo: + for row in frame_komodo: + lines.append(f"- `{bullet_table_row(row, ('product', 'modified'))}`") + else: + lines.append("- No Frame product timestamp row found.") + for source in ("komodo-frame-sections.tsv", "komodo-frame-media.tsv"): + path = reports / source + count = len(read_lines(path)) + lines.append(f"- `{source}` rows: `{count}`") + + tracking_terms = ("frame", "deckard", "roy", "xr", "vr", "wireless adapter", "steam link") + tracking_lines = key_lines(reports / "steamtracking-frame-signals-key-lines.txt", tracking_terms, 10) + lines.extend(["", "### SteamTracking / GameTracking"]) + if tracking_lines: + lines.extend([f"- `{line}`" for line in tracking_lines]) + else: + lines.append("- No Frame-focused SteamTracking key lines found.") + + vr_terms = ("steam frame", "deckard", "roy", "wireless adapter", "steam link vr", "arm64", "linuxarm64") + steamvr_lines = key_lines(reports / "steamvr-depots-key-lines.txt", vr_terms, 6) + steamos_lines = key_lines(reports / "steamos-mirror-key-lines.txt", vr_terms, 6) + lines.extend(["", "### SteamVR / SteamOS"]) + if steamvr_lines or steamos_lines: + lines.extend([f"- SteamVR: `{line}`" for line in steamvr_lines]) + lines.extend([f"- SteamOS: `{line}`" for line in steamos_lines]) + else: + lines.append("- No Frame-relevant SteamVR or SteamOS key lines found.") + + customs_lines = customs_vr_lines(reports / "customs-shipments.md", 6) + lines.extend(["", "### Customs / Shipments"]) + if customs_lines: + lines.extend([f"- `{line}`" for line in customs_lines]) + elif (reports / "customs-shipments-errors.txt").exists(): + lines.append("- Shipment check had errors; inspect `customs-shipments-errors.txt`.") + else: + lines.append("- No VR/XR/headset shipment rows surfaced in the shipment report.") + + lines.extend(["", "## Background Comparator Signals", ""]) + machine_valve_rows = machine_rows(valve_rows) + machine_steamkit_rows = [ + row for row in machine_rows(steamkit_rows) if row.get("changed_since_previous") == "yes" + ] + if machine_valve_rows: + lines.append("- Steam Machine is now a launched comparator, not the active target.") + for row in machine_valve_rows[:6]: + lines.append( + f"- Machine Valve: `{bullet_table_row(row, ('type', 'id', 'status', 'name', 'release_or_price', 'details'))}`" + ) + if machine_steamkit_rows: + for row in machine_steamkit_rows[:6]: + lines.append( + f"- Machine SteamKit moved: `{bullet_table_row(row, ('type', 'id', 'changenumber', 'previous_changenumber', 'changed_since_previous', 'details'))}`" + ) + if not machine_valve_rows and not machine_steamkit_rows: + lines.append("- No Machine comparator rows were available in the saved reports.") + + compare_file = latest_compare_file(reports) + lines.extend(["", "## Next Checks", ""]) + lines.append("- Recheck SteamKit/PICS and Valve `appdetails` / `packagedetails` first.") + lines.append("- Treat SteamTracking or Komodo Frame movement as staging context until package/API visibility changes.") + lines.append("- Keep monitoring VR-labelled shipment rows as corroboration, not launch confirmation.") + if compare_file: + lines.append(f"- Cross-check material deltas in `{compare_file}`.") + lines.append("") + return "\n".join(lines) + + +def main(): + parser = argparse.ArgumentParser(description="Write a Steam Frame focused report from saved watch artifacts.") + parser.add_argument("--run-dir", required=True) + parser.add_argument("--output", help="Optional output path") + args = parser.parse_args() + + run_dir = Path(args.run_dir) + output = Path(args.output) if args.output else run_dir / "reports" / "frame-focus.md" + output.parent.mkdir(parents=True, exist_ok=True) + output.write_text(build(run_dir), encoding="utf-8") + + +if __name__ == "__main__": + main() diff --git a/scripts/write_run_summary.py b/scripts/write_run_summary.py index ff261ec..dd12c7d 100755 --- a/scripts/write_run_summary.py +++ b/scripts/write_run_summary.py @@ -145,6 +145,7 @@ def build(run_dir: Path): f"- Retrieved assets: `{reports / 'retrieved-visual-assets.tsv'}`", f"- Blocked assets: `{reports / 'blocked-visual-assets.tsv'}`", f"- Manual retry URLs: `{reports / 'manual-asset-urls.txt'}`", + f"- Steam Frame focus: `{reports / 'frame-focus.md'}`", f"- SteamKit/PICS packages: `{reports / 'steamkit-pics-packages.tsv'}`", f"- SteamKit/PICS detail: `{reports / 'steamkit-pics-detail.md'}`", f"- Customs shipments: `{reports / 'customs-shipments.md'}`", @@ -156,6 +157,7 @@ def build(run_dir: Path): lines.extend(["", "## Notable Lines", ""]) notable = [] + notable.extend([f"- Frame focus: `{line}`" for line in filtered_first(reports / "frame-focus.md", 8)]) notable.extend([f"- SteamKit/PICS: `{line}`" for line in filtered_first(reports / "steamkit-pics-key-lines.txt", 8)]) notable.extend([f"- SteamDB: `{line}`" for line in filtered_first(reports / "steamdb-key-lines.txt", 5)]) notable.extend([f"- SteamTracking: `{line}`" for line in filtered_first(reports / "steamtracking-pairing-focus.txt", 5)]) diff --git a/tests/test_frame_focus_report.py b/tests/test_frame_focus_report.py new file mode 100644 index 0000000..92ff1b1 --- /dev/null +++ b/tests/test_frame_focus_report.py @@ -0,0 +1,102 @@ +import tempfile +import textwrap +import unittest +from pathlib import Path + +from scripts.write_frame_focus_report import build + + +def write(path: Path, content: str): + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(textwrap.dedent(content).lstrip(), encoding="utf-8") + + +class FrameFocusReportTests(unittest.TestCase): + def make_run_dir(self): + tmp = tempfile.TemporaryDirectory() + self.addCleanup(tmp.cleanup) + run_dir = Path(tmp.name) / "2026-06-24" + (run_dir / "reports").mkdir(parents=True) + return run_dir + + def write_baseline_reports(self, run_dir: Path): + reports = run_dir / "reports" + write( + reports / "valve-reservation-packages.tsv", + """ + type\tproduct\tid\tstatus\tname\trelease_or_price\tdetails + app\tSteam Frame\t4165890\tpublic\tSteam Frame\tComing soon\tpackages=; package_groups=0; coming_soon=True; price= + package\tSteam Frame\t1629484\tprivate\t\t\tpackagedetails success:false + package\tSteam Frame\t1629486\tprivate\t\t\tpackagedetails success:false + app\tSteam Machine\t4165910\tpublic\tSteam Machine\t$1,049.00\tpackages=1629447,1629458,1629446,1629460; package_groups=1; coming_soon=False; price=$1,049.00 + """, + ) + write( + reports / "steamkit-pics-packages.tsv", + """ + type\tproduct\tid\tstatus\tchangenumber\tprevious_changenumber\tchanged_since_previous\tsha_hash\tonly_public\tname\trelated_ids\tdetails + app\tSteam Frame\t4165890\tprivate_metadata_token_required\t35675573\t35675573\tno\tabc\tTrue\tSteam Frame\t\tdepots_count=0 + package\tSteam Frame\t1629484\tprivate_metadata_token_required\t35672606\t35672606\tno\t\tFalse\t\t\tapps_count=0 + package\tSteam Frame\t1629486\tprivate_metadata_token_required\t35672606\t35672606\tno\t\tFalse\t\t\tapps_count=0 + package\tSteam Machine\t1629446\tavailable\t36785426\t36756963\tyes\tdef\tFalse\t\t\tapps_count=1 + """, + ) + write( + reports / "steamdb-reservation-packages.tsv", + """ + product\tpackage_id\tlast_record_update\tlast_changenumber\tpossible_apps\tstatus + Steam Frame\t1629484\t5 May 2026 - 18:50:54 UTC\t35672606\t4165890:Steam Frame\tprivate_exists_only + Steam Frame\t1629486\t5 May 2026 - 18:50:54 UTC\t35672606\t4165890:Steam Frame\tprivate_exists_only + """, + ) + write( + reports / "komodo-product-modified.tsv", + """ + machine\t2026-06-24T16:12:21 + frame\t2026-05-27T16:53:46 + """, + ) + write( + reports / "customs-shipments.md", + """ + | arrival_date | shipper | consignee | description | + | 2026-06-17 | TECH-FRONT | VALVE | VIRTUAL REALITY DEVICES | + """, + ) + + def test_baseline_private_frame_reports_no_public_readiness(self): + run_dir = self.make_run_dir() + self.write_baseline_reports(run_dir) + + report = build(run_dir) + + self.assertIn("No public Steam Frame readiness change detected.", report) + self.assertIn("Frame app: `4165890`", report) + self.assertIn("Frame packages: `1629484`, `1629486`", report) + self.assertIn("status=private", report) + self.assertIn("product=frame; modified=2026-05-27T16:53:46", report) + self.assertIn("Steam Machine is now a launched comparator", report) + self.assertIn("VIRTUAL REALITY DEVICES", report) + + def test_public_frame_package_reports_public_readiness(self): + run_dir = self.make_run_dir() + self.write_baseline_reports(run_dir) + write( + run_dir / "reports" / "valve-reservation-packages.tsv", + """ + type\tproduct\tid\tstatus\tname\trelease_or_price\tdetails + app\tSteam Frame\t4165890\tpublic\tSteam Frame\t$499.00\tpackages=1629484; package_groups=1; coming_soon=False; price=$499.00 + package\tSteam Frame\t1629484\tpublic\tSteam Frame 256GB\t\tapps=4165890:Steam Frame + package\tSteam Frame\t1629486\tprivate\t\t\tpackagedetails success:false + """, + ) + + report = build(run_dir) + + self.assertIn("Steam Frame public readiness detected.", report) + self.assertIn("package_groups=1", report) + self.assertIn("status=public", report) + + +if __name__ == "__main__": + unittest.main()