fix(763): round 2+3 — close the fail-opens the paging change introduced
Two independent cold reviews (Codex GPT-5.6 cross-family, and an isolated Opus agent) converged on the same blocker, which is fixed here along with everything else they found. BLOCKER — the mark walk turned a fail-closed case into a fail-open. The high-water mark gates the post-write race check entirely: `max_id_before=-1` skips it. Before paging, only a failure of the single page-1 request could reach that. Requiring a COMPLETE walk newly routed a page-2 hiccup, an over-cap history, or one malformed id on a later page into the same hole, so a human rejection racing the write was left green where `main` repaired. A partial list now still yields a mark: it can only be LOWER than the true maximum, which makes the check more eager, never blinder. Only a read returning no rows at all abandons it — the pre-existing #849 gap, unchanged and now asserted by a test so it stays visible. WITHDRAWN — the "currency witness". It produced two defects from one mechanism, which is the signal to remove rather than patch twice: counting ANY row above the mark does not witness this job's write, so a stale-but-valid snapshot carrying an unrelated newer row passed while hiding a rejection; and a schema-valid stale read is not retried, so one such response turned a transient anomaly into a permanent sentinel. The hazard has no mechanism here either — Gitea is a single instance with no read replicas. Removing it restores the pre-change exposure on that path, a non-regression. Also fixed, each a fail-open with a fixture and an executed mutation: - `.creator` is type-tested before indexing. `.creator.login` on a non-object exits jq 5 and `set -e` took the step down after the green was posted and before the repair. Reproduced by both reviewers. - the mark is the max over NUMERIC ids only. jq orders strings above every number, so one `"id": "99999"` passed the numeric gate and inflated the mark until nothing looked newer. - an unusable `raced` count now repairs instead of "not acting on it". - `sort=highestindex` (ASC, measured) so a row inserted mid-walk appends at the end rather than at position 0 on a page already read. An unknown sort value silently falls back to DESC, so this is insurance, not load-bearing, and the comment says so. - `ph_ok`/`ph_rows` renamed off `read_existing_verdict`'s `st_ok`. No live bug, but a name collision in a 1400-line step. Tests the reviews showed were missing, each proved by an executed mutation: - verdict beyond a SHORT page (a deliberately unfaithful truncated response — against a faithful double a short page is always the last, so the rule "terminate only on an EMPTY page" was unobservable) - pre-write paging failure still yields a usable mark - pre-write read returning nothing abandons the mark and says so - a TRANSIENT page failure is retried (the retry was unproven code: every other error mode fails on every attempt, so disarming it reddened nothing) - a string id cannot inflate the mark - a malformed `creator` row does not kill the job Stub corrections, both the same class as the earlier `[]`-vs-`null` gap: it served one flat list (so paging was unobservable) and computed its own-post id with `max()` over mixed str/int, which raised TypeError and made the string-id test pass because the DOUBLE crashed rather than because the mark was right. Mutation matrix, all executed, each reddening exactly its named test: retry disarmed; numeric-max reverted; partial-mark fallback removed; short-page terminates; page-1-only walk; post-write fail-closed flipped open; jq type-guard reverted. The unusable-count arm is unreachable by any fixture and is annotated as such rather than claimed as proved. Verification: `scripts/tests` 1090 passed, 2 skipped; decisions_validate and build_decisions_catalog --check both exit 0; terminator, clamp, sort order and id monotonicity all re-measured live on Gitea 1.27.1. refs #763 Decisions-Edit: yes Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1013,10 +1013,10 @@ if "/timeline" in url:
|
||||
if "/statuses/" in url:
|
||||
mode = os.environ.get("STUB_HISTORY_MODE", "none")
|
||||
|
||||
# PAGES FAITHFULLY (ersatztv#763). The job now walks this endpoint to a validated empty page
|
||||
# instead of reading one clamped page, so a stub that describes page 1 only is no longer a
|
||||
# description of anything the job does. Two properties have to hold together, and the second is
|
||||
# the one that is easy to lose:
|
||||
# PAGES FAITHFULLY (ersatztv#763). The job walks this endpoint to a validated empty page instead
|
||||
# of reading one clamped page, so a stub describing page 1 only no longer describes anything the
|
||||
# job does. The snapshot is a LIST OF PAGES, not a flat list, because two modes below need a page
|
||||
# boundary the uniform 50-row slicing cannot express.
|
||||
#
|
||||
# 1. PAGE SIZE AND TERMINATOR match the real endpoint. Measured at 1.27.1 on 2026-08-28 against
|
||||
# PR #761's 114-row head: pages 1 and 2 return 50, page 3 returns 14, page 4 is `[]`. So the
|
||||
@@ -1024,18 +1024,20 @@ if "/statuses/" in url:
|
||||
# `/issues/{n}/timeline` returns and what `/commits/{sha}/status` spells `{"statuses": null}`.
|
||||
# Three distinct empty shapes on one server; each stub owes its own measurement.
|
||||
#
|
||||
# 2. ONE LOGICAL READ IS ONE SNAPSHOT. The counter modes below resolve their rows from how many
|
||||
# times the job has LOOKED, and the job now looks two or three times per logical read. If
|
||||
# page 2 recomputed from an advanced counter it would describe a DIFFERENT history than page
|
||||
# 1 — a state the real server can never be in — and `human-after-post` would surface its
|
||||
# verdict halfway through the pre-write read, inverting the very race being modelled. So the
|
||||
# full list is resolved once, on page 1, and cached; later pages slice that cache.
|
||||
# 2. ONE LOGICAL READ IS ONE SNAPSHOT. The counter modes resolve their rows from how many times
|
||||
# the job has LOOKED, and the job now looks two or three times per logical read. Recomputing
|
||||
# on page 2 from an advanced counter would describe a different history than page 1 and
|
||||
# surface `human-after-post`'s verdict halfway through the PRE-write read, inverting the race
|
||||
# being modelled. So the pages are resolved once, on page 1, and cached.
|
||||
#
|
||||
# THE PAGE GUARD SITS AHEAD OF THE COUNTERS, and unlike before this is now load-bearing. The
|
||||
# previous comment here recorded, honestly, that moving the guard after the counters reddened
|
||||
# NOTHING, because no counting mode ever issued a page-2 request. That is no longer true: every
|
||||
# mode issues at least one terminator request per read, so the guard is what keeps the counter
|
||||
# counting logical reads rather than HTTP round-trips.
|
||||
# This is a MODELLING CHOICE, not a fidelity claim, and the difference matters. These are
|
||||
# independent offset-paginated GETs with no snapshot token, so the REAL history can change
|
||||
# between page requests — overlapping rows and shifting offsets are all reachable live. The
|
||||
# cache deliberately suppresses that, because the tests here are about the job's paging
|
||||
# logic, not about mid-walk mutation. Mid-walk mutation is untested, and saying so is the
|
||||
# honest form of the claim.
|
||||
#
|
||||
# Counter branches are confined to the page-1 arm below, so a later page can never advance them.
|
||||
HIST_PAGE_SIZE = 50
|
||||
hist_page = 1
|
||||
for part in url.split("?", 1)[-1].split("&"):
|
||||
@@ -1044,17 +1046,20 @@ if "/statuses/" in url:
|
||||
hist_page = int(part.split("=", 1)[1])
|
||||
except ValueError:
|
||||
hist_page = 1
|
||||
if hist_page > 1 and mode == "second-page-garbage":
|
||||
print("<html>502 Bad Gateway</html>")
|
||||
sys.exit(0)
|
||||
if hist_page > 1 and mode == "second-page-error":
|
||||
sys.exit(22)
|
||||
|
||||
ordinary = [{"id": 7000 + i, "context": "ci/other", "status": "success",
|
||||
"creator": None, "description": "unrelated"} for i in range(60)]
|
||||
raced_row = {"id": 9000, "context": "review-verdict/h10", "status": "failure",
|
||||
"creator": {"login": "timothy"},
|
||||
"description": "Review-verdict: BLOCKED @ a9e3e23 (base: main)"}
|
||||
|
||||
snapshot = out / "history_snapshot.json"
|
||||
if hist_page > 1:
|
||||
rows = json.loads(snapshot.read_text()) if snapshot.exists() else []
|
||||
else:
|
||||
logical = out / "history_logical_reads.txt"
|
||||
if hist_page == 1:
|
||||
n_logical = (int(logical.read_text()) if logical.exists() else 0) + 1
|
||||
logical.write_text(str(n_logical))
|
||||
rows = []
|
||||
pages = None
|
||||
if mode.startswith("human-after-post"):
|
||||
# The raced verdict: absent when the high-water mark is taken, present afterwards. Its id
|
||||
# is ABOVE the mark, which is what makes it detectable.
|
||||
@@ -1066,9 +1071,8 @@ if "/statuses/" in url:
|
||||
# INHERITANCE test: a verdict from an account off `$H10_REVIEWERS` still counts as
|
||||
# "something human landed while we were writing", because narrowing it here would
|
||||
# leave the exemption green over that row instead of repairing to pending.
|
||||
rows = [{"id": 5000, "context": "review-verdict/h10", "status": "failure",
|
||||
"creator": {"login": os.environ.get("STUB_HISTORY_CREATOR", "timothy")},
|
||||
"description": "Review-verdict: BLOCKED @ a9e3e23 (base: main)"}]
|
||||
rows = [dict(raced_row, id=5000,
|
||||
creator={"login": os.environ.get("STUB_HISTORY_CREATOR", "timothy")})]
|
||||
elif mode == "sentinel-after-post":
|
||||
# Another overlapping run repaired mid-flight: its SENTINEL lands above this run's mark,
|
||||
# while the human row it records sits BELOW the mark and is therefore invisible here.
|
||||
@@ -1087,59 +1091,122 @@ if "/statuses/" in url:
|
||||
rows = [{"id": 10, "context": "review-verdict/h10", "status": "success",
|
||||
"creator": {"login": "timothy"},
|
||||
"description": "Review-verdict: MERGEABLE @ a9e3e23 (base: other)"}]
|
||||
elif mode in ("second-page", "verdict-on-page-2"):
|
||||
elif mode in ("second-page", "verdict-on-page-2", "premark-page2-error",
|
||||
"flaky-page2", "premark-page1-error"):
|
||||
# A history that genuinely runs past one page: 60 ORDINARY rows, no verdict and no
|
||||
# sentinel. Under the pre-#763 page-2 probe the mere existence of these rows forced a
|
||||
# repair; now they are simply read, and `second-page` asserts the exemption STANDS.
|
||||
rows = [{"id": 7000 + i, "context": "ci/other", "status": "success",
|
||||
"creator": None, "description": "unrelated"} for i in range(60)]
|
||||
if mode == "verdict-on-page-2":
|
||||
rows = list(ordinary)
|
||||
if mode in ("verdict-on-page-2", "premark-page2-error"):
|
||||
ctr = out / "history_reads.txt"
|
||||
seen = int(ctr.read_text()) if ctr.exists() else 0
|
||||
ctr.write_text(str(seen + 1))
|
||||
if seen > 0:
|
||||
# THE RACED VERDICT, PLACED BEYOND THE FIRST PAGE. Index 55 puts it on page 2, and
|
||||
# its id is above every ordinary row so it is above the high-water mark too. A job
|
||||
# that reads only page 1 cannot see it — which is exactly the fail-toward-SUCCESS
|
||||
# hole #763 closes.
|
||||
rows.insert(55, {"id": 9000, "context": "review-verdict/h10", "status": "failure",
|
||||
"creator": {"login": "timothy"},
|
||||
"description": "Review-verdict: BLOCKED @ a9e3e23 (base: main)"})
|
||||
# THE JOB'S OWN WRITES APPEAR IN THE HISTORY (ersatztv#763). `/statuses/{sha}` returns one row
|
||||
# per POST, so once this job has posted its exemption the very next read MUST show it. The
|
||||
# stub did not model that at all: the history was whatever the mode described, before and
|
||||
# after the write alike, so every ordinary run looked like a head to which nothing had ever
|
||||
# been posted.
|
||||
#
|
||||
# That gap was invisible while the post-write check only ever asked "is there a HUMAN row
|
||||
# above the mark". It stopped being invisible the moment the check also asserted that the
|
||||
# read reflects the write it is verifying — the currency witness went to zero on every clean
|
||||
# run, because in the stub's world the POST really had left no trace. The double was wrong,
|
||||
# not the check.
|
||||
#
|
||||
# `creator: null` is measured: an Actions-token POST records no creator, which is also what
|
||||
# keeps our own row out of the raced count. The id is one above everything present, mirroring
|
||||
# the real endpoint's monotonic per-head ids and guaranteeing our row sits above the
|
||||
# high-water mark taken before the write.
|
||||
# THE RACED VERDICT, PLACED BEYOND THE FIRST PAGE. Its id is above every ordinary
|
||||
# row, so it is above the high-water mark too. A job that reads only page 1 cannot
|
||||
# see it — the fail-toward-SUCCESS hole #763 closes.
|
||||
rows.insert(55, dict(raced_row))
|
||||
elif mode == "string-id-inflates-mark":
|
||||
# A STRING ID. jq orders strings above every number, so `max` over raw ids returns
|
||||
# `"99999"` — which then passes the numeric gate as a plain `99999` and sets a high-water
|
||||
# mark far above anything real. Every later row looks OLDER than the mark, so the raced
|
||||
# verdict below is invisible and the exemption stands over it. One corrupt row is enough.
|
||||
ctr = out / "history_reads.txt"
|
||||
seen = int(ctr.read_text()) if ctr.exists() else 0
|
||||
ctr.write_text(str(seen + 1))
|
||||
rows = [{"id": "99999", "context": "ci/other", "status": "success",
|
||||
"creator": None, "description": "unrelated"},
|
||||
{"id": 10, "context": "ci/other", "status": "success",
|
||||
"creator": None, "description": "unrelated"}]
|
||||
if seen > 0:
|
||||
rows.append(dict(raced_row))
|
||||
elif mode == "malformed-creator-beside-verdict":
|
||||
# A SCHEMA-CORRUPT ROW NEXT TO A REAL ONE. `creator` is a number, so `.creator.login`
|
||||
# hard-errors in jq ("Cannot index number with string"), jq exits 5, and an unguarded
|
||||
# `raced=$(...)` takes the whole step down under `set -e` — after the exemption `success`
|
||||
# is posted and with the repair never attempted. The genuine verdict beside it is what
|
||||
# makes the consequence visible: the correct behaviour is to drop the malformed row and
|
||||
# still repair.
|
||||
ctr = out / "history_reads.txt"
|
||||
seen = int(ctr.read_text()) if ctr.exists() else 0
|
||||
ctr.write_text(str(seen + 1))
|
||||
if seen > 0:
|
||||
rows = [{"id": 8500, "context": "review-verdict/h10", "status": "failure",
|
||||
"creator": 7, "description": "Review-verdict: BLOCKED @ a9e3e23 (base: main)"},
|
||||
dict(raced_row)]
|
||||
elif mode == "verdict-after-short-page":
|
||||
# DELIBERATELY UNFAITHFUL, and that is the point. Page 2 is SHORT (10 rows) and yet page 3
|
||||
# still carries rows — a shape the measured server does not produce, but exactly what a
|
||||
# truncated or partially-served response looks like. It is the only way to observe the
|
||||
# rule "terminate ONLY on a validated EMPTY page, never on a short one": against a
|
||||
# faithful double a short page is always the last one, so an implementation that stops
|
||||
# there is indistinguishable from a correct one.
|
||||
ctr = out / "history_reads.txt"
|
||||
seen = int(ctr.read_text()) if ctr.exists() else 0
|
||||
ctr.write_text(str(seen + 1))
|
||||
pages = [ordinary[:50], ordinary[50:], [dict(raced_row)] if seen > 0 else []]
|
||||
if pages is None:
|
||||
pages = [rows[i:i + HIST_PAGE_SIZE] for i in range(0, len(rows), HIST_PAGE_SIZE)] or [[]]
|
||||
|
||||
# THE JOB'S OWN WRITES APPEAR IN THE HISTORY. `/statuses/{sha}` returns one row per POST, so
|
||||
# once this job has posted its exemption the next read MUST show it. The stub did not model
|
||||
# that at all — the history was whatever the mode described, before and after the write alike,
|
||||
# so every ordinary run looked like a head nothing had ever been posted to. `creator: null` is
|
||||
# measured (an Actions-token POST records no creator), which is also what keeps our own row
|
||||
# out of the raced count; the id is one above everything present, mirroring the real
|
||||
# endpoint's per-head monotonic ids (verified live: 114 rows, ids strictly increasing with
|
||||
# `created_at`, no duplicates).
|
||||
posted_log = out / "posted_all.jsonl"
|
||||
# `own-write-invisible` withholds exactly this modelling — a history that never shows the
|
||||
# job's own POST, which is what the stub did for every mode before ersatztv#763. It is the
|
||||
# negative control for the currency witness: without it that witness is unproven code, since
|
||||
# every other mode now satisfies it as a side effect of being faithful.
|
||||
if posted_log.exists() and mode != "own-write-invisible":
|
||||
if posted_log.exists():
|
||||
for line in posted_log.read_text().splitlines():
|
||||
if not line.strip():
|
||||
continue
|
||||
body = json.loads(line)
|
||||
rows.append({"id": max([r.get("id") or 0 for r in rows] or [0]) + 1,
|
||||
"context": body.get("context"),
|
||||
"status": body.get("state"),
|
||||
"creator": None,
|
||||
"description": body.get("description", "")})
|
||||
snapshot.write_text(json.dumps(rows))
|
||||
# NUMERIC IDS ONLY when computing the next one. A fixture may deliberately carry a
|
||||
# schema-corrupt id (see `string-id-inflates-mark`), and `max()` over a str beside an
|
||||
# int raises TypeError — which fails the whole stub request, makes the walk look
|
||||
# unreadable, and produces a repair for a reason the test was not asking about. That
|
||||
# is a test passing for the wrong reason: the string-id mutation stayed green because
|
||||
# the double crashed rather than because the mark was computed correctly.
|
||||
nxt = max([r.get("id") for pg in pages for r in pg
|
||||
if isinstance(r.get("id"), int)] or [0]) + 1
|
||||
pages[-1].append({"id": nxt, "context": body.get("context"),
|
||||
"status": body.get("state"), "creator": None,
|
||||
"description": body.get("description", "")})
|
||||
snapshot.write_text(json.dumps(pages))
|
||||
else:
|
||||
n_logical = int(logical.read_text()) if logical.exists() else 1
|
||||
pages = json.loads(snapshot.read_text()) if snapshot.exists() else [[]]
|
||||
|
||||
start = (hist_page - 1) * HIST_PAGE_SIZE
|
||||
print(json.dumps(rows[start:start + HIST_PAGE_SIZE]))
|
||||
if hist_page == 1 and mode == "premark-page1-error":
|
||||
# PAGE 1 ITSELF FAILS, for the whole first logical read (both attempts), so the walk returns
|
||||
# nothing at all and the mark must be abandoned. Counted in its own file because the logical
|
||||
# read counter below is only reached on a SUCCESSFUL page-1 serve.
|
||||
ctr = out / "p1_attempts.txt"
|
||||
seen = int(ctr.read_text()) if ctr.exists() else 0
|
||||
ctr.write_text(str(seen + 1))
|
||||
if seen < 2:
|
||||
sys.exit(22)
|
||||
if hist_page > 1 and mode == "flaky-page2":
|
||||
# TRANSIENT: fails the first attempt of each logical read and succeeds on the retry. This is
|
||||
# the only fixture that exercises the retry at all — the other error modes fail every attempt,
|
||||
# so against them a one-shot walk and a retrying walk are indistinguishable.
|
||||
ctr = out / "page2_attempts.txt"
|
||||
seen = int(ctr.read_text()) if ctr.exists() else 0
|
||||
ctr.write_text(str(seen + 1))
|
||||
if seen % 2 == 0:
|
||||
sys.exit(22)
|
||||
if hist_page > 1 and mode == "second-page-garbage":
|
||||
print("<html>502 Bad Gateway</html>")
|
||||
sys.exit(0)
|
||||
if hist_page > 1 and mode == "second-page-error":
|
||||
sys.exit(22)
|
||||
if hist_page > 1 and mode == "premark-page2-error" and n_logical == 1:
|
||||
# Fails ONLY on the PRE-write read, so the high-water mark must be salvaged from the partial
|
||||
# list; the post-write read then pages cleanly and must still catch the raced verdict.
|
||||
sys.exit(22)
|
||||
|
||||
print(json.dumps(pages[hist_page - 1] if hist_page - 1 < len(pages) else []))
|
||||
sys.exit(0)
|
||||
|
||||
if "/status" in url:
|
||||
@@ -2728,6 +2795,10 @@ def test_the_high_water_MARK_is_captured_BEFORE_the_last_moment_re_read():
|
||||
# definition matters: the definition sits with the other helpers near the top of the step, so
|
||||
# keying on it would place the "fetch" far earlier than the round-trip actually happens and this
|
||||
# assertion would hold vacuously.
|
||||
assert "\npage_statuses\n" in src, (
|
||||
"no bare `page_statuses` call site found; the mark is no longer fetched where this test "
|
||||
"believes it is, and the ordering assertion below would be vacuous"
|
||||
)
|
||||
mark = max(src.index("max_id_before=-1"), src.index("\npage_statuses\n"))
|
||||
# The LAST-MOMENT re-read is the second bare `read_existing_verdict` call.
|
||||
calls = [i for i in range(len(src)) if src.startswith("read_existing_verdict\n", i)]
|
||||
@@ -3607,37 +3678,6 @@ def test_a_status_history_RUNNING_PAST_PAGE_1_is_PAGED_and_the_exemption_STANDS(
|
||||
)
|
||||
|
||||
|
||||
def test_a_post_write_read_that_CANNOT_SEE_OUR_OWN_WRITE_is_not_trusted(tmp_path):
|
||||
"""The currency witness (ersatztv#763), and the reason paging alone is not enough.
|
||||
|
||||
`st_ok=yes` proves the walk reached a validated empty page. It does NOT prove the walk saw a
|
||||
list that includes the POST this job just made — a replica lag, a cache, or a read aimed at the
|
||||
wrong sha all terminate cleanly while showing a pre-write world. A `raced=0` derived from such a
|
||||
read is a green nobody verified, on the one path whose failure direction is toward SUCCESS.
|
||||
|
||||
So the check asserts a POSITIVE fact rather than an absence: our own row was written after the
|
||||
high-water mark was taken, so at least one row above that mark must exist. The stub mode here
|
||||
withholds exactly that one modelling detail and changes nothing else.
|
||||
"""
|
||||
posted, r = _run_classify(tmp_path, _emitting("docs/a.md"), history_mode="own-write-invisible")
|
||||
assert r.returncode == 0, r.stderr
|
||||
seq = _posted_sequence(tmp_path)
|
||||
assert len(seq) == 2, (
|
||||
"the post-write history could not even see this job's own write, yet the exemption was left "
|
||||
f"standing on the strength of it. Posts: {seq}\n{r.stdout[-900:]}"
|
||||
)
|
||||
assert seq[0]["state"] == "success" and seq[1]["state"] == "pending", (
|
||||
f"expected an exemption then a repair to pending; got {seq}"
|
||||
)
|
||||
# Reported as uncertainty, NOT as an overwritten verdict — nothing here says a human ruled.
|
||||
assert "did not reflect this job" in (r.stdout + r.stderr), (
|
||||
f"repaired, but not for the stated reason:\n{r.stdout[-900:]}"
|
||||
)
|
||||
assert "was overwritten" not in (r.stdout + r.stderr), (
|
||||
f"an uncertainty repair was reported as an overwritten human verdict:\n{r.stdout[-900:]}"
|
||||
)
|
||||
|
||||
|
||||
def test_a_raced_verdict_on_PAGE_2_is_detected_and_repaired(tmp_path):
|
||||
"""The hole #763 exists to close, and the half a paging change can still get wrong.
|
||||
|
||||
@@ -3670,6 +3710,172 @@ def test_a_raced_verdict_on_PAGE_2_is_detected_and_repaired(tmp_path):
|
||||
)
|
||||
|
||||
|
||||
def test_a_STRING_id_cannot_inflate_the_high_water_mark(tmp_path):
|
||||
"""jq sorts strings above every number, so one schema-corrupt id silently blinds the race check.
|
||||
|
||||
`max` over raw ids returns `"99999"` rather than the largest real id. `jq -r` then renders it as
|
||||
`99999`, which sails through the `*[!0-9]*` numeric gate, and the mark is set far above anything
|
||||
that exists. Every subsequent row — including a genuine human rejection racing the write — tests
|
||||
as OLDER than the mark and is invisible, so the exemption stands over it.
|
||||
|
||||
It fails toward SUCCESS and needs no attacker: one corrupt row is enough. So the mark is taken
|
||||
over numeric ids only, and a non-numeric id is excluded rather than coerced.
|
||||
"""
|
||||
posted, r = _run_classify(tmp_path, _emitting("docs/a.md"), history_mode="string-id-inflates-mark")
|
||||
assert r.returncode == 0, r.stderr
|
||||
seq = _posted_sequence(tmp_path)
|
||||
assert len(seq) == 2, (
|
||||
"a string id inflated the high-water mark, so the raced verdict tested as older than it and "
|
||||
f"the exemption stands over a human rejection. Posts: {seq}\n{r.stdout[-900:]}"
|
||||
)
|
||||
assert seq[0]["state"] == "success" and seq[1]["state"] == "pending", (
|
||||
f"expected an exemption then a repair to pending; got {seq}"
|
||||
)
|
||||
|
||||
|
||||
def test_a_TRANSIENT_page_failure_is_retried_and_the_walk_still_completes(tmp_path):
|
||||
"""The retry clause, which was unproven code until this fixture existed.
|
||||
|
||||
Every other error mode here fails on EVERY attempt, so a one-shot walk and a retrying walk behave
|
||||
identically against them — mutating `for try in 1 2` to `for try in 1` reddened nothing. That
|
||||
matters because the retry is the stated reason failing closed is affordable: without it, one
|
||||
momentary blip costs the head its exemption permanently, since the repair sentinel is sticky.
|
||||
|
||||
Here page 2 fails the first attempt of each logical read and succeeds on the retry. The history
|
||||
carries no verdict, so the correct outcome is a completed walk and a STANDING exemption.
|
||||
"""
|
||||
posted, r = _run_classify(tmp_path, _emitting("docs/a.md"), history_mode="flaky-page2")
|
||||
assert r.returncode == 0, r.stderr
|
||||
seq = _posted_sequence(tmp_path)
|
||||
assert len(seq) == 1, (
|
||||
"a transient page failure was not retried, so the walk gave up and the exemption was repaired "
|
||||
f"away on a head that nothing raced. Posts: {seq}\n{r.stdout[-900:]}"
|
||||
)
|
||||
assert seq[0]["state"] == "success", f"expected the exemption to stand; got {seq}"
|
||||
|
||||
|
||||
def test_a_PRE_WRITE_read_that_returns_NOTHING_abandons_the_mark_and_says_so(tmp_path):
|
||||
"""The boundary of the partial-list fallback, pinned so the remaining fail-open is explicit.
|
||||
|
||||
A partial list still yields a usable mark. A read that returns NO rows at all cannot: there is
|
||||
nothing to take a maximum over, so `max_id_before` stays -1 and the post-write race check is
|
||||
skipped entirely — the exemption is posted with nothing verifying it afterwards.
|
||||
|
||||
That is the pre-existing gap tracked as ersatztv#849, deliberately unchanged here, and it is
|
||||
asserted rather than left implicit so the degradation is visible in the log and a future change
|
||||
that widens or closes it has to come past this test.
|
||||
"""
|
||||
posted, r = _run_classify(tmp_path, _emitting("docs/a.md"), history_mode="premark-page1-error")
|
||||
assert r.returncode == 0, r.stderr
|
||||
log = r.stdout + r.stderr
|
||||
assert "Could not establish a status high-water mark" in log, (
|
||||
f"the mark was abandoned silently, with no record of the degradation:\n{r.stdout[-900:]}"
|
||||
)
|
||||
assert "taking the high-water mark over the" not in log, (
|
||||
f"a mark was salvaged from an empty read, which there is nothing to compute:\n{r.stdout[-900:]}"
|
||||
)
|
||||
seq = _posted_sequence(tmp_path)
|
||||
assert len(seq) == 1 and seq[0]["state"] == "success", (
|
||||
f"expected the exemption to be posted with the race check skipped; got {seq}"
|
||||
)
|
||||
|
||||
|
||||
def test_a_MALFORMED_creator_row_does_not_kill_the_job_after_the_green_is_posted(tmp_path):
|
||||
"""A row whose `creator` is not an object used to be fatal, at the worst possible moment.
|
||||
|
||||
`.creator != null and .creator.login` hard-errors in jq on any non-object creator; jq exits 5 and
|
||||
under `set -euo pipefail` the assignment takes the step down. That happens AFTER the exemption
|
||||
`success` has been posted and BEFORE the repair is attempted, so one schema-corrupt row leaves a
|
||||
green standing on a head that carries a genuine human rejection — and the job reports failure in a
|
||||
way that looks like an unrelated infrastructure error.
|
||||
|
||||
The fix type-tests `creator` before indexing it, so the malformed row is dropped from the count
|
||||
while the real verdict beside it is still counted and still repaired.
|
||||
"""
|
||||
posted, r = _run_classify(tmp_path, _emitting("docs/a.md"),
|
||||
history_mode="malformed-creator-beside-verdict")
|
||||
assert r.returncode == 0, (
|
||||
f"the step died instead of skipping a malformed row: {r.stderr[-1200:]}"
|
||||
)
|
||||
seq = _posted_sequence(tmp_path)
|
||||
assert len(seq) == 2, (
|
||||
"a malformed `creator` row suppressed the repair, leaving the exemption green over the real "
|
||||
f"verdict beside it. Posts: {seq}\n{r.stdout[-900:]}"
|
||||
)
|
||||
assert seq[0]["state"] == "success" and seq[1]["state"] == "pending", (
|
||||
f"expected an exemption then a repair to pending; got {seq}"
|
||||
)
|
||||
# THE REASON, not just the outcome — this is what makes the type guard individually provable.
|
||||
# Three clauses can each rescue this fixture (the type test, the `|| raced=""` guard, and the
|
||||
# fail-closed unusable-count branch), so the repair alone cannot tell them apart. Only the type
|
||||
# test lets the REAL verdict beside the malformed row be counted as a genuine human race; without
|
||||
# it the count comes back empty and the repair is reported as an unusable count instead.
|
||||
assert "was overwritten" in (r.stdout + r.stderr), (
|
||||
"the malformed row suppressed the real verdict beside it — repaired, but as an unverifiable "
|
||||
f"read rather than as the human rejection it is:\n{r.stdout[-900:]}"
|
||||
)
|
||||
|
||||
|
||||
def test_a_verdict_BEYOND_A_SHORT_PAGE_is_still_found(tmp_path):
|
||||
""""Terminate only on a validated EMPTY page, never on a short one" — the rule that is invisible
|
||||
against a faithful double.
|
||||
|
||||
On the real endpoint a short page IS the last page (measured: 50, 50, 14, `[]`), so an
|
||||
implementation that stops at the first short page is indistinguishable from a correct one, and
|
||||
every other test here would pass against it. The only way to observe the property is to model what
|
||||
it actually guards: a TRUNCATED response. This stub serves 50 rows, then a short page of 10, then
|
||||
a third page carrying the raced verdict.
|
||||
|
||||
A walk that treats the short page as exhaustion never reads page 3, misses the verdict, and leaves
|
||||
the exemption green over a human rejection.
|
||||
"""
|
||||
posted, r = _run_classify(tmp_path, _emitting("docs/a.md"), history_mode="verdict-after-short-page")
|
||||
assert r.returncode == 0, r.stderr
|
||||
seq = _posted_sequence(tmp_path)
|
||||
assert len(seq) == 2, (
|
||||
"a raced verdict beyond a SHORT page was not found, so the walk stopped on a short page "
|
||||
f"instead of on a validated empty one. Posts: {seq}\n{r.stdout[-900:]}"
|
||||
)
|
||||
assert seq[0]["state"] == "success" and seq[1]["state"] == "pending", (
|
||||
f"expected an exemption then a repair to pending; got {seq}"
|
||||
)
|
||||
assert "was overwritten" in (r.stdout + r.stderr), (
|
||||
f"repaired, but not reported as a found verdict:\n{r.stdout[-900:]}"
|
||||
)
|
||||
|
||||
|
||||
def test_a_PRE_WRITE_paging_failure_still_yields_a_usable_high_water_mark(tmp_path):
|
||||
"""The fail-open a paging change can introduce while fixing one (round-2 Blocker).
|
||||
|
||||
The high-water mark gates the post-write race check entirely: `max_id_before=-1` skips it, so a
|
||||
human rejection landing in the write window is neither detected nor repaired. Before paging, only
|
||||
a failure of the single page-1 request could reach that. Requiring a COMPLETE walk for the mark
|
||||
would newly route a page-2 hiccup, an over-cap history, or one malformed id on a later page into
|
||||
the same hole — a WIDER fail-open than the bug being fixed.
|
||||
|
||||
So a partial list still yields a mark. It can only be LOWER than the true maximum, which makes the
|
||||
check more eager, never blinder.
|
||||
|
||||
Here page 2 fails on the PRE-write read only; the post-write read pages cleanly and carries a raced
|
||||
verdict. With the mark salvaged from page 1 the verdict is above it and the exemption is repaired.
|
||||
With `max_id_before=-1` the check never runs and the rejection stays green.
|
||||
"""
|
||||
posted, r = _run_classify(tmp_path, _emitting("docs/a.md"), history_mode="premark-page2-error")
|
||||
assert r.returncode == 0, r.stderr
|
||||
log = r.stdout + r.stderr
|
||||
assert "taking the high-water mark over the" in log, (
|
||||
f"the partial-list fallback did not run, so the mark was not salvaged:\n{r.stdout[-900:]}"
|
||||
)
|
||||
seq = _posted_sequence(tmp_path)
|
||||
assert len(seq) == 2, (
|
||||
"the pre-write read could not be paged completely, the mark was abandoned, and the post-write "
|
||||
f"race check was skipped — leaving a raced rejection green. Posts: {seq}\n{r.stdout[-900:]}"
|
||||
)
|
||||
assert seq[0]["state"] == "success" and seq[1]["state"] == "pending", (
|
||||
f"expected an exemption then a repair to pending; got {seq}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"mode,why",
|
||||
[
|
||||
|
||||
Reference in New Issue
Block a user