diff --git a/AGENTS.md b/AGENTS.md index fea52965f..8d486b7fa 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -113,6 +113,8 @@ Verified against `Makefile`. Grouped by use. | `make test` | Full unit suite — `tests/` + every `solstone/apps/*/tests/`, one parallel run. Format-check runs first; failures block tests. | | `make test-cov` | Same suite with full-repo terminal coverage; used by `make verify`. | | `make test-integration` | Opt-in real local build/process and persisted-index contracts. Never part of `make ci`. | +| `make test-release` | Serial release transaction, entrypoint, packaging, and release-host tool-contract tests. Never part of development `make ci`. | +| `make release-checks` | Full release-host gate: `test-release`, advisory-policy liveness, and real minisign signing. Candidate construction runs this automatically. | | `make test-app APP=` | Run a single app's tests (focus helper). | | `make test-only TEST=` | Run a specific test file or pytest node id (`TEST="-k test_name"` also works). | | `make coverage` | HTML coverage report under `htmlcov/`. Occasional. | @@ -155,12 +157,13 @@ seek green. ### Release rail DESTRUCTIVE: `bash scripts/release.sh --candidate` is fresh construction; before -policy or build work it deletes prior raw build/dist outputs and that version's -stale payload/evidence. It verifies the expected source commit and lock state, -gathers target evidence through configured build/proof-host channels, -pair-promotes payload and evidence, and prints canonical local readiness JSON. -This is candidate evidence only, not publication authorization. Advisory acquisition -is a separate operator operation documented in `scripts/release_advisory_policy.py`. +policy or build work it first runs `make release-checks`, then deletes prior raw +build/dist outputs and that version's stale payload/evidence. It verifies the +expected source commit and lock state, gathers target evidence through +configured build/proof-host channels, pair-promotes payload and evidence, and +prints canonical local readiness JSON. This is candidate evidence only, not +publication authorization. Advisory acquisition is a separate operator +operation documented in `scripts/release_advisory_policy.py`. If retained `dist/release-candidate/` or `target/release-evidence/` bytes already exist, `--candidate` refuses before cleanup unless the operator sets `RELEASE_CANDIDATE_DISCARD_RETAINED=` for an unpublished version or `RELEASE_CANDIDATE_DISCARD_PUBLISHED_TAG=+` for a tagged version; undeterminable retained state is not authorizable. Every candidate attempt generates a fresh proof challenge and requires, for each @@ -307,7 +310,7 @@ sets only the local verification path and may use any local filename. - **Fixture journal:** `tests/fixtures/journal/` — immutable mock input with facets, entities, segments, and index state. The autouse `set_test_journal_path` fixture in `tests/conftest.py` points unit tests at it. Tests that write, scan, or rebuild journal/index state must use `journal_copy` or a smaller `tmp_path` journal (see §8). - **Run one test:** `make test-only TEST=tests/test_utils.py::test_foo` or `TEST="-k test_foo"`. **One app:** `make test-app APP=`. - **`make test` runs everything** — `tests/` and every `solstone/apps/*/tests/` in one parallel run. App tests are not a separate step. -- **`make test` / `make ci` are strict unit/component rails** — mock process, thread, clock, network, and repository boundaries; no real browser, live network, API keys, heavyweight builds, or writes to shared fixture state. Real local build/process and persisted-index contracts use `@pytest.mark.integration` and run only through `make test-integration`. Live product verification uses `make sandbox`. +- **`make test` / `make ci` are strict unit/component rails** — mock process, thread, clock, network, and repository boundaries; no real browser, live network, API keys, heavyweight builds, or writes to shared fixture state. Real local build/process and persisted-index contracts use `@pytest.mark.integration` and run only through `make test-integration`. Release transactions and release-host contracts use `@pytest.mark.release`, run serially through `make test-release`, and join the real release-host gates under `make release-checks`. Live product verification uses `make sandbox`. - **After editing `solstone/convey/` or `solstone/apps/`:** `journal restart-convey` to reload code in a running stack. - **`make dev` + `make sandbox`** both write runtime artifacts into the fixtures journal; `tests/fixtures/journal/.gitignore` covers those — never commit them. - **Test invariants, not snapshots.** A test asserts what must hold in *every* valid state of the system — not what happens to be true today. Never pin a test to hand-edited prose (CHANGELOG / README / docs), to a value the system is *designed* to change (a version, a date, a growing count), or to a transient state. The tell: if doing the correct next thing — cut a release, rename a label, graduate a shipped changelog entry — turns the test red, the test is wrong, not the system. And test the code that *produces* a fact, never the rendered text about it. (A `[Unreleased]`-pinned changelog test was exactly this anti-pattern — its pass condition required the release process to *not* run; removed 2026-05-30.) diff --git a/Makefile b/Makefile index 6c27397a9..e5322e0f1 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,7 @@ export TMPDIR := /var/tmp PYTEST_BASETEMP_INIT := BASETEMP=$$(mktemp -d /var/tmp/solstone-pytest-XXXXXX); trap 'rm -rf "$$BASETEMP"' EXIT INT TERM; PYTEST_BASETEMP_FLAG := --basetemp "$$BASETEMP" -.PHONY: install hopper-install uninstall test test-cov test-integration test-performance test-app test-only format format-check install-checks ci clean clean-install coverage watch versions update update-prices preflight pre-commit skills render-packaging check-rust-fmt check-rust-msrv check-rust-clippy check-rust-test check-rust-ios check-rust-deny check-release-advisory-liveness check-rust-release-manifest audit openapi check-openapi check-openapi-observer-client-contract contract check-contract journal-resolution-vectors check-journal-resolution-vectors build-native-sol-grammar-oracle check-native-sol-grammar-oracle build-native-sol-root-contract check-native-sol-root-contract check-core-sdist-compile-inputs build-native-sol-journal-host-commands check-native-sol-journal-host-commands build-journal-access-rejection-inventory check-journal-access-rejection-inventory check-native-sol-python-manifest build-native-sol-inventory check-native-sol-inventory check-native-sol-architecture check-native-sol-contract-routes check-native-sol-conformance check-native-sol-coverage check-native-sol-no-python-spawn check-native-sol-compat check-native-sol-docs-links check-removed-time-parser-ready dev all sandbox sandbox-stop install-models parakeet-helper parakeet-helper-clean wheel-speakers-analyze-linux wheel-speakers-analyze-linux-x86_64 wheel-speakers-analyze-linux-aarch64 wheel-macos wheel-macos-clean verify verify-api verify-schemathesis update-api-baselines eval-schemas service-logs check-layer-hygiene check-api-conventions check-journal-io-access check-journal-io-mechanic check-journal-config-owner check-call-http-only check-no-legacy-chat check-channel-adapter-scrub check-brain-health-cutover check-tools-http-only check-access-imports-clean check-convey-bind-imports-clean check-schema-bounds check-thin-base-install check-extras-consistency check-cogitate-prompts smoke-cogitate release release-test publish-release publish-release-test FORCE +.PHONY: install hopper-install uninstall test test-cov test-integration test-release release-checks test-performance test-app test-only format format-check install-checks ci clean clean-install coverage watch versions update update-prices preflight pre-commit skills render-packaging check-rust-fmt check-rust-msrv check-rust-clippy check-rust-test check-rust-ios check-rust-deny check-release-advisory-liveness check-rust-release-manifest audit openapi check-openapi check-openapi-observer-client-contract contract check-contract journal-resolution-vectors check-journal-resolution-vectors build-native-sol-grammar-oracle check-native-sol-grammar-oracle build-native-sol-root-contract check-native-sol-root-contract check-core-sdist-compile-inputs build-native-sol-journal-host-commands check-native-sol-journal-host-commands build-journal-access-rejection-inventory check-journal-access-rejection-inventory check-native-sol-python-manifest build-native-sol-inventory check-native-sol-inventory check-native-sol-architecture check-native-sol-contract-routes check-native-sol-conformance check-native-sol-coverage check-native-sol-no-python-spawn check-native-sol-compat check-native-sol-docs-links check-removed-time-parser-ready dev all sandbox sandbox-stop install-models parakeet-helper parakeet-helper-clean wheel-speakers-analyze-linux wheel-speakers-analyze-linux-x86_64 wheel-speakers-analyze-linux-aarch64 wheel-macos wheel-macos-clean verify verify-api verify-schemathesis update-api-baselines eval-schemas service-logs check-layer-hygiene check-api-conventions check-journal-io-access check-journal-io-mechanic check-journal-config-owner check-call-http-only check-no-legacy-chat check-channel-adapter-scrub check-brain-health-cutover check-tools-http-only check-access-imports-clean check-convey-bind-imports-clean check-schema-bounds check-thin-base-install check-extras-consistency check-cogitate-prompts smoke-cogitate release release-test publish-release publish-release-test FORCE # Default target - install package in editable mode all: install @@ -451,7 +451,7 @@ RUFF := $(VENV_BIN)/ruff # `make PYTEST_MAX_WORKERS=16 test` on a dedicated/idle box. PYTEST_MAX_WORKERS ?= 2 PYTEST_XDIST_ARGS := -n auto --maxprocesses $(PYTEST_MAX_WORKERS) --dist loadgroup -PYTEST_UNIT_ARGS := -m "not integration and not performance" +PYTEST_UNIT_ARGS := -m "not integration and not performance and not release" # Check formatting without modifying files — gates `make test` format-check: .installed @@ -474,6 +474,17 @@ test-integration: .installed format-check @echo "Running integration tests..." $(PYTEST_BASETEMP_INIT) $(TEST_ENV) $(PYTEST) $(PYTEST_BASETEMP_FLAG) tests/ solstone/apps/ -q -m integration $(PYTEST_XDIST_ARGS) +# Release transactions and real release-host probes run serially, once per +# candidate attempt, outside development CI. +test-release: .installed format-check + @echo "Running release tests..." + $(PYTEST_BASETEMP_INIT) SOLSTONE_RELEASE_TEST_RAIL=1 $(TEST_ENV) $(PYTEST) $(PYTEST_BASETEMP_FLAG) tests/ solstone/apps/ -q -m release -n 0 + +release-checks: .installed + @$(MAKE) test-release + @$(MAKE) check-release-advisory-liveness + @$(MAKE) check-transparency-minisign + # Wall-clock thresholds are intentionally opt-in so loaded hosts do not make CI flaky. test-performance: .installed format-check @echo "Running performance tests..." @@ -678,12 +689,6 @@ install-checks: .installed @echo "=== Running rust dependency policy check ===" @$(MAKE) check-rust-deny @echo "" - @echo "=== Running release advisory liveness check ===" - @$(MAKE) check-release-advisory-liveness - @echo "" - @echo "=== Checking transparency minisign ===" - @$(MAKE) check-transparency-minisign - @echo "" @echo "=== Checking extras consistency ===" @$(VENV_BIN)/python scripts/check_extras_consistency.py @echo "" diff --git a/docs/testing.md b/docs/testing.md index ee0188302..fc48130bc 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -13,6 +13,13 @@ - Opt-in via `make test-integration`; excluded from `make test` and `make ci` - Real local processes/builds and persisted-index contracts - Still use disposable `tmp_path` state and never the owner's journal +- **Release Tests**: marked `@pytest.mark.release` + - Run serially via `make test-release`; excluded from development `make test` + and `make ci` + - Cover release transactions, release entrypoints, packaging/install probes, + and release-host tool contracts + - `make release-checks` combines them with the real advisory-policy liveness + and minisign gates; candidate construction runs that target automatically - **Naming**: Files `test_*.py`, functions `test_*` - **Fixtures**: Shared fixtures in `tests/conftest.py` @@ -35,6 +42,9 @@ test that writes, scans, or rebuilds journal/index state must use the - `make test` runs all unit tests — `tests/` + every `solstone/apps/*/tests/`, in one parallel run - `make test-cov` — the same suite with coverage reporting - `make test-integration` — opt-in real local build/process and persisted-index contracts +- `make test-release` — serial release transactions and release-host probes +- `make release-checks` — complete candidate-host validation, including the + release tests, advisory-policy liveness, and real minisign signing - `make test-app APP=` and `make test-only TEST=path` are the focused development loop - `make coverage` to generate a coverage report - `make ci` once on the settled final tree before merge or release (install checks plus the full unit suite) diff --git a/pyproject.toml b/pyproject.toml index 4ef2a460b..de3ee3c2d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -249,6 +249,7 @@ python_functions = ["test_*"] markers = [ "integration: exercises real local processes, builds, or persisted index contracts outside unit CI", "performance: asserts a wall-clock performance floor and is opt-in on an idle host", + "release: exercises release transactions, release entrypoints, or release-host tool contracts outside development CI", "real_local_backend_probe: opts out of the deterministic bundled local backend probe monkeypatch", "xdist_group: marks tests to run in the same xdist worker group", ] diff --git a/scripts/release.sh b/scripts/release.sh index 3aa36a1c6..656e5b581 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -17,20 +17,21 @@ # and that version's stale payload/evidence before policy or build work. # # Candidate flow: -# 1. Verify the expected source commit, clean source tree, and core lock. -# 2. Delete prior raw outputs and stale retained payload/evidence for the +# 1. Run the serial release test and release-host tool rail. +# 2. Verify the expected source commit, clean source tree, and core lock. +# 3. Delete prior raw outputs and stale retained payload/evidence for the # version being constructed. -# 3. Build local artifacts and receive macOS artifacts through the externally +# 4. Build local artifacts and receive macOS artifacts through the externally # configured build-host channel. -# 4. Generate a fresh proof challenge, extract the nvattest authority from the +# 5. Generate a fresh proof challenge, extract the nvattest authority from the # candidate's own root wheels and require every target to agree, and # materialize the locked support-wheel set, verifying it against uv.lock. # All of this happens before the ledger is written and before any proof # host is contacted. -# 5. Collect a per-target receipt pair through configured proof-host channels +# 6. Collect a per-target receipt pair through configured proof-host channels # -- the install/smoke proof and a challenge-bound nvattest proof -- then # pair-promote payload and evidence. -# 6. Revalidate payload, manifests, ledger, retained support wheels, and both +# 7. Revalidate payload, manifests, ledger, retained support wheels, and both # receipt classes, then print canonical local readiness JSON. This is not # publication authorization. # @@ -184,6 +185,17 @@ fi REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$REPO_ROOT" +if [[ "$MODE" == "candidate" ]]; then + if [[ "${SOLSTONE_RELEASE_TEST_RAIL:-}" == "1" ]]; then + if [[ -z "${PYTEST_CURRENT_TEST:-}" ]]; then + echo "SOLSTONE_RELEASE_TEST_RAIL is reserved for pytest release-entrypoint probes" >&2 + exit 2 + fi + else + make release-checks + fi +fi + if [[ "$MODE" == "recover" ]]; then exec python3 scripts/release_candidate_driver.py recover \ --version "$RECOVER_VERSION" \ diff --git a/tests/integration/test_release_tool_uv_boundary.py b/tests/integration/test_release_tool_uv_boundary.py index 7b2913b96..f186335e2 100644 --- a/tests/integration/test_release_tool_uv_boundary.py +++ b/tests/integration/test_release_tool_uv_boundary.py @@ -11,7 +11,7 @@ import pytest import scripts.check_release_preflight as preflight import scripts.release_tool_pins as pins -pytestmark = pytest.mark.integration +pytestmark = [pytest.mark.integration, pytest.mark.release] def test_real_uv_banner_parses_and_compares_strictly() -> None: diff --git a/tests/integration/test_root_launcher_install.py b/tests/integration/test_root_launcher_install.py index ce366c240..b420a2857 100644 --- a/tests/integration/test_root_launcher_install.py +++ b/tests/integration/test_root_launcher_install.py @@ -24,7 +24,11 @@ from packaging.utils import canonicalize_name import scripts.check_wheel_contents as wheel_checker ROOT = Path(__file__).resolve().parents[2] -pytestmark = [pytest.mark.integration, pytest.mark.timeout(900)] +pytestmark = [ + pytest.mark.integration, + pytest.mark.release, + pytest.mark.timeout(900), +] @dataclass(frozen=True) diff --git a/tests/integration/test_solstone_core_wheel_install.py b/tests/integration/test_solstone_core_wheel_install.py index bb3dc68ad..463bae185 100644 --- a/tests/integration/test_solstone_core_wheel_install.py +++ b/tests/integration/test_solstone_core_wheel_install.py @@ -17,7 +17,7 @@ import pytest import scripts.check_wheel_contents as checker ROOT = Path(__file__).resolve().parents[2] -pytestmark = pytest.mark.integration +pytestmark = [pytest.mark.integration, pytest.mark.release] @pytest.mark.skipif( diff --git a/tests/test_advisory_mirror_audit.py b/tests/test_advisory_mirror_audit.py index 72a951802..0b116f682 100644 --- a/tests/test_advisory_mirror_audit.py +++ b/tests/test_advisory_mirror_audit.py @@ -312,6 +312,7 @@ def _inventory(root: Path) -> dict[str, tuple[str, int]]: return result +@pytest.mark.release def test_green_packet_emits_exact_success_json_and_uses_bound_snapshot( tmp_path: Path, ) -> None: @@ -350,6 +351,7 @@ def test_green_packet_emits_exact_success_json_and_uses_bound_snapshot( assert "maximum-db-staleness" not in config_text +@pytest.mark.release def test_green_packet_with_real_git_bundle_materialization(tmp_path: Path) -> None: output, runner, _root, _bundle, _receipt, _pubkey = _invoke_green(tmp_path) @@ -363,6 +365,7 @@ def test_green_packet_with_real_git_bundle_materialization(tmp_path: Path) -> No @pytest.mark.parametrize("missing", ["bundle", "receipt", "pubkey", "locator"]) +@pytest.mark.release def test_required_inputs_fail_before_git_or_cargo(tmp_path: Path, missing: str) -> None: root = _audit_root(tmp_path) _repo, commit, bundle = _advisory_repo(tmp_path) @@ -399,6 +402,7 @@ def test_required_inputs_fail_before_git_or_cargo(tmp_path: Path, missing: str) @pytest.mark.parametrize("kind", ["missing", "symlink", "directory"]) +@pytest.mark.release def test_adjacent_signature_is_required_and_regular_before_git_or_cargo( tmp_path: Path, kind: str, @@ -435,6 +439,7 @@ def test_adjacent_signature_is_required_and_regular_before_git_or_cargo( @pytest.mark.parametrize("target_name", ["bundle", "receipt", "pubkey"]) @pytest.mark.parametrize("kind", ["missing", "symlink", "directory"]) +@pytest.mark.release def test_unsafe_input_paths_and_symlinks_fail_before_git_or_cargo( tmp_path: Path, target_name: str, @@ -527,6 +532,7 @@ def test_validate_locator_q3_oracle(locator: str, accepted: bool) -> None: b'{"max_age":86400,"synced_commit":"{commit}","utc":"2026-07-24T11:30:00Z","x":1}\n', ], ) +@pytest.mark.release def test_receipt_body_requires_canonical_bytes(tmp_path: Path, raw: bytes) -> None: _repo, commit, _bundle = _advisory_repo(tmp_path) receipt = tmp_path / "freshness.json" @@ -562,6 +568,7 @@ def test_receipt_fields_are_strict(tmp_path: Path, payload: dict[str, Any]) -> N audit._read_receipt_authority(receipt) +@pytest.mark.release def test_trusted_comment_mismatch_fails(tmp_path: Path) -> None: root = _audit_root(tmp_path) _repo, commit, bundle = _advisory_repo(tmp_path) @@ -587,6 +594,7 @@ def test_trusted_comment_mismatch_fails(tmp_path: Path) -> None: assert "trusted comment mismatch" in exc.value.failures[0].error +@pytest.mark.release def test_pubkey_sha256_mismatch_fails_before_minisign(tmp_path: Path) -> None: _repo, commit, _bundle = _advisory_repo(tmp_path) _receipt, _signature, pubkey, _pubkey_sha, _fake = _write_packet( @@ -635,6 +643,7 @@ def test_pubkey_blob_shape_is_strict(tmp_path: Path, raw: bytes) -> None: ) +@pytest.mark.release def test_signature_mutation_fails(tmp_path: Path) -> None: root = _audit_root(tmp_path) _repo, commit, bundle = _advisory_repo(tmp_path) @@ -664,6 +673,7 @@ def test_signature_mutation_fails(tmp_path: Path) -> None: "utc", ["2026-07-24T12:06:00Z", "2026-07-23T11:59:59Z"], ) +@pytest.mark.release def test_receipt_future_and_stale_times_fail(tmp_path: Path, utc: str) -> None: _repo, commit, _bundle = _advisory_repo(tmp_path) receipt = tmp_path / "freshness.json" @@ -718,6 +728,7 @@ def test_minisign_preflight_uses_product_binary_check( assert calls[2][0] == "verify" +@pytest.mark.release def test_bundle_verify_failure_stops_before_clone_and_cargo(tmp_path: Path) -> None: root = _audit_root(tmp_path) _repo, commit, bundle = _advisory_repo(tmp_path) @@ -758,6 +769,7 @@ def test_bundle_heads_must_be_exact_head_and_main(stdout: str) -> None: audit._parse_bundle_heads(stdout, synced_commit="a" * 40) +@pytest.mark.release def test_clone_head_must_match_receipt_commit(tmp_path: Path) -> None: root = _audit_root(tmp_path) _repo, commit, bundle = _advisory_repo(tmp_path) @@ -781,6 +793,7 @@ def test_clone_head_must_match_receipt_commit(tmp_path: Path) -> None: ) +@pytest.mark.release def test_zero_advisory_clone_fails_before_cargo(tmp_path: Path) -> None: root = _audit_root(tmp_path) _repo, commit, bundle = _advisory_repo(tmp_path, advisory_count=0) @@ -805,6 +818,7 @@ def test_zero_advisory_clone_fails_before_cargo(tmp_path: Path) -> None: assert runner.check_count == 0 +@pytest.mark.release def test_discovery_run_nonzero_is_expected_when_debug_line_present( tmp_path: Path, ) -> None: @@ -831,6 +845,7 @@ def test_discovery_path_must_be_direct_child_of_temp_parent( audit._assert_direct_child(scanned, parent) +@pytest.mark.release def test_discovered_path_must_not_preexist(tmp_path: Path) -> None: root = _audit_root(tmp_path) _repo, commit, bundle = _advisory_repo(tmp_path) @@ -858,6 +873,7 @@ def test_discovered_path_must_not_preexist(tmp_path: Path) -> None: ) +@pytest.mark.release def test_alternate_or_ambient_database_substitution_is_rejected(tmp_path: Path) -> None: runner = HybridRunner(final_scanned_path=tmp_path / "other-db") @@ -866,6 +882,7 @@ def test_alternate_or_ambient_database_substitution_is_rejected(tmp_path: Path) assert exc.value.failures[0].actual == "redacted" +@pytest.mark.release def test_runner_never_sees_remote_git_or_cargo_fetch_operations(tmp_path: Path) -> None: _output, runner, _root, _bundle, _receipt, _pubkey = _invoke_green(tmp_path) flattened = [" ".join(command) for command in runner.events] @@ -877,6 +894,7 @@ def test_runner_never_sees_remote_git_or_cargo_fetch_operations(tmp_path: Path) assert not any("github.com" in item for item in flattened) +@pytest.mark.release def test_final_cargo_deny_failure_is_redacted_and_no_success(tmp_path: Path) -> None: runner = HybridRunner( final_exit=1, @@ -907,6 +925,7 @@ def test_child_output_redaction_masks_locator_temp_path_and_token_canaries() -> assert audit.validate_public_evidence_text("child-output", redacted) == [] +@pytest.mark.release def test_cleanup_failure_suppresses_success_and_combines_errors(tmp_path: Path) -> None: def fail_cleanup(_path: Path) -> None: raise OSError("cleanup failed") @@ -935,6 +954,7 @@ def test_cleanup_failure_suppresses_success_and_combines_errors(tmp_path: Path) ) +@pytest.mark.release def test_cleanup_failure_combines_with_primary_error(tmp_path: Path) -> None: def fail_cleanup(_path: Path) -> None: raise OSError("cleanup failed") @@ -964,6 +984,7 @@ def test_cleanup_failure_combines_with_primary_error(tmp_path: Path) -> None: assert any("cleanup failed" in error for error in errors) +@pytest.mark.release def test_exact_success_schema_and_witness_binding(tmp_path: Path) -> None: output, _runner, root, _bundle, _receipt, _pubkey = _invoke_green(tmp_path) payload = json.loads(output) @@ -986,6 +1007,7 @@ def test_exact_success_schema_and_witness_binding(tmp_path: Path) -> None: assert b"mirror.example.invalid" not in output +@pytest.mark.release def test_success_inventory_is_non_destructive( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, @@ -1030,6 +1052,7 @@ def test_success_inventory_is_non_destructive( "stage", ["input", "locator", "pubkey", "signature", "time", "bundle", "cargo", "cleanup"], ) +@pytest.mark.release def test_failure_inventory_is_non_destructive( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, diff --git a/tests/test_core_sdist_compile_inputs_integration.py b/tests/test_core_sdist_compile_inputs_integration.py index ffc84be64..29c6efcb1 100644 --- a/tests/test_core_sdist_compile_inputs_integration.py +++ b/tests/test_core_sdist_compile_inputs_integration.py @@ -45,6 +45,7 @@ ESCAPED_INCLUDE_ARGUMENT = "../../../../../../missing-outside-extracted-root.txt @pytest.mark.integration +@pytest.mark.release @pytest.mark.timeout(900) def test_core_sdist_compile_inputs_are_required_by_real_wheel_build( tmp_path: Path, @@ -183,7 +184,7 @@ def _pinned_toolchain() -> str: def _require_build_tools() -> None: - for tool in ("uv", "cargo", "zig", "maturin", "rustup"): + for tool in ("uv", "cargo", "zig", "rustup"): if shutil.which(tool) is None: pytest.skip(f"{tool} is not installed") result = subprocess.run( diff --git a/tests/test_release_build_host.py b/tests/test_release_build_host.py index d0d812a39..db74a61a6 100644 --- a/tests/test_release_build_host.py +++ b/tests/test_release_build_host.py @@ -68,6 +68,7 @@ def _git_repo(tmp_path: Path) -> tuple[Path, str]: return repo, _run_git(repo, ["rev-parse", "HEAD"]) +@pytest.mark.release def test_source_bundle_uses_head_and_materializes_from_real_git( tmp_path: Path, ) -> None: diff --git a/tests/test_release_candidate_driver.py b/tests/test_release_candidate_driver.py index 4616ff595..5a24b8011 100644 --- a/tests/test_release_candidate_driver.py +++ b/tests/test_release_candidate_driver.py @@ -841,6 +841,7 @@ def _record_by_role(records: Sequence[dict[str, Any]], role: str) -> dict[str, A return next(record for record in records if record.get("role") == role) +@pytest.mark.release def test_fake_all_host_candidate_and_recovery_are_deterministic( tmp_path: Path, ) -> None: @@ -975,6 +976,7 @@ def test_revalidate_macos_wheels_rejects_unsigned_member_set_mismatch( ) +@pytest.mark.release def test_recovery_uses_explicit_selector_and_preserves_retained_bytes( tmp_path: Path, ) -> None: @@ -996,6 +998,7 @@ def test_recovery_uses_explicit_selector_and_preserves_retained_bytes( assert _structural_snapshot(report.evidence_dir) == before_evidence +@pytest.mark.release def test_recovery_ignores_current_release_metadata_drift( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: @@ -1022,6 +1025,7 @@ def test_recovery_ignores_current_release_metadata_drift( assert recovered.heading == driver.RETAINED_CANDIDATE_VALID_HEADING +@pytest.mark.release def test_candidate_pair_promote_rejects_publication_prerequisite_in_staging( tmp_path: Path, ) -> None: @@ -1049,6 +1053,7 @@ def test_candidate_pair_promote_rejects_publication_prerequisite_in_staging( ) +@pytest.mark.release def test_candidate_final_recheck_rejects_publication_prerequisite_after_promotion( tmp_path: Path, ) -> None: @@ -1077,6 +1082,7 @@ def test_candidate_final_recheck_rejects_publication_prerequisite_after_promotio ) +@pytest.mark.release def test_recovery_accepts_absent_publication_prerequisite(tmp_path: Path) -> None: root, _report = _real_candidate(tmp_path) @@ -1088,6 +1094,7 @@ def test_recovery_accepts_absent_publication_prerequisite(tmp_path: Path) -> Non assert payload["publication_prerequisite_inventory"] == [] +@pytest.mark.release def test_recovery_v2_retained_candidate_reports_current_heading(tmp_path: Path) -> None: root, _report = _real_candidate(tmp_path) @@ -1098,6 +1105,7 @@ def test_recovery_v2_retained_candidate_reports_current_heading(tmp_path: Path) assert payload["retained_ledger_schema_version"] == 2 +@pytest.mark.release def test_recovery_accepts_valid_publication_prerequisite_and_reports_inventory_without_mutation( tmp_path: Path, ) -> None: @@ -1123,6 +1131,7 @@ def test_recovery_accepts_valid_publication_prerequisite_and_reports_inventory_w @pytest.mark.parametrize("derive_v1", (False, True)) +@pytest.mark.release def test_recovery_tombstone_allowance_is_unchanged_for_registered_versions( tmp_path: Path, derive_v1: bool, @@ -1142,6 +1151,7 @@ def test_recovery_tombstone_allowance_is_unchanged_for_registered_versions( assert payload["publication_prerequisite_inventory"] +@pytest.mark.release def test_evidence_inventory_accepts_prerequisite_for_historical_retained_version( tmp_path: Path, ) -> None: @@ -1162,6 +1172,7 @@ def test_evidence_inventory_accepts_prerequisite_for_historical_retained_version assert failures == [] +@pytest.mark.release def test_evidence_inventory_rejects_current_version_prerequisite_for_historical_retained_version( tmp_path: Path, ) -> None: @@ -1186,6 +1197,7 @@ def test_evidence_inventory_rejects_current_version_prerequisite_for_historical_ ) +@pytest.mark.release def test_recovery_resolves_v1_ledger_before_inventory_requires_nvattest( tmp_path: Path, ) -> None: @@ -1205,6 +1217,7 @@ def test_recovery_resolves_v1_ledger_before_inventory_requires_nvattest( assert not (report.evidence_dir / "support").exists() +@pytest.mark.release def test_report_missing_ledger_fails_with_named_error(tmp_path: Path) -> None: root, report = _real_candidate(tmp_path) _ledger_path(report).unlink() @@ -1229,6 +1242,7 @@ def test_report_missing_ledger_fails_with_named_error(tmp_path: Path) -> None: @pytest.mark.parametrize("entry", ("nvattest", "support")) +@pytest.mark.release def test_pre_nvattest_v1_evidence_inventory_rejects_stray_nvattest_family( tmp_path: Path, entry: str, @@ -1252,6 +1266,7 @@ def test_pre_nvattest_v1_evidence_inventory_rejects_stray_nvattest_family( ) +@pytest.mark.release def test_pre_nvattest_v1_consumers_fail_loudly_by_version( tmp_path: Path, ) -> None: @@ -1358,6 +1373,7 @@ def test_pre_nvattest_v1_consumers_fail_loudly_by_version( ), ], ) +@pytest.mark.release def test_recovery_rejects_publication_prerequisite_metadata_mutations_without_reading( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, @@ -1411,6 +1427,7 @@ def test_recovery_rejects_publication_prerequisite_metadata_mutations_without_re ), ], ) +@pytest.mark.release def test_recovery_rejects_publication_prerequisite_read_parse_and_schema_failures_without_mutation( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, @@ -1453,6 +1470,7 @@ def test_recovery_rejects_publication_prerequisite_read_parse_and_schema_failure ("wrong-proof-name-set", "release proof inventory is not exact"), ], ) +@pytest.mark.release def test_recovery_mode_does_not_weaken_rest_of_evidence_gate( tmp_path: Path, mutation: str, @@ -1486,6 +1504,7 @@ def test_recovery_mode_does_not_weaken_rest_of_evidence_gate( assert any(failure.error == expected_error for failure in exc.value.failures) +@pytest.mark.release def test_recovery_rejects_publication_prerequisite_in_payload_directory( tmp_path: Path, ) -> None: @@ -1501,6 +1520,7 @@ def test_recovery_rejects_publication_prerequisite_in_payload_directory( ) +@pytest.mark.release def test_recovery_identity_is_invariant_across_absent_and_present_prerequisite( tmp_path: Path, ) -> None: @@ -1539,6 +1559,7 @@ def test_recovery_identity_is_invariant_across_absent_and_present_prerequisite( assert absent_payload == present_payload +@pytest.mark.release def test_transparency_snapshot_recovery_accepts_prerequisite_without_staging_it( tmp_path: Path, ) -> None: @@ -1573,6 +1594,7 @@ def test_transparency_snapshot_recovery_accepts_prerequisite_without_staging_it( ) +@pytest.mark.release def test_fresh_cleanup_preserves_other_retained_versions_and_recovery( tmp_path: Path, ) -> None: @@ -1596,6 +1618,7 @@ def test_fresh_cleanup_preserves_other_retained_versions_and_recovery( assert recovered.heading == driver.RETAINED_CANDIDATE_VALID_HEADING +@pytest.mark.release def test_candidate_refuses_published_retained_payload_and_evidence_before_cleanup( tmp_path: Path, ) -> None: @@ -1641,6 +1664,7 @@ def test_candidate_refuses_published_retained_payload_and_evidence_before_cleanu _assert_no_post_guard_service_calls(services) +@pytest.mark.release def test_candidate_allows_unpublished_retained_evidence_with_soft_authorization( tmp_path: Path, ) -> None: @@ -1670,6 +1694,7 @@ def test_candidate_allows_unpublished_retained_evidence_with_soft_authorization( assert services.call_counts["clean_outputs"] == 1 +@pytest.mark.release def test_candidate_refuses_published_retained_payload_only(tmp_path: Path) -> None: root = _repo(tmp_path) version = checker._current_version() @@ -1688,6 +1713,7 @@ def test_candidate_refuses_published_retained_payload_only(tmp_path: Path) -> No _assert_no_post_guard_service_calls(services) +@pytest.mark.release def test_candidate_refuses_published_retained_evidence_only(tmp_path: Path) -> None: root = _repo(tmp_path) version = checker._current_version() @@ -1706,6 +1732,7 @@ def test_candidate_refuses_published_retained_evidence_only(tmp_path: Path) -> N _assert_no_post_guard_service_calls(services) +@pytest.mark.release def test_candidate_refuses_discard_authorization_for_other_version( tmp_path: Path, ) -> None: @@ -1763,6 +1790,7 @@ def test_candidate_refuses_discard_authorization_for_other_version( _assert_no_post_guard_service_calls(services) +@pytest.mark.release def test_candidate_soft_authorization_does_not_clear_published_tag_tier( tmp_path: Path, ) -> None: @@ -1782,6 +1810,7 @@ def test_candidate_soft_authorization_does_not_clear_published_tag_tier( _assert_no_post_guard_service_calls(services) +@pytest.mark.release def test_candidate_hard_authorization_satisfies_unpublished_soft_tier( tmp_path: Path, ) -> None: @@ -1798,6 +1827,7 @@ def test_candidate_hard_authorization_satisfies_unpublished_soft_tier( assert services.call_counts["clean_outputs"] == 1 +@pytest.mark.release def test_candidate_refuses_undeterminable_retained_path_state( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: @@ -1827,6 +1857,7 @@ def test_candidate_refuses_undeterminable_retained_path_state( _assert_no_post_guard_service_calls(services) +@pytest.mark.release def test_candidate_refuses_undeterminable_tag_lookup(tmp_path: Path) -> None: root = _repo(tmp_path) version = checker._current_version() @@ -1851,6 +1882,7 @@ def test_candidate_refuses_undeterminable_tag_lookup(tmp_path: Path) -> None: _assert_no_post_guard_service_calls(services) +@pytest.mark.release def test_candidate_default_fixture_refuses_retained_path_without_authorization( tmp_path: Path, ) -> None: @@ -1866,6 +1898,7 @@ def test_candidate_default_fixture_refuses_retained_path_without_authorization( _assert_no_post_guard_service_calls(services) +@pytest.mark.release def test_recovery_rejects_absent_or_mutated_selector(tmp_path: Path) -> None: root = _repo(tmp_path) driver.run_candidate(root, _env(), _services(root)) @@ -1896,6 +1929,7 @@ def test_recovery_rejects_absent_or_mutated_selector(tmp_path: Path) -> None: ) +@pytest.mark.release def test_recovery_rejects_garbage_retained_advisory_identity( tmp_path: Path, ) -> None: @@ -1915,6 +1949,7 @@ def test_recovery_rejects_garbage_retained_advisory_identity( ) +@pytest.mark.release def test_recovery_rejects_impossible_retained_policy_timestamp( tmp_path: Path, ) -> None: @@ -1940,6 +1975,7 @@ def test_recovery_has_no_service_surface() -> None: assert parameters == {"root", "version", "source_commit"} +@pytest.mark.release def test_machine_report_is_canonical_sorted_and_not_publication_authorization( tmp_path: Path, ) -> None: @@ -1974,6 +2010,7 @@ def test_machine_report_is_canonical_sorted_and_not_publication_authorization( assert entry["sha256"] == payload["nvattest_sha256"][target] +@pytest.mark.release def test_candidate_cleanup_receives_release_zig_cache_root(tmp_path: Path) -> None: root = _repo(tmp_path) services = _services(root) @@ -2003,6 +2040,7 @@ def test_candidate_cleanup_receives_release_zig_cache_root(tmp_path: Path) -> No assert not cache_root.exists() +@pytest.mark.release def test_candidate_source_bundle_does_not_preexist_build_host_output( tmp_path: Path, ) -> None: @@ -2157,6 +2195,7 @@ def test_main_uses_generic_fallback_for_invalid_failure_records( ("between-renames", SystemExit), ], ) +@pytest.mark.release def test_candidate_transaction_rolls_back_payload_and_evidence_at_each_rename_point( tmp_path: Path, point: str, @@ -2188,6 +2227,7 @@ def test_candidate_transaction_rolls_back_payload_and_evidence_at_each_rename_po ) +@pytest.mark.release def test_candidate_transaction_aggregates_cleanup_errors( tmp_path: Path, ) -> None: @@ -2219,6 +2259,7 @@ def test_candidate_transaction_aggregates_cleanup_errors( @pytest.mark.parametrize("mutation", ["nested", "extra", "missing", "symlink"]) +@pytest.mark.release def test_candidate_final_recheck_rejects_payload_inventory_mutations( tmp_path: Path, mutation: str, @@ -2257,6 +2298,7 @@ def test_candidate_final_recheck_rejects_payload_inventory_mutations( @pytest.mark.parametrize("mutation", ["extra", "temp", "directory", "proof-symlink"]) +@pytest.mark.release def test_candidate_final_recheck_rejects_evidence_inventory_mutations( tmp_path: Path, mutation: str, @@ -2310,6 +2352,7 @@ def test_candidate_final_recheck_rejects_evidence_inventory_mutations( "native_summary", ], ) +@pytest.mark.release def test_candidate_final_recheck_rejects_deep_ledger_binding_mutations( tmp_path: Path, mutation: str, @@ -2356,6 +2399,7 @@ def test_candidate_final_recheck_rejects_deep_ledger_binding_mutations( _assert_no_ready_cohort(root) +@pytest.mark.release def test_candidate_final_recheck_rejects_clean_status_drift_and_rolls_back( tmp_path: Path, ) -> None: @@ -2376,6 +2420,7 @@ def test_candidate_final_recheck_rejects_clean_status_drift_and_rolls_back( assert exc.value.failures[0].error == "release source tree is not clean" +@pytest.mark.release def test_candidate_final_recheck_rejects_core_lock_drift_and_rolls_back( tmp_path: Path, ) -> None: @@ -2396,6 +2441,7 @@ def test_candidate_final_recheck_rejects_core_lock_drift_and_rolls_back( assert exc.value.failures[0].error == "core lock hash changed before finalization" +@pytest.mark.release def test_recovery_rejects_swapped_replayed_or_mutated_proofs(tmp_path: Path) -> None: root = _repo(tmp_path) report = driver.run_candidate(root, _env(), _services(root)) @@ -2423,6 +2469,7 @@ def test_recovery_rejects_swapped_replayed_or_mutated_proofs(tmp_path: Path) -> ("make", "release-test"), ], ) +@pytest.mark.release def test_publication_entrypoints_fail_closed_before_external_seams( tmp_path: Path, argv: Sequence[str] ) -> None: @@ -2469,6 +2516,7 @@ def test_publication_entrypoints_fail_closed_before_external_seams( assert not log.exists() or log.read_text(encoding="utf-8") == "" +@pytest.mark.release def test_deleted_all_hosts_mode_is_unknown_without_external_seams( tmp_path: Path, ) -> None: @@ -2499,6 +2547,7 @@ def test_deleted_all_hosts_mode_is_unknown_without_external_seams( assert not log.exists() +@pytest.mark.release def test_make_release_targets_have_no_prerequisites() -> None: makefile = (Path(__file__).resolve().parent.parent / "Makefile").read_text( encoding="utf-8" @@ -2511,6 +2560,7 @@ def test_make_release_targets_have_no_prerequisites() -> None: assert before_comment == f"{target}: " +@pytest.mark.release def test_candidate_rejects_models_and_identity_drift(tmp_path: Path) -> None: root = _repo(tmp_path) env = _env() @@ -2537,6 +2587,7 @@ def test_default_services_have_no_fixture_lane_evidence() -> None: ) +@pytest.mark.release def test_tool_skew_is_rejected_before_any_build(tmp_path: Path) -> None: root = _repo(tmp_path) build_called = False @@ -2572,6 +2623,7 @@ def test_drift_model_version_is_not_a_publishable_version() -> None: assert "-" not in DRIFT_MODEL_VERSION +@pytest.mark.release def test_models_decision_is_bound_in_ledger_and_recovery(tmp_path: Path) -> None: root = _repo(tmp_path) env = _env() @@ -3554,6 +3606,7 @@ def test_speakers_analyze_linux_maturin_contract_rejects_missing_or_wrong_tokens "wheel_hash", ], ) +@pytest.mark.release def test_candidate_rejects_native_record_mismatches( tmp_path: Path, mutation: str ) -> None: @@ -3563,6 +3616,7 @@ def test_candidate_rejects_native_record_mismatches( driver.run_candidate(root, _env(), _services(root, native_mutation=mutation)) +@pytest.mark.release def test_candidate_revalidates_macos_wheel_bytes_after_copy_before_ledger( tmp_path: Path, ) -> None: @@ -3596,6 +3650,7 @@ def test_candidate_revalidates_macos_wheel_bytes_after_copy_before_ledger( ) +@pytest.mark.release def test_candidate_rejects_poisoned_core_wheel_content(tmp_path: Path) -> None: root = _repo(tmp_path) base_services = _services(root) @@ -3623,6 +3678,7 @@ def test_candidate_rejects_poisoned_core_wheel_content(tmp_path: Path) -> None: ) +@pytest.mark.release def test_candidate_rejects_coordinator_sourced_macos_tool_evidence( tmp_path: Path, ) -> None: @@ -3646,6 +3702,7 @@ def test_candidate_rejects_coordinator_sourced_macos_tool_evidence( ) +@pytest.mark.release def test_candidate_rejects_forged_host_macos_tool_evidence(tmp_path: Path) -> None: root = _repo(tmp_path) services = _services(root) @@ -3673,6 +3730,7 @@ def test_candidate_rejects_forged_host_macos_tool_evidence(tmp_path: Path) -> No ) +@pytest.mark.release def test_candidate_derives_manifest_evidence_from_single_frozen_tool_observation( tmp_path: Path, ) -> None: @@ -3709,6 +3767,7 @@ def test_candidate_derives_manifest_evidence_from_single_frozen_tool_observation assert manifest["native_tools"]["uv"] == pins.UV_LINUX_FIXTURE_BANNER +@pytest.mark.release def test_recovery_rejects_native_member_path_mutation_with_matching_hash( tmp_path: Path, ) -> None: @@ -3781,6 +3840,7 @@ def test_proof_binding_surfaces_target_install_parse_failure(tmp_path: Path) -> ) +@pytest.mark.release def test_recovery_rejects_empty_linux_native_member_set(tmp_path: Path) -> None: root = _repo(tmp_path) report = driver.run_candidate(root, _env(), _services(root)) @@ -3798,6 +3858,7 @@ def test_recovery_rejects_empty_linux_native_member_set(tmp_path: Path) -> None: ) +@pytest.mark.release def test_recovery_rejects_self_consistent_native_member_forgery( tmp_path: Path, ) -> None: @@ -3837,6 +3898,7 @@ def test_recovery_rejects_self_consistent_native_member_forgery( ) +@pytest.mark.release def test_recovery_rejects_self_consistent_nvattest_authority_forgery( tmp_path: Path, ) -> None: @@ -3882,6 +3944,7 @@ def test_recovery_rejects_self_consistent_nvattest_authority_forgery( ) +@pytest.mark.release def test_recovery_success_preserves_retained_tree_and_uses_no_seams( tmp_path: Path, ) -> None: @@ -3900,6 +3963,7 @@ def test_recovery_success_preserves_retained_tree_and_uses_no_seams( _assert_service_call_counts_zero(services) +@pytest.mark.release def test_recovery_failure_preserves_retained_tree_and_uses_no_seams( tmp_path: Path, ) -> None: @@ -3928,6 +3992,7 @@ def test_recovery_failure_preserves_retained_tree_and_uses_no_seams( ("noncanonical", "nvattest proof bytes are not canonical"), ], ) +@pytest.mark.release def test_recovery_rejects_retained_nvattest_receipt_mutations( tmp_path: Path, mutation: str, @@ -3986,6 +4051,7 @@ def test_recovery_rejects_retained_nvattest_receipt_mutations( _assert_fails_with_error(root, expected_error) +@pytest.mark.release def test_recovery_rejects_retained_nvattest_wrong_challenge( tmp_path: Path, ) -> None: @@ -4001,6 +4067,7 @@ def test_recovery_rejects_retained_nvattest_wrong_challenge( _assert_fails_with_error(root, "retained ledger nvattest challenge is invalid") +@pytest.mark.release def test_recovery_rejects_retained_nvattest_support_wheel_byte_mutation( tmp_path: Path, ) -> None: @@ -4015,6 +4082,7 @@ def test_recovery_rejects_retained_nvattest_support_wheel_byte_mutation( ) +@pytest.mark.release def test_recovery_rejects_retained_nvattest_support_declaration_mutation( tmp_path: Path, ) -> None: diff --git a/tests/test_release_candidate_driver_makefile_contract.py b/tests/test_release_candidate_driver_makefile_contract.py index 359cb48de..a4a8aed7c 100644 --- a/tests/test_release_candidate_driver_makefile_contract.py +++ b/tests/test_release_candidate_driver_makefile_contract.py @@ -16,6 +16,8 @@ from scripts.stage_speakers_analyze_runtime import ( from scripts.stage_speakers_analyze_runtime import ROOT as STAGE_ROOT from scripts.stage_speakers_analyze_runtime import TARGETS as STAGE_TARGETS +pytestmark = pytest.mark.release + REPO_ROOT = Path(__file__).resolve().parents[1] MAKEFILE = REPO_ROOT / "Makefile" STAGE_SCRIPT_ARGV = ("python3", "scripts/stage_speakers_analyze_runtime.py") diff --git a/tests/test_release_candidate_driver_uv_contract.py b/tests/test_release_candidate_driver_uv_contract.py index ef9a4c312..79ebb3d41 100644 --- a/tests/test_release_candidate_driver_uv_contract.py +++ b/tests/test_release_candidate_driver_uv_contract.py @@ -29,6 +29,8 @@ import pytest import scripts.release_candidate_driver as driver +pytestmark = pytest.mark.release + OPTION_DEFINITION_RE = re.compile( r"^(?: {6}| {2}-[A-Za-z], )(?P--[A-Za-z0-9][A-Za-z0-9-]*)\b" ) diff --git a/tests/test_release_candidate_driver_zig_env_contract.py b/tests/test_release_candidate_driver_zig_env_contract.py index 5b52ef030..927c11634 100644 --- a/tests/test_release_candidate_driver_zig_env_contract.py +++ b/tests/test_release_candidate_driver_zig_env_contract.py @@ -15,6 +15,7 @@ resolution path that failed in the release build stack. from __future__ import annotations +import os import re import shutil import subprocess @@ -24,12 +25,16 @@ import pytest import scripts.release_candidate_driver as driver +pytestmark = pytest.mark.release + ZIG_STRING_RE = re.compile(r"\.(?P[A-Za-z_]+)\s*=\s*\"(?P[^\"]*)\"") def _zig_or_skip() -> str: zig = shutil.which("zig") if zig is None: + if os.environ.get("SOLSTONE_RELEASE_TEST_RAIL") == "1": + pytest.fail("zig is required on the release test host") pytest.skip( "zig is not installed; release-driver Zig env contract needs real zig" ) diff --git a/tests/test_release_entrypoint_bootstrap.py b/tests/test_release_entrypoint_bootstrap.py index 26affea3d..fb67e737b 100644 --- a/tests/test_release_entrypoint_bootstrap.py +++ b/tests/test_release_entrypoint_bootstrap.py @@ -12,9 +12,8 @@ from pathlib import Path import pytest -from scripts.release_build_host import BuildHostError, ExternalBuildHostChannel - ROOT = Path(__file__).resolve().parents[1] +pytestmark = pytest.mark.release RETAINED_ROOTS = ( Path("dist") / "release-candidate", Path("target") / "release-evidence", @@ -63,6 +62,8 @@ def _reachability_env(tmp_path: Path) -> tuple[dict[str, str], Path]: (str(sentinel_dir), str(interpreter_dir), "/usr/bin", "/bin") ), "HOME": str(tmp_path / "home"), + "PYTEST_CURRENT_TEST": os.environ["PYTEST_CURRENT_TEST"], + "SOLSTONE_RELEASE_TEST_RAIL": "1", }, log, ) @@ -115,31 +116,35 @@ def _assert_no_external_seam(log: Path) -> None: assert not log.exists() or log.read_text(encoding="utf-8") == "" -def test_candidate_validates_build_host_before_destructive_cleanup() -> None: - """The candidate reachability test depends on this pre-cleanup validation.""" - driver_text = (ROOT / "scripts" / "release_candidate_driver.py").read_text( - encoding="utf-8" +def test_candidate_runs_release_checks_before_driver(tmp_path: Path) -> None: + shim_dir = tmp_path / "shims" + shim_dir.mkdir() + make_record = tmp_path / "make-record" + python_record = tmp_path / "python-record" + make = shim_dir / "make" + make.write_text( + f'#!/bin/sh\nprintf "%s\\n" "$*" > {make_record}\nexit 73\n', + encoding="utf-8", ) - start = driver_text.index("def run_candidate(") - end = driver_text.index("\ndef ", start + 1) - run_candidate_body = driver_text[start:end] - default_services_call = "default_services(" - retained_guard_call = "_retained_candidate_presence(" - cleanup_call = ".clean_outputs(" - - assert default_services_call in run_candidate_body - assert retained_guard_call in run_candidate_body - assert cleanup_call in run_candidate_body - assert run_candidate_body.index(default_services_call) < run_candidate_body.index( - cleanup_call + make.chmod(0o755) + python = shim_dir / "python3" + python.write_text( + f"#!/bin/sh\ntouch {python_record}\nexit 99\n", + encoding="utf-8", ) - assert run_candidate_body.index(retained_guard_call) < run_candidate_body.index( - cleanup_call + python.chmod(0o755) + + result = _run_release( + ["bash", "scripts/release.sh", "--candidate"], + { + "PATH": os.pathsep.join((str(shim_dir), "/usr/bin", "/bin")), + "HOME": str(tmp_path / "home"), + }, ) - with pytest.raises(BuildHostError) as exc: - ExternalBuildHostChannel.from_env({}) - assert exc.value.failures[0].error == "build-host channel is not configured" - assert exc.value.failures[0].expected == "RELEASE_BUILD_HOST_CHANNEL command" + + assert result.returncode == 73 + assert make_record.read_text(encoding="utf-8") == "release-checks\n" + assert not python_record.exists() def test_candidate_entrypoint_reaches_driver_build_host_validation( @@ -241,6 +246,8 @@ def _dispatch_env(tmp_path: Path, record_path: Path) -> tuple[dict[str, str], Pa "RELEASE_ADVISORY_SOURCE_NAME": "dispatch-source", "RELEASE_ADVISORY_DB_URL": "file:///dispatch-db", "RELEASE_ADVISORY_DB_ROOT": str(tmp_path / "advisory-db"), + "PYTEST_CURRENT_TEST": os.environ["PYTEST_CURRENT_TEST"], + "SOLSTONE_RELEASE_TEST_RAIL": "1", } return env, log diff --git a/tests/test_release_native_records.py b/tests/test_release_native_records.py index abb384b77..83b532247 100644 --- a/tests/test_release_native_records.py +++ b/tests/test_release_native_records.py @@ -128,6 +128,7 @@ def _speakers_analyze_facts(script: bytes, dylib: bytes) -> dict: } +@pytest.mark.release def test_native_record_cli_and_makefile_use_package_module() -> None: root = Path(__file__).resolve().parents[1] result = subprocess.run( @@ -411,6 +412,7 @@ def test_record_writer_removes_atomic_temp_on_success_and_failure( assert not (tmp_path / ".bad.json.tmp").exists() +@pytest.mark.release def test_signing_helper_removes_arbitrary_identity_override() -> None: source = Path("scripts/sign-and-notarize-helper.sh").read_text(encoding="utf-8") @@ -418,6 +420,7 @@ def test_signing_helper_removes_arbitrary_identity_override() -> None: assert "MACOS_SIGNER_IDENTITY" in source +@pytest.mark.release def test_signing_helper_records_tool_observations_not_pin_constants() -> None: source = Path("scripts/sign-and-notarize-helper.sh").read_text(encoding="utf-8") diff --git a/tests/test_release_nvattest_proof.py b/tests/test_release_nvattest_proof.py index d895eda0b..a8346e465 100644 --- a/tests/test_release_nvattest_proof.py +++ b/tests/test_release_nvattest_proof.py @@ -518,6 +518,7 @@ def test_default_run_smoke_uses_payload_library_path_offline(tmp_path: Path) -> @pytest.mark.integration +@pytest.mark.release def test_default_run_smoke_uses_payload_library_path_for_real_linux_archive( tmp_path: Path, ) -> None: diff --git a/tests/test_release_publish.py b/tests/test_release_publish.py index 3f6ad51ed..c24d1d6f3 100644 --- a/tests/test_release_publish.py +++ b/tests/test_release_publish.py @@ -1122,6 +1122,7 @@ def test_config_wraps_malformed_ledger(tmp_path: Path) -> None: ) +@pytest.mark.release def test_test_mode_clean_upload_verify_never_invokes_git_or_gh( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: @@ -1142,6 +1143,7 @@ def test_test_mode_clean_upload_verify_never_invokes_git_or_gh( assert result.witness_status.state == "test-skipped" +@pytest.mark.release def test_production_clean_path_orders_upload_verify_tag_witness( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: @@ -1178,6 +1180,7 @@ def test_production_clean_path_orders_upload_verify_tag_witness( assert result.witness_status.state == "created" +@pytest.mark.release def test_production_accepts_published_tombstone_with_empty_base_index( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: @@ -1208,6 +1211,7 @@ def test_production_accepts_published_tombstone_with_empty_base_index( assert result.reused_files == () +@pytest.mark.release def test_test_mode_real_recovery_without_prerequisite_reaches_index_and_upload( tmp_path: Path, ) -> None: @@ -1234,6 +1238,7 @@ def test_test_mode_real_recovery_without_prerequisite_reaches_index_and_upload( ) +@pytest.mark.release def test_test_mode_real_recovery_with_valid_prerequisite_reaches_index_and_upload( tmp_path: Path, ) -> None: @@ -1261,6 +1266,7 @@ def test_test_mode_real_recovery_with_valid_prerequisite_reaches_index_and_uploa ) +@pytest.mark.release def test_test_mode_real_recovery_rejects_invalid_prerequisite_before_seams( tmp_path: Path, ) -> None: @@ -1289,6 +1295,7 @@ def test_test_mode_real_recovery_rejects_invalid_prerequisite_before_seams( assert calls == [] +@pytest.mark.release def test_production_real_recovery_requires_prerequisite_before_transport( tmp_path: Path, ) -> None: @@ -1311,6 +1318,7 @@ def test_production_real_recovery_requires_prerequisite_before_transport( assert calls == ["source-check"] +@pytest.mark.release def test_production_real_recovery_rejects_invalid_prerequisite_before_source_check( tmp_path: Path, ) -> None: @@ -1338,6 +1346,7 @@ def test_production_real_recovery_rejects_invalid_prerequisite_before_source_che assert calls == [] +@pytest.mark.release def test_production_real_recovery_with_valid_prerequisite_never_uploads_it( tmp_path: Path, ) -> None: @@ -1376,6 +1385,7 @@ def test_production_real_recovery_with_valid_prerequisite_never_uploads_it( ) +@pytest.mark.release def test_upload_seam_receives_ledger_pypi_set_with_matching_digests( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: @@ -1445,6 +1455,7 @@ def test_upload_seam_receives_ledger_pypi_set_with_matching_digests( ) +@pytest.mark.release def test_reused_models_are_downloaded_and_matched_before_train_upload( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: @@ -1509,6 +1520,7 @@ def test_reused_models_are_downloaded_and_matched_before_train_upload( ) +@pytest.mark.release def test_reused_models_exact_digest_short_circuits_download( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: @@ -1557,6 +1569,7 @@ def test_reused_models_exact_digest_short_circuits_download( ) +@pytest.mark.release def test_reused_models_idempotent_train_full_rerun_skips_upload( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: @@ -1595,6 +1608,7 @@ def test_reused_models_idempotent_train_full_rerun_skips_upload( assert result.reused_project == f"{publisher.MODEL_PROJECT}=={_models_version()}" +@pytest.mark.release def test_model_absent_uses_legacy_uniform_index_rule( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: @@ -1622,6 +1636,7 @@ def test_model_absent_uses_legacy_uniform_index_rule( late_seams.assert_zero() +@pytest.mark.release def test_reused_model_manifest_mismatch_refuses_before_late_seams( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: @@ -1664,6 +1679,7 @@ def test_reused_model_manifest_mismatch_refuses_before_late_seams( late_seams.assert_zero() +@pytest.mark.release def test_reused_model_fetched_digest_mismatch_refuses_before_late_seams( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: @@ -1704,6 +1720,7 @@ def test_reused_model_fetched_digest_mismatch_refuses_before_late_seams( late_seams.assert_zero() +@pytest.mark.release def test_reused_model_missing_archive_url_refuses_before_late_seams( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: @@ -1748,6 +1765,7 @@ def test_reused_model_missing_archive_url_refuses_before_late_seams( late_seams.assert_zero() +@pytest.mark.release def test_reused_model_partial_published_set_refuses_before_late_seams( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: @@ -1779,6 +1797,7 @@ def test_reused_model_partial_published_set_refuses_before_late_seams( late_seams.assert_zero() +@pytest.mark.release def test_reused_model_extra_published_set_refuses_before_late_seams( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: @@ -1813,6 +1832,7 @@ def test_reused_model_extra_published_set_refuses_before_late_seams( late_seams.assert_zero() +@pytest.mark.release def test_reused_model_train_divergence_refuses_before_late_seams( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: @@ -1847,6 +1867,7 @@ def test_reused_model_train_divergence_refuses_before_late_seams( late_seams.assert_zero() +@pytest.mark.release def test_reused_model_second_observation_mismatch_refuses_before_tag( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: @@ -1906,6 +1927,7 @@ def test_reused_model_second_observation_mismatch_refuses_before_tag( assert "witness" not in calls +@pytest.mark.release def test_byte_divergence_from_recover_prevents_transport( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: @@ -1927,6 +1949,7 @@ def test_byte_divergence_from_recover_prevents_transport( "heading", ("not-ready", RETAINED_PRE_NVATTEST_CANDIDATE_VALID_HEADING), ) +@pytest.mark.release def test_recover_heading_must_be_retained_valid( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, heading: str ) -> None: @@ -1943,6 +1966,7 @@ def test_recover_heading_must_be_retained_valid( @pytest.mark.parametrize("mode", ("production", "test")) +@pytest.mark.release def test_recovery_failure_short_circuits_before_publisher_seams( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, @@ -1977,6 +2001,7 @@ def test_recovery_failure_short_circuits_before_publisher_seams( @pytest.mark.parametrize("mode", ("production", "test")) +@pytest.mark.release def test_retained_authority_binding_failure_short_circuits_before_publisher_seams( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, @@ -2008,6 +2033,7 @@ def test_retained_authority_binding_failure_short_circuits_before_publisher_seam @pytest.mark.parametrize("mode", ("production", "test")) +@pytest.mark.release def test_checkout_authority_divergence_short_circuits_before_publisher_seams( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, @@ -2034,6 +2060,7 @@ def test_checkout_authority_divergence_short_circuits_before_publisher_seams( assert late_calls == [] +@pytest.mark.release def test_unknown_asset_class_prevents_transport( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: @@ -2051,6 +2078,7 @@ def test_unknown_asset_class_prevents_transport( assert calls == [] +@pytest.mark.release def test_models_decision_gate_requires_models_artifacts( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: @@ -2076,6 +2104,7 @@ def test_models_decision_gate_requires_models_artifacts( assert calls == [] +@pytest.mark.release def test_checkout_version_mismatch_refuses_before_transport( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: @@ -2093,6 +2122,7 @@ def test_checkout_version_mismatch_refuses_before_transport( assert calls == [] +@pytest.mark.release def test_retained_ledger_version_mismatch_refuses_before_transport( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: @@ -2130,6 +2160,7 @@ def test_retained_ledger_version_mismatch_refuses_before_transport( assert calls == [] +@pytest.mark.release def test_already_published_skips_upload_and_verifies( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: @@ -2147,6 +2178,7 @@ def test_already_published_skips_upload_and_verifies( assert result.verified is True +@pytest.mark.release def test_already_published_digest_divergence_refuses_before_upload( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: @@ -2162,6 +2194,7 @@ def test_already_published_digest_divergence_refuses_before_upload( assert calls == ["index"] +@pytest.mark.release def test_partially_published_base_index_refuses_before_upload( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: @@ -2177,6 +2210,7 @@ def test_partially_published_base_index_refuses_before_upload( assert calls == ["index"] +@pytest.mark.release def test_upload_failure_is_classified_and_redacts_token( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, @@ -2208,6 +2242,7 @@ def test_upload_failure_is_classified_and_redacts_token( assert calls == ["index", "upload"] +@pytest.mark.release def test_verify_timeout_stops_before_tag_and_witness( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: @@ -2234,6 +2269,7 @@ def test_verify_timeout_stops_before_tag_and_witness( assert "witness" not in calls +@pytest.mark.release def test_production_source_commit_missing_refuses_before_transport( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: @@ -2257,6 +2293,7 @@ def test_production_source_commit_missing_refuses_before_transport( assert calls == ["source-check"] +@pytest.mark.release def test_production_requires_core_unsupported_tombstone_before_transport( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: @@ -2281,6 +2318,7 @@ def test_production_requires_core_unsupported_tombstone_before_transport( assert calls == ["source-check"] +@pytest.mark.release def test_production_missing_changelog_refuses_before_transport( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: @@ -2304,6 +2342,7 @@ def test_production_missing_changelog_refuses_before_transport( assert calls == ["source-check", "changelog"] +@pytest.mark.release def test_remote_tag_at_same_commit_skips_push_but_records_witness( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: @@ -2334,6 +2373,7 @@ def test_remote_tag_at_same_commit_skips_push_but_records_witness( assert result.tag_state == "remote-already-correct" +@pytest.mark.release def test_remote_tag_at_different_commit_refuses_without_push_or_witness( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: @@ -2357,6 +2397,7 @@ def test_remote_tag_at_different_commit_refuses_without_push_or_witness( assert calls == ["source-check", "changelog", "index", "index", "tag-check"] +@pytest.mark.release def test_local_tag_at_different_commit_refuses_without_push_or_witness( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: @@ -2387,6 +2428,7 @@ def test_local_tag_at_different_commit_refuses_without_push_or_witness( ] +@pytest.mark.release def test_tag_push_failure_names_resume_and_skips_witness( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: @@ -2418,6 +2460,7 @@ def test_tag_push_failure_names_resume_and_skips_witness( ] +@pytest.mark.release def test_witness_failure_records_gap_and_exits_success( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture ) -> None: @@ -2443,6 +2486,7 @@ def test_witness_failure_records_gap_and_exits_success( assert calls[-1] == "witness" +@pytest.mark.release def test_missing_gh_records_gap_and_exits_success( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture ) -> None: diff --git a/tests/test_speakers_analyze_wheel_integration.py b/tests/test_speakers_analyze_wheel_integration.py index e73f519b5..acae96f69 100644 --- a/tests/test_speakers_analyze_wheel_integration.py +++ b/tests/test_speakers_analyze_wheel_integration.py @@ -16,6 +16,7 @@ ROOT = Path(__file__).resolve().parents[1] @pytest.mark.integration +@pytest.mark.release @pytest.mark.timeout(300) def test_speakers_analyze_wheel_installs_and_runs_real_inference( tmp_path: Path, @@ -29,9 +30,29 @@ def test_speakers_analyze_wheel_installs_and_runs_real_inference( timeout=300, ) assert build.returncode == 0, build.stderr or build.stdout + models_build = subprocess.run( + [ + "uv", + "build", + "--package", + "solstone-journal-models", + "--wheel", + "--out-dir", + "dist", + ], + cwd=ROOT, + capture_output=True, + text=True, + check=False, + timeout=300, + ) + assert models_build.returncode == 0, models_build.stderr or models_build.stdout wheels = sorted(ROOT.glob("dist/solstone_core_speakers_analyze-*.whl")) assert wheels wheel = wheels[-1] + models_wheels = sorted(ROOT.glob("dist/solstone_journal_models-*.whl")) + assert models_wheels + models_wheel = models_wheels[-1] env_root = tmp_path / "venv" venv.EnvBuilder(with_pip=True, symlinks=False).create(env_root) @@ -45,6 +66,7 @@ def test_speakers_analyze_wheel_installs_and_runs_real_inference( "--no-index", "--no-deps", str(wheel), + str(models_wheel), ], capture_output=True, text=True, @@ -54,9 +76,11 @@ def test_speakers_analyze_wheel_installs_and_runs_real_inference( assert install.returncode == 0, install.stderr or install.stdout executable = env_root / "bin" / "solstone-core-speakers-analyze" + request_text, request_error = smoke._speakers_analyze_request(env_root, python) + assert request_text is not None, request_error run = subprocess.run( [str(executable)], - input=smoke._speakers_analyze_request(env_root), + input=request_text, capture_output=True, text=True, check=False, diff --git a/tests/test_transparency_head_log.py b/tests/test_transparency_head_log.py index 59c9a41d9..65cdc4d6a 100644 --- a/tests/test_transparency_head_log.py +++ b/tests/test_transparency_head_log.py @@ -3,6 +3,8 @@ from __future__ import annotations import subprocess from pathlib import Path +import pytest + from scripts.transparency_core import HEAD_LOG, PRODUCT, canonical_json_bytes from scripts.transparency_head_log import ( HeadLogRow, @@ -65,6 +67,7 @@ def test_append_head_row_preserves_prior_bytes(tmp_path: Path) -> None: assert path.read_bytes() == prior + expected_new +@pytest.mark.release def test_git_witness_status_committed(tmp_path: Path) -> None: _init_repo(tmp_path) append_head_row(tmp_path, _row(1)) @@ -85,6 +88,7 @@ def test_git_witness_status_committed(tmp_path: Path) -> None: assert status.state == "written-and-committed" +@pytest.mark.release def test_git_witness_status_tracked_but_modified(tmp_path: Path) -> None: _init_repo(tmp_path) append_head_row(tmp_path, _row(1)) @@ -106,6 +110,7 @@ def test_git_witness_status_tracked_but_modified(tmp_path: Path) -> None: assert status.state == "written-uncommitted" +@pytest.mark.release def test_git_witness_status_untracked(tmp_path: Path) -> None: _init_repo(tmp_path) append_head_row(tmp_path, _row(1)) diff --git a/tests/test_transparency_publish.py b/tests/test_transparency_publish.py index 84965639f..d318dfb26 100644 --- a/tests/test_transparency_publish.py +++ b/tests/test_transparency_publish.py @@ -661,6 +661,7 @@ def test_archive_channel_rejects_declared_file_mismatch( ) +@pytest.mark.release def test_publish_genesis_uploads_fixed_layout_and_order( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, @@ -755,6 +756,7 @@ def test_publish_genesis_uploads_fixed_layout_and_order( ) +@pytest.mark.release def test_publish_and_resign_accept_lexicographically_inverted_version_chain( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, @@ -808,6 +810,7 @@ def test_publish_and_resign_accept_lexicographically_inverted_version_chain( assert resigned.version == "0.11.0" +@pytest.mark.release def test_genesis_retry_adopts_orphan_latest_signature( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, @@ -871,6 +874,7 @@ def test_publish_config_repr_does_not_expose_secret(tmp_path: Path) -> None: assert "SECRET_TEST" not in repr(_config(tmp_path)) +@pytest.mark.release def test_public_immutable_verification_failure_prevents_mutable_writes( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, @@ -942,6 +946,7 @@ def test_immutable_put_412_foreign_bytes_fail_closed( ) +@pytest.mark.release def test_immutable_put_412_matching_bytes_are_adopted_and_publish_resumes( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, @@ -1001,6 +1006,7 @@ def test_missing_archive_channel_fails_before_upload(tmp_path: Path) -> None: ) +@pytest.mark.release def test_archive_channel_failure_and_digest_mismatch_fail_before_upload( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, @@ -1219,6 +1225,7 @@ def _prepare_stale_stage( return config, signer, transport, stage, first +@pytest.mark.release def test_stale_stage_fails_poisoned_version_before_mutable_write( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, @@ -1261,6 +1268,7 @@ def test_stale_stage_fails_poisoned_version_before_mutable_write( assert mutable_puts == [] +@pytest.mark.release def test_stale_stage_without_remote_prefix_reports_local_staging_dir( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, @@ -1288,6 +1296,7 @@ def test_stale_stage_without_remote_prefix_reports_local_staging_dir( assert all(call["op"] not in {"ARCHIVE", "PUT"} for call in transport.call_log) +@pytest.mark.release def test_stale_stage_with_parseable_remote_entry_reports_permanent_version( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, @@ -1343,6 +1352,7 @@ def test_stale_stage_with_parseable_remote_entry_reports_permanent_version( ) +@pytest.mark.release def test_extending_stage_with_byte_identical_remote_prefix_resumes( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, @@ -1393,6 +1403,7 @@ def test_extending_stage_with_byte_identical_remote_prefix_resumes( assert result.entry_sha256 == stage.entry_sha256 +@pytest.mark.release def test_extending_stage_with_conflicting_remote_prefix_fails_before_archive( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, @@ -1445,6 +1456,7 @@ def test_extending_stage_with_conflicting_remote_prefix_fails_before_archive( assert all(call["op"] != "ARCHIVE" for call in transport.call_log) +@pytest.mark.release def test_publish_blocks_before_archive_when_head_witness_baseline_untracked( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, @@ -1504,6 +1516,7 @@ def test_publish_blocks_before_archive_when_head_witness_baseline_untracked( assert all(call["op"] not in {"ARCHIVE", "PUT"} for call in transport.call_log) +@pytest.mark.release def test_candidate_revalidation_failure_stops_before_upload( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, @@ -1534,6 +1547,7 @@ def test_candidate_revalidation_failure_stops_before_upload( assert all(call["op"] not in {"ARCHIVE", "PUT"} for call in transport.call_log) +@pytest.mark.release def test_stale_proofs_fail_closed_before_signing( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, @@ -1648,6 +1662,7 @@ def test_staged_nvattest_public_evidence_contains_no_private_reach_details( ) +@pytest.mark.release def test_dirty_retained_manifest_fails_closed_before_signing( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, @@ -1673,6 +1688,7 @@ def test_dirty_retained_manifest_fails_closed_before_signing( ) +@pytest.mark.release def test_existing_version_with_matching_digest_is_noop( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, @@ -1718,6 +1734,7 @@ def test_existing_version_with_matching_digest_is_noop( assert all(call["op"] not in {"ARCHIVE", "PUT"} for call in transport.call_log) +@pytest.mark.release def test_existing_version_with_mismatched_digest_is_terminal( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, @@ -2385,6 +2402,7 @@ def _pointer_pair_state( "latest-signature-restore-put", ), ) +@pytest.mark.release def test_crash_injection_classifies_pointer_pair_state( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, @@ -2532,6 +2550,7 @@ def test_crash_injection_classifies_pointer_pair_state( assert signature_puts[-1]["if_match"] is not None +@pytest.mark.release def test_pre_pointer_recheck_failure_stops_before_pointer_write( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, @@ -2568,6 +2587,7 @@ def test_pre_pointer_recheck_failure_stops_before_pointer_write( assert previous.sha256 +@pytest.mark.release def test_resign_pointer_preserves_chain_length_tip_and_version(tmp_path: Path) -> None: signer = FakeTransparencySigner() transport = DirectoryTransparencyTransport(tmp_path / "remote") @@ -2595,6 +2615,7 @@ def test_resign_pointer_preserves_chain_length_tip_and_version(tmp_path: Path) - assert latest.pointer["valid_until"] == "2026-08-07T00:00:00Z" +@pytest.mark.release def test_resign_pointer_failure_restores_old_signature_conditionally( tmp_path: Path, ) -> None: @@ -2629,6 +2650,7 @@ def test_resign_pointer_failure_restores_old_signature_conditionally( assert signature_puts[-1]["if_match"] is not None +@pytest.mark.release def test_resign_pointer_ambiguous_committed_put_reports_success(tmp_path: Path) -> None: signer = FakeTransparencySigner() transport = AmbiguousLatestPointerPutTransport(tmp_path / "remote")