Files
aios/tests/single_owner_governance/test_live_activation.py
T

772 lines
32 KiB
Python

from __future__ import annotations
import json
from pathlib import Path
import sqlite3
import subprocess
import hashlib
import os
from datetime import datetime, timedelta, timezone
import pytest
from jsonschema import Draft202012Validator, ValidationError
from aios_single_owner_governance.activation import (
ActivationEngine,
InjectedCrash,
_verify_approved_entities,
validate_bindings,
)
import aios_single_owner_governance.validator as validator_module
import aios_single_owner_governance.state_machine as state_machine_module
import aios_single_owner_governance.activation as activation_module
from aios_single_owner_governance.gitea_protected_branch import (
CASConflict,
FileProtectedBranchAdapter,
SQLiteProtectedBranchAdapter,
UnsafePolicy,
calculate_snapshot_sha256,
validate_target,
)
from aios_single_owner_governance.journal import sha256_json
MACHINE_CONTEXTS = {
"aios/validator",
"aios/state-machine",
"aios/branch-protection",
"aios/secret-scan",
"aios/evidence-closure",
}
def write_json(path: Path, value: object) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(value, sort_keys=True) + "\n", encoding="utf-8")
def legacy_row() -> dict:
return {
"id": 7,
"repo_id": 1,
"branch_name": "governance/approvals",
"can_push": 0,
"enable_whitelist": 0,
"whitelist_user_i_ds": "null",
"whitelist_team_i_ds": "null",
"enable_merge_whitelist": 1,
"whitelist_deploy_keys": 0,
"merge_whitelist_user_i_ds": "[42,43]",
"merge_whitelist_team_i_ds": "null",
"enable_status_check": 0,
"status_check_contexts": "[]",
"enable_approvals_whitelist": 1,
"approvals_whitelist_user_i_ds": "[42,43]",
"approvals_whitelist_team_i_ds": "null",
"required_approvals": 2,
"block_on_rejected_reviews": 1,
"block_on_official_review_requests": 1,
"block_on_outdated_branch": 1,
"dismiss_stale_approvals": 1,
"ignore_stale_approvals": 0,
"require_signed_commits": 0,
"protected_file_patterns": "",
"unprotected_file_patterns": "",
"created_unix": 1,
"updated_unix": 10,
}
def owner_row() -> dict:
row = legacy_row()
row.update(
{
"approvals_whitelist_user_i_ds": "[2]",
"required_approvals": 1,
"enable_status_check": 1,
"status_check_contexts": json.dumps(sorted(MACHINE_CONTEXTS), separators=(",", ":")),
"updated_unix": 11,
}
)
return row
def git_output(git_dir: Path, *args: str, stdin: str | None = None) -> str:
return subprocess.run(
["git", f"--git-dir={git_dir}", *args], input=stdin, text=True,
capture_output=True, check=True,
env={
**__import__("os").environ,
"GIT_AUTHOR_NAME": "Test", "GIT_AUTHOR_EMAIL": "test@example.invalid",
"GIT_COMMITTER_NAME": "Test", "GIT_COMMITTER_EMAIL": "test@example.invalid",
},
).stdout.strip()
def commit_files(
git_dir: Path, files: dict[str, str], message: str, *, parent: str | None = None
) -> tuple[str, str]:
root: dict[str, object] = {}
for path, raw in files.items():
node = root
parts = path.split("/")
for directory in parts[:-1]:
node = node.setdefault(directory, {}) # type: ignore[assignment]
node[parts[-1]] = git_output(git_dir, "hash-object", "-w", "--stdin", stdin=raw)
def build_tree(node: dict[str, object]) -> str:
lines = []
for name in sorted(node):
value = node[name]
if isinstance(value, dict):
lines.append(f"040000 tree {build_tree(value)}\t{name}")
else:
lines.append(f"100644 blob {value}\t{name}")
return git_output(git_dir, "mktree", stdin="\n".join(lines) + "\n")
tree = build_tree(root)
args = ["commit-tree", tree]
if parent is not None:
args.extend(("-p", parent))
return git_output(git_dir, *args, stdin=message + "\n"), tree
def evidence_fixture(tmp_path: Path, execution_binding: dict) -> dict:
git_dir = tmp_path / "evidence.git"
subprocess.run(["git", "init", "--bare", str(git_dir)], capture_output=True, check=True)
package_root = Path(activation_module.__file__).resolve().parent
source_files: dict[str, str] = {}
runtime_sources = []
for name in (
"activation.py", "gitea_protected_branch.py", "journal.py",
"state_machine.py", "validator.py",
):
source = package_root / name
raw = source.read_text(encoding="utf-8")
git_path = (
"services/aios-single-owner-governance/"
f"aios_single_owner_governance/{name}"
)
source_files[git_path] = raw
runtime_sources.append({
"git_path": git_path,
"runtime_path": str(source.resolve()),
"sha256": hashlib.sha256(source.read_bytes()).hexdigest(),
})
implementation_oid, implementation_tree = commit_files(
git_dir, source_files, "implementation"
)
closure_path = "evidence/live-activation-closure.json"
closure_raw = '{"result":"PASS"}\n'
closure_oid, _ = commit_files(
git_dir, {**source_files, closure_path: closure_raw},
"closure", parent=implementation_oid
)
closure_sha = hashlib.sha256(closure_raw.encode()).hexdigest()
entities = {}
for name, raw in {
"artifact": b"approved-artifact\n",
"rollback_artifact": b"approved-rollback\n",
"test_evidence": b"approved-tests\n",
}.items():
path = tmp_path / "artifacts" / f"{name}.bin"
path.parent.mkdir(exist_ok=True)
path.write_bytes(raw)
entities[f"{name}_path"] = str(path.resolve())
entities[f"{name}_sha256"] = hashlib.sha256(raw).hexdigest()
head_oid = git_output(git_dir, "hash-object", "-w", "--stdin", stdin="head\n")
expiry = datetime.now(timezone.utc) + timedelta(hours=1)
producer_id = 99
producer_name = "trusted-machine-producer"
execution_binding = {
**execution_binding,
"evidence_closure_oid": closure_oid,
"evidence_closure_path": closure_path,
"evidence_closure_sha256": closure_sha,
"implementation_commit_oid": implementation_oid,
"implementation_tree_oid": implementation_tree,
"runtime_sources": runtime_sources,
"machine_status_producer_id": producer_id,
"machine_status_producer_name": producer_name,
**entities,
}
manifest = {
"approval_type": "SINGLE_OWNER_GOVERNANCE_LIVE_ACTIVATION_IMPLEMENTATION_AND_EXECUTION",
"expires_at": expiry.isoformat().replace("+00:00", "Z"),
"execution_binding": execution_binding,
"execution_binding_sha256": sha256_json(execution_binding),
**{key: execution_binding[key] for key in (
"implementation_commit_oid", "implementation_tree_oid", "runtime_sources",
"artifact_path", "artifact_sha256", "rollback_artifact_path",
"rollback_artifact_sha256", "test_evidence_path", "test_evidence_sha256",
"evidence_closure_oid", "evidence_closure_path", "evidence_closure_sha256",
"machine_status_producer_id", "machine_status_producer_name",
)},
}
manifest_raw = json.dumps(manifest, sort_keys=True, separators=(",", ":")) + "\n"
manifest_blob = git_output(git_dir, "hash-object", "-w", "--stdin", stdin=manifest_raw)
leaf = git_output(git_dir, "mktree", stdin=f"100644 blob {manifest_blob}\tactivation.json\n")
approvals = git_output(git_dir, "mktree", stdin=f"040000 tree {leaf}\tsingle-owner\n")
tree = git_output(git_dir, "mktree", stdin=f"040000 tree {approvals}\tapprovals\n")
merge_oid = git_output(git_dir, "commit-tree", tree, stdin="approved\n")
git_output(git_dir, "update-ref", "refs/heads/governance/approvals", merge_oid)
db = tmp_path / "approval.db"
with sqlite3.connect(db) as connection:
connection.executescript(
"CREATE TABLE issue(id INTEGER,repo_id INTEGER,is_closed INTEGER);"
"CREATE TABLE pull_request(issue_id INTEGER,\"index\" INTEGER,base_branch TEXT,has_merged INTEGER,merged_commit_id TEXT,merged_unix INTEGER);"
"CREATE TABLE user(id INTEGER,lower_name TEXT);"
"CREATE TABLE review(id INTEGER,type INTEGER,reviewer_id INTEGER,issue_id INTEGER,commit_id TEXT,stale INTEGER,dismissed INTEGER,created_unix INTEGER);"
"CREATE TABLE commit_status(id INTEGER,repo_id INTEGER,state TEXT,sha TEXT,context TEXT,creator_id INTEGER);"
)
connection.execute("INSERT INTO issue VALUES(10,1,1)")
merged_unix = int(datetime.now(timezone.utc).timestamp())
connection.execute("INSERT INTO pull_request VALUES(10,31,'governance/approvals',1,?,?)", (merge_oid, merged_unix))
connection.execute("INSERT INTO user VALUES(2,'owner')")
connection.execute("INSERT INTO user VALUES(?,?)", (producer_id, producer_name))
connection.execute(
"INSERT INTO review VALUES(52,1,2,10,?,0,0,?)",
(head_oid, int(datetime.now(timezone.utc).timestamp())),
)
machine_checks = {}
for index, context in enumerate(sorted(MACHINE_CONTEXTS), 101):
connection.execute(
"INSERT INTO commit_status VALUES(?,1,'success',?,?,?)",
(index, head_oid, context, producer_id),
)
machine_checks[context] = {
"state": "success", "head_oid": head_oid, "status_id": index,
"creator_id": producer_id, "creator_login": producer_name,
}
return {
"git_dir": git_dir, "merge_oid": merge_oid, "closure_oid": closure_oid,
"head_oid": head_oid, "db": db, "expiry": expiry.isoformat().replace("+00:00", "Z"),
"manifest_path": "approvals/single-owner/activation.json",
"manifest_blob": manifest_blob,
"manifest_sha256": hashlib.sha256(manifest_raw.encode()).hexdigest(),
"execution_binding": execution_binding,
"machine_checks": machine_checks,
**execution_binding,
}
def bindings(tmp_path: Path, adapter: FileProtectedBranchAdapter, **overrides) -> dict:
validator_path = Path(validator_module.__file__).resolve()
state_machine_path = Path(state_machine_module.__file__).resolve()
validator_sha = hashlib.sha256(validator_path.read_bytes()).hexdigest()
state_machine_sha = hashlib.sha256(state_machine_path.read_bytes()).hexdigest()
execution_binding = {
"task_id": "AIOS-LIVE-ACTIVATION-TEST",
"operation_id": "op-live-001",
"expected_validator_sha256": validator_sha,
"expected_state_machine_sha256": state_machine_sha,
"expected_branch_protection_sha256": adapter.calculate_snapshot_sha256(legacy_row()),
"expected_gitea_schema_sha256": "0" * 64,
"target_branch_protection_sha256": calculate_snapshot_sha256(owner_row()),
"backend": "file",
"repo_id": 1,
"branch": "governance/approvals",
"requester": "agent",
"executor": "activation-executor",
"forbidden_operations": {
"main": False, "governance_direct": False, "integration": False,
"tags": False, "force_push": False,
},
}
evidence = evidence_fixture(tmp_path, execution_binding)
value = {
"task_id": "AIOS-LIVE-ACTIVATION-TEST",
"operation_id": "op-live-001",
"expected_governance_oid": evidence["merge_oid"],
"observed_governance_oid": evidence["merge_oid"],
"expected_validator_sha256": validator_sha,
"observed_validator_sha256": validator_sha,
"expected_state_machine_sha256": state_machine_sha,
"observed_state_machine_sha256": state_machine_sha,
"validator_path": str(validator_path),
"state_machine_path": str(state_machine_path),
"governance_git_dir": str(evidence["git_dir"].resolve()),
"governance_ref": "refs/heads/governance/approvals",
"evidence_git_dir": str(evidence["git_dir"].resolve()),
"approval_db_path": str(evidence["db"].resolve()),
"expected_branch_protection_sha256": adapter.calculate_snapshot_sha256(legacy_row()),
"activation_approval_merge_oid": evidence["merge_oid"],
"approval_request_head_oid": evidence["head_oid"],
"activation_approval_pr_number": 31,
"activation_manifest_path": evidence["manifest_path"],
"activation_manifest_blob_oid": evidence["manifest_blob"],
"activation_manifest_sha256": evidence["manifest_sha256"],
"expires_at": evidence["expiry"],
"evidence_closure_oid": evidence["closure_oid"],
"observed_evidence_closure_oid": evidence["closure_oid"],
**{key: evidence[key] for key in (
"implementation_commit_oid", "implementation_tree_oid", "runtime_sources",
"artifact_path", "artifact_sha256", "rollback_artifact_path",
"rollback_artifact_sha256", "test_evidence_path", "test_evidence_sha256",
"evidence_closure_path", "evidence_closure_sha256",
"machine_status_producer_id", "machine_status_producer_name",
)},
"owner_review_oid": 52,
"observed_owner_review_oid": 52,
"requester": "agent",
"executor": "activation-executor",
"wip": False,
"machine_checks": evidence["machine_checks"],
"forbidden_operations": {
"main": False,
"governance_direct": False,
"integration": False,
"tags": False,
"force_push": False,
},
"target_branch_protection": owner_row(),
"backend": "file",
"backend_path": str(adapter.path.resolve()),
"repo_id": 1,
"branch": "governance/approvals",
"expected_gitea_schema_sha256": "0" * 64,
}
value.update(overrides)
return value
def harness(tmp_path: Path):
backend = tmp_path / "protected.json"
write_json(backend, legacy_row())
adapter = FileProtectedBranchAdapter(backend)
engine = ActivationEngine(adapter, tmp_path / "state", tmp_path / "run" / "activation.lock")
request = bindings(tmp_path, adapter)
return backend, adapter, engine, request
def test_live_plan_success(tmp_path: Path) -> None:
_, _, engine, request = harness(tmp_path)
result = engine.plan(request)
assert result["status"] == "PLAN_READY"
assert len(result["plan_id"]) == 64
def test_activation_bindings_schema_matches_runtime_contract(tmp_path: Path) -> None:
_, _, _, request = harness(tmp_path)
schema_path = Path(activation_module.__file__).resolve().parents[1] / "activation-bindings.schema.json"
schema = json.loads(schema_path.read_text(encoding="utf-8"))
Draft202012Validator.check_schema(schema)
validator = Draft202012Validator(schema)
validator.validate(request)
invalid = json.loads(json.dumps(request))
invalid["machine_checks"][sorted(MACHINE_CONTEXTS)[0]]["status_id"] = 0
with pytest.raises(ValidationError):
validator.validate(invalid)
def test_runtime_source_must_be_actual_loaded_module_not_decoy(tmp_path: Path) -> None:
_, _, engine, request = harness(tmp_path)
source = Path(request["runtime_sources"][0]["runtime_path"])
decoy = tmp_path / "decoy.py"
decoy.write_bytes(source.read_bytes())
request["runtime_sources"][0]["runtime_path"] = str(decoy.resolve())
with pytest.raises(RuntimeError, match="RUNTIME_SOURCE_PATH_MISMATCH"):
_verify_approved_entities(request, Path(request["evidence_git_dir"]))
@pytest.mark.parametrize("path_field", ["artifact_path", "rollback_artifact_path", "test_evidence_path"])
def test_approved_entity_digest_is_enforced(tmp_path: Path, path_field: str) -> None:
_, _, engine, request = harness(tmp_path)
Path(request[path_field]).write_bytes(b"tampered\n")
with pytest.raises(RuntimeError, match="APPROVED_ENTITY_DIGEST_MISMATCH"):
engine.plan(request)
def test_evidence_closure_must_be_direct_child_commit(tmp_path: Path) -> None:
_, _, engine, request = harness(tmp_path)
git_dir = Path(request["evidence_git_dir"])
unrelated, _ = commit_files(
git_dir,
{request["evidence_closure_path"]: '{"result":"PASS"}\n'},
"unrelated",
)
request["evidence_closure_oid"] = unrelated
request["observed_evidence_closure_oid"] = unrelated
with pytest.raises(RuntimeError, match="APPROVED_EXECUTION_BINDING_MISMATCH"):
engine.plan(request)
def test_pr_merge_after_expiry_is_denied(tmp_path: Path) -> None:
_, _, engine, request = harness(tmp_path)
with sqlite3.connect(request["approval_db_path"]) as connection:
connection.execute(
"UPDATE pull_request SET merged_unix=?",
(int(_parse_test_utc(request["expires_at"]).timestamp()) + 1,),
)
with pytest.raises(RuntimeError, match="ACTIVATION_APPROVAL_MERGED_AFTER_EXPIRY"):
engine.plan(request)
def _parse_test_utc(value: str) -> datetime:
return datetime.fromisoformat(value.replace("Z", "+00:00"))
def test_machine_status_must_match_bound_producer_and_observed_row(tmp_path: Path) -> None:
_, _, engine, request = harness(tmp_path)
context = sorted(MACHINE_CONTEXTS)[0]
request["machine_checks"][context]["status_id"] += 1000
with pytest.raises(RuntimeError, match="MACHINE_GATE_BINDING_MISMATCH"):
engine.plan(request)
def test_latest_machine_status_from_unapproved_producer_is_denied(tmp_path: Path) -> None:
_, _, engine, request = harness(tmp_path)
context = sorted(MACHINE_CONTEXTS)[0]
with sqlite3.connect(request["approval_db_path"]) as connection:
connection.execute("INSERT INTO user VALUES(100,'untrusted-producer')")
connection.execute(
"INSERT INTO commit_status VALUES(999,1,'success',?,?,100)",
(request["approval_request_head_oid"], context),
)
with pytest.raises(RuntimeError, match="MACHINE_GATE_PRODUCER_MISMATCH"):
engine.plan(request)
def test_live_activate_verify_status_success(tmp_path: Path) -> None:
_, adapter, engine, request = harness(tmp_path)
planned = engine.plan(request)
activated = engine.activate(request, planned["plan_id"])
assert activated["status"] == "ACTIVATED"
assert adapter.read_current() == owner_row()
assert engine.verify(request, request["operation_id"])["status"] == "VERIFIED"
assert engine.status(request["operation_id"])["transaction"]["phase"] == "COMMITTED"
assert (engine.journal.root / "current-snapshot.json").exists()
assert (engine.journal.root / "rollback-snapshot.json").exists()
def test_live_rollback_success(tmp_path: Path) -> None:
_, adapter, engine, request = harness(tmp_path)
planned = engine.plan(request)
engine.activate(request, planned["plan_id"])
assert engine.rollback(request, request["operation_id"], planned["plan_id"])["status"] == "ROLLED_BACK"
assert adapter.read_current() == legacy_row()
assert engine.rollback(request, request["operation_id"], planned["plan_id"])["status"] == "ALREADY_ROLLED_BACK"
def test_terminal_rollback_cannot_be_reactivated(tmp_path: Path) -> None:
_, _, engine, request = harness(tmp_path)
planned = engine.plan(request)
engine.activate(request, planned["plan_id"])
engine.rollback(request, request["operation_id"], planned["plan_id"])
with pytest.raises(RuntimeError, match="OPERATION_TERMINAL_ROLLED_BACK"):
engine.activate(request, planned["plan_id"])
def test_verify_and_rollback_remain_available_after_approval_expiry(
tmp_path: Path, monkeypatch
) -> None:
_, adapter, engine, request = harness(tmp_path)
planned = engine.plan(request)
engine.activate(request, planned["plan_id"])
future = datetime.now(timezone.utc) + timedelta(hours=2)
class FutureDateTime(datetime):
@classmethod
def now(cls, tz=None):
return future if tz is not None else future.replace(tzinfo=None)
monkeypatch.setattr(activation_module, "datetime", FutureDateTime)
assert engine.verify(request, request["operation_id"])["status"] == "VERIFIED"
assert engine.rollback(request, request["operation_id"], planned["plan_id"])["status"] == "ROLLED_BACK"
assert adapter.read_current() == legacy_row()
def test_prepared_crash_can_recover_after_approval_expiry(tmp_path: Path, monkeypatch) -> None:
_, adapter, engine, request = harness(tmp_path)
request["crash_at"] = "before_apply"
planned = engine.plan(request)
with pytest.raises(InjectedCrash):
engine.activate(request, planned["plan_id"])
request.pop("crash_at")
future = datetime.now(timezone.utc) + timedelta(hours=2)
class FutureDateTime(datetime):
@classmethod
def now(cls, tz=None):
return future if tz is not None else future.replace(tzinfo=None)
monkeypatch.setattr(activation_module, "datetime", FutureDateTime)
assert engine.activate(request, planned["plan_id"])["status"] == "ACTIVATED"
assert adapter.read_current() == owner_row()
def test_fresh_activation_rechecks_expiry_at_prepared_boundary(
tmp_path: Path, monkeypatch
) -> None:
_, adapter, engine, request = harness(tmp_path)
planned = engine.plan(request)
expiry = _parse_test_utc(request["expires_at"])
moments = iter((
expiry - timedelta(seconds=2),
expiry - timedelta(seconds=1),
expiry + timedelta(seconds=1),
))
monkeypatch.setattr(activation_module, "_utc_now", lambda: next(moments))
with pytest.raises(RuntimeError, match="APPROVAL_EXPIRED"):
engine.activate(request, planned["plan_id"])
assert adapter.read_current() == legacy_row()
assert engine.journal.load(request["operation_id"]) is None
def test_fresh_activation_rejects_snapshot_drift_at_prepared_boundary(
tmp_path: Path, monkeypatch
) -> None:
_, adapter, engine, request = harness(tmp_path)
planned = engine.plan(request)
original_read = adapter.read_current
reads = 0
def drifting_read() -> dict:
nonlocal reads
reads += 1
if reads == 2:
drifted = original_read()
drifted["can_push"] = 1
drifted["updated_unix"] = 777
return drifted
return original_read()
monkeypatch.setattr(adapter, "read_current", drifting_read)
with pytest.raises(CASConflict, match="PREPARED_BOUNDARY_SNAPSHOT_CHANGED"):
engine.activate(request, planned["plan_id"])
assert original_read() == legacy_row()
assert engine.journal.load(request["operation_id"]) is None
def test_rollback_uses_durable_authorization_after_live_status_rerun(tmp_path: Path) -> None:
_, adapter, engine, request = harness(tmp_path)
planned = engine.plan(request)
engine.activate(request, planned["plan_id"])
context = sorted(MACHINE_CONTEXTS)[0]
with sqlite3.connect(request["approval_db_path"]) as connection:
connection.execute(
"INSERT INTO commit_status VALUES(999,1,'success',?,?,?)",
(
request["approval_request_head_oid"], context,
request["machine_status_producer_id"],
),
)
result = engine.rollback(request, request["operation_id"], planned["plan_id"])
assert result["status"] == "ROLLED_BACK"
assert adapter.read_current() == legacy_row()
def test_missing_approval_denied(tmp_path: Path) -> None:
_, _, engine, request = harness(tmp_path)
request.pop("owner_review_oid")
with pytest.raises(RuntimeError, match="MISSING_BINDINGS"):
engine.plan(request)
@pytest.mark.parametrize(
("field", "value", "message"),
[
("observed_owner_review_oid", 99, "WRONG_APPROVAL_OID_DENIED"),
("observed_governance_oid", "f" * 40, "WRONG_GOVERNANCE_OID_DENIED"),
("observed_evidence_closure_oid", "f" * 40, "WRONG_EVIDENCE_CLOSURE_DENIED"),
],
)
def test_wrong_binding_denied(tmp_path: Path, field: str, value: object, message: str) -> None:
_, _, engine, request = harness(tmp_path)
request[field] = value
with pytest.raises(RuntimeError, match=message):
engine.plan(request)
def test_cas_conflict_denied(tmp_path: Path) -> None:
backend, _, engine, request = harness(tmp_path)
planned = engine.plan(request)
changed = legacy_row()
changed["updated_unix"] = 12
write_json(backend, changed)
with pytest.raises(CASConflict):
engine.activate(request, planned["plan_id"])
@pytest.mark.parametrize("crash_at", ["before_apply", "during_apply", "after_apply_before_receipt", "after_commit_wal"])
def test_process_crash_restart_recovery(tmp_path: Path, crash_at: str) -> None:
_, adapter, engine, request = harness(tmp_path)
request["crash_at"] = crash_at
planned = engine.plan(request)
with pytest.raises(InjectedCrash):
engine.activate(request, planned["plan_id"])
request.pop("crash_at")
result = engine.activate(request, planned["plan_id"])
expected_status = "ALREADY_COMMITTED" if crash_at == "after_commit_wal" else "ACTIVATED"
assert result["status"] == expected_status
assert adapter.read_current() == owner_row()
assert engine.journal.receipt_path(request["operation_id"]).exists()
assert engine.journal.audit.has_event(request["operation_id"], "SINGLE_OWNER_ACTIVATION_COMMITTED")
def test_post_activation_verify_failure_rolls_back(tmp_path: Path, monkeypatch) -> None:
_, adapter, engine, request = harness(tmp_path)
planned = engine.plan(request)
monkeypatch.setattr(adapter, "verify_applied", lambda _: False)
with pytest.raises(RuntimeError, match="POST_ACTIVATION_VERIFY_FAILED"):
engine.activate(request, planned["plan_id"])
assert adapter.read_current() == legacy_row()
assert engine.journal.load(request["operation_id"])["phase"] == "ROLLED_BACK"
def test_operation_id_idempotency(tmp_path: Path) -> None:
_, _, engine, request = harness(tmp_path)
planned = engine.plan(request)
engine.activate(request, planned["plan_id"])
assert engine.activate(request, planned["plan_id"])["status"] == "ALREADY_COMMITTED"
@pytest.mark.parametrize("crash_at", ["after_rollback_cas", "after_rollback_wal"])
def test_rollback_crash_restart_repairs_closure(tmp_path: Path, crash_at: str) -> None:
_, adapter, engine, request = harness(tmp_path)
planned = engine.plan(request)
engine.activate(request, planned["plan_id"])
request["crash_at"] = crash_at
with pytest.raises(InjectedCrash):
engine.rollback(request, request["operation_id"], planned["plan_id"])
request.pop("crash_at")
assert engine.rollback(request, request["operation_id"], planned["plan_id"])["status"] == "ALREADY_ROLLED_BACK"
assert adapter.read_current() == legacy_row()
assert engine.journal.receipt_path(request["operation_id"]).exists()
assert engine.journal.audit.has_event(request["operation_id"], "ACTIVATION_ROLLED_BACK")
def test_operation_id_payload_mismatch_denied(tmp_path: Path) -> None:
_, _, engine, request = harness(tmp_path)
planned = engine.plan(request)
engine.activate(request, planned["plan_id"])
changed = dict(request)
approval_alias = tmp_path / "approval-alias.db"
os.link(request["approval_db_path"], approval_alias)
changed["approval_db_path"] = str(approval_alias)
with pytest.raises(RuntimeError, match="PAYLOAD_MISMATCH"):
engine.activate(changed, planned["plan_id"])
@pytest.mark.parametrize(
("field", "value"),
[("operation_id", "op-live-REPLAY"), ("task_id", "REPLAY-TASK")],
)
def test_approved_execution_binding_replay_denied(tmp_path: Path, field: str, value: str) -> None:
_, _, engine, request = harness(tmp_path)
request[field] = value
with pytest.raises(RuntimeError, match="APPROVED_EXECUTION_BINDING_MISMATCH"):
engine.plan(request)
def test_expiry_must_come_from_approved_manifest(tmp_path: Path) -> None:
_, _, engine, request = harness(tmp_path)
request["expires_at"] = (datetime.now(timezone.utc) + timedelta(hours=12)).isoformat().replace("+00:00", "Z")
with pytest.raises(RuntimeError, match="APPROVAL_EXPIRY_BINDING_MISMATCH"):
engine.plan(request)
def test_target_snapshot_must_match_approved_manifest(tmp_path: Path) -> None:
_, _, engine, request = harness(tmp_path)
request["target_branch_protection"] = {**owner_row(), "updated_unix": 99}
with pytest.raises(RuntimeError, match="APPROVED_EXECUTION_BINDING_MISMATCH"):
engine.plan(request)
def test_approval_database_must_be_backend_database(tmp_path: Path) -> None:
_, _, engine, request = harness(tmp_path)
other_db = tmp_path / "other.db"
other_db.write_bytes(Path(request["approval_db_path"]).read_bytes())
request.update(backend="sqlite", backend_path=str(other_db.resolve()))
with pytest.raises(RuntimeError, match="APPROVAL_DB_BACKEND_DB_IDENTITY_MISMATCH"):
engine.plan(request)
def test_audit_chain_continuity_and_credential_redaction(tmp_path: Path) -> None:
_, _, engine, request = harness(tmp_path)
request["api_token"] = "must-not-appear"
planned = engine.plan(request)
engine.activate(request, planned["plan_id"])
assert len(engine.journal.audit.verify()) == 64
raw = (engine.journal.root / "audit.jsonl").read_text(encoding="utf-8")
assert "must-not-appear" not in raw
assert "[REDACTED]" not in raw # only hashes/events are persisted to audit
@pytest.mark.parametrize(
"mutation",
[
lambda request: request.update(requester="owner"),
lambda request: request.update(executor="owner"),
lambda request: request.update(wip=True),
lambda request: request.update(machine_checks={"anything": {"state": "success", "head_oid": request["approval_request_head_oid"]}}),
lambda request: request.update(forbidden_operations={"main": True}),
lambda request: request.update(forbidden_operations={"tags": True}),
lambda request: request.update(forbidden_operations={"force_push": True}),
],
)
def test_negative_governance_acceptance(tmp_path: Path, mutation) -> None:
_, _, engine, request = harness(tmp_path)
mutation(request)
with pytest.raises(RuntimeError):
engine.plan(request)
@pytest.mark.parametrize(
"mutation",
[
lambda target: target.update(enable_approvals_whitelist=0),
lambda target: target.update(enable_merge_whitelist=0),
lambda target: target.update(status_check_contexts="[]"),
],
)
def test_unsafe_policy_enable_fields_denied(tmp_path: Path, mutation) -> None:
_, _, _, request = harness(tmp_path)
mutation(request["target_branch_protection"])
with pytest.raises(UnsafePolicy):
validate_target(legacy_row(), request["target_branch_protection"])
@pytest.mark.parametrize("invalid_owner", ["codex", "gpt"])
def test_owner_review_must_come_from_gitea_database(tmp_path: Path, invalid_owner: str) -> None:
_, _, engine, request = harness(tmp_path)
with sqlite3.connect(request["approval_db_path"]) as connection:
connection.execute("UPDATE user SET lower_name=? WHERE id=2", (invalid_owner,))
with pytest.raises(RuntimeError, match="OWNER_APPROVAL_INVALID"):
engine.plan(request)
def test_stale_owner_review_denied_from_gitea_database(tmp_path: Path) -> None:
_, _, engine, request = harness(tmp_path)
with sqlite3.connect(request["approval_db_path"]) as connection:
connection.execute("UPDATE review SET stale=1 WHERE id=52")
with pytest.raises(RuntimeError, match="OWNER_APPROVAL_INVALID"):
engine.plan(request)
def create_sqlite_fixture(path: Path) -> None:
row = legacy_row()
columns = []
for key, value in row.items():
kind = "INTEGER" if isinstance(value, int) else "TEXT"
columns.append(f"{key} {kind}")
with sqlite3.connect(path) as connection:
connection.execute(f"CREATE TABLE protected_branch ({','.join(columns)})")
connection.execute(
f"INSERT INTO protected_branch ({','.join(row)}) VALUES ({','.join('?' for _ in row)})",
list(row.values()),
)
def test_sqlite_full_row_cas_and_rollback(tmp_path: Path) -> None:
db = tmp_path / "gitea.db"
create_sqlite_fixture(db)
probe = SQLiteProtectedBranchAdapter(db, expected_schema_sha256="0" * 64)
adapter = SQLiteProtectedBranchAdapter(db, expected_schema_sha256=probe.schema_sha256())
previous = adapter.read_current()
applied = adapter.apply_atomic(calculate_snapshot_sha256(previous), owner_row())
assert applied == owner_row()
restored = adapter.rollback_atomic(calculate_snapshot_sha256(applied), previous)
assert restored == previous