Skip to content

fix(testmon): preserve terminal teardown outcomes - #3958

Merged
Sinity merged 3 commits into
masterfrom
feature/fix/testmon-outcome-ledger
Aug 12, 2026
Merged

fix(testmon): preserve terminal teardown outcomes#3958
Sinity merged 3 commits into
masterfrom
feature/fix/testmon-outcome-ledger

Conversation

@Sinity

@Sinity Sinity commented Aug 12, 2026

Copy link
Copy Markdown
Owner

Summary

Record complete testmon seed outcomes without treating fixture teardown as proof that the test body passed.

Problem

Interrupted or worker-disrupted shards can retain test_finished plus a passing teardown report without a call-phase report. A failed test can therefore be incorrectly terminalized and skipped on resume; an unrecorded call can be treated as passed.

Solution

  • record complete per-node seed outcomes and preserve resumable shard state;
  • require call-phase evidence or a corroborating testmon result before accepting a passing teardown;
  • keep missing, failed, worker-crash, timeout, and interrupted outcomes distinct;
  • isolate unit-test testmon caches so checkout-integrity tests observe real state;
  • preserve the managed event ledger across the host environment scrub.

Verification

  • .venv/bin/python -m devtools test tests/unit/devtools/test_verify.py tests/unit/devtools/test_testmon_state.py tests/unit/devtools/test_checkout_guard.py tests/unit/devtools/test_pytest_progress_plugin.py — 196 passed;
  • .venv/bin/python -m devtools verify --quick — 25/25 steps passed at eda8751e3;
  • current-head Codex review was addressed with a regression test for teardown-only evidence.

This PR is self-contained on top of the already-merged testmon sharding work in #3956.

Summary by CodeRabbit

  • New Features

    • Added durable, resumable test verification through deterministic shards.
    • Verification now records per-shard progress and outcomes, allowing interrupted runs to continue safely.
    • Added validation to ensure complete and consistent test coverage before finalizing results.
  • Bug Fixes

    • Improved handling of teardown outcomes so incomplete tests are not incorrectly marked as successful.
    • Benchmark tests are excluded from standard verification collection.
    • Improved freshness and readiness checks for search-related data.

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The change adds durable testmon seed shards with deterministic planning, ledger validation, serial execution, checkpoint/resume support, outcome aggregation, and protocol-v7 completion checks. It also updates convergence fixtures, pytest isolation, and freshness snapshot return behavior.

Changes

Testmon shard seeding

Layer / File(s) Summary
Shard ledger contracts
devtools/testmon_state.py
Adds shard lifecycle states, deterministic shard planning, ledger validation, terminal checks, and protocol-version validation during attempt stamping.
Shard collection and checkpointing
devtools/verify.py
Collects node IDs separately, creates validated 256-node shards, runs incomplete shards serially, and checkpoints each shard for resume.
Outcome classification and finalization
devtools/verify.py
Aggregates shard results, requires terminal outcomes, preserves missing nodes when only teardown reports pass, and stores shard ledgers in seed payloads.
Testmon verification coverage
tests/unit/devtools/test_verify.py, tests/unit/devtools/test_pytest_progress_plugin.py
Tests deterministic commands, shard resume, database validation, failure propagation, teardown handling, and testmon artifact isolation.

Test infrastructure and API cleanup

Layer / File(s) Summary
Convergence harness updates
tests/infra/convergence_harness.py
Uses direct raw-session writes, requires indexed rows for FTS staleness, removes freshness assertions, and validates readiness through blocked surfaces.
Nested test environment cleanup
tests/conftest.py
Removes nested verify-ledger scrubbing and duplicate environment cleanup.
Freshness and annotation test contracts
polylogue/daemon/fts_startup.py, tests/unit/annotations/*
Changes the freshness snapshot helper to return None and simplifies annotation test setup.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Verify
  participant Pytest
  participant ShardLedger
  participant Checkpoint
  Verify->>Pytest: Run collect-only seed selection
  Verify->>ShardLedger: Create and validate shard plan
  Verify->>Pytest: Execute incomplete shard serially
  Pytest-->>Verify: Return node outcomes and metadata
  Verify->>Checkpoint: Save shard results and ledger status
  Checkpoint-->>Verify: Provide resumable seed state
  Verify->>ShardLedger: Validate terminal ledger
  ShardLedger-->>Verify: Allow seed finalization
Loading

Possibly related PRs

  • Sinity/polylogue#3900: Both changes update testmon state and verification logic for resumable seed attempts and terminal outcomes.
  • Sinity/polylogue#3949: Both changes modify devtools/verify.py and its pytest verification infrastructure.
  • Sinity/polylogue#3956: Both changes address protocol-v7 resumable sharded testmon seeding.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely describes the primary change: preserving terminal teardown outcomes in testmon seed processing.
Description check ✅ Passed The description covers the change, problem, solution, and verification, but it omits the required bead disposition matrix.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/fix/testmon-outcome-ledger

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dcaa7ceaf7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread devtools/verify.py
Comment on lines +2811 to +2813
elif nodeid in finished and any(
report.get("when") == "teardown" and report.get("outcome") == "passed" for report in node_reports
):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not infer a passing call from successful teardown

When the durable event stream drops a call report—the exact scenario handled here—a failed test can still emit a passing teardown report and test_finished, because that teardown outcome only describes fixture cleanup. This branch therefore classifies the node as passed even if the testmon database records it as failed, since it runs before the database fallback; if the database row is also missing, _checkpoint_testmon_seed_shard marks the shard complete and subsequent seed resumes skip it permanently. Keep teardown-only nodes nonterminal unless a prior outcome or the database corroborates the call result.

Useful? React with 👍 / 👎.

@Sinity
Sinity force-pushed the feature/fix/testmon-outcome-ledger branch 2 times, most recently from 4b0ee53 to 7eecafe Compare August 12, 2026 02:08
@Sinity

Sinity commented Aug 12, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Already looking forward to the next diff.

Reviewed commit: 7eecafe518

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Base automatically changed from feature/fix/testmon-sharded-seed to master August 12, 2026 10:35
@Sinity
Sinity force-pushed the feature/fix/testmon-outcome-ledger branch from 7eecafe to 02b2d0d Compare August 12, 2026 10:56
@Sinity

Sinity commented Aug 12, 2026

Copy link
Copy Markdown
Owner Author

@codex review
@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

@Sinity, I will review the changes in #3958.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@Sinity
Sinity merged commit 26e58f4 into master Aug 12, 2026
1 of 3 checks passed
@Sinity
Sinity deleted the feature/fix/testmon-outcome-ledger branch August 12, 2026 11:10

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: eda8751e33

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +35 to +37
checkout_cache = Path(__file__).resolve().parents[3] / ".cache" / "testmon"
if checkout_cache.exists():
shutil.move(str(checkout_cache), str(tmp_path / "checkout-testmon-generated"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve pre-existing testmon state during cleanup

When this module is run in a checkout that already has a valid seed, the autouse fixture executes after the first test and moves the entire checkout-level .cache/testmon directory into that test's temporary directory, even when the test did not create the cache; nothing restores it afterward. This discards the main checkout's reusable seed, forcing a costly reseed and preventing linked worktrees from bootstrapping from it. Quarantine and restore pre-existing state, and remove only cache artifacts created by this test module.

AGENTS.md reference: AGENTS.md:L338-L341

Useful? React with 👍 / 👎.

Comment thread devtools/verify.py
# them are correctness-shaped and lack the benchmark marker, so a
# marker expression alone cannot keep performance probes out of
# the correctness/testmon corpus.
"--ignore=tests/benchmarks",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep correctness benchmarks in verification collection

This ignore applies to the seed, affected-test, and --all commands, so it removes the entire benchmarks tree rather than only tests marked benchmark. That tree deliberately contains correctness gates such as the unmarked tests in test_rebuild_cost_model.py and the scale_small tests in test_scale_tiers.py, whose module contract says they belong to default verification. These tests consequently disappear from the testmon graph and even the documented full non-integration run, allowing regressions in those paths to pass both normal and terminal merge-train verification; retain the tree and rely on the existing marker expression to exclude actual performance probes.

AGENTS.md reference: AGENTS.md:L342-L343

Useful? React with 👍 / 👎.

consume_blob_publication_receipt(source_conn, attachment_receipt, attachment_hash_bytes)
if raw_blob_size != len(payload):
raise AssertionError(f"published raw payload size drifted for {source_path}")
raw_id = write_source_raw_session(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Admit and publish raw fixtures through the production route

When a convergence property reaches rebuild_retained_raw_index, this low-level writer has created each raw row with its default revision_kind='unknown' and revision_authority='quarantined', without the census/logical-key evidence produced by admit_raw_observation; it also records the blob reference without publishing the payload bytes into the archive blob store. rebuild_index_from_source_sync validates frozen source authority before replay and rejects these rows as quarantined or undecided, and the retained payload would be unavailable even if that check were bypassed. Restore the admission plus ArchiveBlobPublisher route so the convergence rebuild properties remain executable.

Useful? React with 👍 / 👎.

assert cold.canonical_provenance_bytes() == original.canonical_provenance_bytes()

with ArchiveStore.open_existing(archive_root, read_only=False) as reopened:
replay = reopened.save_annotation_batch(exact_retry)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reopen the archive writable before retrying the batch

ArchiveStore.open_existing() defaults to read_only=True, so after removing the separate read_only=False context this call reaches _open_user_write_connection() and raises ReadOnlyArchiveError. The test therefore fails before checking either idempotent retry behavior or incompatible provenance; retain the writable reopen or explicitly pass read_only=False before invoking save_annotation_batch.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
tests/unit/devtools/test_pytest_progress_plugin.py (1)

34-43: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Restore the plugin globals before relocating the cache directory.

Line 35-37 runs shutil.move before lines 38-43 restore the plugin module globals. If the move raises — a permission error, a cross-device link error, or an existing destination — the globals stay mutated and every later test in the session observes leaked _SELECTED_COUNT, _DESELECTED_COUNT, and slowest-report state.

Restore the globals first, or wrap the move so it cannot skip restoration.

🐛 Proposed fix
     yield
-    checkout_cache = Path(__file__).resolve().parents[3] / ".cache" / "testmon"
-    if checkout_cache.exists():
-        shutil.move(str(checkout_cache), str(tmp_path / "checkout-testmon-generated"))
     pytest_progress_plugin._SELECTED_COUNT = selected_count
     pytest_progress_plugin._DESELECTED_COUNT = deselected_count
     pytest_progress_plugin._DESELECTED_NODEIDS_SAMPLE[:] = deselected_nodeids
     pytest_progress_plugin._SLOWEST_REPORTS[:] = slowest_reports
     pytest_progress_plugin._COLLECTION_STARTED_AT = collection_started_at
     pytest_progress_plugin._COLLECTION_DURATION_S = collection_duration_s
+    checkout_cache = Path(__file__).resolve().parents[3] / ".cache" / "testmon"
+    if checkout_cache.exists():
+        shutil.move(str(checkout_cache), str(tmp_path / "checkout-testmon-generated"))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/devtools/test_pytest_progress_plugin.py` around lines 34 - 43,
Update the fixture cleanup around pytest_progress_plugin global restoration so
_SELECTED_COUNT, _DESELECTED_COUNT, _DESELECTED_NODEIDS_SAMPLE,
_SLOWEST_REPORTS, _COLLECTION_STARTED_AT, and _COLLECTION_DURATION_S are
restored before calling shutil.move on checkout_cache, or guarantee restoration
with a finally block if the move fails.
devtools/verify.py (1)

1980-2000: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Register the unmarked benchmark tests

tests/benchmarks/test_schema_linear_scaling.py and tests/benchmarks/test_fts_trigger_amPLIFICATION.py are excluded from all correctness lanes by design, but neither file is a registered devtools bench campaign target. Add @pytest.mark.benchmark and register both files, or move their correctness tests out of tests/benchmarks.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@devtools/verify.py` around lines 1980 - 2000, Register the unmarked benchmark
files tests/benchmarks/test_schema_linear_scaling.py and
tests/benchmarks/test_fts_trigger_amPLIFICATION.py as devtools bench campaign
targets, and add the pytest benchmark marker to their tests. Keep them excluded
from correctness lanes through the existing benchmark filtering.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@devtools/verify.py`:
- Around line 2698-2714: Update the shard status handling around
`selection_matches` and the `shard.update` payload to persist an explicit
selection-mismatch diagnostic when pytest’s selected nodeids differ from
`nodeids`. Record the expected and actual node sets (or an equivalent structured
mismatch field) in the shard receipt while preserving the existing
`COMPLETE`/`INCOMPLETE` determination and outcome tracking.
- Line 2636: Update the expected_digest expression in the relevant verification
flow to hash "\n".join(sorted(expected)) instead of the unsorted expected
sequence. Keep the existing None result when expected is empty, ensuring this
digest uses the same canonical ordering as the other digest producers and
consumers.
- Around line 2645-2652: Update _seed_shard_command so each shard uses unique
JUnit and JSON report paths instead of inheriting the shared collection report
arguments. Derive per-shard artifact paths under the run directory using the
shard identity, while preserving the existing testmon and explicit nodeid
arguments so aggregate seed reporting can read every shard’s results.
- Around line 2812-2824: The teardown handling in devtools/verify.py (lines
2812-2824) must only treat a recorded passed result as terminal when
use_database_fallback is enabled; keep recorded failures ungated so they still
block release. Add a test in tests/unit/devtools/test_verify.py (lines 461-489)
using a passed database node outcome with use_database_fallback=False and assert
the result remains missing.
- Around line 3416-3441: Update the seed-shard execution around
_seed_shard_command so each shard invokes pytest with only its shard nodeids as
collection targets, rather than retaining root arguments that trigger
full-corpus collection. Preserve the existing marker expression and shard
execution behavior, then measure and compare full --seed-testmon wall-clock time
against the previous single-process seed to validate the improvement.
- Around line 2617-2631: Update _prepare_testmon_seed_shards and
_testmon_seed_can_resume to compare the freshly collected node-ID set with the
resumed seed’s expected node-ID set. When they differ, reject resume or
initialize a new seed plan from the fresh collection instead of reusing prior
shards, ensuring newly collected tests are seeded.

In `@tests/infra/convergence_harness.py`:
- Around line 237-246: Update the seeding flow around write_source_raw_session
to publish payload through ArchiveBlobPublisher before creating the raw session
reference. Pass the resulting publication receipt into write_source_raw_session
and consume that receipt within the source transaction, ensuring the blob
content exists before the raw_payload reference is recorded.

In `@tests/unit/devtools/test_pytest_progress_plugin.py`:
- Around line 80-94: Redirect all testmon and verification artifacts into
temporary test directories instead of the checkout. In
tests/unit/devtools/test_pytest_progress_plugin.py lines 80-94, set
TESTMON_DATAFILE in the child env to a tmp_path location and remove checkout
cache creation; at lines 34-43, remove the checkout-cache shutil.move while
retaining plugin-global restoration. In tests/unit/devtools/test_verify.py lines
126-140, remove _quarantine_checkout_testmon; in lines 325-337, 354, 422, and
493, call _isolate_verify_artifacts(tmp_path, monkeypatch) in each shard test so
TESTMON_DATA and TESTMON_SEED_ATTEMPT resolve under tmp_path.
- Around line 112-115: Update the test around the child-process environment
setup and event assertions to cover the configured
POLYLOGUE_PYTEST_SELECTION_PATH and POLYLOGUE_PYTEST_SUMMARY_PATH outputs by
asserting both files exist after the run; alternatively remove those environment
variables if the test is intentionally limited to event phases.

In `@tests/unit/devtools/test_verify.py`:
- Around line 1240-1269: Remove the duplicate
test_seed_node_outcomes_keep_unconfirmed_teardown_incomplete test. Add its
reason assertion to the existing
test_seed_outcome_does_not_infer_call_success_from_teardown, preserving the
shared missing outcome and teardown event coverage in that single test.
- Around line 306-311: Replace the tautological indexed assertion in the test
around the pytest command checks with a direct membership assertion for
"--ignore=tests/benchmarks", matching the existing "--collect-only" and
"--testmon" assertions; leave the separate "-n" value validation unchanged.
- Around line 116-117: Update the _quarantine_checkout_testmon fixture
annotation from object to Iterator[None], importing Iterator from
collections.abc if it is not already available, so the yield-based session
fixture has a generator-compatible return type.

---

Outside diff comments:
In `@devtools/verify.py`:
- Around line 1980-2000: Register the unmarked benchmark files
tests/benchmarks/test_schema_linear_scaling.py and
tests/benchmarks/test_fts_trigger_amPLIFICATION.py as devtools bench campaign
targets, and add the pytest benchmark marker to their tests. Keep them excluded
from correctness lanes through the existing benchmark filtering.

In `@tests/unit/devtools/test_pytest_progress_plugin.py`:
- Around line 34-43: Update the fixture cleanup around pytest_progress_plugin
global restoration so _SELECTED_COUNT, _DESELECTED_COUNT,
_DESELECTED_NODEIDS_SAMPLE, _SLOWEST_REPORTS, _COLLECTION_STARTED_AT, and
_COLLECTION_DURATION_S are restored before calling shutil.move on
checkout_cache, or guarantee restoration with a finally block if the move fails.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 8221a540-4074-427f-b34b-4985bbf3e7e5

📥 Commits

Reviewing files that changed from the base of the PR and between 595c4d9 and eda8751.

📒 Files selected for processing (9)
  • devtools/testmon_state.py
  • devtools/verify.py
  • polylogue/daemon/fts_startup.py
  • tests/conftest.py
  • tests/infra/convergence_harness.py
  • tests/unit/annotations/test_durable_storage.py
  • tests/unit/annotations/test_importer.py
  • tests/unit/devtools/test_pytest_progress_plugin.py
  • tests/unit/devtools/test_verify.py
💤 Files with no reviewable changes (2)
  • tests/unit/annotations/test_durable_storage.py
  • tests/conftest.py

Comment thread devtools/verify.py
Comment on lines +2617 to +2631
def _prepare_testmon_seed_shards(
prepared: Mapping[str, Any],
*,
selection: Mapping[str, Any] | None,
) -> dict[str, Any]:
"""Persist the full planned corpus before the first testmon DB mutation."""
expected = sorted(_testmon_seed_expected_nodeids(prepared)) if prepared.get("resume") else []
if not expected:
expected = _seed_selection_nodeids(selection or {}) or []
prior_shards = validate_seed_shard_ledger(prepared.get("shards"), expected_nodeids=expected)
shards = (
prior_shards
if prior_shards is not None
else (seed_shard_plan(expected, shard_size=TESTMON_SEED_SHARD_SIZE) if expected else [])
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect the resume contract to see whether it constrains the collected corpus.
set -euo pipefail

ast-grep run --pattern $'def _testmon_seed_resume_contract($$$) {
  $$$
}' --lang python devtools/verify.py || rg -nP -A25 'def _testmon_seed_resume_contract' devtools/verify.py
rg -nP -B3 -A12 'def _testmon_seed_can_resume' devtools/verify.py

Repository: Sinity/polylogue

Length of output: 2254


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '2520,2665p' devtools/verify.py
sed -n '2880,3050p' devtools/verify.py
sed -n '3360,3470p' devtools/verify.py
rg -n -P -B5 -A15 '_prepare_testmon_seed_shards|_testmon_seed_can_resume|_testmon_seed_resume_contract|_seed_selection_nodeids|_testmon_seed_expected_nodeids|selection' devtools/verify.py

Repository: Sinity/polylogue

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -P -B12 -A24 'prepared_seed_attempt|_prepare_testmon_seed_attempt|seed-testmon collect|_testmon_seed_can_resume|_testmon_seed_resume_contract|git_tree.*worktree_fingerprint|worktree_fingerprint.*git_tree' devtools/verify.py
rg -n -P -B8 -A20 'resume.*seed|seed.*resume|expected_nodeids|selected_nodeids' tests devtools 2>/dev/null | head -n 320

Repository: Sinity/polylogue

Length of output: 37756


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '2400,2488p' devtools/verify.py
sed -n '1880,2010p' devtools/verify.py
sed -n '130,180p' devtools/pytest_progress_plugin.py
sed -n '1,130p' tests/integration/devtools/test_testmon_seed_recovery.py
rg -n -P -B5 -A18 'def _testmon_seed_identity|def _worktree_fingerprint|dependency_environment|pytest_harness|git_tree' devtools/verify.py

Repository: Sinity/polylogue

Length of output: 24260


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from hashlib import sha256

def seed_selection_nodeids(selection):
    nodeids = selection.get("selected_nodeids")
    count = selection.get("selected_count")
    omitted = selection.get("selected_nodeids_omitted")
    if (
        not isinstance(nodeids, list)
        or not nodeids
        or any(not isinstance(nodeid, str) or not nodeid for nodeid in nodeids)
        or len(set(nodeids)) != len(nodeids)
        or not isinstance(count, int)
        or count != len(nodeids)
        or omitted != 0
    ):
        return None
    return sorted(nodeids)

def prepare_expected(prepared, selection):
    expected = (
        sorted(prepared["expected_nodeids"])
        if prepared.get("resume")
        else []
    )
    if not expected:
        expected = seed_selection_nodeids(selection or {}) or []
    return expected

prior = ["tests/test_a.py::test_old"]
fresh = {
    "selected_nodeids": ["tests/test_a.py::test_old", "tests/test_b.py::test_new"],
    "selected_count": 2,
    "selected_nodeids_omitted": 0,
}
result = prepare_expected(
    {"resume": True, "expected_nodeids": prior},
    fresh,
)
assert result == prior
assert set(result) != set(fresh["selected_nodeids"])
print({
    "prior_expected": prior,
    "fresh_selected": fresh["selected_nodeids"],
    "resumed_expected": result,
    "fresh_selection_ignored": True,
})
PY

Repository: Sinity/polylogue

Length of output: 366


Reject resume when the collected node IDs differ. On resume, _prepare_testmon_seed_shards always uses the prior expected_nodeids and ignores the fresh collection. _testmon_seed_can_resume does not compare collection contents. New tests can therefore remain unseeded. Compare both node-ID sets and start a new seed or fail on mismatch.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@devtools/verify.py` around lines 2617 - 2631, Update
_prepare_testmon_seed_shards and _testmon_seed_can_resume to compare the freshly
collected node-ID set with the resumed seed’s expected node-ID set. When they
differ, reject resume or initialize a new seed plan from the fresh collection
instead of reusing prior shards, ensuring newly collected tests are seeded.

Comment thread devtools/verify.py
**dict(prepared),
"expected_nodeids": expected,
"expected_count": len(expected),
"expected_digest": hashlib.sha256("\n".join(expected).encode()).hexdigest() if expected else None,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Use sorted(expected) for the digest to match every other digest site.

Line 2636 hashes "\n".join(expected). Every other producer and consumer hashes "\n".join(sorted(expected)): line 2582, lines 3034 and 3077, _testmon_seed_expected_nodeids at line 2498, and stamp_from_attempt in devtools/testmon_state.py at line 993.

The values agree today only because expected is already sorted at lines 2623 and 2625. If either source stops sorting, the digest silently mismatches and _testmon_seed_expected_nodeids discards the whole ledger, which kills resume. Make the expression canonical here.

🐛 Proposed fix
-        "expected_digest": hashlib.sha256("\n".join(expected).encode()).hexdigest() if expected else None,
+        "expected_digest": hashlib.sha256("\n".join(sorted(expected)).encode()).hexdigest() if expected else None,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"expected_digest": hashlib.sha256("\n".join(expected).encode()).hexdigest() if expected else None,
"expected_digest": hashlib.sha256("\n".join(sorted(expected)).encode()).hexdigest() if expected else None,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@devtools/verify.py` at line 2636, Update the expected_digest expression in
the relevant verification flow to hash "\n".join(sorted(expected)) instead of
the unsorted expected sequence. Keep the existing None result when expected is
empty, ensuring this digest uses the same canonical ordering as the other digest
producers and consumers.

Comment thread devtools/verify.py
Comment on lines +2645 to +2652
def _seed_shard_command(collection_command: Sequence[str], shard: Mapping[str, Any]) -> list[str]:
"""Build a serial, explicit-node pytest-testmon invocation for one shard."""
nodeids = shard.get("nodeids")
if not isinstance(nodeids, list) or not nodeids:
raise ValueError("testmon seed shard is missing nodeids")
command = [argument for argument in collection_command if argument != "--collect-only"]
command.extend(["--testmon", "--testmon-noselect", *nodeids])
return command

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Each shard overwrites the same JSON and JUnit report files.

_seed_shard_command copies the collection command and removes only --collect-only. The command still contains --junitxml={PYTEST_JUNIT_REPORT_DIR}/verify-latest.xml and --json-report-file={PYTEST_REPORT_PATH} from lines 1986-1989.

Every shard therefore writes the same two files. After a sharded seed only the last shard's report remains. _read_pytest_report and _compare_against_last then describe one shard instead of the whole seed, so test counts and duration comparisons are wrong for the seed tier.

Redirect both report paths per shard, or drop them from the shard command and rely on the per-shard artifacts under the run directory.

🐛 Proposed fix: per-shard report paths
-def _seed_shard_command(collection_command: Sequence[str], shard: Mapping[str, Any]) -> list[str]:
+def _seed_shard_command(collection_command: Sequence[str], shard: Mapping[str, Any]) -> list[str]:
     """Build a serial, explicit-node pytest-testmon invocation for one shard."""
     nodeids = shard.get("nodeids")
     if not isinstance(nodeids, list) or not nodeids:
         raise ValueError("testmon seed shard is missing nodeids")
-    command = [argument for argument in collection_command if argument != "--collect-only"]
+    index = shard.get("index")
+
+    def _shard_report_arg(argument: str) -> str:
+        if argument.startswith("--junitxml="):
+            return f"--junitxml={PYTEST_JUNIT_REPORT_DIR}/verify-latest-shard-{index}.xml"
+        if argument.startswith("--json-report-file="):
+            return f"--json-report-file={PYTEST_REPORT_DIR / f'last-pytest-shard-{index}.json'}"
+        return argument
+
+    command = [_shard_report_arg(argument) for argument in collection_command if argument != "--collect-only"]
     command.extend(["--testmon", "--testmon-noselect", *nodeids])
     return command
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@devtools/verify.py` around lines 2645 - 2652, Update _seed_shard_command so
each shard uses unique JUnit and JSON report paths instead of inheriting the
shared collection report arguments. Derive per-shard artifact paths under the
run directory using the shard identity, while preserving the existing testmon
and explicit nodeid arguments so aggregate seed reporting can read every shard’s
results.

Comment thread devtools/verify.py
Comment on lines +2698 to +2714
terminal = all(item.get("outcome") in {"passed", "failed", "error", "skipped"} for item in outcomes)
selection_matches = selected == nodeids
shard.update(
{
"status": SeedShardStatus.COMPLETE.value
if selection_matches and terminal
else SeedShardStatus.INCOMPLETE.value,
"started_at": shard.get("started_at") or datetime.now(timezone.utc).isoformat(),
"finished_at": datetime.now(timezone.utc).isoformat(),
"exit_code": step.get("exit"),
"artifact_dir": step.get("artifact_dir"),
"selection": dict(selection) if isinstance(selection, Mapping) else None,
"database": database,
"node_outcomes": outcomes,
"pytest_step": dict(step),
}
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

A shard that pytest partially deselects can never reach complete.

Line 2699 requires the shard's own selection.json node set to equal the shard node set exactly. Line 2702 marks the shard complete only when that holds and every node has a terminal outcome.

If pytest deselects any node inside a shard — a collection-time skip, a plugin-level deselect, or a nodeid that no longer resolves — selection_matches is False forever. The shard stays incomplete, seed_shard_ledger_is_terminal stays False, stamp_from_attempt returns None, and every later --seed-testmon run re-executes the same shard with the same result. The receipt records status: "incomplete" without naming the mismatch, so the loop is not diagnosable from the ledger.

Record the mismatch explicitly so an operator can see why the shard cannot complete.

🐛 Proposed fix: persist the selection mismatch
     terminal = all(item.get("outcome") in {"passed", "failed", "error", "skipped"} for item in outcomes)
     selection_matches = selected == nodeids
     shard.update(
         {
             "status": SeedShardStatus.COMPLETE.value
             if selection_matches and terminal
             else SeedShardStatus.INCOMPLETE.value,
+            "selection_matches": selection_matches,
+            "unselected_nodeids": sorted(set(nodeids) - set(selected or [])),
             "started_at": shard.get("started_at") or datetime.now(timezone.utc).isoformat(),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
terminal = all(item.get("outcome") in {"passed", "failed", "error", "skipped"} for item in outcomes)
selection_matches = selected == nodeids
shard.update(
{
"status": SeedShardStatus.COMPLETE.value
if selection_matches and terminal
else SeedShardStatus.INCOMPLETE.value,
"started_at": shard.get("started_at") or datetime.now(timezone.utc).isoformat(),
"finished_at": datetime.now(timezone.utc).isoformat(),
"exit_code": step.get("exit"),
"artifact_dir": step.get("artifact_dir"),
"selection": dict(selection) if isinstance(selection, Mapping) else None,
"database": database,
"node_outcomes": outcomes,
"pytest_step": dict(step),
}
)
terminal = all(item.get("outcome") in {"passed", "failed", "error", "skipped"} for item in outcomes)
selection_matches = selected == nodeids
shard.update(
{
"status": SeedShardStatus.COMPLETE.value
if selection_matches and terminal
else SeedShardStatus.INCOMPLETE.value,
"selection_matches": selection_matches,
"unselected_nodeids": sorted(set(nodeids) - set(selected or [])),
"started_at": shard.get("started_at") or datetime.now(timezone.utc).isoformat(),
"finished_at": datetime.now(timezone.utc).isoformat(),
"exit_code": step.get("exit"),
"artifact_dir": step.get("artifact_dir"),
"selection": dict(selection) if isinstance(selection, Mapping) else None,
"database": database,
"node_outcomes": outcomes,
"pytest_step": dict(step),
}
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@devtools/verify.py` around lines 2698 - 2714, Update the shard status
handling around `selection_matches` and the `shard.update` payload to persist an
explicit selection-mismatch diagnostic when pytest’s selected nodeids differ
from `nodeids`. Record the expected and actual node sets (or an equivalent
structured mismatch field) in the shard receipt while preserving the existing
`COMPLETE`/`INCOMPLETE` determination and outcome tracking.

Comment thread devtools/verify.py
Comment on lines +2812 to +2824
elif nodeid in finished and any(
report.get("when") == "teardown" and report.get("outcome") == "passed" for report in node_reports
):
# Teardown describes fixture cleanup, not the test body. It may
# corroborate a terminal testmon row, but it cannot replace a
# missing call report: a failed call can still end with a passing
# teardown, and an unrecorded call must remain resumable.
if recorded.get(nodeid) == "passed":
outcome, reason = "passed", "passing teardown corroborated by testmon success"
elif recorded.get(nodeid) == "failed":
outcome, reason = "failed", "passing teardown contradicted by testmon failure"
else:
outcome, reason = "missing", "passing teardown without call report or testmon result"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

The teardown arm treats a testmon database row as terminal evidence, and no test pins that arm under the shard flow's flag. The shared root cause is that the new branch reads recorded without checking use_database_fallback, so the shard path — which passes use_database_fallback=False at lines 2696 and 2969 specifically to distrust the database — can still classify a node passed from a row the current shard did not produce.

  • devtools/verify.py#L2812-L2824: guard the passed arm with use_database_fallback, and leave the failed arm ungated so a recorded failure always blocks release.
  • tests/unit/devtools/test_verify.py#L461-L489: add a case with database={"node_outcomes": {nodeid: "passed"}} and use_database_fallback=False, asserting the outcome stays missing.
📍 Affects 2 files
  • devtools/verify.py#L2812-L2824 (this comment)
  • tests/unit/devtools/test_verify.py#L461-L489
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@devtools/verify.py` around lines 2812 - 2824, The teardown handling in
devtools/verify.py (lines 2812-2824) must only treat a recorded passed result as
terminal when use_database_fallback is enabled; keep recorded failures ungated
so they still block release. Add a test in tests/unit/devtools/test_verify.py
(lines 461-489) using a passed database node outcome with
use_database_fallback=False and assert the result remains missing.

Comment on lines +80 to 94
checkout_root = Path(__file__).resolve().parents[3]
# The real testmon plugin receives no TESTMON_DATAFILE here by design:
# this regression test models a child process after the host scrub. Give
# its default relative path a parent directory without permitting the
# resulting cache to leak into later tests.
(checkout_root / ".cache" / "testmon").mkdir(parents=True, exist_ok=True)
env = os.environ.copy()
env.update(
{
"POLYLOGUE_PYTEST_EVENTS_DIR": str(events_dir),
"POLYLOGUE_PYTEST_SELECTION_PATH": str(tmp_path / "selection.json"),
"POLYLOGUE_PYTEST_SUMMARY_PATH": str(tmp_path / "summary.json"),
"POLYLOGUE_VERIFY_RUN_ID": "subprocess-regression",
# This child deliberately owns a private destination so the test
# can verify the nested-process isolation contract without making
# its reports part of the outer seed ledger.
"POLYLOGUE_PYTEST_NESTED_PRIVATE": "1",
}
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Unit tests write testmon state into the real checkout, then relocate it during teardown. The shared root cause is that these tests let production code resolve testmon paths against ROOT instead of redirecting those paths to tmp_path. Each site then compensates with a shutil.move of the developer's .cache/testmon, so an interrupted session can leave the checkout without its seed database.

  • tests/unit/devtools/test_pytest_progress_plugin.py#L80-L94: set TESTMON_DATAFILE to a path under tmp_path in the child env and delete the mkdir of checkout_root / ".cache" / "testmon".
  • tests/unit/devtools/test_pytest_progress_plugin.py#L34-L43: delete the shutil.move of the checkout cache once the child writes into tmp_path, and keep only the plugin-global restoration.
  • tests/unit/devtools/test_verify.py#L126-L140: delete the _quarantine_checkout_testmon session fixture once no test writes checkout-level state.
  • tests/unit/devtools/test_verify.py#L325-L337: call _isolate_verify_artifacts(tmp_path, monkeypatch) in each new shard test so TESTMON_DATA and TESTMON_SEED_ATTEMPT resolve under tmp_path; apply the same change at lines 354, 422, and 493.
📍 Affects 2 files
  • tests/unit/devtools/test_pytest_progress_plugin.py#L80-L94 (this comment)
  • tests/unit/devtools/test_pytest_progress_plugin.py#L34-L43
  • tests/unit/devtools/test_verify.py#L126-L140
  • tests/unit/devtools/test_verify.py#L325-L337
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/devtools/test_pytest_progress_plugin.py` around lines 80 - 94,
Redirect all testmon and verification artifacts into temporary test directories
instead of the checkout. In tests/unit/devtools/test_pytest_progress_plugin.py
lines 80-94, set TESTMON_DATAFILE in the child env to a tmp_path location and
remove checkout cache creation; at lines 34-43, remove the checkout-cache
shutil.move while retaining plugin-global restoration. In
tests/unit/devtools/test_verify.py lines 126-140, remove
_quarantine_checkout_testmon; in lines 325-337, 354, 422, and 493, call
_isolate_verify_artifacts(tmp_path, monkeypatch) in each shard test so
TESTMON_DATA and TESTMON_SEED_ATTEMPT resolve under tmp_path.

Comment on lines 112 to +115
assert result.returncode == 0, result.stdout + result.stderr
assert selection_path.read_text(encoding="utf-8") == "selection-sentinel\n"
assert summary_path.read_text(encoding="utf-8") == "summary-sentinel\n"
events = [json.loads(line) for path in events_dir.glob("*.jsonl") for line in path.read_text().splitlines()]
reports = [event for event in events if event.get("event") == "test_report"]
assert len(reports) == 3
assert {(event["nodeid"], event["when"], event["outcome"], event["run_id"]) for event in reports} == {
(
"tests/unit/core/test_identity_law.py::test_session_id_is_origin_native_id",
phase,
"passed",
"subprocess-regression",
)
for phase in ("setup", "call", "teardown")
}
assert {event["when"] for event in reports} == {"setup", "call", "teardown"}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The selection and summary paths are configured but never asserted.

Lines 90-91 set POLYLOGUE_PYTEST_SELECTION_PATH and POLYLOGUE_PYTEST_SUMMARY_PATH for the child. The assertions at lines 112-115 check only the return code and the event report phases.

Either assert that both files exist after the run, or remove the two variables so the setup states what the test verifies.

♻️ Proposed addition
     assert result.returncode == 0, result.stdout + result.stderr
+    assert (tmp_path / "selection.json").is_file()
+    assert (tmp_path / "summary.json").is_file()
     events = [json.loads(line) for path in events_dir.glob("*.jsonl") for line in path.read_text().splitlines()]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
assert result.returncode == 0, result.stdout + result.stderr
assert selection_path.read_text(encoding="utf-8") == "selection-sentinel\n"
assert summary_path.read_text(encoding="utf-8") == "summary-sentinel\n"
events = [json.loads(line) for path in events_dir.glob("*.jsonl") for line in path.read_text().splitlines()]
reports = [event for event in events if event.get("event") == "test_report"]
assert len(reports) == 3
assert {(event["nodeid"], event["when"], event["outcome"], event["run_id"]) for event in reports} == {
(
"tests/unit/core/test_identity_law.py::test_session_id_is_origin_native_id",
phase,
"passed",
"subprocess-regression",
)
for phase in ("setup", "call", "teardown")
}
assert {event["when"] for event in reports} == {"setup", "call", "teardown"}
assert result.returncode == 0, result.stdout + result.stderr
assert (tmp_path / "selection.json").is_file()
assert (tmp_path / "summary.json").is_file()
events = [json.loads(line) for path in events_dir.glob("*.jsonl") for line in path.read_text().splitlines()]
reports = [event for event in events if event.get("event") == "test_report"]
assert {event["when"] for event in reports} == {"setup", "call", "teardown"}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/devtools/test_pytest_progress_plugin.py` around lines 112 - 115,
Update the test around the child-process environment setup and event assertions
to cover the configured POLYLOGUE_PYTEST_SELECTION_PATH and
POLYLOGUE_PYTEST_SUMMARY_PATH outputs by asserting both files exist after the
run; alternatively remove those environment variables if the test is
intentionally limited to event phases.

Comment on lines +116 to +117
@pytest.fixture(scope="session", autouse=True)
def _quarantine_checkout_testmon(tmp_path_factory: pytest.TempPathFactory) -> object:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Confirm the Iterator import and check how other generator fixtures are annotated.
set -euo pipefail

rg -nP '^(from|import)\s' tests/unit/devtools/test_verify.py | head -40
rg -nP -B2 -A2 'def .*\) -> Iterator\[None\]' tests/unit/devtools/ | head -40
rg -nP 'warn_return_any|disallow_untyped_defs|strict\s*=' pyproject.toml

Repository: Sinity/polylogue

Length of output: 1346


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- fixture and imports ---'
sed -n '1,145p' tests/unit/devtools/test_verify.py
printf '%s\n' '--- mypy configuration ---'
sed -n '205,238p' pyproject.toml
printf '%s\n' '--- verify-step references ---'
rg -n -A8 -B8 'build_verify_steps|mypy' devtools tests/unit/devtools pyproject.toml

Repository: Sinity/polylogue

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- mypy availability and generator diagnostic ---'
if command -v mypy >/dev/null 2>&1; then
  mypy --version
  cat <<'PY' | mypy --show-error-codes --no-error-summary /dev/stdin
from collections.abc import Iterator

def as_object() -> object:
    yield None

def as_iterator() -> Iterator[None]:
    yield None
PY
else
  printf '%s\n' 'mypy is not available'
fi

printf '%s\n' '--- sibling fixture imports and implementation ---'
sed -n '1,28p' tests/unit/devtools/test_pytest_progress_plugin.py
sed -n '108,145p' tests/unit/devtools/test_verify.py

Repository: Sinity/polylogue

Length of output: 2767


🌐 Web query:

mypy error generator function return type should be Generator or one of its supertypes object annotation

💡 Result:

This mypy error occurs because a generator function is a function that returns a generator object, and mypy expects the return type annotation to reflect this specific type or its supertypes [1][2]. To resolve this error, you should use the Generator type from collections.abc (or typing in older versions) [3][4][5]. The Generator type is a generic that takes three type arguments: Generator[YieldType, SendType, ReturnType] [1][3][5]. For a simple generator that only yields values, you can set the SendType and ReturnType to None [1][6]: from collections.abc import Generator def my_generator -> Generator[int, None, None]: yield 1 yield 2 If you do not need to support send(), throw(), or close() methods explicitly in your type interface, you may also use Iterator[YieldType] or Iterable[YieldType] as these are valid supertypes that mypy accepts [1][3][5]. However, Generator is the most precise type for functions containing a yield statement [1]. Key guidelines for choosing the correct annotation: - Use Generator[YieldType, None, None] (or simply Generator[YieldType]) if your function only yields values and does not utilize send() or return a value [1][5]. - Use the full Generator[YieldType, SendType, ReturnType] signature if your generator receives values via send() or returns a final value using the return statement [1][5]. - Use Iterator[YieldType] if you want a more general type that does not expose generator-specific methods like send() [1][6]. If you receive this error even when using Generator, ensure you are not accidentally returning an incompatible type, such as a different iterator or a generator expression that mypy is failing to infer correctly [7]. In such cases, explicitly annotating the return type as Generator[...] will typically resolve the conflict [7][8].

Citations:


Annotate the generator fixture as Iterator[None].

_quarantine_checkout_testmon contains yield. Strict mypy requires a generator-compatible return type. Add from collections.abc import Iterator if needed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/devtools/test_verify.py` around lines 116 - 117, Update the
_quarantine_checkout_testmon fixture annotation from object to Iterator[None],
importing Iterator from collections.abc if it is not already available, so the
yield-based session fixture has a generator-compatible return type.

Comment on lines +306 to +311
assert label == "pytest seed-testmon collect"
assert "--collect-only" in command
assert command[command.index("--ignore=tests/benchmarks")] == "--ignore=tests/benchmarks"
assert "--testmon" not in command
assert "-n" in command
assert command[command.index("-n") + 1] == "8"
assert command[command.index("-n") + 1] == "0"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Line 308 is a tautology; assert membership directly.

command[command.index(x)] == x holds for every present x and raises ValueError instead of an assertion failure when x is absent. Use in, which matches the surrounding style at lines 307 and 309 and produces a readable failure.

💚 Proposed fix
     assert label == "pytest seed-testmon collect"
     assert "--collect-only" in command
-    assert command[command.index("--ignore=tests/benchmarks")] == "--ignore=tests/benchmarks"
+    assert "--ignore=tests/benchmarks" in command
     assert "--testmon" not in command
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
assert label == "pytest seed-testmon collect"
assert "--collect-only" in command
assert command[command.index("--ignore=tests/benchmarks")] == "--ignore=tests/benchmarks"
assert "--testmon" not in command
assert "-n" in command
assert command[command.index("-n") + 1] == "8"
assert command[command.index("-n") + 1] == "0"
assert label == "pytest seed-testmon collect"
assert "--collect-only" in command
assert "--ignore=tests/benchmarks" in command
assert "--testmon" not in command
assert "-n" in command
assert command[command.index("-n") + 1] == "0"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/devtools/test_verify.py` around lines 306 - 311, Replace the
tautological indexed assertion in the test around the pytest command checks with
a direct membership assertion for "--ignore=tests/benchmarks", matching the
existing "--collect-only" and "--testmon" assertions; leave the separate "-n"
value validation unchanged.

Comment on lines +1240 to +1269
def test_seed_node_outcomes_keep_unconfirmed_teardown_incomplete(tmp_path: Path) -> None:
"""A terminal teardown does not prove that the missing call phase passed."""
events = tmp_path / "events.jsonl"
events.write_text(
"\n".join(
[
json.dumps({"event": "test_started", "nodeid": "tests/test_a.py::test_finished"}),
json.dumps(
{
"event": "test_report",
"nodeid": "tests/test_a.py::test_finished",
"when": "teardown",
"outcome": "passed",
}
),
json.dumps({"event": "test_finished", "nodeid": "tests/test_a.py::test_finished"}),
]
)
+ "\n"
)

outcomes = _seed_node_outcomes_from_events(
events,
expected_nodeids=["tests/test_a.py::test_finished"],
database={"node_outcomes": {"tests/test_a.py::test_finished": "missing"}},
pytest_step={"diagnosis": "pytest_failed"},
)

assert outcomes[0]["outcome"] == "missing"
assert outcomes[0]["reason"] == "passing teardown without call report or testmon result"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

This test duplicates the first case of test_seed_outcome_does_not_infer_call_success_from_teardown.

Lines 1242-1268 build the same event shape as lines 465-481 — test_started, a passing teardown report, test_finished — and assert the same missing outcome. The only new assertion is the reason string.

Fold the reason assertion into the existing test at lines 461-489 and delete this one, so one test owns the teardown arm.

🧰 Tools
🪛 ast-grep (0.45.1)

[info] 1245-1245: use jsonify instead of json.dumps for JSON output
Context: json.dumps({"event": "test_started", "nodeid": "tests/test_a.py::test_finished"})
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)


[info] 1246-1253: use jsonify instead of json.dumps for JSON output
Context: json.dumps(
{
"event": "test_report",
"nodeid": "tests/test_a.py::test_finished",
"when": "teardown",
"outcome": "passed",
}
)
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)


[info] 1254-1254: use jsonify instead of json.dumps for JSON output
Context: json.dumps({"event": "test_finished", "nodeid": "tests/test_a.py::test_finished"})
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/devtools/test_verify.py` around lines 1240 - 1269, Remove the
duplicate test_seed_node_outcomes_keep_unconfirmed_teardown_incomplete test. Add
its reason assertion to the existing
test_seed_outcome_does_not_infer_call_success_from_teardown, preserving the
shared missing outcome and teardown event coverage in that single test.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant