diff --git a/benchmarks/measure_upstream_propagation.py b/benchmarks/measure_upstream_propagation.py new file mode 100755 index 00000000..c485be5c --- /dev/null +++ b/benchmarks/measure_upstream_propagation.py @@ -0,0 +1,294 @@ +#!/usr/bin/env python3 +"""Measure upstream usage-window rollover propagation via the real API. + +At an Anthropic usage-window reset boundary, the five_hour window +identity (resets_at) does not update instantly. This harness polls the +REAL Anthropic usage API at https://api.anthropic.com/api/oauth/usage, +one request per second, and records the wall-clock delta between the +nominal resets_at and the first response serving the new window. + +The constant this measures -- MIN_LEAD_SECONDS in the out-of-repo +resume-arm-time helper -- is labelled UNMEASURED until a live +measurement succeeds. If no credentials are available, the run writes +an UNMEASURED evidence record to benchmarks/results/ so the label is +never silently retired. + +Usage: + TAOSMD_ANTHROPIC_CREDS=/path/creds.json python3 benchmarks/measure_upstream_propagation.py + python3 benchmarks/measure_upstream_propagation.py --reset-at 2026-08-17T08:00:00+00:00 --creds /path/creds.json + python3 benchmarks/measure_upstream_propagation.py --dry-run --creds /path/creds.json +""" + +from __future__ import annotations + +import argparse +import datetime +import json +import os +import sys +import time +from pathlib import Path + +import httpx + +_BENCH_DIR = Path(__file__).resolve().parent +_REPO_ROOT = _BENCH_DIR.parent +_RESULTS_DIR = _BENCH_DIR / "results" +_EVIDENCE_FILE = _RESULTS_DIR / "tsk-rcnct6_upstream_propagation.json" + +USAGE_API_URL = "https://api.anthropic.com/api/oauth/usage" + +_HEADERS = { + "anthropic-beta": "oauth-2025-04-20", +} + + +def resolve_token(creds_path: str | None = None) -> str: + """Resolve the Anthropic OAuth token from a credentials file. + + The path is configurable via --creds or TAOSMD_ANTHROPIC_CREDS so no + hardcoded absolute path is baked into the source. + """ + path = creds_path or os.environ.get("TAOSMD_ANTHROPIC_CREDS") + if not path: + raise SystemExit( + "No credentials path: set TAOSMD_ANTHROPIC_CREDS or pass --creds" + ) + with open(path, "r") as f: + creds = json.load(f) + return creds["claudeAiOauth"]["accessToken"] + + +def fetch_usage(token: str, client: httpx.Client | None = None) -> dict: + """Fetch live usage data from the real Anthropic usage API. + + Returns the parsed JSON body. Does NOT return canned or mock data. + """ + owns_client = client is None + if owns_client: + client = httpx.Client(timeout=15.0) + try: + resp = client.get( + USAGE_API_URL, + headers={**_HEADERS, "Authorization": f"Bearer {token}"}, + ) + resp.raise_for_status() + return resp.json() + finally: + if owns_client: + client.close() + + +def parse_reset(resets_at: str) -> datetime.datetime: + """Parse a resets_at ISO timestamp into an aware datetime.""" + cleaned = resets_at.replace("Z", "+00:00") + dt = datetime.datetime.fromisoformat(cleaned) + if dt.tzinfo is None: + dt = dt.replace(tzinfo=datetime.timezone.utc) + return dt + + +def measure_at_boundary( + reset_at: datetime.datetime, + token: str, + poll_interval: float = 1.0, + max_wait: float = 600.0, + fetch_fn=None, + sleep_fn=None, +) -> dict: + """Poll the real API each second around a reset boundary. + + Records the wall-clock delta between *reset_at* and the first + response whose five_hour.resets_at differs from the pre-reset + value, proving the window identity has rolled over. Utilization + is recorded as a corroborating signal but never serves as the flip + trigger: on a shared account utilization moves inside a window as a + matter of course, and on an idle window it never moves at all, so + neither direction is a reliable signal. + """ + if fetch_fn is None: + fetch_fn = fetch_usage + if sleep_fn is None: + sleep_fn = time.sleep + + samples: list[dict] = [] + now = datetime.datetime.now(datetime.timezone.utc) + + pre_util = None + pre_resets_at = None + + # Capture the pre-boundary baseline BEFORE waiting for the boundary, + # so a propagation faster than the first post-boundary poll is + # still measurable by construction. + try: + pre_usage = fetch_fn(token) + pre_window = pre_usage.get("five_hour", {}) + pre_util = pre_window.get("utilization") + pre_resets_at = pre_window.get("resets_at") + except Exception as exc: # noqa: BLE001 - record, proceed without baseline + samples.append({"time": now.isoformat(), "pre_boundary_error": f"{type(exc).__name__}: {exc}"}) + + # Wait for the boundary to arrive. This wait is NOT counted + # against the polling budget. + while now < reset_at: + wait = (reset_at - now).total_seconds() + if wait <= 0: + break + sleep_fn(min(wait, poll_interval)) + now = datetime.datetime.now(datetime.timezone.utc) + + # Start the polling budget AFTER the boundary arrives, so the + # pre-boundary wait does not consume max_wait. (Previously t0 + # was set before the wait loop, exhausting the budget on a + # far-future boundary and never polling once.) + t0 = time.monotonic() + + while (time.monotonic() - t0) < max_wait: + now = datetime.datetime.now(datetime.timezone.utc) + try: + usage = fetch_fn(token) + except Exception as exc: # noqa: BLE001 - record, keep polling + samples.append({"time": now.isoformat(), "error": f"{type(exc).__name__}: {exc}"}) + sleep_fn(poll_interval) + continue + + window = usage.get("five_hour", {}) + cur_resets_at = window.get("resets_at") + util = window.get("utilization") + + samples.append({ + "time": now.isoformat(), + "resets_at": cur_resets_at, + "utilization": util, + }) + + # Key the flip on resets_at changing (window identity), not + # utilization. Utilization may move inside a window as a + # matter of course (shared account), so it cannot trigger a + # false positive. A rollover on an idle window is invisible to + # utilization, so it cannot be the trigger either. + if ( + pre_resets_at is not None + and cur_resets_at is not None + and cur_resets_at != pre_resets_at + ): + delta = (now - reset_at).total_seconds() + return { + "reset_at": reset_at.isoformat(), + "flipped_at": now.isoformat(), + "propagation_seconds": delta, + "pre_reset_utilization": pre_util, + "post_reset_utilization": util, + "pre_resets_at": pre_resets_at, + "post_resets_at": cur_resets_at, + "samples": samples, + "status": "MEASURED", + } + + sleep_fn(poll_interval) + + return { + "reset_at": reset_at.isoformat(), + "flipped_at": None, + "propagation_seconds": None, + "pre_reset_utilization": pre_util, + "pre_resets_at": pre_resets_at, + "samples": samples, + "status": "NO_FLIP_DETECTED", + } + + +def write_evidence(result: dict, path: Path | None = None) -> Path: + """Write evidence to the repo's benchmarks/results/ directory.""" + out = path or _EVIDENCE_FILE + out.parent.mkdir(parents=True, exist_ok=True) + out.write_text(json.dumps(result, indent=2)) + return out + + +def record_unmeasured(creds_path: str | None = None) -> dict: + """Record that the measurement could not be performed live.""" + reason = "No live Anthropic credentials available in this environment." + if creds_path and not Path(creds_path).exists(): + reason = f"Credentials file not found: {creds_path}" + result = { + "measurement": "upstream usage-window rollover propagation", + "target_constant": "MIN_LEAD_SECONDS", + "target_location": "MIN_LEAD_SECONDS constant in the out-of-repo resume-arm-time helper", + "status": "UNMEASURED", + "reason": reason, + "procedure": ( + "At a reset boundary, poll the live Anthropic usage API " + "https://api.anthropic.com/api/oauth/usage each second and " + "record the wall-clock delta between the nominal resets_at and " + "the first response serving the new five_hour window (detected " + "by resets_at changing). " + "See measure_at_boundary() in this file." + ), + "recorded_at": datetime.datetime.now(datetime.timezone.utc).isoformat(), + "measured_samples": [], + "script": "benchmarks/measure_upstream_propagation.py", + } + write_evidence(result) + return result + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Measure upstream usage-window rollover propagation via the real Anthropic API." + ) + parser.add_argument("--creds", help="Path to credentials JSON (or set TAOSMD_ANTHROPIC_CREDS)") + parser.add_argument("--reset-at", help="ISO timestamp of the reset boundary to measure") + parser.add_argument("--poll-interval", type=float, default=1.0, help="Seconds between polls") + parser.add_argument("--max-wait", type=float, default=600.0, help="Max seconds to wait for flip") + parser.add_argument("--dry-run", action="store_true", help="Check credentials and API reachability") + args = parser.parse_args() + + try: + token = resolve_token(args.creds) + except SystemExit: + result = record_unmeasured(args.creds) + print(f"No credentials available. Evidence written as UNMEASURED: {_EVIDENCE_FILE}") + return 1 + + if args.dry_run: + try: + usage = fetch_usage(token) + resets_at = usage.get("five_hour", {}).get("resets_at", "N/A") + print(f"API reachable. five_hour resets_at: {resets_at}") + return 0 + except Exception as exc: # noqa: BLE001 - report any failure to reach API + print(f"API unreachable: {exc}", file=sys.stderr) + return 2 + + if args.reset_at: + reset_at = parse_reset(args.reset_at) + else: + try: + usage = fetch_usage(token) + except Exception as exc: # noqa: BLE001 - report any failure to fetch + print(f"Failed to fetch usage: {exc}", file=sys.stderr) + return 2 + resets_at = usage.get("five_hour", {}).get("resets_at") + if not resets_at: + print("No five_hour.resets_at in API response", file=sys.stderr) + return 2 + reset_at = parse_reset(resets_at) + print(f"Next reset boundary: {reset_at.isoformat()}") + + print(f"Polling the real API at {reset_at.isoformat()} each second...") + result = measure_at_boundary( + reset_at, token, + poll_interval=args.poll_interval, + max_wait=args.max_wait, + ) + path = write_evidence(result) + print(f"Evidence written: {path}") + print(f"Status: {result['status']}") + if result.get("propagation_seconds") is not None: + print(f"Propagation: {result['propagation_seconds']:.3f}s") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/benchmarks/results/tsk-rcnct6_upstream_propagation.json b/benchmarks/results/tsk-rcnct6_upstream_propagation.json new file mode 100644 index 00000000..15abc634 --- /dev/null +++ b/benchmarks/results/tsk-rcnct6_upstream_propagation.json @@ -0,0 +1,11 @@ +{ + "measurement": "upstream usage-window rollover propagation", + "target_constant": "MIN_LEAD_SECONDS", + "target_location": "MIN_LEAD_SECONDS constant in the out-of-repo resume-arm-time helper", + "status": "UNMEASURED", + "reason": "No live Anthropic credentials available in this environment. The measurement requires polling https://api.anthropic.com/api/oauth/usage across a real 5h reset boundary (bounded wait).", + "procedure": "At a reset boundary, poll the live Anthropic usage API https://api.anthropic.com/api/oauth/usage each second and record the wall-clock delta between the nominal resets_at and the first response serving the new five_hour window (detected by resets_at changing). See measure_at_boundary() in benchmarks/measure_upstream_propagation.py.", + "recorded_at": "2026-08-17T23:05:00+00:00", + "measured_samples": [], + "script": "benchmarks/measure_upstream_propagation.py" +} diff --git a/changelog.d/tsk-3te4pi-measure-upstream-propagation.md b/changelog.d/tsk-3te4pi-measure-upstream-propagation.md new file mode 100644 index 00000000..d2508dbe --- /dev/null +++ b/changelog.d/tsk-3te4pi-measure-upstream-propagation.md @@ -0,0 +1,16 @@ +### Fixed + +- Keyed `measure_at_boundary` flip detection on `resets_at` (window + identity) instead of `utilization`, which could not distinguish a + rollover from ordinary account consumption on a shared account. +- Started the polling budget (`max_wait`) when the boundary arrives + instead of at process start, so a far-future boundary no longer + exhausts the budget before any poll occurs. The pre-boundary + baseline (`pre_resets_at`, `pre_util`) is now sampled before the + boundary, making a propagation faster than the first poll measurable. +- Aligned the evidence file's `target_location` and procedure text with + the code: `resets_at` is the flip trigger, and the helper is described + as out-of-repo instead of citing a non-existent `scripts/resume_arm_time.py`. +- Split the flip-detection test into ARM A (consumption without + rollover) and ARM B (rollover with flat utilization) as disagreement + controls. diff --git a/changelog.d/tsk-rcnct6-measure-upstream-propagation.md b/changelog.d/tsk-rcnct6-measure-upstream-propagation.md new file mode 100644 index 00000000..994da604 --- /dev/null +++ b/changelog.d/tsk-rcnct6-measure-upstream-propagation.md @@ -0,0 +1,8 @@ +### Fixed + +- Replaced the mock-API simulation in the upstream usage-window rollover + propagation measurement with a harness that polls the real Anthropic + usage API at `api.anthropic.com/api/oauth/usage`. Evidence is now + committed to `benchmarks/results/` instead of `/tmp`, with no hardcoded + paths, no external-file mutation, and `MIN_LEAD_SECONDS` left labelled + UNMEASURED pending a live measurement. diff --git a/tests/test_measure_upstream_propagation.py b/tests/test_measure_upstream_propagation.py new file mode 100644 index 00000000..109fa21a --- /dev/null +++ b/tests/test_measure_upstream_propagation.py @@ -0,0 +1,347 @@ +"""Red-first tests for the upstream usage-window rollover measurement. + +The 14 source-text grep tests verify the credit items from PR #325 +(real API, no mock, evidence committed in-repo, not future-dated, +no fake range, no external mutation, no hardcoded paths, no +contradictory claims, no scripts at root). + +The functional tests verify the four remaining blockers from +revision tsk-3te4pi, using disagreement controls (ARM A, ARM B) for +the two behavioural blockers: + + BLOCKER 1: measure_at_boundary must key the flip on resets_at + (window identity), not utilization. + BLOCKER 2: The polling budget must start when the boundary arrives, + and the pre-boundary baseline must be sampled before the + boundary. + BLOCKER 3: The evidence file's procedure must match what the code + actually implements. + BLOCKER 4: Flip-detection tests must use one fixture per signal, + so a false positive (utilization shift without rollover) + and a false negative (rollover on an idle window) are + both caught. +""" + +from __future__ import annotations + +import datetime +import importlib.util +import json +import sys +import time +from pathlib import Path + +import httpx + +REPO_ROOT = Path(__file__).resolve().parent.parent +SCRIPT_PATH = REPO_ROOT / "benchmarks" / "measure_upstream_propagation.py" +RESULTS_DIR = REPO_ROOT / "benchmarks" / "results" +EVIDENCE_PATH = RESULTS_DIR / "tsk-rcnct6_upstream_propagation.json" + + +def _load_module(): + """Load benchmarks/measure_upstream_propagation.py as a module.""" + spec = importlib.util.spec_from_file_location( + "measure_upstream_propagation", SCRIPT_PATH + ) + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + return module + + +def _source(): + return SCRIPT_PATH.read_text() + + +# --- BLOCKER 1 (credit): real API, not a mock --- + +def test_no_mock_api_class(): + """The harness must call the real Anthropic API, never substitute a mock.""" + src = _source() + assert "class MockAPI" not in src, "MockAPI class present: not a real measurement" + + +def test_no_window_flips_dict(): + """No pre-baked window_flips data that replaces real API responses.""" + src = _source() + assert "window_flips" not in src + + +def test_uses_real_api_endpoint(): + """The harness must target the real Anthropic usage endpoint.""" + src = _source() + assert "api.anthropic.com" in src + assert "oauth/usage" in src + + +def test_fetch_usage_uses_httpx(): + """fetch_usage must issue a real HTTP request via httpx.""" + src = _source() + assert "def fetch_usage" in src + assert "httpx" in src + + +def test_no_simulation_keywords(): + """The harness must not describe itself as a simulation.""" + src_lower = _source().lower() + for forbidden in ("simulate", "simulator", "simulation"): + assert forbidden not in src_lower, f"forbidden word '{forbidden}' in source" + + +# --- BLOCKER 2 (credit): evidence committed in-repo, not /tmp --- + +def test_evidence_file_exists_in_repo(): + """Evidence must be committed in benchmarks/results/, not in /tmp.""" + assert EVIDENCE_PATH.is_file(), "evidence file must be committed to the repo" + + +def test_no_tmp_result_paths(): + """No hardcoded /tmp paths for results or evidence.""" + src = _source() + assert "/tmp/" not in src, "hardcoded /tmp path found in measurement script" + + +# --- BLOCKER 3 (credit): evidence not future-dated --- + +def test_evidence_not_future_dated(): + """Evidence must not be dated after today.""" + today = datetime.datetime.now(tz=datetime.timezone.utc).date().isoformat() + data = json.loads(EVIDENCE_PATH.read_text()) + recorded = data.get("recorded_at", "") + assert recorded, "evidence must carry a recorded_at timestamp" + assert recorded[:10] <= today, f"evidence dated in the future: {recorded}" + + +# --- BLOCKER 4 (credit): no fake constant range --- + +def test_evidence_no_fake_constant_range(): + """Evidence must not claim a fake 10.0s measurement.""" + blob = json.dumps(json.loads(EVIDENCE_PATH.read_text())) + assert "10.0s-10.0s" not in blob + assert '"average_delay": 10.0' not in blob + + +def test_evidence_status_is_unmeasured(): + """With no live credentials, status must remain UNMEASURED.""" + data = json.loads(EVIDENCE_PATH.read_text()) + assert data["status"] == "UNMEASURED" + + +# --- BLOCKER 5 (credit): no external-file mutation --- + +def test_no_external_file_mutation(): + """No code that rewrites files outside the repository.""" + src = _source() + assert "apply_change" not in src, "script generates external mutation: apply_change" + assert "sed -i" not in src + assert "cp ~/.taos-team" not in src + assert "resume_arm_time.py.backup" not in src + + +def test_no_hardcoded_external_paths(): + """No hardcoded paths to ~/.taos-team or /home/jay.""" + src = _source() + assert "/home/jay/.taos-team" not in src, "hardcoded external path" + assert "/home/jay/.claude/.credentials.json" not in src + + +# --- BLOCKER 6 (credit): no contradictory claims --- + +def test_no_contradictory_measurement_claims(): + """Source must not claim the quantity was measured when it was not.""" + src = _source() + assert "10.0s on average" not in src + assert "Measured propagation is" not in src + + +# --- REQUIRED (credit): no scratch scripts at repo root --- + +def test_no_scripts_at_root(): + """Measurement and helper scripts must not live at the repository root.""" + assert not (REPO_ROOT / "measure_upstream_propagation.py").exists() + assert not (REPO_ROOT / "analyze_and_update.py").exists() + assert not (REPO_ROOT / "test_api_access.py").exists() + assert not (REPO_ROOT / "test_measurement.py").exists() + + +# --- functional: fetch_usage returns parsed JSON from the real endpoint --- + +def test_fetch_usage_returns_parsed_json(): + """fetch_usage must return the parsed JSON body, not canned data.""" + mod = _load_module() + payload = { + "five_hour": {"utilization": 0.12, "resets_at": "2026-08-17T08:00:00Z"}, + "seven_day": {"utilization": 0.03, "resets_at": "2026-08-18T02:00:00Z"}, + } + transport = httpx.MockTransport( + lambda req: httpx.Response(200, json=payload) + ) + with httpx.Client(transport=transport) as client: + result = mod.fetch_usage("token", client=client) + assert result["five_hour"]["utilization"] == 0.12 + + +# --- BLOCKER 1 (tsk-3te4pi): key flip on resets_at, not utilization --- +# Disagreement control: ARM A (consumption without rollover) and ARM B +# (rollover with flat utilization) must give opposite verdicts. + +def test_arm_a_consumption_without_rollover_not_measured(): + """ARM A: resets_at pinned, utilization changes. No rollover occurred. + + The old code keyed on utilization and would return MEASURED here + (a false positive). The corrected code keys on resets_at and + returns NO_FLIP_DETECTED. + """ + mod = _load_module() + reset_at = datetime.datetime(2020, 1, 1, tzinfo=datetime.timezone.utc) + + state = {"call": 0} + + def fake_fetch(token, client=None): + state["call"] += 1 + if state["call"] <= 1: + return { + "five_hour": {"utilization": 0.20, "resets_at": "2020-01-01T13:00:00Z"} + } + return { + "five_hour": {"utilization": 0.22, "resets_at": "2020-01-01T13:00:00Z"} + } + + result = mod.measure_at_boundary( + reset_at, "token", fetch_fn=fake_fetch, sleep_fn=lambda s: None, max_wait=0.1 + ) + assert result["status"] == "NO_FLIP_DETECTED", ( + "consumption without rollover must not be reported as a flip" + ) + + +def test_arm_b_rollover_with_flat_utilization_is_measured(): + """ARM B: resets_at changes, utilization flat. Rollover occurred. + + The old code keyed on utilization and would return NO_FLIP_DETECTED + here (a false negative). The corrected code keys on resets_at and + returns MEASURED. + + Also verifies BLOCKER 2: the pre-boundary baseline (pre_resets_at, + pre_reset_utilization) is captured before the boundary. + """ + mod = _load_module() + reset_at = datetime.datetime(2020, 1, 1, tzinfo=datetime.timezone.utc) + + state = {"call": 0} + + def fake_fetch(token, client=None): + state["call"] += 1 + if state["call"] <= 1: + return { + "five_hour": {"utilization": 0.0, "resets_at": "2020-01-01T08:00:00Z"} + } + return { + "five_hour": {"utilization": 0.0, "resets_at": "2020-01-01T13:00:00Z"} + } + + result = mod.measure_at_boundary( + reset_at, "token", fetch_fn=fake_fetch, sleep_fn=lambda s: None, max_wait=0.1 + ) + assert result["status"] == "MEASURED", ( + "rollover with flat utilization must be detected" + ) + assert result["propagation_seconds"] is not None + assert result["pre_resets_at"] == "2020-01-01T08:00:00Z" + assert result["post_resets_at"] == "2020-01-01T13:00:00Z" + assert result["pre_reset_utilization"] == 0.0 + # flips detected on identity, not utilization + assert result["pre_resets_at"] != result["post_resets_at"] + assert result["pre_reset_utilization"] == result["post_reset_utilization"] + + +# --- BLOCKER 2 (tsk-3te4pi): budget starts after boundary, baseline before --- + +def test_polling_budget_starts_after_boundary(): + """The polling budget must start when the boundary arrives. + + Old code set t0 before the pre-boundary wait, so a boundary that is + farther in the future than max_wait exhausts the budget and never + polls. New code sets t0 only after the boundary, so polling proceeds. + """ + mod = _load_module() + reset_at = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(seconds=1.5) + + state = {"call": 0} + + def fake_fetch(token, client=None): + state["call"] += 1 + return { + "five_hour": {"utilization": 0.80, "resets_at": "2020-01-01T08:00:00Z"} + } + + result = mod.measure_at_boundary( + reset_at, "token", + fetch_fn=fake_fetch, sleep_fn=time.sleep, + poll_interval=0.3, max_wait=1.0, + ) + assert state["call"] >= 2, ( + f"expected at least one pre-boundary + one post-boundary fetch, got {state['call']}" + ) + assert result["status"] == "NO_FLIP_DETECTED" + + +def test_pre_reset_baseline_in_no_flip_result(): + """When no flip occurs, pre_resets_at and pre_util are still present.""" + mod = _load_module() + reset_at = datetime.datetime(2020, 1, 1, tzinfo=datetime.timezone.utc) + + def no_flip(token, client=None): + return {"five_hour": {"utilization": 0.80, "resets_at": "2020-01-01T08:00:00Z"}} + + result = mod.measure_at_boundary( + reset_at, "token", + fetch_fn=no_flip, sleep_fn=lambda s: None, + max_wait=0.1, + ) + assert result["status"] == "NO_FLIP_DETECTED" + assert result["pre_resets_at"] == "2020-01-01T08:00:00Z" + assert result["pre_reset_utilization"] == 0.80 + + +# --- BLOCKER 4 (tsk-3te4pi): no-flip timeout still works --- + +def test_measure_at_boundary_no_flip_times_out(): + """When the window never flips, status is NO_FLIP_DETECTED.""" + mod = _load_module() + reset_at = datetime.datetime(2020, 1, 1, tzinfo=datetime.timezone.utc) + + def no_flip(token, client=None): + return { + "five_hour": {"utilization": 0.80, "resets_at": "2020-01-01T08:00:00Z"} + } + + result = mod.measure_at_boundary( + reset_at, "token", + fetch_fn=no_flip, sleep_fn=lambda s: None, + max_wait=0.1, + ) + assert result["status"] == "NO_FLIP_DETECTED" + + +# --- BLOCKER 3 (tsk-3te4pi): evidence procedure matches code --- + +def test_evidence_procedure_matches_code(): + """The evidence file's procedure must match what the code implements. + + The evidence says the flip is keyed on resets_at (window identity). + The code must do the same: compare pre_resets_at against the current + resets_at, never against utilization alone. + """ + data = json.loads(EVIDENCE_PATH.read_text()) + procedure = data.get("procedure", "") + + assert "resets_at" in procedure, ( + "evidence procedure must reference resets_at as the flip signal" + ) + + src = _source() + assert "pre_resets_at" in src, "code must capture pre-boundary resets_at" + assert "cur_resets_at" in src, "code must compare against current resets_at" + assert "post_resets_at" in src, "code must record the post-flip resets_at"