teleo-infrastructure/tests/test_gcp_leoclean_nosend_preflight.py

727 lines
27 KiB
Python

"""Contracts for the read-only leoclean GCP staging preflight."""
from __future__ import annotations
import copy
import hashlib
import json
import os
import stat
import subprocess
import sys
from collections.abc import Callable
from pathlib import Path
import pytest
from ops import check_gcp_leoclean_nosend_preflight as preflight
REVISION = "a" * 40
PRIVATE_IP = "10.61.0.3"
def live_fixtures() -> dict[str, object]:
return {
"project": {"lifecycleState": "ACTIVE", "projectId": "teleo-501523"},
"instance": {
"name": "teleo-staging-1",
"status": "RUNNING",
"zone": "https://www.googleapis.com/compute/v1/projects/teleo-501523/zones/europe-west6-a",
"machineType": (
"https://www.googleapis.com/compute/v1/projects/teleo-501523/zones/europe-west6-a/"
"machineTypes/e2-standard-4"
),
"cpuPlatform": "Intel Broadwell",
"serviceAccounts": [{"email": "sa-teleo-staging-vm@teleo-501523.iam.gserviceaccount.com"}],
"disks": [
{
"boot": True,
"source": (
"https://www.googleapis.com/compute/v1/projects/teleo-501523/zones/europe-west6-a/"
"disks/teleo-staging-1"
),
}
],
"networkInterfaces": [
{
"network": (
"https://www.googleapis.com/compute/v1/projects/teleo-501523/global/networks/teleo-staging-net"
),
"subnetwork": (
"https://www.googleapis.com/compute/v1/projects/teleo-501523/regions/europe-west6/"
"subnetworks/teleo-staging-europe-west6"
),
"networkIP": "10.60.0.2",
"accessConfigs": [],
"ipv6AccessConfigs": [],
"stackType": "IPV4_ONLY",
}
],
"tags": {"items": ["teleo-staging-ssh"]},
},
"disk": {
"name": "teleo-staging-1",
"status": "READY",
"sizeGb": "80",
"zone": "https://www.googleapis.com/compute/v1/projects/teleo-501523/zones/europe-west6-a",
"architecture": "X86_64",
"users": [
"https://www.googleapis.com/compute/v1/projects/teleo-501523/zones/europe-west6-a/"
"instances/teleo-staging-1"
],
},
"subnet": {
"name": "teleo-staging-europe-west6",
"region": "https://www.googleapis.com/compute/v1/projects/teleo-501523/regions/europe-west6",
"network": "https://www.googleapis.com/compute/v1/projects/teleo-501523/global/networks/teleo-staging-net",
"ipCidrRange": "10.60.0.0/20",
"privateIpGoogleAccess": True,
},
"artifact": {
"name": "projects/teleo-501523/locations/europe-west6/repositories/teleo",
"format": "DOCKER",
"mode": "STANDARD_REPOSITORY",
"dockerConfig": {"immutableTags": True},
},
"cloudsql": {
"name": "teleo-pgvector-standby",
"state": "RUNNABLE",
"region": "europe-west6",
"databaseVersion": "POSTGRES_16",
"ipAddresses": [{"type": "PRIVATE", "ipAddress": PRIVATE_IP}],
"settings": {
"ipConfiguration": {
"ipv4Enabled": False,
"privateNetwork": "projects/teleo-501523/global/networks/teleo-staging-net",
"sslMode": "ENCRYPTED_ONLY",
}
},
},
"host": {
"architecture": "x86_64",
"project": "teleo-501523",
"instance": "teleo-staging-1",
"service_account": "sa-teleo-staging-vm@teleo-501523.iam.gserviceaccount.com",
"docker_service_load_state": "loaded",
"docker_service_active_state": "active",
"docker_service_sub_state": "running",
"docker_service_main_pid": "1701",
"docker_service_n_restarts": "0",
"docker_server_version": "29.4.0",
"docker_server_os": "linux",
"docker_server_arch": "amd64",
"docker_root_free_bytes": str(20 * 1024**3),
"unit_load_state": "not-found",
"container_collision_count": "0",
"cloudsql_tcp": "reachable",
},
}
def fixture_runner(
values: dict[str, object],
calls: list[list[str]] | None = None,
) -> Callable[[list[str]], subprocess.CompletedProcess[str]]:
def run(argv: list[str]) -> subprocess.CompletedProcess[str]:
if calls is not None:
calls.append(argv)
if argv[:3] == ["gcloud", "projects", "describe"]:
value: object = values["project"]
elif argv[:4] == ["gcloud", "compute", "instances", "describe"]:
value = values["instance"]
elif argv[:4] == ["gcloud", "compute", "disks", "describe"]:
value = values["disk"]
elif argv[:5] == ["gcloud", "compute", "networks", "subnets", "describe"]:
value = values["subnet"]
elif argv[:4] == ["gcloud", "artifacts", "repositories", "describe"]:
value = values["artifact"]
elif argv[:4] == ["gcloud", "sql", "instances", "describe"]:
value = values["cloudsql"]
elif argv[:3] == ["gcloud", "compute", "ssh"]:
host = values["host"]
assert isinstance(host, dict)
output = "".join(f"{key}={item}\n" for key, item in host.items())
return subprocess.CompletedProcess(argv, 0, stdout=output, stderr="")
else: # pragma: no cover - the assertion shows unexpected command drift
raise AssertionError(argv)
return subprocess.CompletedProcess(argv, 0, stdout=json.dumps(value), stderr="")
return run
def test_exact_read_only_preflight_passes_without_exposing_private_ip() -> None:
calls: list[list[str]] = []
payload = preflight.build_preflight(runner=fixture_runner(live_fixtures(), calls), source_revision=REVISION)
assert payload["status"] == "pass"
assert payload["source_revision"] == REVISION
assert payload["target"] == {
"project": "teleo-501523",
"zone": "europe-west6-a",
"instance": "teleo-staging-1",
"service": "leoclean-gcp-nosend.service",
"container": "livingip-leoclean-nosend",
"platform": "linux/amd64",
}
assert payload["authority"] == {
"mode": "bounded_observation_preflight",
"secret_values_read": False,
"database_credentials_used": False,
"database_queries_run": False,
"control_plane_observations_before_ssh_are_describe_only": True,
"docker_service_proven_active_before_client_calls": True,
"docker_service_identity_stable_after_client_calls": True,
"remote_host_runtime_mutations": False,
"ssh_access_may_register_time_bounded_key": True,
"ssh_key_maximum_lifetime": "5m",
}
assert payload["host"]["cloudsql_private_tcp_5432"] == "reachable"
assert payload["host"]["docker"]["service"] == {
"load_state": "loaded",
"active_state": "active",
"sub_state": "running",
"main_pid": 1701,
"n_restarts": 0,
"stable_across_client_observation": True,
}
assert (
payload["control_plane"]["cloudsql"]["private_ip_sha256"]
== hashlib.sha256(PRIVATE_IP.encode("ascii")).hexdigest()
)
assert PRIVATE_IP not in json.dumps(payload)
assert len(calls) == 7
assert calls[:-1] == [
[
"gcloud",
"sql",
"instances",
"describe",
"teleo-pgvector-standby",
"--project",
"teleo-501523",
"--format=json(name,state,region,databaseVersion,ipAddresses,settings.ipConfiguration)",
],
[
"gcloud",
"projects",
"describe",
"teleo-501523",
"--format=json(projectId,lifecycleState)",
],
[
"gcloud",
"compute",
"instances",
"describe",
"teleo-staging-1",
"--project",
"teleo-501523",
"--zone",
"europe-west6-a",
"--format=json(name,status,zone,machineType,cpuPlatform,serviceAccounts,disks,networkInterfaces,tags.items)",
],
[
"gcloud",
"compute",
"disks",
"describe",
"teleo-staging-1",
"--project",
"teleo-501523",
"--zone",
"europe-west6-a",
"--format=json(name,status,sizeGb,zone,architecture,users)",
],
[
"gcloud",
"compute",
"networks",
"subnets",
"describe",
"teleo-staging-europe-west6",
"--project",
"teleo-501523",
"--region",
"europe-west6",
"--format=json(name,region,network,ipCidrRange,privateIpGoogleAccess)",
],
[
"gcloud",
"artifacts",
"repositories",
"describe",
"teleo",
"--project",
"teleo-501523",
"--location",
"europe-west6",
"--format=json(name,format,mode,dockerConfig.immutableTags)",
],
]
ssh = calls[-1]
assert ssh == preflight._ssh_argv(PRIVATE_IP)
assert "--tunnel-through-iap" in ssh
assert "--ssh-key-expire-after=5m" in ssh
remote = next(item.removeprefix("--command=") for item in ssh if item.startswith("--command="))
syntax = subprocess.run(["/bin/bash", "-n"], input=remote, text=True, capture_output=True, check=False)
assert syntax.returncode == 0, syntax.stderr
assert remote.startswith("set -euo pipefail\n")
first_docker_client = remote.index("sudo -n /usr/bin/docker version")
assert remote.index("docker_service_active_state_before=") < first_docker_client
assert remote.index('test "$docker_service_active_state_before" = active') < first_docker_client
assert remote.index('test "$docker_service_sub_state_before" = running') < first_docker_client
assert remote.index('test "$docker_service_main_pid_before" -gt 1') < first_docker_client
last_docker_client = remote.index("sudo -n /usr/bin/docker ps")
assert remote.index("docker_service_active_state_after=", last_docker_client) > last_docker_client
assert (
remote.index(
'test "$docker_service_main_pid_after" = "$docker_service_main_pid_before"',
last_docker_client,
)
> last_docker_client
)
assert (
remote.index(
'test "$docker_service_n_restarts_after" = "$docker_service_n_restarts_before"',
last_docker_client,
)
> last_docker_client
)
for forbidden in (
"docker run",
"docker pull",
"docker rm",
"secretmanager",
"secrets versions access",
"systemctl start",
"systemctl restart",
"systemctl enable",
):
assert forbidden not in remote.casefold()
@pytest.mark.parametrize(
("path", "field", "unsafe", "message"),
[
(("instance", "serviceAccounts"), None, [{"email": "postgres-admin@example.com"}], "service account"),
(("disk",), "sizeGb", "40", "boot disk"),
(("disk",), "architecture", "ARM64", "architecture"),
(("disk",), "users", [], "attachment"),
(("instance", "networkInterfaces"), None, [], "network interface"),
(("instance", "networkInterfaces", 0), "accessConfigs", [{"natIP": "203.0.113.10"}], "public"),
(("instance", "networkInterfaces", 0), "ipv6AccessConfigs", [{"externalIpv6": "2001:db8::1"}], "IPv6"),
(("instance", "networkInterfaces", 0), "externalIpv6", "2001:db8::1", "IPv6"),
(("instance", "networkInterfaces", 0), "networkIP", "10.99.0.2", "reviewed subnet"),
(("subnet",), "privateIpGoogleAccess", False, "Private Google Access"),
(("subnet",), "ipCidrRange", "10.99.0.0/24", "CIDR drifted"),
(("artifact", "dockerConfig"), "immutableTags", False, "not immutable"),
(("cloudsql", "settings", "ipConfiguration"), "ipv4Enabled", True, "public IPv4"),
(("cloudsql",), "databaseVersion", "POSTGRES_160", "exactly PostgreSQL 16"),
(("host",), "docker_service_load_state", "not-found", "not loaded"),
(("host",), "docker_service_active_state", "inactive", "not active"),
(("host",), "docker_service_sub_state", "dead", "not running"),
(("host",), "docker_service_main_pid", "0", "MainPID"),
(("host",), "docker_service_n_restarts", "-1", "restart count"),
(("host",), "container_collision_count", "1", "container name"),
(("host",), "unit_load_state", "loaded", "unit name"),
(("host",), "docker_root_free_bytes", "1024", "free space"),
(("host",), "cloudsql_tcp", "unreachable", "TCP/5432"),
],
)
def test_preflight_fails_closed_on_prerequisite_drift(
path: tuple[str | int, ...],
field: str | None,
unsafe: object,
message: str,
) -> None:
values = copy.deepcopy(live_fixtures())
target: object = values
for part in path:
if isinstance(part, int):
assert isinstance(target, list)
target = target[part]
else:
assert isinstance(target, dict)
target = target[part]
if field is None:
assert isinstance(target, list)
parent: object = values
for part in path[:-1]:
if isinstance(part, int):
assert isinstance(parent, list)
parent = parent[part]
else:
assert isinstance(parent, dict)
parent = parent[part]
assert isinstance(parent, dict)
last = path[-1]
assert isinstance(last, str)
parent[last] = unsafe
else:
assert isinstance(target, dict)
target[field] = unsafe
with pytest.raises(preflight.PreflightError, match=message):
preflight.build_preflight(runner=fixture_runner(values), source_revision=REVISION)
def test_authentication_failure_is_classified_without_echoing_error() -> None:
sentinel = "sensitive-browser-session-sentinel"
def fail(argv: list[str]) -> subprocess.CompletedProcess[str]:
return subprocess.CompletedProcess(
argv,
1,
stdout="",
stderr=f"Reauthentication failed. {sentinel}. Run gcloud auth login.",
)
with pytest.raises(preflight.PreflightError) as caught:
preflight.build_preflight(runner=fail, source_revision=REVISION)
assert caught.value.category == "authentication_required"
assert sentinel not in str(caught.value)
@pytest.mark.parametrize(
("mutation", "message"),
[
({"ipAddresses": [{"type": "PRIVATE", "ipAddress": "10.99.0.2"}]}, "address drifted"),
(
{
"ipAddresses": [
{"type": "PRIVATE", "ipAddress": PRIVATE_IP},
{"type": "PRIMARY", "ipAddress": "203.0.113.10"},
]
},
"private-only",
),
(
{"settings": {"ipConfiguration": {"sslMode": "TRUSTED_CLIENT_CERTIFICATE_REQUIRED"}}},
"TLS mode",
),
],
)
def test_cloudsql_endpoint_and_tls_are_exact(mutation: dict[str, object], message: str) -> None:
values = copy.deepcopy(live_fixtures())
cloudsql = values["cloudsql"]
assert isinstance(cloudsql, dict)
if "settings" in mutation:
settings = cloudsql["settings"]
assert isinstance(settings, dict)
ip_config = settings["ipConfiguration"]
assert isinstance(ip_config, dict)
changed = mutation["settings"]
assert isinstance(changed, dict)
changed_ip_config = changed["ipConfiguration"]
assert isinstance(changed_ip_config, dict)
ip_config.update(changed_ip_config)
else:
cloudsql.update(mutation)
with pytest.raises(preflight.PreflightError, match=message):
preflight.build_preflight(runner=fixture_runner(values), source_revision=REVISION)
def test_host_output_rejects_extra_duplicate_or_unbounded_fields() -> None:
valid = live_fixtures()["host"]
assert isinstance(valid, dict)
encoded = "".join(f"{key}={item}\n" for key, item in valid.items())
for unsafe in (
encoded + "unexpected=value\n",
encoded + "project=duplicate\n",
encoded.replace("docker_server_os=linux", "docker_server_os=linux value with spaces"),
):
with pytest.raises(preflight.PreflightError, match="host preflight"):
preflight._parse_host_output(unsafe)
def test_command_gate_rejects_mutating_or_unreviewed_gcloud_calls() -> None:
for command in (
["gcloud", "compute", "instances", "start", "teleo-staging-1"],
["gcloud", "secrets", "versions", "access", "latest"],
["gcloud", "compute", "scp", "file", "teleo-staging-1:/tmp/file"],
["gcloud", "compute", "ssh", "teleo-staging-1", "--command=sudo systemctl restart x"],
["gcloud", "compute", "ssh", "teleo-staging-1", "--command=touch /tmp/preflight-was-here"],
["bash", "-c", "true"],
):
with pytest.raises(preflight.PreflightError):
preflight._assert_read_only_command(command)
def test_source_revision_requires_clean_tracked_commit(tmp_path: Path) -> None:
subprocess.run(["git", "init", "--quiet", str(tmp_path)], check=True)
tracked = tmp_path / "tracked.txt"
tracked.write_text("committed\n", encoding="utf-8")
subprocess.run(["git", "-C", str(tmp_path), "add", "tracked.txt"], check=True)
subprocess.run(
[
"git",
"-C",
str(tmp_path),
"-c",
"user.name=Preflight Test",
"-c",
"user.email=preflight@example.invalid",
"commit",
"--quiet",
"-m",
"fixture",
],
check=True,
)
revision = subprocess.run(
["git", "-C", str(tmp_path), "rev-parse", "HEAD"],
text=True,
capture_output=True,
check=True,
).stdout.strip()
assert preflight._source_revision(tmp_path, critical_paths=("tracked.txt",)) == revision
(tmp_path / "untracked.txt").write_text("drift\n", encoding="utf-8")
with pytest.raises(preflight.PreflightError, match="clean tracked"):
preflight._source_revision(tmp_path, critical_paths=("tracked.txt",))
def test_preflight_rejects_an_unrelated_clean_repository(tmp_path: Path) -> None:
with pytest.raises(preflight.PreflightError, match="containing this script"):
preflight._validated_repo_root(tmp_path)
def test_direct_cli_imports_then_rejects_an_unrelated_repository(tmp_path: Path) -> None:
completed = subprocess.run(
[
sys.executable,
str(preflight.SOURCE_ROOT / "ops" / "check_gcp_leoclean_nosend_preflight.py"),
"--repo-root",
str(tmp_path),
],
text=True,
capture_output=True,
check=False,
)
assert completed.returncode == 1
assert json.loads(completed.stdout) == {
"category": "invalid_source_root",
"schema": "livingip.leocleanNoSendGcpPreflight.v1",
"status": "blocked",
}
def _private_output_directory(tmp_path: Path) -> Path:
directory = tmp_path / "private-receipts"
directory.mkdir(mode=0o700)
directory.chmod(0o700)
return directory
def _stub_successful_cli(monkeypatch: pytest.MonkeyPatch) -> dict[str, object]:
payload: dict[str, object] = {
"schema": preflight.SCHEMA,
"status": "pass",
"source_revision": REVISION,
}
monkeypatch.setattr(preflight, "_source_revision", lambda *_args, **_kwargs: REVISION)
monkeypatch.setattr(preflight, "build_preflight", lambda **_kwargs: payload)
return payload
def test_cli_publishes_one_private_no_clobber_receipt(
tmp_path: Path,
capsys: pytest.CaptureFixture[str],
monkeypatch: pytest.MonkeyPatch,
) -> None:
payload = _stub_successful_cli(monkeypatch)
output = _private_output_directory(tmp_path) / "preflight.json"
assert preflight.main(["--output", str(output)]) == 0
assert json.loads(capsys.readouterr().out) == payload
assert json.loads(output.read_text(encoding="utf-8")) == payload
observed = output.stat(follow_symlinks=False)
assert stat.S_ISREG(observed.st_mode)
assert stat.S_IMODE(observed.st_mode) == 0o600
assert observed.st_uid == os.geteuid()
assert observed.st_nlink == 2
retained = list(output.parent.glob(".preflight.json.*.tmp"))
assert len(retained) == 1
assert retained[0].stat().st_ino == observed.st_ino
@pytest.mark.parametrize("entry_kind", ["file", "directory", "symlink"])
def test_cli_rejects_every_preexisting_output_before_live_observation(
entry_kind: str,
tmp_path: Path,
capsys: pytest.CaptureFixture[str],
monkeypatch: pytest.MonkeyPatch,
) -> None:
output = _private_output_directory(tmp_path) / "preflight.json"
if entry_kind == "file":
output.write_text("existing\n", encoding="utf-8")
elif entry_kind == "directory":
output.mkdir()
else:
target = output.with_name("target.json")
target.write_text("target\n", encoding="utf-8")
output.symlink_to(target)
called = False
def unexpected_live_observation(**_kwargs: object) -> dict[str, object]:
nonlocal called
called = True
raise AssertionError("live observation must not run")
monkeypatch.setattr(preflight, "build_preflight", unexpected_live_observation)
assert preflight.main(["--output", str(output)]) == 1
assert called is False
assert json.loads(capsys.readouterr().out) == {
"category": "invalid_output_path",
"schema": preflight.SCHEMA,
"status": "blocked",
}
@pytest.mark.parametrize("unsafe_mode", [0o720, 0o702, 0o777])
def test_cli_rejects_group_or_world_writable_output_parent(
unsafe_mode: int,
tmp_path: Path,
capsys: pytest.CaptureFixture[str],
monkeypatch: pytest.MonkeyPatch,
) -> None:
parent = _private_output_directory(tmp_path)
parent.chmod(unsafe_mode)
called = False
def unexpected_live_observation(**_kwargs: object) -> dict[str, object]:
nonlocal called
called = True
raise AssertionError("live observation must not run")
monkeypatch.setattr(preflight, "build_preflight", unexpected_live_observation)
assert preflight.main(["--output", str(parent / "preflight.json")]) == 1
assert called is False
assert json.loads(capsys.readouterr().out)["category"] == "invalid_output_path"
def test_publication_race_fails_without_overwriting_the_created_entry(
tmp_path: Path,
capsys: pytest.CaptureFixture[str],
monkeypatch: pytest.MonkeyPatch,
) -> None:
output = _private_output_directory(tmp_path) / "preflight.json"
monkeypatch.setattr(preflight, "_source_revision", lambda *_args, **_kwargs: REVISION)
def race(**_kwargs: object) -> dict[str, object]:
output.write_text("replacement-sentinel\n", encoding="utf-8")
return {"schema": preflight.SCHEMA, "status": "pass", "source_revision": REVISION}
monkeypatch.setattr(preflight, "build_preflight", race)
assert preflight.main(["--output", str(output)]) == 1
assert output.read_text(encoding="utf-8") == "replacement-sentinel\n"
assert list(output.parent.glob(".preflight.json.*.tmp")) == []
assert json.loads(capsys.readouterr().out) == {
"category": "receipt_publication_failed",
"schema": preflight.SCHEMA,
"status": "blocked",
}
def test_link_race_preserves_the_complete_staged_inode_without_overwrite(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
output = _private_output_directory(tmp_path) / "preflight.json"
parent_descriptor = preflight._open_private_output_parent(output)
payload = {"schema": preflight.SCHEMA, "status": "pass", "source_revision": REVISION}
real_link = preflight.os.link
def race_link(source: str, destination: str, **kwargs: object) -> None:
output.write_text("replacement-sentinel\n", encoding="utf-8")
real_link(source, destination, **kwargs)
monkeypatch.setattr(preflight.os, "link", race_link)
try:
with pytest.raises(preflight.PreflightError) as caught:
preflight._publish_private_receipt(output, parent_descriptor, payload)
finally:
os.close(parent_descriptor)
assert caught.value.category == "receipt_publication_failed"
assert output.read_text(encoding="utf-8") == "replacement-sentinel\n"
retained = list(output.parent.glob(".preflight.json.*.tmp"))
assert len(retained) == 1
assert json.loads(retained[0].read_text(encoding="utf-8")) == payload
assert retained[0].stat().st_nlink == 1
def test_prepublication_fsync_failure_is_bounded_and_preserves_only_staged_residue(
tmp_path: Path,
capsys: pytest.CaptureFixture[str],
monkeypatch: pytest.MonkeyPatch,
) -> None:
_stub_successful_cli(monkeypatch)
output = _private_output_directory(tmp_path) / "preflight.json"
real_fsync = preflight.os.fsync
sentinel = "private-publication-fault-sentinel"
def fail_file_fsync(descriptor: int) -> None:
if stat.S_ISREG(os.fstat(descriptor).st_mode):
raise OSError(sentinel)
real_fsync(descriptor)
monkeypatch.setattr(preflight.os, "fsync", fail_file_fsync)
assert preflight.main(["--output", str(output)]) == 1
stdout = capsys.readouterr().out
assert json.loads(stdout) == {
"category": "receipt_publication_failed",
"schema": preflight.SCHEMA,
"status": "blocked",
}
assert sentinel not in stdout
assert not output.exists()
retained = list(output.parent.glob(".preflight.json.*.tmp"))
assert len(retained) == 1
assert stat.S_IMODE(retained[0].stat().st_mode) == 0o600
assert retained[0].stat().st_nlink == 1
assert json.loads(retained[0].read_text(encoding="utf-8"))["status"] == "pass"
def test_prepublication_write_failure_is_bounded_and_never_creates_canonical_output(
tmp_path: Path,
capsys: pytest.CaptureFixture[str],
monkeypatch: pytest.MonkeyPatch,
) -> None:
_stub_successful_cli(monkeypatch)
output = _private_output_directory(tmp_path) / "preflight.json"
sentinel = "private-write-fault-sentinel"
def fail_after_partial_write(descriptor: int, payload: bytes) -> None:
os.write(descriptor, payload[:16])
raise OSError(sentinel)
monkeypatch.setattr(preflight, "_write_all", fail_after_partial_write)
assert preflight.main(["--output", str(output)]) == 1
stdout = capsys.readouterr().out
assert json.loads(stdout) == {
"category": "receipt_publication_failed",
"schema": preflight.SCHEMA,
"status": "blocked",
}
assert sentinel not in stdout
assert not output.exists()
retained = list(output.parent.glob(".preflight.json.*.tmp"))
assert len(retained) == 1
assert stat.S_IMODE(retained[0].stat().st_mode) == 0o600
assert retained[0].stat().st_nlink == 1
with pytest.raises(json.JSONDecodeError):
json.loads(retained[0].read_text(encoding="utf-8"))