mirror of
https://github.com/razzant/ouroboros.git
synced 2026-10-03 04:07:04 +00:00
Preserve declared-entry hash failures and typed evidence omissions under Python 3.13. Keep the approved Windows artifact refusal explicit, isolate custody and nested pytest state, and synchronize the permission, lock, CSP, TLS and dispatcher fixtures without changing runtime policy.
393 lines
15 KiB
Python
393 lines
15 KiB
Python
"""The exact commit receipt: what binds it, what may read it, and what may never erase it.
|
|
|
|
Split out of ``tests/test_evolution_state_integrity_v3.py`` by theme: the receipt bound to
|
|
campaign, transaction and task; the second commit blocked before review; the receipt race
|
|
ahead of the git commit; revoked authority; the rescue link and campaign sidecar that share
|
|
the CAS; and the stale, terminal and panicking writers that must not overwrite it.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pathlib
|
|
import threading
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
|
|
from tests._evolution_state_shared import (
|
|
_active_transaction,
|
|
_patch_commit_seam,
|
|
)
|
|
|
|
|
|
def test_exact_commit_receipt_is_bound_to_campaign_transaction_and_task(tmp_path):
|
|
from supervisor import evolution_lifecycle
|
|
|
|
campaign, tx = _active_transaction(tmp_path)
|
|
claim = {
|
|
"campaign_id": campaign["id"],
|
|
"transaction_id": tx["transaction_id"],
|
|
"task_id": tx["task_id"],
|
|
}
|
|
assert evolution_lifecycle.check_evolution_authority(**claim)["ok"] is True
|
|
|
|
receipt = evolution_lifecycle.record_evolution_commit(**claim, commit_sha="a" * 40)
|
|
|
|
assert receipt["ok"] is True
|
|
assert receipt["commit_sha"] == "a" * 40
|
|
stored = evolution_lifecycle._read_evolution_campaign()["active_transaction"]
|
|
assert stored["commit_receipt"] == receipt
|
|
assert evolution_lifecycle.check_evolution_authority(
|
|
**claim, commit_sha="b" * 40,
|
|
)["reason"] == "commit_receipt_mismatch"
|
|
|
|
campaign_state = evolution_lifecycle._read_evolution_campaign()
|
|
campaign_state["active_transaction"].pop("commit_receipt")
|
|
assert evolution_lifecycle._write_evolution_campaign(campaign_state) is True
|
|
assert evolution_lifecycle.check_evolution_authority(
|
|
**claim, commit_sha="a" * 40,
|
|
)["reason"] == "commit_receipt_missing"
|
|
|
|
|
|
def test_second_evolution_commit_is_blocked_before_review(tmp_path, monkeypatch):
|
|
from ouroboros.tools import git as git_tools
|
|
from supervisor import evolution_lifecycle
|
|
|
|
campaign, tx = _active_transaction(tmp_path)
|
|
assert evolution_lifecycle.record_evolution_commit(
|
|
campaign["id"], tx["transaction_id"], tx["task_id"], "a" * 40,
|
|
)["ok"] is True
|
|
review_calls = []
|
|
monkeypatch.setattr(git_tools, "_task_attributed_commit_paths", lambda *a, **k: (None, None, "", None))
|
|
monkeypatch.setattr(git_tools, "_check_overlapping_review_attempt", lambda *a, **k: "")
|
|
_patch_commit_seam(monkeypatch, "_record_commit_attempt", lambda *a, **k: None)
|
|
monkeypatch.setattr(git_tools, "_acquire_git_lock", lambda *a, **k: pathlib.Path("lock"))
|
|
monkeypatch.setattr(git_tools, "_release_git_lock", lambda *a, **k: None)
|
|
monkeypatch.setattr(git_tools, "_prepare_review_commit_worktree", lambda *a, **k: (False, ""))
|
|
_patch_commit_seam(monkeypatch, "_run_reviewed_stage_cycle",
|
|
lambda *a, **k: review_calls.append(True) or {"status": "passed"},
|
|
)
|
|
ctx = SimpleNamespace(
|
|
repo_dir=tmp_path,
|
|
drive_root=tmp_path,
|
|
current_task_type="evolution",
|
|
task_id=tx["task_id"],
|
|
task_metadata={"evolution_transaction": tx},
|
|
)
|
|
|
|
result = git_tools._repo_commit_push(ctx, "second commit")
|
|
|
|
assert "transaction_already_committed" in result
|
|
assert "No reviewer was called" in result
|
|
assert review_calls == []
|
|
|
|
|
|
def test_receipt_race_blocks_evolution_before_git_commit(tmp_path, monkeypatch):
|
|
from ouroboros.tools import git as git_tools
|
|
from supervisor import evolution_lifecycle
|
|
|
|
campaign, tx = _active_transaction(tmp_path)
|
|
ctx = SimpleNamespace(
|
|
repo_dir=tmp_path,
|
|
current_task_type="evolution",
|
|
task_id=tx["task_id"],
|
|
task_metadata={"evolution_transaction": tx},
|
|
)
|
|
_patch_commit_seam(monkeypatch, "_record_commit_attempt", lambda *a, **k: None)
|
|
claim, error = git_tools._check_evolution_commit_stage(
|
|
ctx, "commit", 0.0, phase="pre_review_authority",
|
|
)
|
|
assert error == ""
|
|
assert evolution_lifecycle.record_evolution_commit(
|
|
**claim, commit_sha="b" * 40,
|
|
)["ok"] is True
|
|
|
|
_claim, error = git_tools._check_evolution_commit_stage(
|
|
ctx, "commit", 0.0, phase="pre_commit_authority",
|
|
)
|
|
|
|
assert "transaction_already_committed" in error
|
|
assert "Nothing was committed" in error
|
|
|
|
|
|
def test_revoked_authority_leaves_commit_unrecorded(tmp_path):
|
|
from supervisor import evolution_lifecycle, state
|
|
|
|
campaign, tx = _active_transaction(tmp_path)
|
|
live = state.load_state()
|
|
live["evolution_mode_enabled"] = False
|
|
live["evolution_owner_stopped"] = True
|
|
state.save_state(live)
|
|
|
|
receipt = evolution_lifecycle.record_evolution_commit(
|
|
campaign["id"], tx["transaction_id"], tx["task_id"], "c" * 40,
|
|
)
|
|
|
|
assert receipt == {"ok": False, "reason": "owner_stopped", "commit_sha": "c" * 40}
|
|
assert evolution_lifecycle._read_evolution_campaign()["active_transaction"]["commit_sha"] == ""
|
|
|
|
|
|
def test_exact_receipt_remains_authority_after_post_task_autostop(tmp_path):
|
|
from supervisor import evolution_lifecycle, state
|
|
|
|
campaign, tx = _active_transaction(tmp_path)
|
|
sha = "9" * 40
|
|
claim = {
|
|
"campaign_id": campaign["id"],
|
|
"transaction_id": tx["transaction_id"],
|
|
"task_id": tx["task_id"],
|
|
}
|
|
assert evolution_lifecycle.record_evolution_commit(**claim, commit_sha=sha)["ok"] is True
|
|
state.update_state(lambda live: live.update(
|
|
evolution_mode_enabled=False,
|
|
post_task_autostop=False,
|
|
))
|
|
|
|
assert evolution_lifecycle.check_evolution_authority(
|
|
**claim, commit_sha=sha,
|
|
)["ok"] is True
|
|
assert evolution_lifecycle.check_evolution_authority(**claim)["reason"] == "evolution_disabled"
|
|
|
|
|
|
@pytest.mark.parametrize("held_lock", ["state", "campaign"])
|
|
def test_rescue_link_uses_shared_campaign_cas_and_preserves_commit_receipt(
|
|
tmp_path, monkeypatch, held_lock,
|
|
):
|
|
from ouroboros import platform_layer
|
|
from ouroboros.platform_layer import (
|
|
acquire_exclusive_file_lock,
|
|
release_exclusive_file_lock,
|
|
)
|
|
from ouroboros.utils import atomic_write_json
|
|
from supervisor import evolution_lifecycle, git_ops, state
|
|
|
|
campaign, tx = _active_transaction(tmp_path)
|
|
sha = "3" * 40
|
|
assert evolution_lifecycle.record_evolution_commit(
|
|
campaign["id"], tx["transaction_id"], tx["task_id"], sha,
|
|
)["ok"] is True
|
|
monkeypatch.setattr(evolution_lifecycle, "EVOLUTION_CAMPAIGN_CAS_TIMEOUT_SEC", 1.0)
|
|
monkeypatch.setattr(git_ops, "DRIVE_ROOT", tmp_path)
|
|
campaign_path = tmp_path / "state" / "evolution_campaign.json"
|
|
if held_lock == "state":
|
|
lock_path = tmp_path / "locks" / "state.lock"
|
|
lock_fd = state.acquire_file_lock(lock_path, timeout_sec=1.0)
|
|
release = state.release_file_lock
|
|
else:
|
|
lock_path = campaign_path.with_name(campaign_path.name + ".lock")
|
|
lock_fd = acquire_exclusive_file_lock(lock_path, timeout_sec=1.0)
|
|
release = release_exclusive_file_lock
|
|
assert lock_fd is not None
|
|
done = threading.Event()
|
|
acquiring = threading.Event()
|
|
released = threading.Event()
|
|
lock_owner = state if held_lock == "state" else platform_layer
|
|
acquire = lock_owner.acquire_exclusive_file_lock
|
|
|
|
def _acquire_after_interleave(path, **kwargs):
|
|
if path == lock_path:
|
|
acquiring.set()
|
|
released.wait()
|
|
return acquire(path, **kwargs)
|
|
|
|
# Prove contention separately, then hand off at the real acquisition seam.
|
|
# File I/O and thread scheduling must not consume the CAS timeout while the
|
|
# fixture deliberately holds the lock needed by the successful update.
|
|
monkeypatch.setattr(lock_owner, "acquire_exclusive_file_lock", _acquire_after_interleave)
|
|
|
|
def _link() -> None:
|
|
git_ops._link_rescue_to_evolution_transaction(
|
|
{"rescue_ref": "rescue/test", "path": "/tmp/rescue-test"},
|
|
"test",
|
|
)
|
|
done.set()
|
|
|
|
thread = threading.Thread(target=_link, daemon=True)
|
|
thread.start()
|
|
try:
|
|
assert acquire_exclusive_file_lock(lock_path, timeout_sec=0.001) is None
|
|
assert acquiring.wait(2.0) is True
|
|
assert done.wait(0.1) is False
|
|
current = evolution_lifecycle._read_evolution_campaign()
|
|
current["active_transaction"]["interleaved"] = held_lock
|
|
atomic_write_json(campaign_path, current, trailing_newline=True)
|
|
finally:
|
|
release(lock_path, lock_fd)
|
|
released.set()
|
|
thread.join(timeout=1.0)
|
|
assert done.wait(2.0) is True
|
|
assert not thread.is_alive()
|
|
|
|
stored = evolution_lifecycle._read_evolution_campaign()["active_transaction"]
|
|
assert stored["commit_sha"] == sha
|
|
assert stored["commit_receipt"]["commit_sha"] == sha
|
|
assert stored["rescue_ref"] == "rescue/test"
|
|
assert stored["interleaved"] == held_lock
|
|
|
|
|
|
def test_commit_receipt_uses_campaign_sidecar_before_rescue(tmp_path, monkeypatch):
|
|
from ouroboros import platform_layer
|
|
from ouroboros.platform_layer import (
|
|
acquire_exclusive_file_lock,
|
|
release_exclusive_file_lock,
|
|
)
|
|
from supervisor import evolution_lifecycle
|
|
|
|
campaign, tx = _active_transaction(tmp_path)
|
|
monkeypatch.setattr(evolution_lifecycle, "EVOLUTION_CAMPAIGN_CAS_TIMEOUT_SEC", 1.0)
|
|
campaign_path = tmp_path / "state" / "evolution_campaign.json"
|
|
lock_path = campaign_path.with_name(campaign_path.name + ".lock")
|
|
lock_fd = acquire_exclusive_file_lock(lock_path, timeout_sec=1.0)
|
|
assert lock_fd is not None
|
|
done = threading.Event()
|
|
acquiring = threading.Event()
|
|
released = threading.Event()
|
|
result = {}
|
|
|
|
def _acquire_after_release(path, **kwargs):
|
|
if path == lock_path:
|
|
acquiring.set()
|
|
released.wait()
|
|
return acquire_exclusive_file_lock(path, **kwargs)
|
|
|
|
# Keep real contention, but start the acquisition timeout after the fixture
|
|
# releases its lock, as in the rescue interleaving test above.
|
|
monkeypatch.setattr(platform_layer, "acquire_exclusive_file_lock", _acquire_after_release)
|
|
|
|
def _record() -> None:
|
|
result.update(evolution_lifecycle.record_evolution_commit(
|
|
campaign["id"], tx["transaction_id"], tx["task_id"], "4" * 40,
|
|
))
|
|
done.set()
|
|
|
|
thread = threading.Thread(target=_record, daemon=True)
|
|
thread.start()
|
|
try:
|
|
assert acquire_exclusive_file_lock(lock_path, timeout_sec=0.001) is None
|
|
assert acquiring.wait(2.0) is True
|
|
assert done.wait(0.1) is False
|
|
finally:
|
|
release_exclusive_file_lock(lock_path, lock_fd)
|
|
released.set()
|
|
thread.join(timeout=2.0)
|
|
assert done.wait(2.0) is True
|
|
assert not thread.is_alive()
|
|
assert result["ok"] is True
|
|
assert evolution_lifecycle._read_evolution_campaign()["active_transaction"][
|
|
"commit_receipt"
|
|
]["commit_sha"] == "4" * 40
|
|
|
|
|
|
def test_campaign_sidecar_contention_releases_state_lock_quickly(tmp_path):
|
|
from ouroboros.platform_layer import (
|
|
acquire_exclusive_file_lock,
|
|
release_exclusive_file_lock,
|
|
)
|
|
from supervisor import evolution_lifecycle, state
|
|
|
|
campaign, tx = _active_transaction(tmp_path)
|
|
campaign_path = tmp_path / "state" / "evolution_campaign.json"
|
|
sidecar = campaign_path.with_name(campaign_path.name + ".lock")
|
|
sidecar_fd = acquire_exclusive_file_lock(sidecar, timeout_sec=1.0)
|
|
assert sidecar_fd is not None
|
|
try:
|
|
result = evolution_lifecycle.record_evolution_commit(
|
|
campaign["id"], tx["transaction_id"], tx["task_id"], "6" * 40,
|
|
)
|
|
assert result["ok"] is False
|
|
state_fd = state.acquire_file_lock(state.STATE_LOCK_PATH, timeout_sec=0.2)
|
|
assert state_fd is not None
|
|
state.release_file_lock(state.STATE_LOCK_PATH, state_fd)
|
|
finally:
|
|
release_exclusive_file_lock(sidecar, sidecar_fd)
|
|
|
|
|
|
def test_sent_owner_report_clear_cannot_erase_concurrent_commit_receipt(tmp_path, monkeypatch):
|
|
from supervisor import evolution_lifecycle, queue
|
|
|
|
campaign, tx = _active_transaction(tmp_path)
|
|
report = {"cycle_outcome": "absorbed", "task_id": "previous"}
|
|
current = evolution_lifecycle._read_evolution_campaign()
|
|
current["pending_owner_report"] = report
|
|
assert evolution_lifecycle._write_evolution_campaign(current) is True
|
|
|
|
def _send_then_record(*args, **kwargs):
|
|
receipt = evolution_lifecycle.record_evolution_commit(
|
|
campaign["id"], tx["transaction_id"], tx["task_id"], "5" * 40,
|
|
)
|
|
assert receipt["ok"] is True
|
|
|
|
monkeypatch.setattr(queue, "notify_owner_cycle_outcome", _send_then_record)
|
|
|
|
queue._deliver_pending_owner_report()
|
|
|
|
stored = evolution_lifecycle._read_evolution_campaign()
|
|
assert "pending_owner_report" not in stored
|
|
assert stored["active_transaction"]["commit_receipt"]["commit_sha"] == "5" * 40
|
|
|
|
|
|
def test_terminal_campaign_cannot_be_resurrected_by_a_stale_writer(tmp_path):
|
|
from supervisor import evolution_lifecycle
|
|
|
|
campaign, _ = _active_transaction(tmp_path)
|
|
stale = dict(campaign)
|
|
evolution_lifecycle.complete_evolution_campaign("owner stop", cleanup_worktree=False)
|
|
stale["status"] = "active"
|
|
|
|
assert evolution_lifecycle._write_evolution_campaign(stale) is False
|
|
assert evolution_lifecycle._read_evolution_campaign()["status"] == "stopped"
|
|
|
|
|
|
def test_stale_campaign_cannot_overwrite_a_new_campaign(tmp_path):
|
|
from supervisor import evolution_lifecycle, queue, state
|
|
|
|
state.init(tmp_path)
|
|
queue.init(tmp_path)
|
|
first = evolution_lifecycle.start_evolution_campaign("First", source="test")
|
|
stale = dict(first)
|
|
evolution_lifecycle.complete_evolution_campaign("done", cleanup_worktree=False)
|
|
second = evolution_lifecycle.start_evolution_campaign("Second", source="test")
|
|
|
|
stale["status"] = "active"
|
|
assert evolution_lifecycle._write_evolution_campaign(stale) is False
|
|
assert evolution_lifecycle._read_evolution_campaign()["id"] == second["id"]
|
|
|
|
|
|
def test_panic_campaign_close_uses_nonblocking_state_lock(tmp_path, monkeypatch):
|
|
from supervisor import evolution_lifecycle, queue, state
|
|
|
|
state.init(tmp_path)
|
|
queue.init(tmp_path)
|
|
evolution_lifecycle.start_evolution_campaign("Improve", source="test")
|
|
timeouts = []
|
|
monkeypatch.setattr(
|
|
state,
|
|
"acquire_file_lock",
|
|
lambda path, timeout_sec=4.0, **kw: timeouts.append(timeout_sec) or None,
|
|
)
|
|
|
|
evolution_lifecycle.complete_evolution_campaign(
|
|
"panic stop", status="stopped", cleanup_worktree=False,
|
|
)
|
|
|
|
assert timeouts == [0.001]
|
|
|
|
|
|
def test_unreadable_campaign_file_refuses_writes_instead_of_treating_it_as_absent(tmp_path, monkeypatch):
|
|
"""A campaign file that EXISTS but cannot be read (a transient Windows sharing
|
|
violation, corruption) is not "no campaign": the stale-write guard cannot compare
|
|
ids, so the write is refused — never allowed as if the slot were free. An absent
|
|
file stays writable (the first campaign of a fresh data root)."""
|
|
from supervisor import evolution_lifecycle, queue, state
|
|
|
|
state.init(tmp_path)
|
|
queue.init(tmp_path)
|
|
path = evolution_lifecycle._evolution_campaign_path()
|
|
assert not path.exists()
|
|
fresh = {"schema_version": 1, "id": "abc12345", "status": "active", "objective": "First"}
|
|
assert evolution_lifecycle._write_evolution_campaign(dict(fresh)) is True
|
|
assert path.is_file()
|
|
monkeypatch.setattr(evolution_lifecycle, "read_json_dict", lambda _p: None)
|
|
assert evolution_lifecycle._write_evolution_campaign({**fresh, "id": "other001"}) is False
|
|
monkeypatch.undo()
|
|
assert evolution_lifecycle._read_evolution_campaign()["id"] == "abc12345"
|