"""Closure regressions for production CodeQL alerts #1110 through #1129.""" import ast import itertools import random import re import subprocess import sys from pathlib import Path import pytest from src.agent_loop import ( _contextual_email_subject, _looks_like_agent_reasoning_preamble, _payload_fields, _pipeline_request_parts, ) from src.text_scanning import iter_prefixed_token_matches from src.tool_parsing import _iter_gemma_fallback_items, _parse_gemma_tool_call PAYLOADS = { "note_update": r'''\s+(.+?)\s+so\s+its\s+content\s+is\s+['"]([^'"]+)['"]''', "email_mutation": r"\s+(?:all|every|the)?\s*(?:my\s+)?(.+?)\s+(?:emails?|mail|messages?)\b", "checklist": r"\s+(.+?)\s+with\s+(.+?)$", "tag": r"\s+(?:the\s+)?(.+?)\s+tag\s+to\s+#?([a-z][a-z0-9_-]{1,30})\b", "change": r"\s+(.+?)\s+to\s+(.+?)(?=\s+(?:in|and|then|before)\b|[.;]|$)", "replace": r"\s+(.+?)\s+with\s+(.+?)(?=\s+(?:in|and|then|before)\b|[.;]|$)", } @pytest.mark.parametrize("grammar,pattern", PAYLOADS.items()) def test_payload_fields_preserve_whitespace_and_optional_leader_priority(grammar, pattern): legacy = re.compile(pattern, re.I) spaces = [" ", "\t", "\n", "\r", " ", "\t\n\t", "\n\n\n", "\u2003"] words = ["", "x", "all", "my", "the", "with", "to"] ends = [" emails", " checklist", " so its content is 'v'", " with y", " to y", " tag to ab"] cases = [a + b + a + c for a, b, c in itertools.product(spaces, words, ends)] rng = random.Random(1117) tokens = spaces + words + ends + ["in", "and", ".", ";", "'v'"] cases += ["".join(rng.choices(tokens, k=15)) for _ in range(5000)] for text in cases: match = legacy.match(text) assert _payload_fields(text, grammar) == (match.groups() if match else None), text def test_pipeline_fields_preserve_empty_looking_captures(): pipeline = re.compile( r"\bpipeline\s+using\s+([^\s,]+)\s+to\s+(.+?),\s*then\s+([^\s,]+)\s+to\s+(.+?)(?:[.!?]\s*)?$", re.I, ) for space, first, second in itertools.product( [" ", " ", "\t\n\t", "\n", "\r"], ["x", "", ",then n to", "!"], ["x", "", "!", "! ", "X\n"], ): text = "pipeline using m to" + space + first + ",then n to" + space + second match = pipeline.search(text) assert _pipeline_request_parts(text) == (match.groups() if match else None), text def test_gemma_fallback_preserves_permissive_values_and_multiline_failures(): legacy = re.compile(r'''(\w+)\s*:\s*["']?(.*?)["']?(?=\s*,\s*\w+\s*:|\s*\})''') cases = ["{query: hello, x: 'world'}", "{query:'a,b:c'}", "a:\n\t}", "0:\t\tX", "a:'', b: }", "a: multiline\nfails, b: ok}"] rng = random.Random(1128) tokens = ["a", "b", "0", ":", ",", "}", "'", '"', " ", "\t", "\n", "\u2003"] cases += ["".join(rng.choices(tokens, k=40)) for _ in range(15000)] for text in cases: expected = [(match.group(1), match.group(2).strip()) for match in legacy.finditer(text)] assert list(_iter_gemma_fallback_items(text)) == expected, text block = _parse_gemma_tool_call("web_search", "{query: hello world}") assert block.tool_type == "web_search" assert block.content == "hello world" def test_subject_capture_preserves_optional_formatting_and_whitespace(): legacy = re.compile(r'''\bsubject\s*(?:\*\*)?\s*:?\s*(?:"|\*")?(.+?)(?:"|\n|$)''', re.I) rng = random.Random(1123) tokens = ["subject", "subjectx", "**", ":", '*"', '"', "a", " ", "\t", "\n", "\r"] cases = ["Subject:", "Subject:\t", "Subject**:\n\n", "Subject \t\nTitle", "Subject\n\n", 'subject""'] cases += ["".join(rng.choices(tokens, k=16)) for _ in range(10000)] for text in cases: match = legacy.search(text) assert _contextual_email_subject(text) == (match.group(1) if match else None), text def _regex_literals(path): root = Path(__file__).resolve().parents[1] tree = ast.parse((root / path).read_text()) return [node.value for node in ast.walk(tree) if isinstance(node, ast.Constant) and isinstance(node.value, str)] def test_terminal_read_only_removal_preserves_comma_and_whitespace_behavior(): legacy = ( r"[.!?]\s*read[- ]only(?:\s+(?:please|pls|plz))?\s*,?\s*" r"(?:do\s+not|don['’]?t|dont)\s+(?:change|edit|modify)\s+" r"(?:or\s+send\s+)?anything[.!?]*\s*$" ) current = next(p for p in _regex_literals("src/turn_contract.py") if p.startswith(r"[.!?]\s*+read[- ]only")) for space, optional, comma, ending in itertools.product( ["", " ", "\t", "\n\t"], ["", " please", " pls"], ["", ","], ["", ".", "!?", "X"], ): text = "open calendar!read only" + optional + space + comma + space + "do not edit anything" + ending assert re.sub(current, "", text, flags=re.I) == re.sub(legacy, "", text, flags=re.I), text def test_bounded_session_listing_and_reasoning_tail_preserve_regex_behavior(): literals = _regex_literals("src/agent_loop.py") listing = next(p for p in literals if p.startswith(r"\b(?:list|show|view)\b.{0,30}") and "my" in p) old_listing = listing.replace(r"\s++", r"\s+").replace(r"\s*+", r"\s*") preamble = next(p for p in literals if p.startswith(r"(?:but\s+)?(?:now\s+)?") and "[^.!?]*+" in p) old_preamble = preamble.replace("[^.!?]*+", "[^.!?]*") rng = random.Random(1119) tokens = ["list", "my", "recent", "chat", "now let me verify", " ", "\t", "\n", ".", "!", "x"] cases = ["".join(rng.choices(tokens, k=15)) for _ in range(3000)] for text in cases: assert bool(re.search(listing, text, re.I)) == bool(re.search(old_listing, text, re.I)), text assert bool(re.match(preamble, text)) == bool(re.match(old_preamble, text)), text assert not _looks_like_agent_reasoning_preamble("Answer. now let me verify\t\t!!") def test_calendar_digit_guard_prevents_suffix_retries_without_changing_units(): patterns = [p for p in _regex_literals("src/tools/calendar.py") if p.startswith(r"(?