899 lines
36 KiB
Python
899 lines
36 KiB
Python
"""Unit tests for the pure finalize-time red-evidence gate module.
|
|
|
|
The tracker/classifiers here are shared verbatim between the live agent loop
|
|
and offline transcript replay (``scripts/experiments/replay_finalize_gate.py``),
|
|
so these tests pin the exact detector semantics that both callers rely on.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from opensquilla.engine.finalize_evidence_gate import (
|
|
FINALIZE_EVIDENCE_GATE_CHALLENGE_LIMIT,
|
|
FinalizeEvidenceTracker,
|
|
classify_gate_command,
|
|
command_execution_profiles,
|
|
command_removal_targets,
|
|
execution_signals_from_result,
|
|
finalize_evidence_challenge_message,
|
|
finalize_evidence_gate_key,
|
|
green_profiles_deselect_red,
|
|
has_stash_reversal,
|
|
is_detector_findings_exit,
|
|
looks_repro_artifact_path,
|
|
scan_stash_effects,
|
|
)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Path / command classifiers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("path", "expected"),
|
|
[
|
|
("repro.py", True),
|
|
("reproduce_issue.rb", True),
|
|
("debug_overlap.java", True),
|
|
("verify_fix.sh", True),
|
|
("/tmp/squilla-scratch/test_fix.py", True),
|
|
("/tmp/anything.js", True),
|
|
("/tmp/notes.txt", False),
|
|
("src/main.py", False),
|
|
("tests/test_parser.py", False),
|
|
("", False),
|
|
],
|
|
)
|
|
def test_looks_repro_artifact_path(path: str, expected: bool) -> None:
|
|
assert looks_repro_artifact_path(path) is expected
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("path", "expected"),
|
|
[
|
|
# Marker must be an exact stem token: real source files whose names
|
|
# merely contain a marker substring are source edits.
|
|
("preprocessing.py", False),
|
|
("inspectdb.py", False),
|
|
("DebugOverlaps.java", False),
|
|
# Nested repository paths are always source edits, whatever the name.
|
|
("app/views/debug.py", False),
|
|
("lib/debug.py", False),
|
|
("src/utils/reproduce_helper.py", False),
|
|
("/testbed/app/reproduce.py", False),
|
|
# Shallow paths with an exact marker token still qualify.
|
|
("./repro.py", True),
|
|
("debug-issue.py", True),
|
|
("/testbed/reproduce.py", True),
|
|
],
|
|
)
|
|
def test_looks_repro_artifact_path_requires_anchored_shallow_marker(
|
|
path: str, expected: bool
|
|
) -> None:
|
|
assert looks_repro_artifact_path(path) is expected
|
|
|
|
|
|
def test_nested_marker_named_write_counts_as_source_edit() -> None:
|
|
# A fix landing in app/views/debug.py must arm the gate, not register
|
|
# a scratch artifact (which would silently disarm it for the whole run).
|
|
tracker = FinalizeEvidenceTracker()
|
|
tracker.observe_write("app/views/debug.py", iteration=1)
|
|
observation = tracker.build_observation(has_workspace_diff=True)
|
|
assert observation.source_edit_seen is True
|
|
assert observation.triggers == ["no_execution_after_final_edit"]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("command", "expected"),
|
|
[
|
|
("cat foo.py | grep bar", "inspection"),
|
|
("git status && git diff", "inspection"),
|
|
("cd /testbed && ls -la", "inspection"),
|
|
("python repro.py", "execution"),
|
|
("cd /testbed && pytest -x tests/", "execution"),
|
|
("timeout 60 cargo test 2>&1 | tail -30", "execution"),
|
|
("sudo npm test", "execution"),
|
|
("bundle exec rspec spec/foo_spec.rb", "execution"),
|
|
# Unknown heads err toward execution (suppresses the gate).
|
|
("./gradlew test", "execution"),
|
|
],
|
|
)
|
|
def test_classify_gate_command(command: str, expected: str) -> None:
|
|
assert classify_gate_command(command) == expected
|
|
|
|
|
|
def test_heredoc_body_is_not_segment_split() -> None:
|
|
command = (
|
|
"cat > /tmp/squilla-scratch/repro.py << 'EOF'\n"
|
|
"import subprocess; subprocess.run(['pytest'])\n"
|
|
"EOF"
|
|
)
|
|
# The heredoc body contains executable-looking text, but the command as a
|
|
# whole is a file write via cat: inspection-only.
|
|
assert classify_gate_command(command) == "inspection"
|
|
|
|
|
|
def test_command_after_heredoc_terminator_is_classified() -> None:
|
|
command = (
|
|
"cat > notes.md << EOF\n"
|
|
"run pytest tests/ later\n"
|
|
"EOF\n"
|
|
"python repro.py"
|
|
)
|
|
assert classify_gate_command(command) == "execution"
|
|
|
|
|
|
def test_quoted_heredoc_operator_does_not_swallow_following_lines() -> None:
|
|
# ``<< EOF`` inside a quoted string is not a heredoc opener; the pytest
|
|
# line after it must still classify as execution.
|
|
command = 'echo "see << EOF marker"\npytest tests/'
|
|
assert classify_gate_command(command) == "execution"
|
|
|
|
|
|
def test_newline_separates_segments_like_semicolon() -> None:
|
|
assert classify_gate_command("git status\npytest tests/") == "execution"
|
|
assert command_removal_targets("git stash\nrm /tmp/repro.py") == ["/tmp/repro.py"]
|
|
|
|
|
|
def test_quoted_newline_stays_inside_one_segment() -> None:
|
|
profiles = command_execution_profiles("python -c 'import x\nprint(1)'")
|
|
assert len(profiles) == 1
|
|
|
|
|
|
def test_escaped_separator_stays_inside_segment() -> None:
|
|
# A backslash-escaped ``;`` (as in ``find ... -exec ... \;``) must not
|
|
# start a new segment: here ``pytest`` stays an argument of echo.
|
|
assert classify_gate_command("echo skip \\; pytest tests/") == "inspection"
|
|
|
|
|
|
def test_bit_shift_is_not_mistaken_for_heredoc() -> None:
|
|
assert classify_gate_command('python -c "print(1 << 2)"') == "execution"
|
|
|
|
|
|
def test_command_removal_targets() -> None:
|
|
assert command_removal_targets("rm -f /tmp/repro.py") == ["/tmp/repro.py"]
|
|
assert command_removal_targets("cd /testbed && rm -rf a.py b.py") == ["a.py", "b.py"]
|
|
assert command_removal_targets("pytest tests/") == []
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("command", "expected"),
|
|
[
|
|
# ``env`` and ``exec`` are wrappers: the wrapped program decides.
|
|
("env FOO=1 pytest tests/", "execution"),
|
|
("env -i PATH=/usr/bin python repro.py", "execution"),
|
|
("exec pytest tests/", "execution"),
|
|
# Bare ``env`` prints the environment; nothing runs.
|
|
("env", "inspection"),
|
|
# ``command -v prog`` probes for existence; it runs nothing.
|
|
("command -v pytest", "inspection"),
|
|
("command pytest tests/", "execution"),
|
|
],
|
|
)
|
|
def test_wrapper_head_classification(command: str, expected: str) -> None:
|
|
assert classify_gate_command(command) == expected
|
|
|
|
|
|
def test_has_stash_reversal() -> None:
|
|
assert has_stash_reversal("git stash && python repro.py") is True
|
|
assert has_stash_reversal("git stash pop && pytest tests/") is False
|
|
assert has_stash_reversal("git stash apply && pytest tests/") is False
|
|
assert has_stash_reversal("pytest tests/") is False
|
|
|
|
|
|
def test_has_stash_reversal_parses_git_global_flags() -> None:
|
|
assert has_stash_reversal("git -C /repo stash && pytest tests/") is True
|
|
# ``-c key=val`` takes a value token; ``stash`` is still the subcommand.
|
|
assert has_stash_reversal("git -c core.pager=cat stash && pytest tests/") is True
|
|
|
|
|
|
def test_stash_inspection_subcommands_carry_no_event() -> None:
|
|
assert has_stash_reversal("git stash list && pytest tests/") is False
|
|
assert has_stash_reversal("git stash show -p && pytest tests/") is False
|
|
|
|
|
|
def test_scan_stash_effects_threads_state_across_calls() -> None:
|
|
assert scan_stash_effects("git stash", initially_stashed=False) == (False, True)
|
|
assert scan_stash_effects("pytest tests/", initially_stashed=True) == (True, True)
|
|
assert scan_stash_effects("git stash pop", initially_stashed=True) == (False, False)
|
|
assert scan_stash_effects("git stash pop && pytest tests/", initially_stashed=True) == (
|
|
False,
|
|
False,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Detector findings demotion
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("command", "exit_code", "timed_out", "expected"),
|
|
[
|
|
("rubocop app.rb", 1, False, True),
|
|
("cd /testbed && ruff check src/", 1, False, True),
|
|
# Interpreter-launched and module-launched detectors resolve to the
|
|
# real program.
|
|
("php php-cs-fixer fix --dry-run src.php", 1, False, True),
|
|
("python -m mypy src/", 1, False, True),
|
|
# Exit >= 2 is a real tool error: stays red.
|
|
("rubocop app.rb", 2, False, False),
|
|
# Mixed with a non-detector execution segment: stays red.
|
|
("ruff check . && pytest tests/", 1, False, False),
|
|
("pytest tests/", 1, False, False),
|
|
("rubocop app.rb", 1, True, False),
|
|
("rubocop app.rb", None, False, False),
|
|
],
|
|
)
|
|
def test_is_detector_findings_exit(
|
|
command: str, exit_code: int | None, timed_out: bool, expected: bool
|
|
) -> None:
|
|
assert is_detector_findings_exit(command, exit_code, timed_out) is expected
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Execution signals
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_execution_signals_prefer_sidecar_masked_pipeline_failure() -> None:
|
|
red, exit_code, timed_out, reason = execution_signals_from_result(
|
|
tool_name="exec_command",
|
|
content_text="exit_code=0\nerror[E0308]: mismatched types",
|
|
execution_status={
|
|
"version": 1,
|
|
"status": "error",
|
|
"exit_code": 0,
|
|
"timed_out": False,
|
|
"reason": "masked_pipeline_failure",
|
|
},
|
|
is_error=True,
|
|
)
|
|
assert red is True
|
|
assert exit_code == 0
|
|
assert timed_out is False
|
|
assert reason == "masked_pipeline_failure"
|
|
|
|
|
|
def test_execution_signals_sidecar_success() -> None:
|
|
red, exit_code, timed_out, reason = execution_signals_from_result(
|
|
tool_name="exec_command",
|
|
content_text="exit_code=0\nall good",
|
|
execution_status={"version": 1, "status": "success", "exit_code": 0},
|
|
is_error=False,
|
|
)
|
|
assert red is False
|
|
assert exit_code == 0
|
|
assert reason is None
|
|
|
|
|
|
def test_execution_signals_rederived_from_text_without_sidecar() -> None:
|
|
red, exit_code, timed_out, reason = execution_signals_from_result(
|
|
tool_name="exec_command",
|
|
content_text="exit_code=2\nFAILED tests/test_x.py::test_y",
|
|
execution_status=None,
|
|
is_error=False,
|
|
)
|
|
assert red is True
|
|
assert exit_code == 2
|
|
assert reason == "nonzero_exit"
|
|
|
|
|
|
def test_execution_signals_fall_back_to_is_error() -> None:
|
|
red, exit_code, timed_out, reason = execution_signals_from_result(
|
|
tool_name="background_process",
|
|
content_text="opaque output",
|
|
execution_status=None,
|
|
is_error=True,
|
|
)
|
|
assert red is True
|
|
assert exit_code is None
|
|
assert timed_out is False
|
|
assert reason is None
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Deselection profiles
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_green_deselects_red_via_k_not() -> None:
|
|
red = command_execution_profiles("cd /testbed && pytest tests/test_a.py")
|
|
green = command_execution_profiles(
|
|
"cd /testbed && pytest tests/test_a.py -k 'not test_bad'"
|
|
)
|
|
assert green_profiles_deselect_red(green, red) is True
|
|
|
|
|
|
def test_green_deselects_red_via_deselect_flag() -> None:
|
|
red = command_execution_profiles("pytest tests/test_a.py")
|
|
green = command_execution_profiles(
|
|
"pytest tests/test_a.py --deselect tests/test_a.py::test_bad"
|
|
)
|
|
assert green_profiles_deselect_red(green, red) is True
|
|
|
|
|
|
def test_green_rerun_without_narrowing_does_not_deselect() -> None:
|
|
red = command_execution_profiles("pytest tests/test_a.py")
|
|
green = command_execution_profiles("pytest tests/test_a.py -v")
|
|
assert green_profiles_deselect_red(green, red) is False
|
|
|
|
|
|
def test_green_positive_selection_is_not_deselection() -> None:
|
|
red = command_execution_profiles("pytest tests/test_a.py")
|
|
green = command_execution_profiles("pytest tests/test_a.py -k 'test_good'")
|
|
assert green_profiles_deselect_red(green, red) is False
|
|
|
|
|
|
def test_green_different_program_does_not_deselect() -> None:
|
|
red = command_execution_profiles("pytest tests/test_a.py")
|
|
green = command_execution_profiles("tox -e py311 -k 'not test_bad'")
|
|
assert green_profiles_deselect_red(green, red) is False
|
|
|
|
|
|
def test_interpreter_module_launch_profiles_as_real_program() -> None:
|
|
(profile,) = command_execution_profiles("python -m pytest tests/test_a.py")
|
|
assert profile.head == "pytest"
|
|
# The module token names the program, not a test target.
|
|
assert profile.positionals == frozenset({"tests/test_a.py"})
|
|
|
|
|
|
def test_deselection_matches_across_module_and_direct_launch() -> None:
|
|
red = command_execution_profiles("python -m pytest tests/test_a.py")
|
|
green = command_execution_profiles("pytest tests/test_a.py -k 'not test_bad'")
|
|
assert green_profiles_deselect_red(green, red) is True
|
|
|
|
red = command_execution_profiles("pytest tests/test_a.py")
|
|
green = command_execution_profiles("python -m pytest tests/test_a.py -k 'not test_bad'")
|
|
assert green_profiles_deselect_red(green, red) is True
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Tracker triggers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _tracker_with_source_edit() -> FinalizeEvidenceTracker:
|
|
tracker = FinalizeEvidenceTracker()
|
|
tracker.observe_write("src/main.py", iteration=1)
|
|
return tracker
|
|
|
|
|
|
def test_trigger_red_execution_after_final_edit() -> None:
|
|
tracker = _tracker_with_source_edit()
|
|
tracker.observe_execution(
|
|
"cd /testbed && pytest tests/test_a.py",
|
|
red=True,
|
|
exit_code=1,
|
|
failure_anchors=["FAILED tests/test_a.py::test_x"],
|
|
iteration=2,
|
|
)
|
|
observation = tracker.build_observation(has_workspace_diff=True)
|
|
assert observation.triggers[0] == "red_execution_after_final_edit"
|
|
assert observation.should_challenge is True
|
|
assert observation.red_command == "cd /testbed && pytest tests/test_a.py"
|
|
assert observation.red_exit_code == 1
|
|
assert observation.red_failure_anchors == ["FAILED tests/test_a.py::test_x"]
|
|
|
|
|
|
def test_no_triggers_without_workspace_diff() -> None:
|
|
tracker = _tracker_with_source_edit()
|
|
tracker.observe_execution("pytest tests/", red=True, exit_code=1, iteration=2)
|
|
observation = tracker.build_observation(has_workspace_diff=False)
|
|
assert observation.triggers == []
|
|
assert observation.should_challenge is False
|
|
assert observation.primary_reason == "finalize_evidence_ok"
|
|
|
|
|
|
def test_no_triggers_without_source_edit() -> None:
|
|
tracker = FinalizeEvidenceTracker()
|
|
tracker.observe_execution("pytest tests/", red=True, exit_code=1, iteration=1)
|
|
observation = tracker.build_observation(has_workspace_diff=True)
|
|
assert observation.triggers == []
|
|
|
|
|
|
def test_trigger_red_repro_outstanding_after_final_edit() -> None:
|
|
tracker = _tracker_with_source_edit()
|
|
tracker.observe_write("/tmp/squilla-scratch/repro.py", iteration=2)
|
|
tracker.observe_execution(
|
|
"python /tmp/squilla-scratch/repro.py", red=True, exit_code=1, iteration=3
|
|
)
|
|
tracker.observe_execution("pytest tests/test_other.py", red=False, exit_code=0, iteration=4)
|
|
observation = tracker.build_observation(has_workspace_diff=True)
|
|
assert observation.triggers == ["red_repro_outstanding_after_final_edit"]
|
|
assert observation.red_artifact_paths == ["/tmp/squilla-scratch/repro.py"]
|
|
|
|
|
|
def test_repro_going_green_clears_outstanding() -> None:
|
|
tracker = _tracker_with_source_edit()
|
|
tracker.observe_write("/tmp/squilla-scratch/repro.py", iteration=2)
|
|
tracker.observe_execution(
|
|
"python /tmp/squilla-scratch/repro.py", red=True, exit_code=1, iteration=3
|
|
)
|
|
tracker.observe_execution(
|
|
"python /tmp/squilla-scratch/repro.py", red=False, exit_code=0, iteration=4
|
|
)
|
|
observation = tracker.build_observation(has_workspace_diff=True)
|
|
assert observation.triggers == []
|
|
|
|
|
|
def test_source_edit_resets_post_edit_window() -> None:
|
|
tracker = _tracker_with_source_edit()
|
|
tracker.observe_execution("pytest tests/", red=True, exit_code=1, iteration=2)
|
|
tracker.observe_write("src/main.py", iteration=3)
|
|
observation = tracker.build_observation(has_workspace_diff=True)
|
|
assert observation.triggers == ["no_execution_after_final_edit"]
|
|
assert observation.post_edit_execution_count == 0
|
|
|
|
|
|
def test_scratch_write_does_not_reset_window() -> None:
|
|
tracker = _tracker_with_source_edit()
|
|
tracker.observe_execution("pytest tests/", red=False, exit_code=0, iteration=2)
|
|
tracker.observe_write("/tmp/squilla-scratch/inspect_state.py", iteration=3, scratch=True)
|
|
observation = tracker.build_observation(has_workspace_diff=True)
|
|
assert observation.triggers == []
|
|
assert observation.post_edit_execution_count == 1
|
|
|
|
|
|
def test_trigger_red_evidence_deselected_after_final_edit() -> None:
|
|
tracker = _tracker_with_source_edit()
|
|
tracker.observe_execution("pytest tests/test_a.py", red=True, exit_code=1, iteration=2)
|
|
tracker.observe_execution(
|
|
"pytest tests/test_a.py -k 'not test_bad'", red=False, exit_code=0, iteration=3
|
|
)
|
|
observation = tracker.build_observation(has_workspace_diff=True)
|
|
assert observation.triggers == ["red_evidence_deselected_after_final_edit"]
|
|
assert observation.red_command == "pytest tests/test_a.py"
|
|
|
|
|
|
def test_command_form_error_red_cannot_be_deselected() -> None:
|
|
tracker = _tracker_with_source_edit()
|
|
tracker.observe_execution("pytest tests/test_a.py", red=True, exit_code=127, iteration=2)
|
|
tracker.observe_execution(
|
|
"pytest tests/test_a.py -k 'not test_bad'", red=False, exit_code=0, iteration=3
|
|
)
|
|
observation = tracker.build_observation(has_workspace_diff=True)
|
|
assert observation.triggers == []
|
|
|
|
|
|
def test_trigger_red_majority_after_final_edit() -> None:
|
|
tracker = _tracker_with_source_edit()
|
|
for index in range(5):
|
|
tracker.observe_execution(
|
|
f"pytest tests/test_{index}.py", red=True, exit_code=1, iteration=2 + index
|
|
)
|
|
for index in range(4):
|
|
tracker.observe_execution(
|
|
f"python check_{index}.py", red=False, exit_code=0, iteration=7 + index
|
|
)
|
|
observation = tracker.build_observation(has_workspace_diff=True)
|
|
assert observation.triggers == ["red_majority_after_final_edit"]
|
|
assert observation.post_edit_red_count == 5
|
|
assert observation.post_edit_execution_count == 9
|
|
|
|
|
|
def test_red_minority_does_not_trigger_majority() -> None:
|
|
tracker = _tracker_with_source_edit()
|
|
for index in range(4):
|
|
tracker.observe_execution(
|
|
f"pytest tests/test_{index}.py", red=True, exit_code=1, iteration=2 + index
|
|
)
|
|
for index in range(5):
|
|
tracker.observe_execution(
|
|
f"python check_{index}.py", red=False, exit_code=0, iteration=6 + index
|
|
)
|
|
observation = tracker.build_observation(has_workspace_diff=True)
|
|
assert observation.triggers == []
|
|
|
|
|
|
def test_trigger_never_green_repro_deleted() -> None:
|
|
tracker = _tracker_with_source_edit()
|
|
tracker.observe_write("/tmp/squilla-scratch/repro.py", iteration=2)
|
|
tracker.observe_execution(
|
|
"python /tmp/squilla-scratch/repro.py", red=True, exit_code=1, iteration=3
|
|
)
|
|
tracker.observe_execution("rm /tmp/squilla-scratch/repro.py", red=False, iteration=4)
|
|
tracker.observe_execution("pytest tests/test_other.py", red=False, exit_code=0, iteration=5)
|
|
observation = tracker.build_observation(has_workspace_diff=True)
|
|
assert observation.triggers == ["never_green_repro_deleted"]
|
|
assert observation.deleted_never_green_repro_paths == ["/tmp/squilla-scratch/repro.py"]
|
|
|
|
|
|
def test_ever_green_repro_deleted_is_fine() -> None:
|
|
tracker = _tracker_with_source_edit()
|
|
tracker.observe_write("/tmp/squilla-scratch/repro.py", iteration=2)
|
|
tracker.observe_execution(
|
|
"python /tmp/squilla-scratch/repro.py", red=True, exit_code=1, iteration=3
|
|
)
|
|
tracker.observe_execution(
|
|
"python /tmp/squilla-scratch/repro.py", red=False, exit_code=0, iteration=4
|
|
)
|
|
tracker.observe_execution("rm /tmp/squilla-scratch/repro.py", red=False, iteration=5)
|
|
observation = tracker.build_observation(has_workspace_diff=True)
|
|
assert observation.triggers == []
|
|
|
|
|
|
def test_trigger_no_execution_after_final_edit() -> None:
|
|
tracker = _tracker_with_source_edit()
|
|
observation = tracker.build_observation(has_workspace_diff=True)
|
|
assert observation.triggers == ["no_execution_after_final_edit"]
|
|
|
|
|
|
def test_stash_reversal_red_is_not_recorded() -> None:
|
|
tracker = _tracker_with_source_edit()
|
|
tracker.observe_execution(
|
|
"git stash && python repro.py", red=True, exit_code=1, iteration=2
|
|
)
|
|
observation = tracker.build_observation(has_workspace_diff=True)
|
|
# The expected-red stash run neither fires a red trigger nor counts as
|
|
# post-edit verification.
|
|
assert observation.triggers == ["no_execution_after_final_edit"]
|
|
assert observation.post_edit_execution_count == 0
|
|
|
|
|
|
def test_stash_state_carries_across_separate_calls() -> None:
|
|
# ``git stash`` in one call and ``pytest`` in the next behave exactly
|
|
# like ``git stash && pytest`` in one call: the red run happens on the
|
|
# reverted tree and is expected, so it is not failure evidence.
|
|
tracker = _tracker_with_source_edit()
|
|
tracker.observe_execution("git stash", red=False, exit_code=0, iteration=2)
|
|
tracker.observe_execution("pytest tests/test_a.py", red=True, exit_code=1, iteration=3)
|
|
tracker.observe_execution("git stash pop", red=False, exit_code=0, iteration=4)
|
|
observation = tracker.build_observation(has_workspace_diff=True)
|
|
assert observation.triggers == ["no_execution_after_final_edit"]
|
|
assert observation.post_edit_execution_count == 0
|
|
|
|
# After the fix is restored, a red run counts again.
|
|
tracker.observe_execution("pytest tests/test_a.py", red=True, exit_code=1, iteration=5)
|
|
observation = tracker.build_observation(has_workspace_diff=True)
|
|
assert observation.triggers[0] == "red_execution_after_final_edit"
|
|
|
|
|
|
def test_green_run_alongside_rm_does_not_credit_deleted_artifact() -> None:
|
|
# ``rm repro.py && pytest tests/`` must not attribute the green pytest
|
|
# result to the deleted script: the never-green deletion still fires.
|
|
tracker = _tracker_with_source_edit()
|
|
tracker.observe_write("/tmp/squilla-scratch/repro.py", iteration=2)
|
|
tracker.observe_execution(
|
|
"python /tmp/squilla-scratch/repro.py", red=True, exit_code=1, iteration=3
|
|
)
|
|
tracker.observe_execution(
|
|
"rm /tmp/squilla-scratch/repro.py && pytest tests/",
|
|
red=False,
|
|
exit_code=0,
|
|
iteration=4,
|
|
)
|
|
observation = tracker.build_observation(has_workspace_diff=True)
|
|
assert observation.triggers == ["never_green_repro_deleted"]
|
|
assert observation.deleted_never_green_repro_paths == ["/tmp/squilla-scratch/repro.py"]
|
|
|
|
|
|
def test_detector_findings_exit_is_demoted_in_tracker() -> None:
|
|
tracker = _tracker_with_source_edit()
|
|
tracker.observe_execution("rubocop app.rb", red=True, exit_code=1, iteration=2)
|
|
observation = tracker.build_observation(has_workspace_diff=True)
|
|
assert observation.triggers == []
|
|
assert observation.post_edit_execution_count == 1
|
|
assert observation.post_edit_red_count == 0
|
|
|
|
|
|
def test_inspection_command_is_not_recorded() -> None:
|
|
tracker = _tracker_with_source_edit()
|
|
tracker.observe_execution("git diff && git status", red=True, exit_code=1, iteration=2)
|
|
observation = tracker.build_observation(has_workspace_diff=True)
|
|
assert observation.triggers == ["no_execution_after_final_edit"]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Non-verification executions (denied / harness errors / form errors)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("status_reason", "exit_code"),
|
|
[
|
|
# Sandbox / policy blocks and approval denials: nothing ran.
|
|
("denied", None),
|
|
("approval_denied", None),
|
|
# Harness-internal tool errors and cancellations: no signal.
|
|
("runtime_error", None),
|
|
("cancelled", None),
|
|
# Harness-level blocks minted with "The tool was not run".
|
|
("provider_context_projection_reused", None),
|
|
("projected_diagnostic_requires_retrieval", None),
|
|
("tool_failure_loop_exhausted", None),
|
|
("tool_run_budget_exhausted", None),
|
|
("invalid_tool_arguments", None),
|
|
# Launched or awaiting approval: no outcome in either polarity.
|
|
("background_running", None),
|
|
("approval_pending", None),
|
|
# Command not found / not executable: says nothing about the patch.
|
|
("nonzero_exit", 127),
|
|
("nonzero_exit", 126),
|
|
],
|
|
)
|
|
def test_non_verification_red_is_not_recorded(
|
|
status_reason: str, exit_code: int | None
|
|
) -> None:
|
|
tracker = _tracker_with_source_edit()
|
|
tracker.observe_execution(
|
|
"pytest tests/test_a.py",
|
|
red=True,
|
|
exit_code=exit_code,
|
|
status_reason=status_reason,
|
|
iteration=2,
|
|
)
|
|
observation = tracker.build_observation(has_workspace_diff=True)
|
|
# Like the stash skip: neither red evidence nor post-edit verification.
|
|
assert observation.triggers == ["no_execution_after_final_edit"]
|
|
assert observation.post_edit_execution_count == 0
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("status_reason", "exit_code", "timed_out"),
|
|
[
|
|
# Genuine test/build failures must stay red.
|
|
("nonzero_exit", 1, False),
|
|
("nonzero_exit", 2, False),
|
|
# Adapter-detected masked pipeline failures are legitimate red.
|
|
("masked_pipeline_failure", 0, False),
|
|
# A timeout can be a patch-induced hang: stays red.
|
|
("tool_timeout", None, True),
|
|
("killed", None, False),
|
|
],
|
|
)
|
|
def test_genuine_verification_failures_stay_red(
|
|
status_reason: str, exit_code: int | None, timed_out: bool
|
|
) -> None:
|
|
tracker = _tracker_with_source_edit()
|
|
tracker.observe_execution(
|
|
"pytest tests/test_a.py",
|
|
red=True,
|
|
exit_code=exit_code,
|
|
timed_out=timed_out,
|
|
status_reason=status_reason,
|
|
iteration=2,
|
|
)
|
|
observation = tracker.build_observation(has_workspace_diff=True)
|
|
assert observation.triggers[0] == "red_execution_after_final_edit"
|
|
assert observation.post_edit_red_count == 1
|
|
|
|
|
|
def test_denied_rm_does_not_mark_artifact_deleted() -> None:
|
|
# A policy-denied ``rm`` deleted nothing: the repro still exists and must
|
|
# stay an outstanding red, not become a never-green deletion.
|
|
tracker = _tracker_with_source_edit()
|
|
tracker.observe_write("/tmp/squilla-scratch/repro.py", iteration=2)
|
|
tracker.observe_execution(
|
|
"python /tmp/squilla-scratch/repro.py", red=True, exit_code=1, iteration=3
|
|
)
|
|
tracker.observe_execution(
|
|
"rm /tmp/squilla-scratch/repro.py",
|
|
red=True,
|
|
status_reason="denied",
|
|
iteration=4,
|
|
)
|
|
tracker.observe_execution("pytest tests/test_other.py", red=False, exit_code=0, iteration=5)
|
|
observation = tracker.build_observation(has_workspace_diff=True)
|
|
assert observation.triggers == ["red_repro_outstanding_after_final_edit"]
|
|
assert observation.deleted_never_green_repro_paths == []
|
|
|
|
|
|
def test_rm_before_form_error_exit_still_marks_artifact_deleted() -> None:
|
|
# Two-tier skip placement: exit 126/127 skips the execution *record*, but
|
|
# side effects from earlier segments of the same call really happened —
|
|
# in ``rm repro.py && ./missing.sh`` the rm ran before the 127.
|
|
tracker = _tracker_with_source_edit()
|
|
tracker.observe_write("/tmp/squilla-scratch/repro.py", iteration=2)
|
|
tracker.observe_execution(
|
|
"python /tmp/squilla-scratch/repro.py", red=True, exit_code=1, iteration=3
|
|
)
|
|
tracker.observe_execution(
|
|
"rm /tmp/squilla-scratch/repro.py && ./missing.sh",
|
|
red=True,
|
|
exit_code=127,
|
|
iteration=4,
|
|
)
|
|
observation = tracker.build_observation(has_workspace_diff=True)
|
|
assert "never_green_repro_deleted" in observation.triggers
|
|
assert observation.deleted_never_green_repro_paths == [
|
|
"/tmp/squilla-scratch/repro.py"
|
|
]
|
|
|
|
|
|
def test_denied_git_stash_does_not_flip_stash_state() -> None:
|
|
# A policy-denied ``git stash`` stashed nothing: the fix is still in the
|
|
# tree, so a following red run is genuine evidence about the workspace
|
|
# and must not be skipped as a stash-reversal bug check.
|
|
tracker = _tracker_with_source_edit()
|
|
tracker.observe_execution("git stash", red=True, status_reason="denied", iteration=2)
|
|
tracker.observe_execution("pytest tests/test_a.py", red=True, exit_code=1, iteration=3)
|
|
observation = tracker.build_observation(has_workspace_diff=True)
|
|
assert observation.triggers[0] == "red_execution_after_final_edit"
|
|
assert observation.post_edit_red_count == 1
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Scratch-note writes (non-source, non-artifact)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"path",
|
|
[
|
|
"/tmp/squilla-scratch/FIX_SUMMARY.md",
|
|
"/tmp/notes.txt",
|
|
"/var/tmp/OBSERVATIONS.md",
|
|
],
|
|
)
|
|
def test_nonsource_scratch_note_write_does_not_reset_window(path: str) -> None:
|
|
# coreutils-6682 live defect: a plain write of FIX_SUMMARY.md under the
|
|
# scratch dir was treated as the final source edit, producing a false
|
|
# no_execution_after_final_edit challenge.
|
|
tracker = _tracker_with_source_edit()
|
|
tracker.observe_execution("pytest tests/", red=False, exit_code=0, iteration=2)
|
|
tracker.observe_write(path, iteration=3)
|
|
observation = tracker.build_observation(has_workspace_diff=True)
|
|
assert observation.triggers == []
|
|
assert observation.post_edit_execution_count == 1
|
|
|
|
|
|
def test_repo_doc_write_still_counts_as_source_edit() -> None:
|
|
# Non-script files under the repository tree (changelogs, docs) are real
|
|
# edits and must arm/reset the gate as before.
|
|
tracker = FinalizeEvidenceTracker()
|
|
tracker.observe_write("docs/CHANGES.rst", iteration=1)
|
|
observation = tracker.build_observation(has_workspace_diff=True)
|
|
assert observation.source_edit_seen is True
|
|
assert observation.triggers == ["no_execution_after_final_edit"]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"path",
|
|
[
|
|
# A repo path merely containing a ``tmp`` directory segment.
|
|
"/testbed/tests/tmp/expected_output.txt",
|
|
# A relative ``tmp/`` path resolves under the repo cwd, not /tmp.
|
|
"tmp/config.yml",
|
|
],
|
|
)
|
|
def test_repo_tmp_like_path_still_counts_as_source_edit(path: str) -> None:
|
|
# The scratch-note skip is anchored to absolute /tmp, /var/tmp, and
|
|
# squilla-scratch dirs; it must not over-match tmp-named repo paths.
|
|
tracker = FinalizeEvidenceTracker()
|
|
tracker.observe_write(path, iteration=1)
|
|
observation = tracker.build_observation(has_workspace_diff=True)
|
|
assert observation.source_edit_seen is True
|
|
|
|
|
|
def test_pathless_write_still_counts_as_source_edit() -> None:
|
|
# ``apply_patch`` carries no single path argument; it must keep counting
|
|
# as a source edit.
|
|
tracker = FinalizeEvidenceTracker()
|
|
tracker.observe_write(None, iteration=1)
|
|
observation = tracker.build_observation(has_workspace_diff=True)
|
|
assert observation.source_edit_seen is True
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Green reruns never auto-discharge reds (supersession deliberately deferred)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_superset_green_rerun_does_not_discharge_red_majority() -> None:
|
|
# Deliberate polarity choice: a broader green suite run does NOT discharge
|
|
# repeated reds of a narrower selection — failing self-repros stay binding
|
|
# (the retired patch-evidence protocol showed the harm of teaching the
|
|
# model that unrelated/broader greens override its own red). Only an exact
|
|
# green rerun of the tracked artifact clears it (``ever_green``); anything
|
|
# else goes through the challenge's escape hatch.
|
|
tracker = _tracker_with_source_edit()
|
|
for index in range(5):
|
|
tracker.observe_execution(
|
|
"gradle test --tests org.foo.BarTest",
|
|
red=True,
|
|
exit_code=1,
|
|
iteration=2 + index,
|
|
)
|
|
tracker.observe_execution("gradle test", red=False, exit_code=0, iteration=7)
|
|
observation = tracker.build_observation(has_workspace_diff=True)
|
|
assert observation.triggers == ["red_majority_after_final_edit"]
|
|
|
|
|
|
def test_narrower_green_rerun_does_not_discharge_red_majority() -> None:
|
|
tracker = _tracker_with_source_edit()
|
|
for index in range(5):
|
|
tracker.observe_execution(
|
|
"pytest tests/test_a.py", red=True, exit_code=1, iteration=2 + index
|
|
)
|
|
tracker.observe_execution(
|
|
"pytest tests/test_a.py -k 'not test_bad'", red=False, exit_code=0, iteration=7
|
|
)
|
|
observation = tracker.build_observation(has_workspace_diff=True)
|
|
# A deselecting green discharges nothing: both the deselection and the
|
|
# red majority fire.
|
|
assert "red_evidence_deselected_after_final_edit" in observation.triggers
|
|
assert "red_majority_after_final_edit" in observation.triggers
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Dedup key and challenge messages
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _observation_with_red(command: str):
|
|
tracker = _tracker_with_source_edit()
|
|
tracker.observe_execution(command, red=True, exit_code=1, iteration=2)
|
|
return tracker.build_observation(has_workspace_diff=True)
|
|
|
|
|
|
def test_gate_key_stable_for_same_state_and_distinct_for_new_red() -> None:
|
|
first = _observation_with_red("pytest tests/test_a.py")
|
|
second = _observation_with_red("pytest tests/test_a.py")
|
|
third = _observation_with_red("pytest tests/test_b.py")
|
|
assert finalize_evidence_gate_key(first) == finalize_evidence_gate_key(second)
|
|
assert finalize_evidence_gate_key(first) != finalize_evidence_gate_key(third)
|
|
|
|
|
|
def test_challenge_message_red_execution_polarity() -> None:
|
|
observation = _observation_with_red("pytest tests/test_a.py")
|
|
message = finalize_evidence_challenge_message(observation)
|
|
assert message.startswith("[Finalize evidence check]")
|
|
assert "pytest tests/test_a.py" in message
|
|
assert "exit code 1" in message
|
|
assert "binding evidence" in message
|
|
assert "do not override it" in message
|
|
assert "Do not finalize yet" in message
|
|
# Escape hatch: an invalid command may be justified instead of chased.
|
|
assert "explicitly justify" in message
|
|
|
|
|
|
def test_challenge_message_never_green_repro_deleted() -> None:
|
|
tracker = _tracker_with_source_edit()
|
|
tracker.observe_write("/tmp/squilla-scratch/repro.py", iteration=2)
|
|
tracker.observe_execution(
|
|
"python /tmp/squilla-scratch/repro.py", red=True, exit_code=1, iteration=3
|
|
)
|
|
tracker.observe_execution("rm /tmp/squilla-scratch/repro.py", red=False, iteration=4)
|
|
tracker.observe_execution("pytest tests/", red=False, exit_code=0, iteration=5)
|
|
message = finalize_evidence_challenge_message(
|
|
tracker.build_observation(has_workspace_diff=True)
|
|
)
|
|
assert "/tmp/squilla-scratch/repro.py" in message
|
|
assert "deleted before a passing run" in message
|
|
assert "binding evidence" in message
|
|
|
|
|
|
def test_challenge_message_no_execution() -> None:
|
|
tracker = _tracker_with_source_edit()
|
|
message = finalize_evidence_challenge_message(
|
|
tracker.build_observation(has_workspace_diff=True)
|
|
)
|
|
assert "no execution-level command ran after your final source edit" in message
|
|
|
|
|
|
def test_challenge_messages_never_use_protocol_polarity() -> None:
|
|
"""The retired patch-evidence protocol harmed runs by demanding minimality
|
|
and devaluing self-written repros; no challenge message may echo either."""
|
|
|
|
tracker = _tracker_with_source_edit()
|
|
tracker.observe_write("/tmp/squilla-scratch/repro.py", iteration=2)
|
|
tracker.observe_execution(
|
|
"python /tmp/squilla-scratch/repro.py", red=True, exit_code=1, iteration=3
|
|
)
|
|
observations = [
|
|
tracker.build_observation(has_workspace_diff=True),
|
|
_observation_with_red("pytest tests/test_a.py"),
|
|
_tracker_with_source_edit().build_observation(has_workspace_diff=True),
|
|
]
|
|
for observation in observations:
|
|
message = finalize_evidence_challenge_message(observation).lower()
|
|
assert "minimal" not in message
|
|
assert "localized" not in message
|
|
assert "not sufficient" not in message
|
|
|
|
|
|
def test_challenge_limit_constant() -> None:
|
|
assert FINALIZE_EVIDENCE_GATE_CHALLENGE_LIMIT == 2
|