Publish curated Unraid-AppOps-Gateway source from a480f280f26f4aa48c61394fa841d2233067c208

This commit is contained in:
Public Source Publisher committed 2026-10-04 11:55:06 +00:00
1 parent e73772cdf2
commit 820244dbb3
13 files changed
+2141 -26

No files matched your search

+575 -3
View File
@@ -469,7 +469,7 @@ def test_appops_legacy_intent_is_root_private_and_compare_and_swap_bound(
"legacyTree": deploy_module.APPOPS_LEGACY_OLD_TREE,
"databaseTree": "1" * 40, "entrypointBlob": "2" * 40,
"candidateRevision": revision, "composeBlob": "3" * 40,
"dockerfileBlob": "4" * 40,
"dockerfileBlob": "73e083dcfef25402665ace5a0bf07a083cf37432",
}
protected_main = {
"semanticSha256": "5" * 64, "revision": revision,
@@ -677,7 +677,7 @@ def _appops_rollback_intent_fixture(deploy_module, revision: str, capture_sha256
"legacyRevision": old_revision, "legacyTree": deploy_module.APPOPS_LEGACY_OLD_TREE,
"databaseTree": "1" * 40, "entrypointBlob": "2" * 40,
"candidateRevision": revision, "composeBlob": "3" * 40,
"dockerfileBlob": "4" * 40,
"dockerfileBlob": "73e083dcfef25402665ace5a0bf07a083cf37432",
}
protected_main = {
"semanticSha256": "5" * 64, "revision": revision,
@@ -730,6 +730,40 @@ def _appops_rollback_intent_fixture(deploy_module, revision: str, capture_sha256
return repository, metadata, source_proof, protected_main, backup, intent
def test_appops_legacy_intent_accepts_only_the_reviewed_dockerfile_blob_pair(
deploy_module
):
_repository, _metadata, _source, _main, _backup, intent = (
_appops_rollback_intent_fixture(deploy_module, "c" * 40, "a" * 64)
)
deploy_module._appops_validate_intent_payload(intent)
reviewed_update = copy.deepcopy(intent)
reviewed_source = reviewed_update["migrationProof"]["source"]
reviewed_source["dockerfileBlob"] = "aae97717280b8495a93bc277e290eeb515b76862"
reviewed_evidence = reviewed_update["rollbackSnapshot"]["appops_legacy"]
reviewed_evidence["sourceProofSha256"] = deploy_module._canonical_json_sha256(
reviewed_source
)
reviewed_evidence["migrationProofSha256"] = deploy_module._canonical_json_sha256(
reviewed_update["migrationProof"]
)
deploy_module._appops_validate_intent_payload(reviewed_update)
unknown_update = copy.deepcopy(reviewed_update)
unknown_source = unknown_update["migrationProof"]["source"]
unknown_source["dockerfileBlob"] = "b" * 40
unknown_evidence = unknown_update["rollbackSnapshot"]["appops_legacy"]
unknown_evidence["sourceProofSha256"] = deploy_module._canonical_json_sha256(
unknown_source
)
unknown_evidence["migrationProofSha256"] = deploy_module._canonical_json_sha256(
unknown_update["migrationProof"]
)
with pytest.raises(deploy_module.DeployError, match="source proof is invalid"):
deploy_module._appops_validate_intent_payload(unknown_update)
def test_appops_cli_rollback_retries_after_state_write_crash_without_synthetic_receipt(
deploy_module, monkeypatch, tmp_path, capsys
):
@@ -1333,7 +1367,7 @@ def test_appops_durable_rollback_requires_completed_intent_and_verified_backup(
"legacyTree": deploy_module.APPOPS_LEGACY_OLD_TREE,
"databaseTree": "1" * 40, "entrypointBlob": "2" * 40,
"candidateRevision": revision, "composeBlob": "3" * 40,
"dockerfileBlob": "4" * 40,
"dockerfileBlob": "73e083dcfef25402665ace5a0bf07a083cf37432",
}
protected_main = {
"semanticSha256": "5" * 64, "revision": revision,
@@ -1503,6 +1537,297 @@ def test_appops_state_write_rejects_compare_and_swap_drift(deploy_module, monkey
assert path.read_bytes() == original
def _build_reviewed_appops_seed_history(tmp_path, *, candidate_mutation=None):
"""Build the reviewed DB and Dockerfile transitions without old Git history."""
repository = tmp_path / "reviewed-source.git"
working = tmp_path / "reviewed-source-worktree"
working.mkdir()
subprocess.run(["git", "init", "--bare", str(repository)], check=True, capture_output=True)
subprocess.run(["git", "init", str(working)], check=True, capture_output=True)
for key, value in (
("user.name", "AppOps fixture"),
("user.email", "appops-fixture@example.invalid"),
("core.autocrlf", "false"),
("core.filemode", "true"),
):
subprocess.run(["git", "-C", str(working), "config", key, value], check=True)
subprocess.run(
["git", "-C", str(working), "remote", "add", "origin", str(repository)],
check=True,
)
db = working / "backend" / "app" / "db"
db.mkdir(parents=True)
current_db = ROOT / "backend" / "app" / "db"
def normalize(path):
return path.read_bytes().replace(b"\r\n", b"\n")
candidate_dockerfile = normalize(ROOT / "Dockerfile")
lock_setup = (
b"RUN set -eu; \\\n"
b" lock_parent=/run/itworx-appops; \\\n"
b" lock_root=\"$lock_parent/forge-oidc-provision\"; \\\n"
)
requirements_copy = b"COPY backend/requirements.txt /tmp/requirements.txt\n"
assert candidate_dockerfile.count(lock_setup) == 1
lock_start = candidate_dockerfile.index(lock_setup)
copy_start = candidate_dockerfile.index(requirements_copy, lock_start)
legacy_dockerfile = candidate_dockerfile[:lock_start] + candidate_dockerfile[copy_start:]
assert hashlib.sha1(
f"blob {len(legacy_dockerfile)}\0".encode() + legacy_dockerfile
).hexdigest() == "73e083dcfef25402665ace5a0bf07a083cf37432"
assert hashlib.sha1(
f"blob {len(candidate_dockerfile)}\0".encode() + candidate_dockerfile
).hexdigest() == "aae97717280b8495a93bc277e290eeb515b76862"
current_init = normalize(current_db / "init_db.py")
settings_marker = b"settings = Settings()\n"
assert current_init.count(settings_marker) == 1
body = current_init.split(settings_marker, 1)[1]
old_header = (
b"from datetime import UTC, datetime\n\n"
b"from sqlalchemy import inspect, text\n"
b"from sqlmodel import SQLModel, select\n"
b"from . import session as db_session\n"
b"from .session import get_session\n"
b"from ..core.config import Settings\n"
b"from ..services.app_classifier import classify_container\n"
b"from ..services.app_registry import category_label\n"
b"from .models import App, AppContainerMapping, Connector, DiscoveredContainer, "
b"NotificationChannel, NotificationEvent, NotificationRule, Policy, WorkMeshPromotion\n\n\n"
)
old_init = old_header + settings_marker + body
forge_seed_rows = (
b' Policy(id="policy-forge-oidc-provision", action="forge.oidc.provision", target="itworx-forge", voice="blocked", manual="allowed", risk="critical", reason="The fixed Forge OIDC client may be provisioned only by an authenticated manual operator route; voice and generic Authentik mutations remain blocked."),\n',
b' Policy(id="policy-forge-secret-prepare", action="forge.secrets.prepare", target="itworx-forge", voice="blocked", manual="blocked", risk="critical", reason="Preparing the fixed Forge bootstrap secret bundle is default-denied and requires an explicit exact-target manual operator policy; voice remains blocked."),\n',
)
for row in forge_seed_rows:
assert current_init.count(row) == 1
old_init = old_init.replace(row, b"", 1)
(db / "init_db.py").write_bytes(old_init)
for filename in ("models.py", "session.py"):
(db / filename).write_bytes(normalize(current_db / filename))
entrypoint = working / "docker" / "entrypoint.sh"
entrypoint.parent.mkdir(parents=True)
entrypoint.write_bytes(normalize(ROOT / "docker" / "entrypoint.sh"))
entrypoint.chmod(0o644)
(working / "docker-compose.yml").write_bytes(normalize(ROOT / "docker-compose.yml"))
(working / "Dockerfile").write_bytes(legacy_dockerfile)
def git(*args):
return subprocess.run(
["git", "-C", str(working), *args],
check=True, capture_output=True, text=True,
).stdout.strip()
git("add", "--all")
git("commit", "-m", "fixed legacy AppOps source")
old_revision = git("rev-parse", "HEAD")
git("push", "origin", "HEAD:refs/heads/main")
old_tree = git("rev-parse", f"{old_revision}^{{tree}}")
old_database_tree = git("rev-parse", f"{old_revision}:backend/app/db")
(db / "init_db.py").write_bytes(current_init)
if candidate_mutation == "extra_database_file":
(db / "unreviewed.py").write_text("UNREVIEWED = True\n", encoding="utf-8")
elif candidate_mutation == "models_content":
(db / "models.py").write_bytes(normalize(current_db / "models.py") + b"\n# unreviewed\n")
elif candidate_mutation == "session_content":
(db / "session.py").write_bytes(normalize(current_db / "session.py") + b"\n# unreviewed\n")
elif candidate_mutation == "init_db_content":
(db / "init_db.py").write_bytes(current_init + b"\n# unreviewed database behavior\n")
elif candidate_mutation == "forge_policy_changed":
changed_init = current_init.replace(
b'action="forge.oidc.provision"',
b'action="forge.oidc.provision-unreviewed"',
1,
)
assert changed_init != current_init
(db / "init_db.py").write_bytes(changed_init)
elif candidate_mutation == "database_file_mode":
(db / "session.py").chmod(0o755)
elif candidate_mutation == "entrypoint_content":
entrypoint.write_bytes(entrypoint.read_bytes() + b"# unreviewed entrypoint\n")
elif candidate_mutation not in {
None, "dockerfile_one_byte", "dockerfile_legacy_unchanged",
}:
raise AssertionError("unknown fixture mutation")
if candidate_mutation == "dockerfile_legacy_unchanged":
(working / "Dockerfile").write_bytes(legacy_dockerfile)
else:
candidate_dockerfile = normalize(ROOT / "Dockerfile")
if candidate_mutation == "dockerfile_one_byte":
original_mode = b'mkdir -m 0700 "$lock_root"'
changed_mode = b'mkdir -m 0701 "$lock_root"'
assert candidate_dockerfile.count(original_mode) == 1
candidate_dockerfile = candidate_dockerfile.replace(
original_mode, changed_mode, 1
)
assert len(candidate_dockerfile) == len(normalize(ROOT / "Dockerfile"))
assert sum(left != right for left, right in zip(
candidate_dockerfile, normalize(ROOT / "Dockerfile")
)) == 1
(working / "Dockerfile").write_bytes(candidate_dockerfile)
git("add", "--all")
git("commit", "-m", "candidate source")
candidate_revision = git("rev-parse", "HEAD")
git("push", "origin", "HEAD:refs/heads/main")
candidate_tree = git("rev-parse", f"{candidate_revision}^{{tree}}")
candidate_database_tree = git("rev-parse", f"{candidate_revision}:backend/app/db")
candidate_entrypoint = git("rev-parse", f"{candidate_revision}:docker/entrypoint.sh")
old_dockerfile_blob = git("rev-parse", f"{old_revision}:Dockerfile")
candidate_dockerfile_blob = git("rev-parse", f"{candidate_revision}:Dockerfile")
return {
"repository": repository,
"oldRevision": old_revision,
"oldTree": old_tree,
"oldDatabaseTree": old_database_tree,
"candidateRevision": candidate_revision,
"candidateTree": candidate_tree,
"candidateDatabaseTree": candidate_database_tree,
"candidateEntrypoint": candidate_entrypoint,
"oldDockerfileBlob": old_dockerfile_blob,
"candidateDockerfileBlob": candidate_dockerfile_blob,
}
def _reviewed_appops_source_project(deploy_module):
return {
"_repository_name": "jens/unraid-appops-gateway",
"branch": "main",
"gitea_repository_id": 9,
"source_dir": "/srv/example/appdata/unraid-appops-gateway/source",
"compose_project": "unraid-appops-gateway",
"compose_files": ["docker-compose.yml"],
"containers": ["Unraid-AppOps-Gateway"],
"verify_oci_revision": True,
"environment": {
"APPOPS_DATA_PATH": deploy_module.APPOPS_LEGACY_DATA_PATH,
"APPOPS_PUBLISH_HOST": deploy_module.APPOPS_LEGACY_PUBLISH_HOST,
"APPOPS_PROXY_NETWORK": "itworx-mcp-hub-npm",
},
}
def test_appops_legacy_source_proof_accepts_the_exact_main_184_dockerfile_update(
deploy_module, monkeypatch, tmp_path
):
history = _build_reviewed_appops_seed_history(tmp_path)
monkeypatch.setattr(deploy_module, "APPOPS_LEGACY_OLD_REVISION", history["oldRevision"])
monkeypatch.setattr(deploy_module, "APPOPS_LEGACY_OLD_TREE", history["oldTree"])
proof = deploy_module.verify_appops_legacy_source(
history["repository"],
_reviewed_appops_source_project(deploy_module),
history["candidateRevision"],
)
assert proof == {
"legacyRevision": history["oldRevision"],
"legacyTree": history["oldTree"],
"databaseTree": "c87c9aff5a0f0e55b9209e2f085cf1663876520a",
"entrypointBlob": "4f71ff74e67d7f15e3cf6b3e79b654158dfea2f8",
"candidateRevision": history["candidateRevision"],
"composeBlob": "243aee2c6924775bed7706373a5777f4e619e053",
"dockerfileBlob": "aae97717280b8495a93bc277e290eeb515b76862",
}
def test_appops_legacy_source_proof_keeps_the_old_dockerfile_candidate_valid(
deploy_module, monkeypatch, tmp_path
):
history = _build_reviewed_appops_seed_history(
tmp_path, candidate_mutation="dockerfile_legacy_unchanged"
)
monkeypatch.setattr(deploy_module, "APPOPS_LEGACY_OLD_REVISION", history["oldRevision"])
monkeypatch.setattr(deploy_module, "APPOPS_LEGACY_OLD_TREE", history["oldTree"])
proof = deploy_module.verify_appops_legacy_source(
history["repository"],
_reviewed_appops_source_project(deploy_module),
history["candidateRevision"],
)
assert proof["dockerfileBlob"] == "73e083dcfef25402665ace5a0bf07a083cf37432"
def test_appops_legacy_source_proof_rejects_a_one_byte_dockerfile_change(
deploy_module, monkeypatch, tmp_path
):
history = _build_reviewed_appops_seed_history(
tmp_path, candidate_mutation="dockerfile_one_byte"
)
monkeypatch.setattr(deploy_module, "APPOPS_LEGACY_OLD_REVISION", history["oldRevision"])
monkeypatch.setattr(deploy_module, "APPOPS_LEGACY_OLD_TREE", history["oldTree"])
with pytest.raises(deploy_module.DeployError, match="fixed legacy source"):
deploy_module.verify_appops_legacy_source(
history["repository"],
_reviewed_appops_source_project(deploy_module),
history["candidateRevision"],
)
def test_appops_schema_compatibility_accepts_the_exact_reviewed_seed_tree_pair(
deploy_module, monkeypatch, tmp_path
):
history = _build_reviewed_appops_seed_history(tmp_path)
assert history["oldDatabaseTree"] == "d7f8a6188f6c6e0c69bce0e547df31d36ed84a28"
assert history["candidateDatabaseTree"] == "c87c9aff5a0f0e55b9209e2f085cf1663876520a"
assert history["candidateEntrypoint"] == "4f71ff74e67d7f15e3cf6b3e79b654158dfea2f8"
monkeypatch.setattr(deploy_module, "APPOPS_LEGACY_OLD_REVISION", history["oldRevision"])
monkeypatch.setattr(deploy_module, "APPOPS_LEGACY_OLD_TREE", history["oldTree"])
proof = deploy_module.verify_appops_schema_compatibility(
history["repository"], history["candidateRevision"]
)
assert proof == {
"legacyRevision": history["oldRevision"],
"legacyTree": history["oldTree"],
"databaseTree": "c87c9aff5a0f0e55b9209e2f085cf1663876520a",
"entrypointBlob": "4f71ff74e67d7f15e3cf6b3e79b654158dfea2f8",
"candidateRevision": history["candidateRevision"],
}
@pytest.mark.parametrize(
"mutation",
[
"extra_database_file",
"models_content",
"session_content",
"init_db_content",
"forge_policy_changed",
"database_file_mode",
"entrypoint_content",
],
)
def test_appops_seed_tree_exception_rejects_every_unreviewed_tree_or_entrypoint_change(
deploy_module, monkeypatch, tmp_path, mutation
):
history = _build_reviewed_appops_seed_history(
tmp_path, candidate_mutation=mutation
)
assert history["oldDatabaseTree"] == "d7f8a6188f6c6e0c69bce0e547df31d36ed84a28"
if mutation == "entrypoint_content":
assert history["candidateDatabaseTree"] == "c87c9aff5a0f0e55b9209e2f085cf1663876520a"
assert history["candidateEntrypoint"] != "4f71ff74e67d7f15e3cf6b3e79b654158dfea2f8"
else:
assert history["candidateDatabaseTree"] != "c87c9aff5a0f0e55b9209e2f085cf1663876520a"
monkeypatch.setattr(deploy_module, "APPOPS_LEGACY_OLD_REVISION", history["oldRevision"])
monkeypatch.setattr(deploy_module, "APPOPS_LEGACY_OLD_TREE", history["oldTree"])
with pytest.raises(deploy_module.DeployError, match="database schema or entrypoint"):
deploy_module.verify_appops_schema_compatibility(
history["repository"], history["candidateRevision"]
)
def test_appops_schema_compatibility_compares_full_database_tree_and_entrypoint(
deploy_module, tmp_path, monkeypatch
):
@@ -1553,6 +1878,253 @@ def test_appops_schema_compatibility_compares_full_database_tree_and_entrypoint(
deploy_module.verify_appops_schema_compatibility(repository, changed)
def _write_appops_failed_revalidation_pair(
deploy_module, monkeypatch, tmp_path, *, parent_source="typed_self_upgrade",
parent_status="validation_timeout", sibling_source=None, include_sibling=True,
):
revision = "c" * 40
identity = {
"gitea_repository_id": 9,
"ref": "refs/heads/main",
"revision": revision,
}
semantic = hashlib.sha256(
json.dumps(identity, sort_keys=True, separators=(",", ":")).encode("utf-8")
).hexdigest()
state_root = tmp_path / "state"
inbox = state_root / "webhook-inbox"
inbox.mkdir(parents=True, mode=0o700)
inbox.chmod(0o700)
monkeypatch.setattr(deploy_module, "STATE_ROOT", state_root)
delivery = "12345678-1234-4234-8234-123456789012"
body_sha256 = "a" * 64
required_contexts = ["Managed validation / full (push)"]
parent = {
"schema": "gitea-webhook-inbox.v2",
"semantic_sha256": semantic,
"body_sha256": body_sha256,
"delivery": delivery,
**identity,
"repository": "jens/unraid-appops-gateway",
"source": parent_source,
"status": parent_status,
"reason": parent_status,
"validation": {
"validation_gate_version": 1,
"required_contexts": required_contexts,
"timeout_seconds": 1800,
"poll_interval_seconds": 5,
"max_poll_interval_seconds": 30,
"deadline_at": 1900,
"next_check_at": 1900,
"attempts": 360,
"observed": [],
"last_result": (
"timeout" if parent_status == "validation_timeout" else "reject"
),
},
"audit": [
{"stage": "awaiting_validation"},
{"stage": parent_status, "reason": parent_status},
],
}
parent_marker = inbox / f"{semantic}.json"
parent_marker.write_text(json.dumps(parent), encoding="utf-8")
parent_marker.chmod(0o600)
sibling = {
"schema": "gitea-webhook-revalidation.v1",
"semantic_sha256": semantic,
"revalidation_of": semantic,
"body_sha256": body_sha256,
"delivery": delivery,
**identity,
"repository": "jens/unraid-appops-gateway",
"status": "failed",
"reason": "deployment_failed",
"validation": {
"validation_gate_version": 1,
"required_contexts": required_contexts,
"timeout_seconds": 1800,
"poll_interval_seconds": 5,
"max_poll_interval_seconds": 30,
"deadline_at": 2800,
"next_check_at": 2500,
"attempts": 1,
"observed": [{
"context": "Managed validation / full (push)",
"status": "success",
}],
"last_result": "pass",
},
"audit": [
{"stage": "revalidation_requested"},
{"stage": "validation_passed"},
{"stage": "deployment_started"},
{"stage": "deployment_failed", "reason": "deployment_failed"},
],
}
if sibling_source is not None:
sibling["source"] = sibling_source
sibling_marker = inbox / f"{semantic}.revalidation.json"
if include_sibling:
sibling_marker.write_text(json.dumps(sibling), encoding="utf-8")
sibling_marker.chmod(0o600)
return {
"semantic": semantic,
"revision": revision,
"parent": parent,
"sibling": sibling,
"parent_marker": parent_marker,
"sibling_marker": sibling_marker,
}
@pytest.mark.parametrize("parent_source", ["gitea_webhook", "typed_self_upgrade"])
@pytest.mark.parametrize("parent_status", ["validation_timeout", "validation_failed"])
def test_appops_failed_delivery_accepts_only_a_chained_failed_revalidation(
deploy_module, monkeypatch, tmp_path, parent_source, parent_status
):
pair = _write_appops_failed_revalidation_pair(
deploy_module,
monkeypatch,
tmp_path,
parent_source=parent_source,
parent_status=parent_status,
)
original_bytes = pair["parent_marker"].read_bytes()
sibling_bytes = pair["sibling_marker"].read_bytes()
proof = deploy_module.verify_appops_failed_green_delivery(pair["revision"])
assert proof["revision"] == pair["revision"]
assert proof["source"] == parent_source
assert proof["delivery"] == "12345678-1234-4234-8234-123456789012"
assert proof["bodySha256"] == "a" * 64
assert proof["deliveryStatus"] == "failed"
assert proof["deliveryReason"] == "deployment_failed"
with pytest.raises(deploy_module.DeployError, match="protected full-green"):
deploy_module.verify_appops_green_delivery(pair["revision"])
assert pair["parent_marker"].read_bytes() == original_bytes
assert pair["sibling_marker"].read_bytes() == sibling_bytes
@pytest.mark.parametrize(
("which", "field", "value"),
[
("parent", "repository", "attacker/repo"),
("parent", "gitea_repository_id", 8),
("parent", "ref", "refs/heads/other"),
("parent", "revision", "d" * 40),
("parent", "semantic_sha256", "b" * 64),
("parent", "body_sha256", "b" * 64),
("parent", "delivery", "87654321-4321-4321-8321-210987654321"),
("parent", "schema", "gitea-webhook-inbox.v1"),
("parent", "status", "failed"),
("parent", "source", "unknown_source"),
("parent", "reason", "validation_failed"),
("parent", "validation.timeout_seconds", 900),
("parent", "validation.required_contexts", ["Managed validation / pull_request"]),
("parent", "audit", [{"stage": "validation_timeout", "reason": "deployment_failed"}]),
("sibling", "repository", "attacker/repo"),
("sibling", "gitea_repository_id", 8),
("sibling", "ref", "refs/heads/other"),
("sibling", "revision", "d" * 40),
("sibling", "semantic_sha256", "b" * 64),
("sibling", "body_sha256", "b" * 64),
("sibling", "delivery", "87654321-4321-4321-8321-210987654321"),
("sibling", "schema", "gitea-webhook-inbox.v2"),
("sibling", "revalidation_of", "b" * 64),
("sibling", "source", "typed_self_upgrade"),
("sibling", "source", "gitea_webhook"),
("sibling", "status", "deployed"),
("sibling", "reason", "deployment_interrupted"),
("sibling", "validation.timeout_seconds", 900),
("sibling", "validation.validation_gate_version", True),
("sibling", "validation.required_contexts", ["Managed validation / pull_request"]),
("sibling", "validation.observed", [{
"context": "Managed validation / full (push)", "status": "pending",
}]),
("sibling", "audit", [{"stage": "validation_passed"}]),
("sibling", "audit", [
{"stage": "revalidation_requested"},
{"stage": "deployment_started"},
{"stage": "validation_passed"},
{"stage": "deployment_failed", "reason": "deployment_failed"},
]),
],
)
def test_appops_failed_delivery_rejects_an_unbound_or_incomplete_revalidation(
deploy_module, monkeypatch, tmp_path, which, field, value
):
pair = _write_appops_failed_revalidation_pair(
deploy_module, monkeypatch, tmp_path
)
target = pair[which]
keys = field.split(".")
for key in keys[:-1]:
target = target[key]
target[keys[-1]] = value
marker = pair[f"{which}_marker"]
marker.write_text(json.dumps(pair[which]), encoding="utf-8")
marker.chmod(0o600)
with pytest.raises(deploy_module.DeployError, match="protected full-green"):
deploy_module.verify_appops_failed_green_delivery(pair["revision"])
def test_appops_failed_delivery_does_not_use_a_revalidation_without_its_original(
deploy_module, monkeypatch, tmp_path
):
pair = _write_appops_failed_revalidation_pair(
deploy_module, monkeypatch, tmp_path
)
pair["parent_marker"].unlink()
with pytest.raises(deploy_module.DeployError, match="protected full-green"):
deploy_module.verify_appops_failed_green_delivery(pair["revision"])
def test_appops_failed_delivery_does_not_reopen_an_original_validation_timeout(
deploy_module, monkeypatch, tmp_path
):
pair = _write_appops_failed_revalidation_pair(
deploy_module, monkeypatch, tmp_path, include_sibling=False
)
with pytest.raises(deploy_module.DeployError, match="protected full-green"):
deploy_module.verify_appops_failed_green_delivery(pair["revision"])
def test_appops_failed_delivery_keeps_the_original_primary_failure_path(
deploy_module, monkeypatch, tmp_path
):
pair = _write_appops_failed_revalidation_pair(
deploy_module, monkeypatch, tmp_path, include_sibling=False
)
primary = pair["parent"]
primary["status"] = "failed"
primary["reason"] = "deployment_failed"
primary["validation"]["observed"] = [{
"context": "Managed validation / full (push)", "status": "success",
}]
primary["validation"]["last_result"] = "pass"
primary["audit"] = [
{"stage": "validation_passed"},
{"stage": "deployment_started"},
{"stage": "deployment_failed", "reason": "deployment_failed"},
]
pair["parent_marker"].write_text(json.dumps(primary), encoding="utf-8")
pair["parent_marker"].chmod(0o600)
proof = deploy_module.verify_appops_failed_green_delivery(pair["revision"])
assert proof["revision"] == pair["revision"]
assert proof["source"] == "typed_self_upgrade"
assert proof["deliveryStatus"] == "failed"
def test_appops_migration_requires_private_exact_full_green_delivery(
deploy_module, monkeypatch, tmp_path
):