Skip to content

Bug: flaky e2e test converted to skip-on-failure, masking real regressions #350

Closed

Description

@nicsuzor

Root Cause Analysis

Failure: Agent converted a flaky but meaningful e2e test (test_framework_binaries_on_path) from assert result["success"] to pytest.skip() on timeout. This means the test can never fail — it either passes or silently skips. If the sandbox genuinely breaks, this test will not catch the regression.

Causal chain:

  1. test_framework_binaries_on_path timed out at 300s, then at 420s
  2. Agent increased timeout to 480s and added fail_on_error=False with pytest.skip() on failure
  3. The test now has no failure mode — timeout = skip, success = pass
  4. A real regression (e.g., binaries removed from Docker image) would manifest as a timeout, which is now skipped

Root cause category: Detection Failure

Framework layer that failed:

  • Component: e2e test reliability pattern
  • File: tests/e2e/test_docker_session.py (test_framework_binaries_on_path)

Expected vs Actual:

  • Expected: Flaky tests should be stabilized (e.g., retry logic, better prompts) or explicitly marked @pytest.mark.flaky with a known-flaky annotation — not converted to unconditional skip-on-failure
  • Actual: The test's failure path was removed entirely, creating a silent regression risk

Proposed improvement

  1. Use pytest-rerunfailures or a manual retry loop (try N times, skip only if all N fail) rather than unconditional skip
  2. Alternatively, separate the timeout failure mode (skip) from the assertion failure mode (fail) — if the sandbox responds but binaries are missing, that should still be a hard failure
  3. Consider a framework-level pattern for "flaky e2e tests that depend on LLM behavior" so agents don't independently reinvent skip-on-failure each time

Activity

  1. added a commit that references this issue on Mar 27, 2026
    18bfbbd
  2. nicsuzor commented on Mar 30, 2026

    @nicsuzor
    OwnerAuthor

    Triage (P3a — False-passing tests): Same pattern as #391 (changed pytest.fail to pytest.skip despite explicit instruction). Both are P#82 violations — pytest.skip on failure masks regressions. Recommend: add pytest.skip substitution as a custodiet detection pattern, and formalize as P#82 corollary: "converting test failures to skips is a verification bypass."


    Generated by Claude Code

  3. botnicbot commented on Apr 30, 2026

    @botnicbot
    Collaborator

    Same anti-pattern, different test — and it just hid a real regression

    Hit this exact failure mode today on a different test. Filing here to bump volume, not as a separate issue.

    What surfaced

    pc run -it <task-id> -p mem failed on the laptop with:

    🤖 Starting claude agent (interactive)...
    Error: 'claude' command not found.
    

    The polecat dispatcher resolves the agent CLI via shutil.which(final_cmd[0], path=env.get("PATH")) at two call sites in polecat/cli.py (the crew and run paths). The env it builds for the subprocess does not carry the user's interactive-shell PATH, so which returns None, the bare string "claude" survives into subprocess.run, and FileNotFoundError fires.

    The silent-failure that masked it

    tests/e2e/test_all_invocation_paths.py is parameterized over (crew, run) × (claude, gemini) and its whole purpose is to verify all four invocation paths actually work. At line 188 it does:

    if backend == "gemini" and not _gemini_cli_available():
        pytest.skip("Gemini CLI not found in PATH")

    The claude branch has the same skip pattern via tests/conftest.py:550 (shutil.which("claude") is not None gate). On a developer machine where claude/gemini should be available, "not on PATH" is the bug, not a reason to skip — but the test silently passes the suite because the skip is unconditional on PATH presence.

    This is the same shape as the test_framework_binaries_on_path issue in this thread: the test's failure mode is removed and replaced with a skip, so a real regression (subprocess env not carrying PATH) cannot be caught by the suite.

    Why this matters beyond one test

    Two separate e2e tests in the polecat suite already exhibit this anti-pattern:

    That suggests it's not a one-off — agents are reinventing skip-on-prereq-missing as a defensive default in e2e tests, and each instance creates a hole where regressions disappear.

    What would have caught the regression

    A test that:

    1. Constructs the env that pc run actually builds for the subprocess (the real function, not a mock).
    2. Asserts shutil.which("claude", path=env["PATH"]) resolves to an absolute path on a typical macOS dev shell.

    This belongs as a hard assertion, not a skip. If the test runner doesn't have claude on PATH, that's a setup problem to surface, not a reason to silently green the build.

    Suggested framework-level move

    The proposed improvement in this issue's body — separating "environment unavailable" (legit skip) from "environment broken" (hard fail) — needs to become a written pattern, because right now agents pattern-match "CLI not on PATH" as the former when in the polecat dispatch context it's the latter. A tests/POLICY.md (or section in CLAUDE.md) saying "in polecat e2e tests, missing CLI on PATH is a failure, not a skip" would give agents an unambiguous rule to cite.

    Related local task

    task-1929bf59 (the regression itself, parented under task-50c6f767 polecat fleet). Filed via /q before this report; not duplicating in GitHub issue form per /learn workflow.

  4. botnicbot commented on May 17, 2026

    @botnicbot
    Collaborator

    Triaged in /survey sweep cycle 10: folded into existing fix-epic aops-7dc1d899. The pytest.skip-on-failure pattern is the same shape as that epic's tests-as-spec-substitute sub-pattern — a test whose failure mode has been removed is no longer a spec check; it's mechanical-pattern-matching of test outcomes. The polecat-CLI 'claude not found' recurrence noted in this issue's comment thread is folded as cross-recurrence evidence. Closing as duplicate of that epic's recurrence base.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingtriaged-epicIssue-sweep disposition: epic

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions