From 88805d906dccd6c78c58d7125e284c21c134b708 Mon Sep 17 00:00:00 2001 From: Adam Moussa Date: Fri, 17 Jul 2026 17:03:44 -0400 Subject: [PATCH] Reject --all combined with --prefix/--since in reprocess.py --all is a distinct mode (the demoted full-prefix sweep), but the args.all branch unconditionally set prefix=inbound/ and since=None, so passing it alongside a narrower selector silently discarded that selector. `--all --since 2026-07-01` swept the entire corpus instead of the bounded window, triggering every documented --all hazard (Bedrock re-bill, metric double- count, merged-field regression) on objects the operator never targeted -- contradicting the tool's safety goal. Add the missing mutual-exclusion guard alongside the existing --key one, and pin --all+--prefix, --all+--since, and all three together as argparse rejections. --- scripts/reprocess.py | 7 +++++ tests/test_reprocess_contract.py | 51 ++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/scripts/reprocess.py b/scripts/reprocess.py index 22b783c..1037cd6 100644 --- a/scripts/reprocess.py +++ b/scripts/reprocess.py @@ -171,6 +171,13 @@ def main(): # --key is mutually exclusive with the listing selectors and with --all. if args.key and (args.prefix or args.since or args.all): parser.error("--key cannot be combined with --prefix, --since, or --all") + # --all is a distinct MODE (the demoted full-prefix sweep), not a modifier: + # reject it alongside the targeting selectors. Without this, the args.all + # clobber branch below would silently discard a narrower --prefix/--since + # (e.g. `--all --since 2026-07-01` would sweep the ENTIRE corpus, not the + # bounded window) and trigger every --all hazard on unintended objects. + if args.all and (args.prefix or args.since): + parser.error("--all cannot be combined with --prefix or --since") account_id = boto3.client("sts").get_caller_identity()["Account"] function_name = PIPELINES[args.pipeline]["function"] diff --git a/tests/test_reprocess_contract.py b/tests/test_reprocess_contract.py index ccc38d3..a899966 100644 --- a/tests/test_reprocess_contract.py +++ b/tests/test_reprocess_contract.py @@ -21,6 +21,9 @@ replay single-decode-correct if a future handler ever adds unquote_plus. import importlib.util import os import pathlib +import sys + +import pytest # reprocess.py builds its boto3 clients inside main()/functions, so importing # it is side-effect-free (no AWS calls, no clients at module scope). Set dummy @@ -71,3 +74,51 @@ def test_build_s3_event_shape_and_raw_key(): assert "+" in emitted # '%41' NOT decoded to 'A': assert "%41" in emitted and "A .eml" not in emitted + + +# --- Selector mutual-exclusion: --all is a MODE, not a modifier --------------- +# reprocess.main() reaches its first AWS call (sts get_caller_identity) only +# AFTER the argparse mutual-exclusion gate, so an invalid selector combination +# raises SystemExit(2) with no AWS access needed. This pins that `--all` cannot +# be silently combined with a narrower --prefix/--since (which the args.all +# clobber branch would otherwise discard -> unintended whole-corpus sweep). +def _expect_argparse_reject(monkeypatch, capsys, argv): + monkeypatch.setattr(sys, "argv", ["reprocess.py", *argv]) + with pytest.raises(SystemExit) as exc: + reprocess.main() + assert exc.value.code == 2 # argparse error exit code + return capsys.readouterr().err + + +def test_all_rejects_prefix(monkeypatch, capsys): + err = _expect_argparse_reject( + monkeypatch, + capsys, + ["--pipeline", "po", "--all", "--prefix", "inbound/2026/07/"], + ) + assert "--all cannot be combined with --prefix or --since" in err + + +def test_all_rejects_since(monkeypatch, capsys): + err = _expect_argparse_reject( + monkeypatch, + capsys, + ["--pipeline", "po", "--all", "--since", "2026-07-01T00:00:00Z"], + ) + assert "--all cannot be combined with --prefix or --since" in err + + +def test_all_rejects_prefix_and_since_together(monkeypatch, capsys): + _expect_argparse_reject( + monkeypatch, + capsys, + [ + "--pipeline", + "po", + "--all", + "--prefix", + "inbound/2026/", + "--since", + "2026-07-01T00:00:00Z", + ], + )