Skip to content

Step 3b-2a-2d: tests without planted inputs, deterministic (D39, D40) - #30

Open
ChrisHuie wants to merge 30 commits into
mainfrom
feature/tests-without-planted-inputs
Open

ChrisHuie wants to merge 30 commits into
mainfrom
feature/tests-without-planted-inputs

Conversation

@ChrisHuie

Copy link
Copy Markdown
Contributor

Redesign plan Step 3b-2a-2d: tests without planted inputs, deterministic (D39, D40).

What changes

D39: no planted inputs. Every test that changed a file or a copy of one, or wrote a revision, a file, a call, a connection, or a decoy holding a flaw to see whether a check catches it, is replaced. Earlier schemas come from real revisions (upgrade(path, "<revision>")); earlier repository files are copied unchanged into tests/fixtures/history/<sha>/ (index in its README); GitHub-side inputs are recorded units, or stubs built from them in memory in a shape GitHub or the environment can send. The table at the end lists each test replaced, removed, or renamed, the rule it held, and the test that holds it now.

D40: the same result every time. The plan's D40 row, CLAUDE.md, CONTRIBUTING.md, and docs/testing.md state the rule. Enforcement:

  • tests/determinism.py, installed by tests/conftest.py at configure time, refuses in the pytest process a connection other than a Unix socket, every host-name lookup the socket module offers, and SystemClock.now, SystemClock.sleep, and its unseeded random source. It sets no_proxy=*, so a client reading proxies from the environment or the macOS settings is refused at GitHub's own host on any machine. A refusal raises NotDeterministic (a BaseException) and is recorded; the record is checked after every test and at the end of the session, so a test fails even where the code under test caught the exception. Each refusal and the Unix-socket pass-through has a test, as does the source built with githubkit's own transport under four proxy settings.
  • tests/test_suite_rules.py reads source: no test file reads the wall clock, sleeps on it, or draws unseeded random numbers; the package reads them only in its two real clocks (SystemClock, db.types.utc_now), asserted as the exact list of four reads; and no test function is defined inside another function, where pytest never collects it. Each scan counts the files it read against a second walk of the directory.
  • scripts/check_junit.py --same-as OTHER.xml fails on each test whose outcome differs between two reports or that only one holds. A test pytest reports in two <testcase> elements (a failure, then a teardown error) has the outcome error+failed. Tested on both sides over reports pytest wrote.

Code changes

  • src/github_activity_db/cli/sync.py: the report's seconds come from the run's clock, not time.monotonic. tests/test_cli_sync.py::test_the_report_times_the_run_on_its_clock advances the fake clock 7 s during the resolve and asserts the report line.
  • alembic/env.py: the upgrade holds the connection's info mapping before it runs and records a failure there. An interrupt inside a statement, which SQLAlchemy answers by invalidating the connection, now propagates as the KeyboardInterrupt itself; before, reading connection.info on the invalidated connection raised PendingRollbackError in its place. Every refusal env.py made is unchanged.

Tests no longer collected, restored. Four functions of tests/test_cli_app.py::TestInstalledEntryPoint (six test ids) had become functions nested in a module-level test; they are collected again, and the collection rule above holds it.

Evidence at head 5f5b0e39691e61dbbb6d89cfd9e4d7a6e44b1100

  • Two full runs: 1828 tests, 0 failed, 0 errored, 0 skipped each; python scripts/check_junit.py first.xml --max-skipped 0 --same-as second.xml exits 0.
  • uv run pre-commit run --all-files: every hook passed.
  • Against the parent commits' code, on the same inputs:
    • The package scan over c37d9a8's src/ lists cli/sync.py:108: time.monotonic and cli/sync.py:118: time.monotonic beside the four real-clock reads; at head it lists only the four.
    • The collection rule over 8f721e0's tests/test_cli_app.py lists the four nested functions; at head, none.
    • test_the_report_times_the_run_on_its_clock fails on the parent's cli/sync.py.
    • test_an_interrupt_after_the_second_revision_applies_none fails on the parent's env.py with PendingRollbackError: Can't reconnect until invalid transaction is rolled back, and passes at head.
    • With HTTPS_PROXY=http://192.0.2.10:3128, the parent's network-refusal test fails (its refusal is the proxy's connect); head's passes under HTTPS_PROXY, https_proxy, ALL_PROXY, and none.
    • For a report pytest wrote for a test that fails and then errors in teardown, the parent's check_junit.py --same-as exits 1 with appears twice; head's reads it and reports 1 failed, 1 errored, and no difference.

Outside this step

These stay as they are; each limits what a test can assert, not what it runs:

  • Exceptions without structured fields, so tests keep a match= on the message: ItemConflict (only .number), HeadCheckError, MigrationRefused, CannotCheck, DictionaryError, ScenarioError, replay.problems (strings), the source layer's UnexpectedResponse (beyond status), Forbidden, RepositoryShapeError, ResponseShapeError, LookupShapeError, ServerError, the tracking.toml validators, and the prose refusals of scripts/wheel_smoke.py, check_db_journal.py, check_frozen_db.py, and check_junit.py's problems(). SyncReport.error and RepositoryReport.reason hold '<class>: <message>' text.
  • db.types.utc_now has no injectable clock; tests/db/test_types.py replaces the module's datetime name to reach a fixed instant.
  • PyJWT is not in uv.lock, so githubkit's App auth flow raises RuntimeError before it parses anything; its malformed-key ValueError cannot be produced.
  • logging.InterceptHandler attributes an intercepted stdlib record to logging.callHandlers, not its caller; test_intercept_stdlib_logging asserts level and message only.
  • scripts/check_log_placeholders.py and check_any_aliases.py report files scanned, not the calls or assignments compared.
  • The engine's lock tests set db.engine.SQLITE_BUSY_TIMEOUT_MS to 100 ms (D39's timing input); SyncOptions has no busy-timeout option.
  • An interrupt during a statement is the only error SQLAlchemy reads as a disconnect that a test exercises in env.py.

Tests replaced, removed, or renamed

File Test Action Rule held, and what holds it now
tests/db/test_migrations.py TestChain::test_every_command_refuses_a_file_holding_a_foreign_key_violation; TestChain::test_revision_module_code_that_breaks_a_reference_is_refused; TestOneTransaction::test_a_foreign_key_violation_fails_the_upgrade_before_commit replaced test_every_command_commits_a_file_of_seeded_rows (current, check, stamp head, upgrade at head) runs over a file seeded by _seed_a_pull_request_with_children, which now returns rows written per table. Each command succeeds and the snapshot is unchanged. foreign_key_rows (COUNT(*) of every table with PRAGMA foreign_key_list rows) equals the rows written to those tables. test_the_chain_commits_a_file_of_seeded_rows does the same over step3b1_database upgraded to head. Passing side only, as the plan states.
tests/db/test_migrations.py TestOneTransaction::test_a_failure_in_the_second_revision_applies_neither replaced Replaced by test_an_interrupt_after_the_second_revision_applies_none. An Engine-class after_cursor_execute listener raises KeyboardInterrupt (an interrupt at a set point) right after alembic_version is set to the second revision after the first schema, on the real chain from first_schema with rows. Snapshot and revision (b3c4d5e6f7g8) are unchanged. Observed: sqlalchemy PendingRollbackError with the KeyboardInterrupt as context. The test asserts that exact chain and catches both types, so a future fix fails this test instead of ending the run. env.py now lets the interrupt propagate; the test asserts the KeyboardInterrupt instance raised, exactly one invalidation by SQLAlchemy, and the unchanged file and revision.
tests/db/test_migrations.py TestRevisionSources::test_flags_each_rule_on_planted_files replaced revision_source returns RevisionSource(problems, imports, views). test_every_revision_follows_the_rules asserts no problems, the import statements checked == the tokenizer's 'import' keywords, and the VIEWS assignments evaluated == the revisions whose loaded module has VIEWS (ScriptDirectory). The generated-revision test asserts RevisionSource([], import_keywords(path), 0).
tests/db/test_migrations.py TestStep3Migration::test_aborts_while_a_tag_table_holds_rows[pr_user_tags] replaced Valid rows with foreign_keys ON: two user_tags rows and pr_user_tags (1, 1) on the template's PR 1; foreign_key_check == []. The upgrade is refused with held 'user_tags 2, pr_user_tags 1' and an unchanged snapshot. The user_tags param is unchanged.
tests/db/test_migrations.py TestTransactionControl::test_a_revision_cannot_end_the_transaction; test_a_statement_run_before_sqlite_ended_the_transaction_is_refused_after; test_a_revision_runs_only_the_actions_the_authorizer_lists; test_a_revision_cannot_attach_another_database replaced Replaced by test_the_authorizer_answers_each_call_as_the_upgrade_allows. env.py's own authorizer is captured through a RecordingConnection factory (pass-through set_authorizer). It is called directly with calls it must refuse at each of the 3 revisions of the real chain from first_schema, while in_transaction is True: ATTACH, DETACH, SAVEPOINT, ANALYZE, temp table/view/trigger, temp schema insert, virtual table, BEGIN, COMMIT, ROLLBACK, and journal_mode/legacy_alter_table/ignore_check_constraints/writable_schema with a value give SQLITE_DENY; user_version, table_info, foreign_keys, INSERT and SELECT give SQLITE_OK. After the commit, BEGIN gives DENY and COMMIT and ROLLBACK give OK. The chain reaches head. Not covered: the deny once SQLite itself ended the transaction (state open, not in_transaction), held on the passing side only. The URL's statement-cache override (?cached_statements=128) is held by TestTransactionControl::test_the_driver_keeps_no_prepared_statements: the driver receives cached_statements=0 on its one connection.
tests/db/test_migrations.py TestBatchAlter::test_a_batch_alter_under_a_view_fails_and_applies_nothing removed Passing side: the probe reads legacy_alter_table 0. TestStep3b1Migration::test_upgrades_a_database_without_rows now asserts Step 3a's views exist before and that views(db) == every frozen view at head after the real 94a52e4f1d57 drop/rebuild/recreate.
tests/db/test_migrations.py TestOneTransaction::test_a_connection_already_inside_a_transaction_is_refused removed Passing side: the probe row above (foreign_keys 0, in_transaction 1, autocommit, exactly one row). Close-on-failure now has a refusing side from a caller input. test_the_file_sqlite_opened_is_refused_by_its_name records the driver connection through a pass-through sqlite3.dbapi2.connect wrapper and asserts it raises sqlite3.ProgrammingError after configure_connection refused it.
tests/db/test_migrations.py TestStep3b1Migration::test_the_definition_reading_sees_a_changed_table; test_the_definition_reading_sees_a_literal_s_whitespace removed Both CHANGED_TABLES tests (33f8570f6540->94a52e4f1d57 and 94a52e4f1d57->2d2ba76b177c, the latter a real added column) now also assert len(table_definitions(db)) == len(schema_tables(db)) at each revision. The whitespace rule is held on the passing side (table_definitions returns sqlite_master.sql unaltered).
tests/db/test_migrations.py TestChain::test_upgrade_builds_head_with_no_autogenerate_diff; TestRefusals::test_a_caller_supplied_connection_is_refused; TestRefusals::test_sqlalchemy_url_is_refused renamed The 'decoy' variable and file are now settings_target / settings-target.db.
tests/db/test_migrations.py TestRefusals::test_refuses_the_tracked_file_of_a_copied_checkout; TestRevisionSources::test_the_newest_view_revision_has_a_literal_views_constant renamed 'Positive control' wording removed from the comment and the docstring.
tests/db/test_schema.py TestBuiltByMigrations::test_flags_every_use_and_no_text replaced create_all_uses now also returns the number of files read; that count == the .py files os.walk finds under src/tests/scripts/alembic. len(uses) == the tokenizer's create_all NAME tokens. The lines of string and comment tokens holding the text are non-empty and disjoint from the uses. The only use is still tests/db/test_schema.py. The run_sync and nested-def spellings are held on the passing side only.
tests/db/test_schema.py TestForeignKeyIndexes::test_a_missing_index_is_reported replaced test_every_foreign_key_column_leads_an_index asserts len(foreign_key_columns(head)) == sum of metadata ForeignKeyConstraints, alongside == EXEMPT. foreign_key_columns is a new schema_support reader that unindexed_foreign_keys now iterates.
tests/db/test_schema.py TestModelsComparison::test_a_missing_index_is_reported replaced Refusing side: in test_a_revision_s_changes_are_read_in_the_tables_it_lists, the created index names at 2d2ba76b177c minus 94a52e4f1d57 == {ix_sync_failures_last_run_id}. Passing side: each table's created indexes at head equal the metadata Index objects compiled with CreateIndex.
tests/db/test_schema.py TestModelsComparison::test_a_planted_difference_is_reported replaced Passing side: new test_the_readings_at_head_are_the_models_declarations checks each table against SQLAlchemy metadata: column (name, compiled type, NOT NULL, pk), foreign keys built from ForeignKeyConstraint elements, created index SQL == CreateIndex compiled, definition == create_table_items(CreateTable compiled), CHECK names == CheckConstraint objects, flags from dialect_options, triggers []. Refusing side: test_a_revision_s_changes_are_read_in_the_tables_it_lists (94a52e4f1d57 vs 2d2ba76b177c: changed tables == that revision's CHANGED_TABLES; keys columns/indexes/foreign_keys/definition/flags; added column last_run_id, index, FK) and test_a_check_s_changed_text_is_read (33f8570f6540 vs 94a52e4f1d57: {ck_github_users_type, ck_pull_requests_state}). Neighbouring fixed revisions keep them fixed. Renamed CHECK, unnamed CHECK/UNIQUE, FK action/name, nullability-only and type-only shapes are held on the passing side and by the scratch reader test.
tests/db/test_schema.py TestModelsComparison::test_a_table_option_is_reported replaced Passing side: flags per table == ('table', len(columns), not with_rowid, strict) from metadata dialect_options. The scratch reader test asserts the exact flags of a STRICT table [('table',2,0,1)] and a WITHOUT ROWID table [('table',1,1,0)].
tests/db/test_schema.py TestModelsComparison::test_index_sql_and_triggers_are_compared replaced Passing side: index SQL == CreateIndex compiled and triggers == [] for every metadata table.
tests/db/test_schema.py TestModelsComparison::test_whitespace_inside_a_literal_is_a_difference replaced The scratch reader test asserts the CHECK item, the partial-index WHERE and the trigger RAISE exactly, with 'a b' kept. Passing side: the definition at head == items of CreateTable compiled, compared exactly.
tests/db/test_schema.py TestObjectsOutsideTheComparison::test_each_planted_object_is_reported replaced test_the_views_a_later_revision_drops_are_reported: the db at 33f8570f6540 against views frozen through 94a52e4f1d57 == [('view', n) for n in sorted(DROPPED_VIEWS)]. test_head_has_none also asserts COUNT(*) of sqlite_master at head == tables + created indexes + UNIQUE autoindexes + non-rowid PK autoindexes (from metadata) + len(frozen) + len(ALEMBIC_OBJECTS). Trigger-on-view, alembic_version index/trigger and sqlite_stat1 shapes are held on the passing side only.
tests/db/test_schema.py TestPlanRules::test_a_changed_rule_is_reported replaced test_an_earlier_revision_reads_its_own_columns_and_keys: nullable_columns(33f8570f6540) - nullable_columns(94a52e4f1d57) == {(github_users, deleted_at), (sync_runs, halt_reason)}. The tables whose unique sets differ == (tables 94a52e4f1d57 adds) | {repositories}, and repositories gained ('u', ('node_id',)). delete_rule_problems == (set(), OUTSIDE) at both revisions. test_references_cascade... adds len(delete_actions(head)) == sum of metadata foreign_keys. The delete rule is held on the passing side.
tests/db/test_schema.py TestPlanRules::test_a_primary_key_reference_set_null_is_reported; test_a_primary_key_column_and_a_second_reference_are_read replaced test_the_cascade_exceptions_are_the_primary_key_references adds: the github_users references read == metadata FKs to github_users. New test_every_composite_primary_key_column_is_declared_not_null takes composite-PK columns from metadata and asserts the set is non-empty and disjoint from nullable_columns(head). New test_the_plan_readers_read_each_column_and_key reads a scratch database of valid SQL with exact results: a nullable composite PK column, and one column with FKs to two tables, both read by delete_actions.
tests/db/test_schema.py TestViews::test_a_changed_view_is_reported replaced test_an_earlier_revision_s_views_are_reported: view_differences(db at 33f8570f6540, frozen_views(through=94a52e4f1d57)) == missing (frozen minus 33f8570f6540.VIEWS) + not frozen (DROPPED_VIEWS) + differs (REPLACED_VIEWS), built from the revision modules' constants. Against its own frozen views 94a52e4f1d57 gives []. schema_support.frozen_views gained an optional through (default head).
tests/db/test_schema.py TestViews::test_whitespace_inside_a_literal_is_a_difference replaced Passing side: views(head) == {name: frozen sql.strip()} for every frozen view, by plain equality. The scratch reader test asserts views() returns CREATE VIEW v AS SELECT 'a b' AS value exactly.
tests/db/test_schema.py _tampered, _redefined (input-building helpers) removed Removed with every test that built a changed copy of head_template. Earlier-schema inputs now come from a module fixture revisions holding databases built by upgrade(path, rev) at 33f8570f6540, 94a52e4f1d57 and 2d2ba76b177c (read-only).
tests/db/test_schema.py TestModelsComparison::test_the_comparison_reads_every_table_and_check renamed Docstring no longer says 'Positive control'.
tests/db/test_tracked_database.py TestFrozenContent::test_a_changed_copy_fails replaced test_a_database_the_migrations_wrote_fails passes the tracked file, head_template, and a database upgraded to FIRST_SCHEMA_HEAD to check_frozen_db.py. It asserts exit 1, that the printed digests (parsed from ': sha256 ') equal hashlib.sha256 of each file, and that the tracked file's digest is neither of the others. test_the_tracked_file_has_the_pinned_digest now asserts the printed digest instead of prose.
tests/db/test_views.py TestConcentratedParticipation::test_a_lookup_by_the_account_alone_is_reported removed SINGLE_COLUMN_INDEXES and the copy-and-alter path in growth() were removed; the class databases are now read-only. test_work_grows_with_the_pull_requests asserts COUNT() per view per size == rows_read(prs) from the seed loop (prs, 2prs, 4*prs), then growth < 6. EXPLAIN QUERY PLAN was not added.
tests/source/test_hydrate.py test_an_assignee_github_id_listed_twice_under_two_node_ids / test_an_assignee_github_id_listed_again_on_a_later_page replaced Passing side only: test_each_assignee_github_id_is_distinct[first-page, overflow], over recorded #15771 and a second page holding EXTRA_ASSIGNEE. The record's github_ids equal the nodes' databaseIds, and the distinct count equals the node count taken from the unit. Refusing side: test_an_assignee_github_id_under_two_node_ids[first-page, overflow], databaseId 175728472 under node ids BOT_kgDOCnlnWA and MDM6Qm90MTc1NzI4NDcy (gap 103), DuplicateEntry (15771, assignedActors, 175728472).
tests/source/test_hydrate.py test_an_assignee_node_id_listed_twice_with_two_github_ids replaced Renamed test_an_assignee_listed_twice[first-page, overflow]. The same node is listed again, with the same databaseId, as when a list shifts between pages. The test asserts type DuplicateEntry and (connection, key) == ('assignedActors', node id).
tests/source/test_recorder.py test_a_failed_recording_writes_nothing replaced The directory first holds real main([]) output written at an earlier clock. A second main([]) runs the real recorder over a replay whose go-gdpr resolve has isPrivate set, and raises PrivateRepository(full_name='prebid/go-gdpr'). The test asserts that the file bytes are unchanged. The '{}' placeholder file and the stub that raised are gone.
tests/source/test_recorder.py test_a_first_sync_that_fails_names_why replaced Asserts the structured report from RecordingFailed.report instead of the full message: exit_code 1, status failed, items_hydrated 0, no failures, the repository's (full_name, record None, outcome None), the error class (Unauthorized or NoRepositoryResolved), and the reason class (None or LabelCountMismatch). The class is parsed from ': '.
tests/source/test_recorder.py test_a_recording_writes_one_file_per_unit replaced main([]) runs at an earlier clock over every scenario, and its file list equals the committed RECORDED list. It runs again without the prebid-cache scenario: that repository's files are gone, the directory holds exactly the later units' dumps, and recorded_at is the later clock's time. The 'stale' placeholder is gone.
tests/source/test_recorder.py test_an_oversized_unit_writes_nothing replaced MAX_FILE_BYTES is no longer patched. The replay serves the largest recorded compare page with its first commit's message lengthened past MAX_FILE_BYTES, and the real recorder records it. main returns 1, and the earlier recording's bytes are unchanged. GitHub does not send duplicate nodes, and served through the replay they would fail the source's compare-count and duplicate checks. A longer commit message is a shape GitHub can send.
tests/source/test_recorder.py test_only_documents_replaces_graphql_units_and_keeps_compare_and_rate_limits replaced An earlier real recording, then --only documents over SCENARIOS without prebid-cache. Compare and reading files keep their earlier bytes, with the count taken from replay.units; GraphQL files equal the new units; the dropped repository's files are gone. The 'compare page added' rule moved to the new test_only_documents_adds_a_compare_page_the_recordings_lack: an earlier recording without #8858, then --only documents adds every compare page, counted from replay.units, and keeps the readings. The edited recorded_at markers and the re-keyed resolve are gone.
tests/source/test_recorder.py test_only_first_sync_writes_its_directory_alone replaced MAX_FILE_BYTES is no longer patched, and the '{}' files are gone. The target first holds write_units() output of a real go-gdpr recording that includes #21. The other recordings directory is an unchanged copy of RECORDED. The stub runs the real first sync over the first-sync recordings; in the oversized case it adds the lengthened compare page. Written case: the target equals the first-sync units and the committed FIRST_SYNC list, and the earlier pull request file is gone. Oversized case: exit 1 and the target's bytes are unchanged. Both cases assert that RECORDED's copy is byte-identical.
tests/source/test_recorder.py test_only_rate_limits_adds_them_and_keeps_every_other_unit replaced First an earlier real recording, then --only rate-limits at a later clock. Asserts calls == [{'scenarios': (), 'users': ()}], both reading files replaced, every other file byte-identical, and the file count equal to the earlier unit count.
tests/source/test_recorder.py recorded_units() and _first_sync_units() helpers removed Their callers now run the real recorder over the replay on a FakeClock.
tests/source/test_recordings.py test_a_changed_batch_document_changes_the_stamp replaced Refusing side: test_a_unit_recorded_for_an_earlier_document_is_refused[pull_request]. It uses the Step 2 unit for Prebid.js#13481, copied byte for byte from c1f474b into tests/fixtures/history/c1f474b/. The real source hydrates #13481, and the test asserts UnexpectedResponse.status == 418 and the exact problem list. Passing side: test_a_batch_stamp_does_not_depend_on_its_size, now parametrized over range(1, PR_BATCH_SIZE + 1). It asserts that the canonical text equals the whole normalized one-alias document, and that the text contains the repository wrapper and the normalized RATE_LIMIT.
tests/source/test_recordings.py test_a_changed_continuation_document_changes_the_stamp replaced Refusing side: test_a_unit_recorded_for_an_earlier_document_is_refused[overflow]. It uses the Step 2 comments continuation of prebid-server#4343 from c1f474b and hydrates #4343 through the real source. Passing side: test_a_continuation_stamp_does_not_depend_on_its_neighbours now also asserts canonical_overflow(query, 2, key) == the normalized one-alias continuation, for every connection in PULL_REQUEST_CONNECTIONS.
tests/source/test_recordings.py test_a_compare_request_with_other_parameters_finds_no_unit replaced Replaced by test_the_compare_requests_the_source_sends_find_their_units. It hydrates #8858 (commits_source compare). The compare_key of each REST request the source sent equals the keys of the recorded compare units, and the count is taken from replay.units.
tests/source/test_recordings.py test_the_replay_refuses_a_batch_alias_bound_to_another_number / _batch_declaring_an_unsent_number / _batch_aliases_out_of_order replaced Passing side only, because only a defect of the documents or the hydrator can fail this check. The new test is test_each_batch_the_hydrator_sends_binds_its_own_numbers[1..PR_BATCH_SIZE]. The real source hydrates copies of #13481. The test asserts replay.problems == [], outcomes Hydrated in order, and batch seen.numbers == [numbers]. It also asserts len(variables) == size + 2, counted from the numbers the test sent.
tests/source/test_recordings.py test_the_replay_refuses_a_continuation_bound_to_another_cursor / _with_an_unbound_variable / _reading_another_alias_id / _declaring_an_unused_alias replaced Passing side only. The new test is test_each_continuation_request_binds_its_own_variables[1..OVERFLOW_ALIASES_PER_REQUEST]. The items are copies of #12975, each with a synthetic second page on labels or closingIssuesReferences, alternating. The test asserts problems == [], each item's continued connection == 2 entries, and one Overflow request with 2*aliases variables.
tests/source/test_recordings.py test_the_replay_refuses_a_request_for_another_document replaced Replaced by test_a_unit_recorded_for_an_earlier_document_is_refused, with three params: the batch alias, the comments continuation, and the go-gdpr resolve, all from c1f474b. Each test checks that its earlier unit has the same kind and key as the current one but a different stamp. It then requests the unit through the real GitHubSource and asserts UnexpectedResponse.status == 418 and exactly one problem with the exact text.
tests/source/test_scenario.py test_a_cursor_the_model_never_served_is_reported replaced Passing side only: test_each_cursor_the_source_sends_is_one_the_model_served, with (comments, 101), (comments, 250), and (commits, 250). It asserts all nodes are hydrated, the Overflow count == ceil(n/PAGE_SIZE)-1, every after0 sent is in pull.cursors[connection].known, and nothing is unanswered. The 'forged' modify is gone.
tests/source/test_scenario.py test_a_document_the_model_was_not_checked_against_is_refused replaced Replaced by test_the_model_stamps_each_kind_as_the_current_documents_do. It asserts _templates().stamps equals the current document's stamp for every (kind, connection), with the key set taken from the RECORDED directory layout and covering every RENDERED kind. It also asserts a current Resolve against the model returns 200 with no problems. The refusing side is in test_recordings, from the c1f474b history units.
tests/source/test_scenario.py test_a_missing_repository_is_answered_only_for_a_checked_document replaced Replaced by test_a_missing_repository_is_answered_null_for_the_current_documents: index and batch for an absent repository each return (200, None) with problems == []. test_a_repository_gone now asserts the exact operations [Resolve, Resolve, PullRequestIndex, PullRequestBatch] with no problems. test_a_repository_gone_between_a_batch_and_its_overflow asserts [Resolve, PullRequestBatch, Overflow, PullRequestBatch] with no problems.
tests/source/test_transport.py TestCredentials::test_a_defect_inside_the_client_is_fatal replaced Replaced by test_the_rate_limit_readers_are_githubkits: every name in _GITHUBKIT_RATE_LIMIT_READERS is a function of the installed githubkit GitHubCore, and the count found equals the set's size, which is above 0. The GitHub-producible unreadable headers in TestOutOfRangeValues and TestMalformedHeaders keep the passing side. The refusing side of the ClientFailure arm stays with the RuntimeError test and the httpx TooManyRedirects, UnsupportedProtocol, LocalProtocolError, and ProxyError cases. The refusing side of a non-reader ValueError inside the client is held by TestCredentials::test_a_token_refresh_githubkit_cannot_parse_is_fatal: githubkit's own OAuth token refresh, answered 200 with an HTML page by something between the client and GitHub, ends in ClientFailure from RequestError from JSONDecodeError, with no wait.
tests/source/test_transport.py TestFatal::test_a_defect_in_the_request_path_is_not_a_rate_limit removed This client held a defect. The rule (a TypeError not raised by githubkit's readers propagates) is now held on its passing side by test_the_rate_limit_readers_are_githubkits and the unreadable-header tests. The propagating arm at transport.py:316 is reached only by a client that breaks githubkit's call contract, so it is tested on its passing side.
tests/sync/engine_support.py World.expects_account_missing removed Two lines (attribute and comment) deleted; only test_engine.py read it and its only setter was the removed patched test. This is a removal in a file marked additive-only; no other file references the attribute (grep: test_seeded_runs.py and test_cli_sync_report.py import World but not it).
tests/sync/test_connection.py test_a_constraint_failure_is_a_local_database_error replaced Now test_a_begin_that_fails_on_a_lock_is_a_local_database_error: a holder's BEGIN IMMEDIATE, open_writer(busy_timeout_ms=50); transaction() raises LocalDatabaseError with cause sqlite_errorname SQLITE_BUSY, the body never runs, and the next transaction on the same writer commits rest_calls=5. Body rollback on a failed commit stays held by test_a_commit_that_fails_on_a_lock_leaves_the_connection_usable.
tests/sync/test_connection.py test_the_body_s_exception_rolls_back renamed KeyError('planted') -> KeyError("the body's own error").
tests/sync/test_engine.py test_a_close_that_fails_leaves_the_row_running_and_exits_1 replaced A Reader opens at the close's 'UPDATE sync_runs SET finished_at' and holds through the busy timeout, so only the close's commit fails. Asserts errors==['SQLITE_BUSY'], row (running, None), report (running, 1), report.error exact, the item written, and the next sync raises ClaimRefused with blocking==(run_id,).
tests/sync/test_engine.py test_a_database_error_mid_batch_is_fatal_and_the_next_run_resumes replaced 26 pulls; scenario.after(PullRequestBatch, when numbers==[26]) opens a Reader (another sqlite3 connection, BEGIN + SELECT = SHARED lock); #26's commit fails SQLITE_BUSY; a handle_error listener records sqlite_errorname and ends the read, so the close lands. Asserts errors==['SQLITE_BUSY'], report.error exact, ended==('failed',1,{failed}), numbers 1..25, counters (26,25), resumed run plans [26] and completes. Uses the short_busy_timeout fixture (100 ms).
tests/sync/test_engine.py test_a_database_not_at_head_starts_no_run replaced upgrade(tmp_path/'older.db', MIGRATION_REVISIONS[-2]) (derived, not a literal revision); world.url points at it; HeadCheckError match on 'revision , not the code's head <HEAD_REVISION>'; sync_runs count 0 in that file; no request.
tests/sync/test_engine.py test_a_run_ending_after_step_7_keeps_its_halt_reason replaced Both endings kept. A real untracked repository (project 'dropped', untracked, synced) gives the prune a statement: a before_cursor_execute listener at 'DELETE FROM repositories' raises KeyboardInterrupt (interrupt) or opens a Reader (fatal), whose SHARED lock fails the prune's commit SQLITE_BUSY; handle_error ends the read so the failed close lands. Asserts one DELETE seen, errors, ended failed/1 or status aborted, halt_reasons exact (Prebid.js halt, dropped None), 26 rows kept, dropped's row kept.
tests/sync/test_engine.py test_an_account_missing_refusal_is_an_item_failure replaced Replaced by test_every_commit_link_reaches_the_writer_with_its_account (passing side): pull 3's commits link accounts that were own-sighted, stored by an earlier run, sighted in another item of the batch, found by a lookup (a Bot), and answered NOT_FOUND (deleted before stored); asserts the linked pr_commits rows equal the linked ids counted from the scenario's commit nodes (5), batch [2,3], one UserLookup, no failures, complete/0. The world fixture keeps its zero-AccountMissing check with no exemption. The writer's own refusing side stays in test_writer.py::test_a_link_neither_stored_nor_given_is_refused (a call it must refuse).
tests/sync/test_engine.py test_an_interrupted_close_that_does_not_land_leaves_the_row_running replaced 'database error' param: the listener opens a Reader before raising KeyboardInterrupt('complete'); close_interrupted's commit fails SQLITE_BUSY and is suppressed. Asserts interrupted list, errors (['SQLITE_BUSY'] or []), re-raised args, row running, ClaimRefused next. 'second interrupt' param unchanged in substance.
tests/sync/test_engine.py test_any_other_exception_after_the_claim_closes_the_run_failed removed Patched apply_in_place_updates to raise RuntimeError (defect-only). The same except-Exception arm is held by test_a_fatal_source_error_closes_the_run_failed (recorded synthetic 401; now asserts report.error.split(':')[0]=='Unauthorized') and by the database-error tests above (LocalDatabaseError).
tests/sync/test_runs.py test_the_claim_refuses_a_database_not_at_head replaced File at MIGRATION_REVISIONS[-2] via upgrade(); HeadCheckError match on the revision clause; sync_runs count 0.
tests/sync/test_runs.py test_another_user_version_is_refused removed Set PRAGMA user_version on a migrated copy (defect-only branch: the revision check refuses every real earlier file first). Passing side moved into test_the_head_check_passes_at_head: on its own sqlite3 connection it reads alembic_version and PRAGMA user_version and asserts them equal to alembic ScriptDirectory's head and the newest key of tests/schema_versions.json (the 2 values check_head compares) before check_head passes.
tests/sync/test_snapshot.py test_the_lock_probe_sees_a_half_consumed_read replaced Half-read server-side cursor removed. test_no_read_is_left_half_consumed replaces the literal 7 with the statements recorded by before_cursor_execute: reads==len(sent)>0, all SELECT, distinct FROM tables==len(sent). New test_the_lock_probe_refuses_while_another_connection_reads: another connection's open read makes commit_does_not_wait raise sqlite_errorname SQLITE_BUSY, and the probe passes once it ends.
tests/sync/test_snapshot.py test_the_digest_sees_a_committed_write removed Positive control that committed an UPDATE. test_writes_nothing now compares every byte, asserts the byte count equals PRAGMA page_count * page_size, and that the snapshot's item_rows sum equals count(*) of pull_requests read by sqlite3.
tests/sync/test_writer.py test_an_unchanged_default_branch_leaves_targets_default_branch replaced No written wrong value. Two valid PRs (bases master, main); rows with targets_default_branch and base_ref=default_branch computed in SQL asserted exactly; the same-branch write's statements are recorded (before_cursor_execute on Engine): an INSERT INTO repositories is seen and no UPDATE pull_requests; rows unchanged. The changing-side test now also asserts exactly one UPDATE pull_requests statement, so the matcher is shown to see that statement.
tests/sync/test_writer.py test_one_account_in_two_shapes_or_under_two_nodes_in_one_item renamed Docstring 'The controls:' reworded to 'Accepted:'.
tests/test_check_any_aliases.py _run (helper) replaced Now runs the script on given paths with env PATH only. It no longer writes any source.
tests/test_check_any_aliases.py test_rejects_an_alias_to_any replaced Replaced by the new test_src_has_no_alias_to_any: src/ exits 0 with 0 aliases, and the scanned count equals an os.walk count of .py/.pyi files. Passing side only; the alias spellings have no input at head.
tests/test_check_any_aliases.py test_accepts_other_uses_of_any removed Held by test_src_has_no_alias_to_any; real src/ uses Any outside aliases.
tests/test_check_any_aliases.py test_scans_stub_files removed Held by the scanned count equalling the os.walk count of .py and .pyi files.
tests/test_check_junit.py _report (helper) replaced A module fixture runs real pytest once and keeps its report. _report parses that report and replaces its testsuite with copies whose counters are changed in memory. The single ET.fromstring call carries # noqa: S314; tests/ is outside the src/ cap.
tests/test_check_log_placeholders.py _run (helper) replaced Now runs the script on the paths it is given, with env PATH only. It no longer writes any source.
tests/test_check_log_placeholders.py test_accepts_brace_style_and_other_calls removed Held by the same src/ passing side with the exact file count.
tests/test_check_log_placeholders.py test_flags_a_percent_style_message removed The rule is now held only on its passing side, by test_src_has_no_percent_style_messages: src/ exits 0 with 0 messages, and the files scanned equal an os.walk count of .py/.pyi files.
tests/test_check_log_placeholders.py test_reports_each_site removed No real site exists, so the per-site listing format has no input. The count rule is held by the src/ passing side.
tests/test_check_query_lock.py test_a_removed_entry_fails; test_a_deleted_lock_removes_every_entry; TestSchemaLock::{test_a_pair_removed_from_a_base_entry_fails, test_a_renamed_pair_fails, test_a_removed_entry_fails, test_a_deleted_lock_removes_every_entry, test_a_pair_removed_from_an_object_entry_fails} replaced Real lock files placed over a later base: query 21a4267->c1f474b (the removed key computed from both files), c1f474b deleted; schema 21a4267->6fedb05 (version 2 removed), 6fedb05 deleted, 77663a7->21a4267 (the lost pair computed: sync_failures.last_run_id). A pair removed from a list-form entry and a renamed pair have no real producer; those two tests were removed and the rule is held by the object-entry removal.
tests/test_check_query_lock.py test_an_absolute_lock_path_is_an_error_not_a_pass; test_the_checker_runs_isolated_from_site_packages; test_the_verdict_does_not_depend_on_the_working_directory; TestSchemaLock::test_both_locks_are_checked_by_default; TestSchemaLock::test_lock_selects_one replaced Absolute --lock over the committed, unedited 21a4267 lock exits 2. Both locks by default: both real reverse pairs give exit 1 with stderr lines prefixed by the query lock and then the schema lock. --lock selects one: query reverse pair, schema unchanged; the default exits 1; --lock tests/schema_versions.json exits 0 with stdout exactly the schema kept line and empty stderr.
tests/test_check_query_lock.py test_the_step_fails_an_edited_entry; test_the_step_runs_the_base_checker_not_the_pull_requests; test_the_step_fails_a_pair_removed_from_a_schema_entry; helpers edit_entry, edit_entry_and_relax_checker replaced The base pushes the current checker with 21a4267's query lock and 77663a7's schema lock. The PR puts back c1f474b's query lock (test_the_step_fails_a_removed_entry, exit 1) or 21a4267's schema lock (exit 1, lost sync_failures.last_run_id). For the base-checker test the PR also deletes its copy of the checker; the step exits 1 and its stdout names runner-temp/check_query_lock.py. Running the PR's missing copy would exit 2. Both helpers were removed.
tests/test_check_query_lock.py TestSchemaLock::{test_a_pair_added_to_a_superseded_entry_fails, test_a_pair_added_to_a_superseded_object_entry_fails, test_a_view_added_to_a_superseded_entry_fails} removed Passing side: test_each_change_the_history_made_passes (version 1 kept with version 2 added; a pair added to the newest version 2).
tests/test_check_query_lock.py TestSchemaLock::{test_an_edit_to_a_superseded_entry_fails, test_a_view_edit_below_the_newest_entry_at_the_base_fails, test_a_removed_view_fails, test_an_entry_rewritten_as_a_list_loses_its_views} removed Passing side: the real repository locks and the history pairs pass. The 6 version-2 view digests are held against frozen_views by test_data_dictionary.py.
tests/test_check_query_lock.py test_a_changed_entry_fails; TestSchemaLock::test_a_changed_view_digest_fails removed Passing side: new test_each_change_the_history_made_passes (query c1f474b->21a4267, schema 6fedb05->21a4267, schema 21a4267->77663a7, each exit 0 with the 'N entries at main kept; M entries now' line computed from json of both files) and test_the_repository_locks_are_read_by_their_rules.
tests/test_cli_app.py test_a_traceback_shows_no_local_variables replaced No command added to the app. Passing side: the 'ghactivity' console_scripts entry point loads the root app object, and app.pretty_exceptions_show_locals is False (typer's Typer.call attaches the called app's setting to any exception, so sub-app commands get the root's setting; read from the installed typer/main.py).
tests/test_cli_app.py test_an_unreadable_env_file_exits_1_without_a_traceback[unreadable] replaced Replaced by TestInstalledEntryPoint::test_an_env_file_that_is_not_utf8_exits_1_without_a_traceback (a real non-UTF-8 .env through the installed entry point) and test_an_env_file_the_os_refuses_to_read_exits_1 (pydantic-settings' read of .env answers PermissionError, whatever uid runs the tests; exit 1, SystemExit).
tests/test_cli_app.py test_a_commands_own_exit_code_passes_through; test_an_aborted_command_line renamed Variable planted -> commands.
tests/test_cli_sync.py test_a_database_not_at_head_exits_1 replaced File at MIGRATION_REVISIONS[-2]; DATABASE_URL set to it and settings cache cleared; exit 1, the 'Sync did not start: the database is at revision , not the code's head ' line, no sync_runs rows in that file, no request.
tests/test_cli_sync.py child tests (7) and _child/CHILD renamed plant/{plant} and the SIGINT comment renamed to setup. Child env is now PATH plus fixed values: HOME=cwd (tmp_path), GITHUB_SHA=test (no git), token, DATABASE_URL, TRACKING_CONFIG_PATH; test_the_installed_command_exits_with_the_engines_code asserts trigger manual and code_version test in sync_runs.
tests/test_config.py TestDocumentedDatabaseUrl::test_names_the_local_database[.env.example, docs/development.md] replaced The checks on document wording are removed. TestDefaultDatabaseUrl keeps test_the_field_default_is_the_local_database and adds test_a_env_copied_from_the_example_reads_the_local_database: .env.example is copied unchanged to tmp_path/.env, and Settings() under chdir(tmp_path) gives the local database URL.
tests/test_count_suppressions.py test_accepts_files_it_reads_as_ruff_does replaced Held by the same in-process refusal() == None over every regular file under src/. The BOM, CRLF and coding-declaration cases have no real input.
tests/test_count_suppressions.py test_at_the_cap_passes_and_one_more_fails replaced Now test_at_the_cap_passes_and_one_below_fails, parametrized by kind. Caps come from the second reading of src/; exit 0 at the cap, exit 1 one below, and the sorted listed sites equal the second reading's sites.
tests/test_count_suppressions.py test_counts_every_noqa_spelling replaced Replaced by test_src_is_within_the_hook_caps (src/ at caps 5/3: exit 0, empty stderr, scanned == os.walk count, counts == a second tokenize reading) and by the noqa case of test_at_the_cap_passes_and_one_below_fails (both sides on src/: exit 0 at cap k, exit 1 at k-1 listing exactly the second reading's sites, which is runs.py:457 at base).
tests/test_count_suppressions.py test_counts_every_type_ignore_spelling replaced Passing side on src/ with the type: ignore count equal to the second reading's count (computed, not a literal 0), plus the type: ignore case of the cap test. src/ holds no type: ignore, so its below-cap run uses -1 and lists nothing.
tests/test_count_suppressions.py test_directive_text_outside_a_directive_comment_is_not_counted replaced Runs on scripts/count_suppressions.py, whose docstring STRING token holds # noqa and # type: ignore (checked with tokenize). It asserts exit 0, scanned 1, and counts equal to the second reading of its comment tokens. It uses the default caps and computed counts instead of caps 0 and a literal (0, 0).
tests/test_count_suppressions.py test_refuses_a_file_it_cannot_read_as_ruff_does replaced Now test_reads_every_file_under_src_as_ruff_does, run in process with the script loaded by path. It asserts all_files([src]) equals the os.walk list of regular files and refusal() is None for each of them, so the identify branch runs on py.typed and tracking.toml. test_src_is_within_the_hook_caps also asserts empty stderr. The individual refused shapes (latin-1, unicode_escape, unknown codec, lone CR, .PY, .PYI, executable shebangs, .wsgi) have no real input and are passing-side only.
tests/test_count_suppressions.py _package (helper) removed Only fed the removed test-written inputs.
tests/test_count_suppressions.py test_counts_across_files_and_subdirectories removed Held by the scanned count over the nested src/ tree equalling the os.walk count.
tests/test_count_suppressions.py test_counts_stub_files removed Held by the scanned count equalling an os.walk count of .py and .pyi files. The repository has no .pyi file.
tests/test_count_suppressions.py test_skips_entries_named_py_that_are_not_files removed Held by the scanned count equalling an os.walk count of regular .py/.pyi files. The skip side has no real input.
tests/test_data_dictionary.py TestDictionary::test_differences_are_reported_both_ways replaced Inputs are history dictionaries at 21a4267 and 77663a7 (tests/fixtures/history) and databases at 33f8570f6540, 94a52e4f1d57 and 2d2ba76b177c. dict@77663a7 vs db@94a52e4f1d57 == ['sync_failures.last_run_id is not at head']. dict@21a4267 vs db@2d2ba76b177c == ['no entry for sync_failures.last_run_id']. The object lines of dict@21a4267 vs db@33f8570f6540 == no entry for sorted(DROPPED_VIEWS) + 'is not at head' for the objects db@94a52e4f1d57 adds. New test_each_commit_s_dictionary_matched_its_schema checks each dictionary against its own revision: [].
tests/test_data_dictionary.py TestSchemaLock::test_a_listed_pair_that_is_not_consumer_at_head_is_reported; test_a_consumer_pair_missing_from_the_entry_is_reported replaced Version 2 of the schema lock at 21a4267 vs consumer pairs of dict@77663a7 == ['sync_failures.last_run_id is missing from the entry']. Version 2 at 77663a7 vs dict@21a4267 == ['sync_failures.last_run_id is not a consumer column at head']. Each commit's own pair gives [].
tests/test_data_dictionary.py TestSchemaLock::test_a_view_returning_other_rows_fails_the_lock removed test_the_digest_is_of_the_exact_sql now also asserts view_digest == sha256 for every frozen view at head, with len(frozen) == len(views(head)). The lock-digest test (current entry == digests of frozen SQL) is unchanged.
tests/test_data_dictionary.py test_database_columns_reads_every_user_object renamed planted.db renamed scratch.db.
tests/test_db_engine.py TestSqliteConnectionSettings::test_listener_failure_closes_the_connection replaced test_an_interrupt_in_the_listener_closes_the_connection: a stub cursor raises KeyboardInterrupt at the first pragma (an interrupt at a set point). It asserts KeyboardInterrupt propagates, executed == ['PRAGMA foreign_keys=ON'], the one cursor is closed, and the connection is closed.
tests/test_declared_dependencies.py test_a_name_several_distributions_install_needs_all_of_them_declared replaced undeclared_imports gained an optional owners map, defaulting to packages_distributions(). The test passes the real map with sqlalchemy installed by [SQLAlchemy, identify] (one Undeclared with providers (identify, sqlalchemy) per sqlalchemy importer), by [SQLAlchemy, alembic] (none), or by no distribution (one per importer, providers ()). The top_level.txt/no-RECORD cases tested importlib.metadata and were dropped.
tests/test_declared_dependencies.py test_reports_undeclared_imports_in_a_planted_package replaced Now test_reports_what_an_earlier_pyproject_did_not_declare. It reads src/ against tests/fixtures/history/ea7d12e/pyproject.toml, the real file from the parent of the commit that declared httpx (gap 76), copied byte-identical. Expected: one Undeclared per src file whose text imports a dependency declared since. Those names come from packaging's Requirement names (httpx and click), and the files from a regex. At base that is cli/app.py and cli/sync.py (click) and source/client.py and source/transport.py (httpx).
tests/test_declared_dependencies.py _install (helper) removed No distributions are installed on disk. The environment's answer is stubbed in memory (next record).
tests/test_import_boundaries.py test_flags_database_imports_from_source replaced Replaced by test_the_scan_finds_each_database_import_in_sync: the (file, line) set of database_imports over sync/ equals a regex match of sqlalchemy/github_activity_db.db import statements (20 statements at base). Resolving relative from ..db imports has no real input and is passing-side only.
tests/test_import_boundaries.py test_does_not_flag_other_text removed Held by the exact-count passing side over the 39 real files.
tests/test_import_boundaries.py test_flags_every_githubkit_import_spelling removed The refusing side is the renamed source/ test (module-level and TYPE_CHECKING from-imports); the passing side is the exact-count scan. import githubkit, the importlib/import forms, name= and class-body imports have no real input.
tests/test_import_boundaries.py test_the_scan_sees_source_importing_githubkit renamed Renamed test_the_scan_finds_each_githubkit_import_in_source and strengthened: the (file, line) set of githubkit_imports over source/ equals a regex match of ^\s*(from|import) githubkit\b over the same text. At base that is 5 statements and 14 findings, including 2 inside TYPE_CHECKING blocks.
tests/test_server_side_cursors.py test_no_server_side_cursor_in_src replaced Bound > 30 replaced by equality with an os.walk count of *.py under src/. The unused pytest import was removed.
tests/test_server_side_cursors.py test_does_not_flag_other_code removed Held by the src/ passing side; snapshot.py's docstring names the options.
tests/test_server_side_cursors.py test_flags_each_spelling removed Held on its passing side by the exact-count src/ scan.
tests/test_suite_rules.py test_every_subprocess_wait_in_the_suite_has_a_timeout replaced Bound len(files) > 30 replaced by equality with an os.walk count of *.py under tests/. Recounting with an ast walk would also re-derive the checker's own walk.
tests/test_suite_rules.py test_flags_each_shape_it_does_not_model removed Held on its passing side by the exact-file-count scan of tests/. The unused pytest import was removed.
tests/test_wheel_smoke.py test_an_override_cannot_hide_a_missing_packaged_file replaced Replaced by test_rejects_an_override_before_reading_it: an override naming a missing file exits 1 with one stdout line naming that path and empty stderr, so the refusal comes before any read. Its ordering against a missing packaged file has no input.
tests/test_wheel_smoke.py package_without_resource (fixture) removed The package copy is gone.
tests/test_wheel_smoke.py test_fails_when_the_packaged_file_is_missing removed A wheel without tracking.toml comes only from this project's build configuration. The rule is held by the passing side (count equals tomllib's) and by CI's built-wheel job.

Ran: the full suite twice on CPython 3.13 at 5f5b0e39691e61dbbb6d89cfd9e4d7a6e44b1100 and the --same-as comparison; pre-commit over all files; the parent-commit runs listed above.
Not run: CI's Python 3.12 job (CI runs it on this PR); a live GitHub request; a real SIGINT to alembic upgrade from a shell; the macOS system-proxy path (read from source: no_proxy in the environment short-circuits it).

D40 records the maintainer's rule: the same code gives every test the same
outcome on any machine, date, and run order; tests read no wall clock, reach
no network, use no unseeded randomness, and bound every wait; they assert on
structured results, never on a document's wording; hard inputs come from
software the project builds; and acceptance names a test, a check, or a
command, never a text search or a review.

- The plan, CLAUDE.md, docs/testing.md, and CONTRIBUTING.md state D40.
- Steps 3b-2a-2d, 3b-2a-3, 3b-2b-1, and 3b-2b-2 accept on software: the
  step's tests failing on the parent's code and passing on the head, or two
  suite runs agreeing.
- scripts/check_junit.py --same-as fails unless a second JUnit report holds
  the same tests with the same outcome each.
tests/db/test_schema.py
- Remove _tampered and _redefined and every test that rebuilt a copy of the head template
  (changed definitions, a dropped index, probe indexes and triggers, literal whitespace, table
  options, changed views, objects outside the comparison, changed plan rules, a dropped
  foreign-key index) and the create_all checker's written module.
- Head against SQLAlchemy's metadata, table by table: columns, foreign keys, created index SQL
  (CreateIndex), definition items (CreateTable), named CHECKs, flags, no trigger.
- Databases the migrations built at 33f8570f6540, 94a52e4f1d57, and 2d2ba76b177c: the readings
  differ only in the table 2d2ba76b177c lists, in its added column, index, and foreign key;
  94a52e4f1d57's changed CHECK text; view_differences and objects_outside_the_comparison
  against the views frozen through 94a52e4f1d57, expected from DROPPED_VIEWS, REPLACED_VIEWS,
  and VIEWS; nullable columns and UNIQUE sets across 33f8570f6540 and 94a52e4f1d57.
- Scratch databases of valid SQL for the readers: CHECK text, partial and expression indexes,
  a trigger and a view with whitespace in a literal, STRICT and WITHOUT ROWID flags, a nullable
  composite primary-key column, and a column with two foreign keys.
- Exact counts from the models: sqlite_master rows at head, delete actions, references to
  github_users, foreign keys examined for an index; create_all_uses counts the files it reads
  (checked with os.walk) and the tokenizer's names and strings.
- CHECK failures assert sqlite_errorname SQLITE_CONSTRAINT_CHECK and the constraint's name.

tests/db/schema_support.py
- frozen_views takes an optional revision to stop at (default head); foreign_key_columns
  lists the foreign keys unindexed_foreign_keys examines.

tests/db/test_migrations.py
- Remove the test-only revisions holding an orphan row, module-level code, a failure, COMMIT,
  END, ROLLBACK, BEGIN, refused statements, ATTACH, or a batch alter under views; the
  connection put inside a transaction; the copies changed for the definition reading; the
  revision checker's written files.
- current, check, stamp, and upgrade over a file of seeded rows, and the chain from
  94a52e4f1d57 over seeded rows, each with the rows of every table with a foreign key.
- An interrupt at a set point after the second revision: the file stays at the first schema.
- env.py's own authorizer, captured through a recording connection factory and called at each
  revision of the real chain and after the commit, with every answer exact.
- A failed commit and the write lock on the real chain from 94a52e4f1d57, the timeout from the
  URL, sqlite_errorname SQLITE_BUSY, and the statements run.
- The tag-table refusal over valid tag rows; the step 3a upgrade's views as frozen; table
  counts for the CHANGED_TABLES tests; legacy_alter_table in the connection probe; the
  refused tracked-file connection is closed; revision_source counts imports and VIEWS against
  the tokenizer and the loaded modules.
- Settings read no .env from the checkout; the alembic command runs outside it.

tests/db/test_tracked_database.py
- The frozen-content check fails on databases the migrations wrote (head, first schema); the
  printed digests equal the files' SHA-256.
- git and pre-commit run with an explicit environment (own HOME, no global or system config);
  the scripts with PATH only.

tests/db/test_views.py
- Remove the row id projected under a consumer name and the single-column index copies; exact
  row counts for the views compared.

tests/db/test_types.py
- The column default reads a fixed clock; rejected values assert the driver error's class.

tests/db/test_application_engine.py, tests/test_db_engine.py
- Settings read no .env from the checkout; foreign-key failures assert sqlite_errorname; the
  connect listener's failure is an interrupt at its first statement.

tests/test_data_dictionary.py
- differences and lock_differences over the dictionary and schema lock of 21a4267 and 77663a7
  and databases at 33f8570f6540, 94a52e4f1d57, and 2d2ba76b177c; every frozen view's digest;
  the scratch database renamed.

tests/test_check_query_lock.py
- Removed and deleted entries, pairs, and versions come from the lock files of c1f474b,
  6fedb05, 21a4267, and 77663a7; the history's own changes pass; edits with no real producer
  are held on the passing side.
- The CI step runs on those files; the base checker is shown by a pull request that deletes
  its copy.
- git, the checker, and the step run with an explicit environment and fixed commit dates.

tests/fixtures/history
- The lock files and data dictionaries above, copied unchanged from git show, listed in
  README.md.
tests/sync/test_engine.py
- A database error mid-batch, a close that fails, a failed close after
  step 7, and an interrupted close whose own commit fails now come from
  another connection holding a read open (SQLite's SHARED lock) through
  the busy timeout, not from triggers or patched writer calls. A
  handle_error listener records each error's sqlite_errorname
  (SQLITE_BUSY) and, where the run goes on to close, ends the read
  first. The engine's connections wait 100 ms instead of 30 s
  (db.engine.SQLITE_BUSY_TIMEOUT_MS, a short_busy_timeout fixture).
- The run ending after step 7 prunes a real untracked repository; the
  interrupt is raised at the prune's DELETE, and the fatal case fails
  the prune's commit on the held read. Both keep the halt reason, and
  the prune deletes nothing.
- The AccountMissing test that patched write_pull_request is replaced
  by a passing-side test: commit links whose account was sighted by the
  item, stored earlier, sighted in the batch, found by a lookup, and
  answered NOT_FOUND, with the linked pr_commits rows compared to the
  linked commits counted from the scenario model.
- The RuntimeError test that patched apply_in_place_updates is removed;
  the same except arm is held by the recorded 401 and by the database
  error above. The 401 test checks the error's class field.
- A database not at head is a file built by the migrations to the
  revision before head (MIGRATION_REVISIONS[-2]), not an edited
  alembic_version.

tests/sync/engine_support.py
- World.expects_account_missing is removed with the patched test; the
  world fixture's AccountMissing check has no exemption.

tests/sync/test_runs.py
- The user_version test is removed; the head check's passing side reads
  alembic_version and PRAGMA user_version on its own connection and
  compares them with alembic's script head and the newest key of
  tests/schema_versions.json.
- The claim's head refusal uses a file at the revision before head.
- The held-lock claim asserts sqlite_errorname SQLITE_BUSY and the
  connection's busy_timeout instead of elapsed time; the claim-in-
  progress test commits on an event set at the claim's BEGIN IMMEDIATE
  instead of sleeping, and asserts busy_timeout instead of elapsed time.
- The refused stale reclaim drops its message substring; blocking is
  asserted.
- git runs with HOME and GIT_CEILING_DIRECTORIES at tmp_path, no global
  or system config, no inherited GIT_* variables, fixed author and
  committer dates, and an environment built from PATH and fixed values.

tests/sync/test_writer.py
- The unchanged-default-branch test records the statements the write
  sends and asserts none updates pull_requests, over valid rows; the
  changing side asserts the one update it sends.
- Each ItemConflict refusal also asserts the error's number field.
- "The controls" in a docstring is reworded.

tests/sync/test_snapshot.py
- The digest's positive control is removed; test_writes_nothing compares
  every byte of a file whose size equals page_count * page_size and
  whose pull requests the reader read.
- The half-read cursor control is removed; the read count comes from the
  statements recorded as they reach the database, one per table, and
  the probe's refusing side is another connection's open read.

tests/sync/test_connection.py
- The CHECK-violating insert is replaced by a BEGIN IMMEDIATE that fails
  on another connection's write lock; lock errors assert
  sqlite_errorname SQLITE_BUSY instead of message text.
- The body's own KeyError no longer says "planted".

tests/test_cli_sync.py
- A database not at head is a file at the revision before head.
- The replay fixture sets GITHUB_SHA and unsets GITHUB_EVENT_NAME, so
  no git runs and the trigger is fixed; the first sync and the option
  test assert the trigger and code version, and the run's counters are
  asserted in sync_runs.
- Child processes get an environment of PATH and fixed values (HOME is
  the test's directory, GITHUB_SHA set); the child setup hook is renamed
  from plant to setup.

tests/test_cli_app.py
- The traceback test no longer adds a failing command to the app; it
  asserts the console script loads the root app and that app's
  pretty_exceptions_show_locals is False.
- The exit-code test apps are renamed from planted to commands.
- Installed-script children get PATH and HOME (the test's directory)
  only.
tests/test_check_log_placeholders.py: remove the written-source cases
(flagged calls, accepted calls, per-site listing). The run over src/ asserts
exit 0, no message found, and the files scanned equal an os.walk count of
.py/.pyi files. The empty-directory refusal asserts exit 1 and empty stdout
instead of stderr wording. Subprocess env is PATH only.

tests/test_count_suppressions.py: remove the written spellings, packages,
refused/accepted files, the chmod 000 file, and the non-file entries. src/
at the hook caps: exit 0, empty stderr, files scanned equal to os.walk,
counts equal to a second reading with tokenize. Caps at and one below each
count over src/: exit 0, then exit 1 listing exactly the sites the second
reading finds. scripts/count_suppressions.py, whose docstring holds
directive text, is counted as its comments alone. refusal() returns None
for every regular file under src/ (os.walk list). identify's read of a
file stubbed to raise PermissionError: refusal() reports it and main()
exits 1 naming each non-Python file; unstubbed, exit 0. Empty directory:
exit 1, empty stdout.

tests/test_check_any_aliases.py: remove the written alias, non-alias, and
stub cases. Add the run over src/: exit 0, no alias, files scanned equal to
os.walk. Empty directory: exit 1, empty stdout.

tests/test_suite_rules.py: remove the written-shape test; the scan over
tests/ asserts its file count equals os.walk's.

tests/test_import_boundaries.py: remove the written spellings, text, and
package copy. Both passing sides assert the file count from os.walk and
that the (file, line) of every import the scan read equals a tokenize
reading of the same files. source/'s githubkit imports and sync/'s
SQLAlchemy and db/ imports are found at exactly the statements a text
match finds.

tests/test_server_side_cursors.py: remove the written calls; the src/ scan
asserts its file count equals os.walk's.

tests/test_declared_dependencies.py: remove the written package and the
distributions installed on disk. src/ against pyproject.toml at ea7d12e
(tests/fixtures/history, before httpx was declared) reports exactly each
file importing a dependency declared since, by text match and packaging's
requirement names. The environment's top-level map, stubbed in memory
with sqlalchemy installed by an undeclared sibling, by declared ones, or
by none, gives one finding per sqlalchemy importer, none, and one with no
provider. undeclared_imports takes an optional owners map. The src/ pass
asserts the files scanned equal os.walk's.

tests/test_wheel_smoke.py: remove the package copy without tracking.toml.
Runs use -I as CI does, an explicit env, and a temporary working
directory. The packaged count equals tomllib's count of the source file;
refusals assert exit 1, empty stderr, and the value named. An override
naming a missing file is refused before any read.

tests/test_check_junit.py: reports are pytest's own, or built in memory
from one with its counters changed. Assertions are exit codes, the
counters the gate prints, the number of problems, and empty stdout for
an unreadable report or a refused allowance. Nested pytest gets its own
pytest.ini and an explicit env without plugin autoload.

tests/fixtures/history/: ea7d12e's pyproject.toml, unchanged, and a
README naming its source commit.
tests/source/recorder.py: record() and record_first_sync() date units from the run's clock
(SystemClock by default) instead of datetime.now. PrivateRepository carries full_name;
RecordingFailed carries the failed items, the failed lookups, and the first sync's report.

tests/source/test_recorder.py: every record() and first sync runs on a FakeClock and asserts
recorded_at from it. Refusals assert structured fields: PrivateRepository.full_name,
(number, CompareRefused, 404) for a failed item, ServerError causes for every looked-up id,
and the first sync report's exit code, status, error and reason classes, and failure numbers.
main() runs over the real recorder and the replay: an earlier recording at one clock, a later
one at another, compared by file bytes. A scenario no longer listed leaves no file; a private
resolve leaves the earlier recording; --only rate-limits replaces only the readings;
--only documents drops a scenario's GraphQL units, keeps compare pages and readings, and adds
the compare pages an earlier recording lacked. The patched MAX_FILE_BYTES is replaced by the
largest recorded compare page with its first commit's message lengthened past the limit, for
main() and --only first-sync. The '{}' and 'stale' placeholder files are replaced by recorder
output.

tests/source/test_recordings.py: the edited batch and continuation documents and the replay
refusals of edited bindings are replaced. Refusing side: units the recorder wrote at Step 2
(c1f474b), committed unchanged under tests/fixtures/history/, are refused by the replay when
the real source requests them (batch alias, comments continuation, resolve). Passing side:
batch stamps at every size 1..PR_BATCH_SIZE equal the whole one-alias document, wrapper and
rateLimit included; continuation stamps equal the one-alias document; every batch size and
every continuation request size the hydrator sends passes the binding checks, with the
aliases and variables counted; the compare requests for #8858 find exactly the recorded
compare units. The no-unit refusal asserts the whole problem list.

tests/source/test_scenario.py: the edited Resolve, index, and batch documents and the
endCursor rewritten to a cursor never served are replaced: the model's stamps equal the
current documents' for every kind and continued connection in the recordings' layout; a
missing repository is answered null for the current index and batch documents; the
repository-gone tests count the requests answered; each cursor the source sends is one the
model served, one continuation per further page. The unanswered() test sends a request the
replay cannot answer instead of appending to problems.

tests/source/test_hydrate.py: an assignee listed twice is the same node again (first page
and overflow); the github_id comparison is tested on its passing side over one page and two.
PagingStalled asserts its fields instead of its message.

tests/source/test_resolve_index_users.py: IndexOrderViolation asserts full_name, connection,
number (from the recorded index page), and problem.

tests/source/test_transport.py: the TypeError and ValueError raised from inside a client and
the incompatible client are replaced by a check that the rate-limit reader names are methods
of the installed githubkit; the JWT client failure asserts its RequestError cause; the system
clock's jitter runs on a seeded random.Random.

tests/test_logging.py: no sleep; level selection reads the console handler's lines; file,
stdlib, and bound-context tests assert loguru records' level, message, and extra.

tests/test_config.py: the docs/development.md and .env.example wording test is replaced by
Settings reading a .env copied from .env.example; get_settings runs in a temporary working
directory with its cache cleared.

tests/test_tracking_config.py: the loader's refusals assert the cause type.
…nv test

- cli/sync.py measured the report's seconds with time.monotonic(), a wall
  clock in every code path a CLI sync test runs. It now reads the clock the
  run reads (common.clock()), and a test advances the fake clock seven
  seconds during the resolve and finds "7 s" in the report.
- The unreadable .env test made the file mode 000, which a root user can
  still read. The OS's refusal is now stubbed at the dotenv read
  (PermissionError), whatever uid runs the tests; the not-UTF-8 case stays a
  real file through the installed command.
- D39's inputs name a timing setting a test shortens (SQLite's busy
  timeout), which changes how long a wait lasts and no outcome.
The module-level .env test sat inside TestInstalledEntryPoint, so the
class's last four methods (six test ids) became functions nested in it,
which pytest never collects. The test now follows the class.
tests/determinism.py, installed for the session by the conftest, refuses
a network connection other than a Unix socket, every host-name lookup,
and SystemClock's now, sleep, and unseeded random source. A refusal
raises NotDeterministic (a BaseException) and is recorded; a test that
reached one fails after it runs even where the code caught it.

tests/test_suite_rules.py reads source: no test file reads the wall
clock, sleeps on it, or draws unseeded random numbers; the package
reads them only in SystemClock and db.types.utc_now; and no test
function is defined inside another function. scripts/check_junit.py
--same-as is tested on both sides over reports pytest wrote.

The plan's 3b-2a-2d scope names the one src change (the sync report's
timing), and CLAUDE.md, the testing guide, and CONTRIBUTING.md describe
what enforces D40.
- tests/determinism.py sets no_proxy to *, so the source built without
  a recorded transport is refused at GitHub's host whatever proxy the
  environment or the macOS settings name; it refuses every host-name
  lookup the socket module offers, and connect_ex reports its own name.
  The conftest installs it at configure time and checks the record after
  every test and at the end of the session, without clearing it first.
- Tests for connect_ex, each lookup, getfqdn, the Unix-socket pass-through
  of connect and connect_ex, and the source under four proxy settings.
- The source scan registers `import os.path` as os, counts Random(None)
  as unseeded, and reads annotations that hold a call or Annotated
  metadata; its docstring names what it reads and what is out of scope.
- check_junit --same-as reads a test pytest reports in two <testcase>
  elements (a failure, then a teardown error) as one outcome,
  error+failed, instead of refusing the report.
- docs/testing.md no longer describes the replaced tests.
…it readers and an assignee databaseId under two node ids

test_transport.py: a ValueError raised inside the HTTP client by anything
other than githubkit's rate-limit readers is a ClientFailure with no wait.
The input is githubkit's own OAuth token refresh, run inside client.send,
parsing a 200 from github.com/login/oauth/access_token whose body is the
endpoint's form-encoded default format: response.json() raises
JSONDecodeError. The test asserts the ClientFailure, its RequestError
cause, the JSONDecodeError's msg, doc, and pos, no sleep, and that the
token request was the only request sent.

test_hydrate.py: databaseId 175728472 under node ids BOT_kgDOCnlnWA and
MDM6Qm90MTc1NzI4NDcy among #15771's assignees, on one page and across an
overflow page, fails the item with DuplicateEntry(15771, "assignedActors",
175728472); the node-id comparison passes both entries.
…tatement cache

SQLAlchemy invalidates the migration connection when an interrupt ends a
statement. env.py's handler then wrote Connection.info, which revalidates
the connection and raised PendingRollbackError in place of the interrupt.
The handler now writes the authorizer's mapping, held before the upgrade
runs, so the original exception propagates and the failed state is still
recorded.

- test_an_interrupt_after_the_second_revision_applies_none: the
  KeyboardInterrupt raised at the set point is the exception the upgrade
  ends with, the pool invalidated the connection once for it, and the file
  keeps its revision and contents.
- test_a_revision_that_refuses_leaves_env_py_only_its_rollback: after the
  real chain's refusal of a repositories row, the authorizer allows
  ROLLBACK and refuses BEGIN and COMMIT.
- test_the_driver_keeps_no_prepared_statements: the driver receives
  cached_statements=0 whether or not the caller's URL carries
  ?cached_statements=128.
- The token-refresh test's non-JSON answer is something between the
  client and GitHub (a captive portal or a proxy's block page) answering
  200 with HTML; githubkit asks GitHub for JSON, so GitHub's own
  form-encoded format is not what that request gets.
- The refusing-revision test asserts SQLAlchemy never invalidated the
  connection, the premise its docstring states.
- The plan's 3b-2a-2d scope names the env.py change, and the
  prepared-statement evidence row says what the test holds.
D39 bans mutation testing (changing the code under test or a check to
see whether a test notices), false content planted in the project's real
files or documents, and scoring a reviewer on seeded flaws. Its first
wording also banned ordinary negative tests; the clarified wording
expects them: a test builds its own invalid or failing input and feeds
it to the unchanged code, and every check is tested on both sides.

- The rule in the plan's D39 row, its Step 3 restatement, CLAUDE.md,
  docs/testing.md, and CONTRIBUTING.md; D40's tail in each copy.
- Step 3b-2a-2d keeps the negative tests on main, beside the tests it
  adds over real revisions and history files.
- The REST sampler's one mismatch per field, verify's stored-row checks
  on a copy with a row deleted, and snapshot's refusals on a changed
  copy return to their steps.
The negative tests main had for the source and lock checkers, the
wheel smoke check, the suppression counter, the import boundaries, the
server-side cursor scan, the suite rules, the data dictionary's lock,
the engine's connect listener, and the settings default, back as they
were, beside the tests this step added.

- test_check_any_aliases, test_check_log_placeholders: _run_on_source
  writes the sample module and runs the check over it with head's _run.
- test_wheel_smoke: _run_package runs the check with -s and PYTHONPATH,
  since -I ignores PYTHONPATH, for the copy without tracking.toml.
- test_check_query_lock: V3 follows V2, which it extends.
- Not restored: test_config's check of the documented DATABASE_URL lines
  (a document's wording, D40); two query-lock cases that
  test_the_verdict_does_not_depend_on_how_the_checker_runs holds.
The negative tests main had for the replay's refusals of edited query
documents and requests, the recordings' stamps, the scenario model's
refusals, the transport's client defects, the hydrator's assignee listed
twice with two databaseIds, the writer's connection on a constraint
failure, the engine with the writer raising, the run claim on another
user_version, and the snapshot digest and lock probe, back as they were.

- test_recordings: _first_refused reads the first problem the replay
  records, as main's _refused returned it.
- The engine's world fixture and World.expects_account_missing let the
  test that fakes the writer's AccountMissing refusal leave that failure.
- Not restored: two assignee tests that
  test_an_assignee_github_id_under_two_node_ids holds with real ids.
The negative tests main had for env.py (a revision that fails, one that
leaves a foreign-key violation, one that ends the transaction, attaches
a database, or runs an action the authorizer does not list, a batch
alter under a view, a connection already inside a transaction, a file
holding a violation), the revision-rule checker, the schema comparison
and plan rules on changed copies, the frozen file's changed copy, and
the participation views' growth with single-column indexes, back as
they were, beside the tests this step added over real revisions.

- _batch_alter_revision takes main's form again, which also builds the
  revision without the view statements.
- create_all_uses returns the uses and the files read; the restored
  caller reads the uses.
- The participation test builds its copies with the single-column
  indexes, then measures them with head's growth.
Each spelling the determinism scan refuses (the wall clock through
datetime, date, and time; sleeps; unseeded random, secrets, os.urandom,
and uuid4; aliases, from-imports, a module as a value, and a call in an
annotation) is reported with its line, and the spellings it allows
(tzset, a seeded Random, an annotation, sleep(0), uuid5, a datetime
built from its fields, another object's now) are not. The collection
scan reports a test function or class nested in a function, and not one
at module or class level.
env.py held the authorizer's state mapping before the upgrade, so after
SQLAlchemy invalidated the connection and a revision called rollback(),
the next statement reconnected on a second driver connection whose own
mapping still read "open". env.py's ROLLBACK of that connection was then
refused, and the upgrade ended with DatabaseError "not authorized" in
place of the revision's error; a revision that went on instead returned
with nothing committed and the file behind. Once the upgrade's connection
is open, a connect listener now closes and refuses any other, so the
statement fails with StatementError wrapping MigrationRefused, and a
second connection a revision asks the engine for is refused as well.

Tests (tests/db/test_migrations.py):
- test_no_statement_reconnects_after_the_connection_is_invalidated (the
  revision goes on / raises) and test_a_revision_gets_no_second_connection
  fail at f83a06a and pass here.
- The interrupt test also runs SystemExit(3), and
  test_an_error_read_as_a_disconnect_applies_none covers the disconnect
  route the env.py comment names.
- TestRevisionSources.test_flags_each_rule_on_planted_files runs
  revision_source, the checker the real revision files go through, and
  asserts its exact problems and counts; the copy revision_source_problems
  is removed.
- The authorizer tests assert DatabaseError and SQLITE_AUTH instead of
  the message text.
- test_aborts_while_a_tag_table_holds_rows has main's inputs back,
  including the pr_user_tags row whose tag is missing; the valid-file case
  is test_aborts_while_a_pull_request_is_tagged_in_a_valid_file.

tests/db/test_schema.py's module docstring names the changed copies of
the head template among the schemas its readings tell apart.
…ones (D39)

Main's bodies return under main's names, each with the input the test
built in its own database or a stub that raises:
- test_engine: a trigger refusing #2's insert mid-batch, a stub prune that
  raises RuntimeError or KeyboardInterrupt, an alembic_version edited to
  'older', a trigger refusing the close, and a trigger refusing the
  aborted close.
- test_runs: PRAGMA user_version = 1 at the claim.
- test_writer: a wrong targets_default_branch an unchanged default branch
  leaves in place.
Exact results where main's asserted part of one: the run's error string,
and the HeadCheckError message for the edited revision and user_version.

The lock-based and real-revision bodies keep their cases under new names:
test_an_item_commit_that_fails_on_a_lock_is_fatal_and_the_next_run_resumes,
test_a_run_ending_at_the_prune_s_delete_keeps_step_7_s_halt_reason,
test_a_database_at_the_previous_revision_starts_no_run,
test_a_close_whose_commit_fails_on_a_lock_leaves_the_row_running_and_exits_1,
test_an_interrupted_close_whose_commit_fails_on_a_lock_leaves_the_row_running
(the lock case only; main's test holds the second-interrupt case),
test_the_claim_refuses_a_database_at_the_previous_revision, and
test_an_unchanged_default_branch_sends_no_update_of_pull_requests.

test_snapshot: test_writes_nothing and its positive control share one
helper, which reads the snapshot, runs the control's UPDATE when given,
and commits; both compare the file's bytes, and the control asserts the
rows it changed. The reader's statements are 7, one on each of the 7
tables named. The half-consumed-read probe and the constraint failure
assert sqlite_errorname (SQLITE_BUSY, SQLITE_CONSTRAINT_NOTNULL) and the
cause's class instead of message text.
- test_cli_app: test_a_traceback_shows_no_local_variables runs a
  test-only command that raises with a credential-like local through the
  root app in a child interpreter again, and asserts the local is not
  printed. A second test does the same for a command in the github group,
  whose own Typer keeps typer's default of showing locals. The
  entry-point and setting check is kept as
  test_the_console_script_calls_the_root_app_which_hides_local_variables.
- test_cli_app: the refused .env read is run with a token set and a
  recording source, and asserts no settings were cached and no source was
  opened; a second test reads the .env and asserts the source is opened
  once with the token and the settings are cached.
- test_cli_sync: test_a_database_not_at_head_exits_1 edits its own
  database's alembic_version to an unknown revision again, asserting the
  exact refusal; the real-revision case is kept as
  test_a_database_at_the_revision_before_head_exits_1.
…s; record the history copies' blob ids

Main's tests whose input the test built (edited lock entries, a relaxed checker copy in the
pull request, written databases, installed sample distributions, non-directive comments) are
back under main's names; the bodies that read tests/fixtures/history keep their own names:

- test_check_query_lock: test_a_removed_entry_fails, TestSchemaLock's test_a_removed_entry_fails,
  test_a_pair_removed_from_an_object_entry_fails, test_both_locks_are_checked_by_default,
  test_lock_selects_one, test_the_step_fails_a_pair_removed_from_a_schema_entry, and
  test_the_step_runs_the_base_checker_not_the_pull_requests. The history-based refusals assert
  the exact problem lines.
- test_count_suppressions: NOT_SUPPRESSIONS and
  test_directive_text_outside_a_directive_comment_is_not_counted; the docstring case is
  test_the_scripts_own_docstring_is_not_counted. The src cap test runs noqa only and refuses an
  empty site list.
- test_data_dictionary: the in-memory test_differences_are_reported_both_ways.
- test_declared_dependencies: test_a_name_several_distributions_install_needs_all_of_them_declared
  over distributions written to a temporary site directory.

The empty-input refusals of the alias, placeholder, and suppression checkers assert their one
stderr line. The import-boundary scans assert their site counts (5 and 20). test_config keeps
TestDocumentedDatabaseUrl's node id and drops the identical TestDefaultDatabaseUrl test.
tests/fixtures/history/README.md records each copy's git blob id, and
tests/test_history_fixtures.py recomputes it for every tracked copy.
The after-test refusal check: a pytest run over sample tests under the
conftest's hooks and fixtures, each catching a refusal at collection, in
a test, in a module fixture's setup or teardown, or in a session
fixture's teardown, gives the exact outcome of each test in its JUnit
report.

Run order: tests/conftest.py takes --reverse-order, which reverses the
collected items after every other plugin has ordered them. CI runs the
suite a second time with it, under PYTHONHASHSEED 2 (the first run sets
1), and compares the two JUnit reports with check_junit.py
--max-skipped 0 --same-as. A sample order-dependent pair shows the option
and the comparison on both sides.

The guard:
- db.types.utc_now reads the wall clock through _wall_clock, which
  tests/determinism.py refuses for the session; replacing it leaves the
  datetime name UtcDateTime's type check reads alone. test_types replaces
  the seam and writes an explicit timestamp beside the default; the two
  test_db_engine tests that wrote UserTag without created_at pass one.
- SystemClock's now and sleep are refused at the names source/clock.py
  reads (datetime, asyncio), so a test gives SystemClock a fixed datetime
  or a recording sleep and checks its bodies.
- A host-name lookup called with keyword arguments, and any method of
  SystemClock._random, are refused and recorded.

The scans: the determinism scan also reads alembic/, its file count
checked against MIGRATION_REVISIONS, and treats a starred argument to
Random as unseeded; the collection scan also reports a test function or
class inside a class whose name does not start with Test. Sample source
covers star imports of each modelled module and annotations holding a
call. Every scan refuses an empty file list.

test_unreadable_report_fails calls check_junit.main in process and
asserts it returns 1 and the exact stderr line; the Unix-socket test
asserts connect's None and connect_ex's 0 exactly.
…ad partial ones

- The head-check refusals in the engine and run tests compare the whole
  message, the only result HeadCheckError gives.
- The interrupted close records the database errors each ending meets:
  none for a second interrupt, the trigger's refusal for the other.
- The first-sync failure states the run's error, GraphQL points, REST
  calls, and the repository's reason in full.
- The restored query-lock bodies compare each problem line the checker
  prints, built from its message templates.
- The history fixtures: a repository the test builds shows only tracked
  files are compared.
- The guard: the other clock readings are refused, and asyncio answers
  every name but sleep; the scan reads scripts/; an upgrade from the
  first schema opens one driver connection.
- tests/pytest_runs.py: the one pytest runner and check_junit loader the
  two test modules shared as copies.
- ruff skips tests/fixtures/history, whose pyproject.toml is a copy.
- D40 in its four copies: no result a test asserts comes from the wall
  clock; a timeout that bounds a wait and a log record's time are not
  results. What determinism.py refuses is named: connections, lookups,
  the real clocks, the unseeded random source.
- Step 3b-2a-2d: its scope (the CI run in reverse order, db.types'
  clock seam, env.py's one connection, ruff's exclusion), what it keeps
  and why six tests are not kept, and its acceptance as CI checks it.
- Step 3b-2b-2: verify's createdAt filter is tested on both sides.
- docs/testing.md: what enforces D40, the layout, the two-run command as
  CI runs it, the CI job; docs/development.md: the CI job;
  docs/database.md: an upgrade runs on one driver connection.
- alembic/env.py: a refused second connection is recorded, and an
  upgrade whose revision caught the refusal and went on fails before the
  commit; the file keeps its revision and contents.
- CI: the two-run comparison also runs after a failing reversed run, so
  each test whose outcome differs is named.
- D40 keeps the maintainer's wording in its four copies; what the guard
  does not reach (a socket's sendto and bind, child processes, the
  clocks behind a timeout, a log record's time) is listed as its limits
  in docs/testing.md.
- Exact results: the step-7 run's error and the aborted row's exit code;
  the run that blocks the claim after an interrupted close; the scripts
  the scan read.
- The plan: the six tests not kept and the recorder rewrites that keep
  main's names; the child-count checks of verify and Step 4a on a copy
  with a row deleted; the createdAt case's filteredCount; the report
  seconds under a wall-clock step. docs/testing.md and
  docs/development.md name tests/fixtures/history.
…evision helpers

tests/support.py holds the helpers several test packages repeated:
- files_under(root, suffixes): the files os.walk lists under a directory, by suffix (None for
  every file), sorted. It replaces the counters in test_check_any_aliases, test_check_log_
  placeholders, test_declared_dependencies, test_import_boundaries, test_server_side_cursors,
  test_suite_rules, and db/test_schema, and the walk in test_count_suppressions' _walk, which
  keeps its file-as-root case and regular-file filter. db/test_schema keeps its name-ending
  match, which rglob("*.py") also applies.
- sqlite_error_name(error): the result code of the sqlite3 error under SQLAlchemy's DBAPIError,
  for db/test_migrations, sync/test_connection (through __cause__), and db/test_schema's
  failed_check, which keeps its IntegrityError and CHECK-name assertions.
- git_environment(home, **extra): PATH, a home of the test's own, and no global or system
  configuration, plus the variables each caller names: fixed dates in test_check_query_lock,
  the ceiling directory and fixed dates in sync/test_runs' git_env (which still sets them in
  the process environment), and nothing more in db/test_tracked_database and
  test_history_fixtures.
- listening(name, listener, target): moved from db/test_migrations; it replaces the
  event.listen/event.remove pairs in sync/test_engine (database_errors and five tests; the
  three whose finally also called reader.release() keep it there, after the listener's
  removal) and sync/test_writer's _sent_during.

STEP3A, STEP3B1, and LAST_RUN_ID are defined once, in tests/db/alembic_support.py; sync/test_runs
uses STEP3A for its older revision. tests/conftest.py's session-scoped revision_template builds
the read-only database at a revision once per session; the module-scoped revisions fixtures of
db/test_schema and test_data_dictionary read the three revisions from it.

The collected node ids are unchanged.
alembic/env.py is main's again. Its fix (an upgrade's exception ends it
as itself, and an upgrade runs on one driver connection) moves, with its
six tests and its paragraph in docs/database.md, to a change of its own;
this step changes nothing under alembic/.
@ChrisHuie

Copy link
Copy Markdown
Contributor Author

Head c4e793f8906573970a35c7f1946c3aafb6a9f276. Where this comment and the description differ, this comment describes the head.

D39 as clarified

D39 bans three things: mutation testing (changing the code under test or a check, on disk or in memory, to see whether a test notices); false content planted in the project's real files or documents to see whether a check or reviewer catches it; and scoring a reviewer or agent on seeded flaws. Negative tests are expected: a test builds its own invalid or failing input (a sample test that fails, an invalid configuration, an edited copy in its temporary directory, a test-only revision that raises, a trigger in its own database, a stub that raises, a recorded response changed in memory) and feeds it to the unchanged code; every check is tested on both sides. The first wording also banned negative tests, and the description's table lists many as replaced or removed under it. They are kept.

Tests from main

  • Every test on main is at head under its name, except the six below (a name comparison over the 63 test files on 9d68f7b).
  • Where this branch had rewritten a test under main's name, main's test-built input is back under that name; most bodies differ from main's only where assertions were made exact or helpers were shared. A rewrite that holds more (a real earlier file or revision, an exact count) keeps a name of its own. In tests/source/test_recorder.py, six rewrites keep main's names: each drives the same refusal through the recorder's real path (a recorded unit changed in memory) where main replaced the recorder's function or limit.
Test on main, not at head Reason
test_cli_app.py::TestInstalledEntryPoint::test_an_unreadable_env_file_exits_1_without_a_traceback Its mode-000 case passes or fails with the uid (D40). test_an_env_file_that_is_not_utf8_exits_1_without_a_traceback and test_an_env_file_the_os_refuses_to_read_exits_1 hold both cases.
test_hydrate.py::test_an_assignee_github_id_listed_twice_under_two_node_ids, test_an_assignee_github_id_listed_again_on_a_later_page test_an_assignee_github_id_under_two_node_ids[first-page, overflow]: a recorded unit changed in memory to list one bot's two GitHub node ids.
test_check_query_lock.py::test_the_checker_runs_isolated_from_site_packages, test_the_verdict_does_not_depend_on_the_working_directory test_the_verdict_does_not_depend_on_how_the_checker_runs, over real lock files.
test_config.py::TestDocumentedDatabaseUrl::test_names_the_local_database It checks the wording of .env.example and docs/development.md (D40).

What D40's enforcement is at head

  • tests/determinism.py, installed from collection on, refuses a connection other than to a Unix socket, each host-name lookup function the socket module offers, the package's two real clocks (source/clock.py's readings and asyncio.sleep, and db.types.utc_now, which reads the time through _wall_clock), and every draw from SystemClock's unseeded random source; no_proxy is *. Refusals are recorded and checked after every test and at the end of the session; each refusal and that check's failing side have tests.
  • tests/test_suite_rules.py: no test file reads the wall clock, sleeps on it, or draws unseeded random numbers; the package reads them only in its two real clocks; alembic/ and scripts/ read none; no test is defined inside a function or a non-Test class. Each scan is tested on sample source holding each spelling it refuses and over every file it reads.
  • scripts/check_junit.py --same-as compares two runs test by test; a test pytest reports twice (a failure, then a teardown error) has the outcome error+failed.
  • CI's tests job runs the suite a second time in reverse order (--reverse-order) under another PYTHONHASHSEED and compares the two reports with --same-as, also after a failing reversed run.
  • Limits, stated in docs/testing.md: a socket's sendto and bind, processes a test starts, the clocks behind a timeout, and a log record's time are not refused at run time.

Other changes since the description

  • Code: cli/sync.py times its report on the run's clock; db/types.py reads the time through _wall_clock. Nothing under alembic/ changes: the env.py fix (an upgrade's exception ends it as itself, on one driver connection) is alembic/env.py: an upgrade's exception ends it as itself, on one connection #31.
  • tests/fixtures/history/README.md rows record each copy's git blob id, and tests/test_history_fixtures.py checks them. Shared test helpers live in tests/support.py, tests/pytest_runs.py, tests/db/alembic_support.py (revision ids), and tests/conftest.py (revision_template).
  • pyproject.toml: ruff skips tests/fixtures/history/.

Evidence at c4e793f8906573970a35c7f1946c3aafb6a9f276

  • PYTHONHASHSEED=1 pytest and PYTHONHASHSEED=2 pytest --reverse-order: 2314 tests, 0 failed, 0 errored, 0 skipped each; scripts/check_junit.py first.xml --max-skipped 0 --same-as second.xml exits 0.
  • pre-commit run --all-files: every hook passed.
  • CI on c4e793f8906573970a35c7f1946c3aafb6a9f276 was queued when this comment was written; its result is on this pull request's checks.

Not run: a live GitHub request; a real SIGINT to ghactivity sync from a shell.

…-planted-inputs

# Conflicts:
#	tests/db/test_migrations.py

@ChrisHuie ChrisHuie left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Reviewed ee231d2e540fd8eaeb6dd96ff2d951355911388d. One P2 is attached inline with a self-contained reproduction, scope limits, and outcome-based acceptance criteria.

Comment thread tests/conftest.py Outdated
Comment on lines +67 to +71
@pytest.fixture(autouse=True, scope="session")
def nothing_refused_after_the_last_test() -> Iterator[None]:
"""The refusals recorded after the last test's check, in fixtures torn down after it."""
yield
assert determinism.drain() == []

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[P2] Check recorded refusals after all session fixture teardown

At ee231d2e540fd8eaeb6dd96ff2d951355911388d, this final check is itself a session-autouse fixture. Another session-autouse fixture can initialize before it and therefore finalize afterward. If that cleanup catches NotDeterministic, the operation is blocked and its refusal is recorded after the last drain, but pytest exits 0 and the JUnit gate accepts the run. That contradicts the documented check through session teardown.

The existing test_a_refusal_the_code_caught_fails_the_next_check requests an ordinary session fixture from the last test. That fixture starts after this guard, so its finalizer runs before the check and does not expose this ordering.

Executable reproduction against unchanged checkout code

From the repository root with the locked dev environment installed, run:

uv run --no-sync python - <<'PY'
import os, subprocess, sys, tempfile
from pathlib import Path

repo = Path.cwd()
fixture = """import pytest
from github_activity_db.source.clock import SystemClock
from tests import determinism

@pytest.fixture(scope="session", autouse=True)
def cleanup_fixture():
    yield
    try:
        SystemClock().now()
    except determinism.NotDeterministic:
        pass
    assert [(r.what, r.detail) for r in determinism.REFUSED] == [
        ("wall clock", "source.clock: datetime.now(UTC)")
    ]
"""
with tempfile.TemporaryDirectory() as tmp:
    folder = Path(tmp)
    (folder / "fixture_plugin.py").write_text(fixture)
    (folder / "test_sample.py").write_text("def test_ok():\n    assert True\n")
    env = {
        "PATH": os.environ.get("PATH", ""),
        "PYTHONPATH": os.pathsep.join([str(repo / "src"), str(repo)]),
        "PYTEST_DISABLE_PLUGIN_AUTOLOAD": "1",
        "PYTHONDONTWRITEBYTECODE": "1",
    }
    results = []
    for name, plugins, reverse, seed in [
        ("forward", ["fixture_plugin", "tests.conftest"], False, "1"),
        ("reverse", ["fixture_plugin", "tests.conftest"], True, "2"),
        ("control", ["tests.conftest", "fixture_plugin"], False, "1"),
    ]:
        cmd = [sys.executable, "-m", "pytest", "-q", "-p", "no:cacheprovider",
               "-c", str(repo / "pyproject.toml"), "--rootdir", str(folder),
               "--confcutdir", str(folder), f"--junitxml={name}.xml"]
        for plugin in [*plugins, "pytest_asyncio.plugin", "pytest_cov.plugin"]:
            cmd += ["-p", plugin]
        if reverse:
            cmd += ["--reverse-order"]
        run = subprocess.run(cmd + [str(folder / "test_sample.py")], cwd=folder,
                             env={**env, "PYTHONHASHSEED": seed},
                             capture_output=True, text=True, timeout=60)
        gate = subprocess.run(
            [sys.executable, str(repo / "scripts/check_junit.py"),
             str(folder / f"{name}.xml"), "--max-skipped", "0"],
            capture_output=True, text=True, timeout=60)
        results.append((name, run.returncode, gate.returncode))
    compare = subprocess.run(
        [sys.executable, str(repo / "scripts/check_junit.py"),
         str(folder / "forward.xml"), "--max-skipped", "0",
         "--same-as", str(folder / "reverse.xml")],
        capture_output=True, text=True, timeout=60)
    print("name, pytest exit, JUnit gate exit:", results)
    print("forward/reverse comparison exit:", compare.returncode)
PY

Observed on Python 3.12.10 and 3.13.0 with pytest 9.0.2:

name, pytest exit, JUnit gate exit: [('forward', 0, 0), ('reverse', 0, 0), ('control', 1, 1)]
forward/reverse comparison exit: 0

This loads the complete project conftest and the actual project pytest configuration, including its strict settings and warning filters. It writes only a temporary sample project. The fixture's assertion proves the real refusal is recorded; it never clears the guard's state. The control changes only plugin registration order. The one-test reversed run demonstrates comparator acceptance, not a claim that reversing test order changes the verdict. Thus neither the source-rule checks nor a second run/comparison provide a backstop for this lifecycle gap.

Scope and nuance: the guarded operation still cannot execute. Ordinary except Exception does not swallow this BaseException; the case needs a direct catch of NotDeterministic, a broader catch, or equivalent cleanup handling. No current application test was shown escaping detection. This is a defect in the new guard's promised verification result, not evidence that the existing 2,322-test suite is presently nondeterministic. Separate controls also confirmed the same mechanism for async cleanup, a joined worker thread and a socket-connection refusal.

Acceptance criteria:

  • Check recorded refusals after the covered session-fixture cleanup, while keeping the guard active during that cleanup. Renaming this fixture or adding dependencies to selected fixtures does not establish a final-check guarantee.
  • Add a subprocess regression for a session-autouse cleanup that catches the real refusal. Require a failing pytest/CI result in both plugin registration orders, including the forward/reverse runs; the otherwise-identical clean input must still pass without a pending refusal.
  • Preserve ordinary per-test and collection checks, existing failures, and restoration of the patched boundaries.
  • Identify the recorded refusal in the failure diagnostic. A valid session-hook fix may fail pytest after JUnit has already been serialized: pytest's nonzero exit fails the existing workflow, so retroactively rewriting XML to invent a failed testcase is not required.

The reproduction above intentionally asserts the presence of the refusal during the fixture's cleanup. The regression's final oracle should be the subprocess/session result after cleanup, so it can verify a repaired implementation.

Ran: the exact reproduction above on Python 3.12 and 3.13; full-plugin and strict-config lifecycle controls; the actual JUnit and comparison commands. The earlier four complete runs at this unchanged head each passed 2,322 tests.
Not run: a modified implementation or proposed fix; an exhaustive matrix of arbitrary third-party plugin behavior.

… undone

The final check was a session-scoped fixture, so a session fixture set up
before it (a plugin's, a root conftest's) was torn down after it, and a
refusal caught there left pytest exiting 0. The check now runs in
tests/conftest.py's pytest_unconfigure, after the guard is undone: each
refusal is written to stderr and an exit status of 0 becomes 1.

run_pytest gives its sample processes a fixed hash seed and makes the
directories of the files it writes.
@ChrisHuie

Copy link
Copy Markdown
Contributor Author

Head 0177681b84bfbf341ac92a304553cae38e7f5de2. Where this comment and the description differ, this comment describes the head.

Since the last head comment (c4e793f)

The P2: a refusal recorded after the last check

  • At ee231d2 the last check was the session fixture nothing_refused_after_the_last_test. A session fixture set up before it (a plugin's, a root conftest's) is torn down after it, so a refusal caught there was recorded, never checked, and pytest exited 0.
  • tests/conftest.py's pytest_unconfigure now undoes the guard, drains the record, writes each refusal to stderr as refused after the last check: (what, detail), and turns an exit status of 0 into 1. The session is found among the registered plugins by its type. Other exit statuses are kept. The session fixture is deleted.
  • A refusal recorded after the last test's check is not in the JUnit report, so check_junit.py and --same-as do not show it; pytest's exit status, which fails the CI step, does. docs/testing.md says so.
  • --fixtures and --fixtures-per-test exit 0 whatever the status (pytest's own behaviour).

Tests (tests/test_suite_rules.py)

Test Input Asserts
test_a_refusal_caught_after_the_last_tests_check_fails_the_run (6 cases) the real clock's refusal caught in a session fixture of a plugin registered before the guard and after it (each in order and last to first), in a plugin's pytest_unconfigure that runs before the guard's, and in a root conftest's session fixture with the guard in a directory's conftest; each beside a twin that reads a FakeClock refusing twin: exit 1, every test passed, the refusal parsed from stderr, and every stderr line a refusal; clean twin: exit 0, nothing on stderr; both: the 11 guarded names patched at session finish and none after, and no refusal left recorded
test_a_refusal_the_code_caught_fails_the_next_check the existing sample, now loading the guard with -p tests.conftest test_e_last::test_last passed (it was an error), exit 1, its refusal on stderr
test_a_sample_run_hashes_with_a_fixed_seed a sample test reading sys.flags.hash_randomization 0: tests/pytest_runs.py now sets PYTHONHASHSEED=0 for the pytest processes it starts (D40)

Against ee231d2's tests/conftest.py the 7 guard tests fail, each on an exit-code or outcome difference; against its tests/pytest_runs.py the hash-seed test fails.


Ran, at 0177681: the full suite with PYTHONHASHSEED=1 and with --reverse-order under PYTHONHASHSEED=2 on Python 3.13.0 (2329 passed each; check_junit.py --max-skipped 0 --same-as exit 0; stderr empty); pre-commit; CI (lint and types, tests on Python 3.12 and 3.13, built wheel: all passed).
Not run: the full suite on Python 3.12 outside CI; pytest-xdist.

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