772 lines
32 KiB
Python
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
|