mirror of
https://github.com/razzant/ouroboros.git
synced 2026-10-03 04:07:04 +00:00
The Windows full-test leg of #1247 found three defects in the new lock-scope tests and one pre-existing runtime wart the tests exposed. Runtime: after `reset --hard` the snapshot copies the source's exact bytes over the checkout. Under `core.autocrlf=true` (Git for Windows' default) the checkout wrote CRLF and recorded that size in the index; the copy restored the source's LF bytes, and git trusts a size mismatch as a modification without re-hashing, so every such file (`.gitmodules` on the runner) read as modified in the child's `git status` for the run's whole life. One `update-index -z --stdin` over the copied paths re-hashes them through the same clean filters the baseline used and re-records their stat; the blob is unchanged. Tests: the lock holder reports its own pid (a venv `python.exe` on Windows is a launcher whose child is the interpreter, so `Popen.pid` is not the pid in the lock) and exits on stdin EOF so a killed launcher never leaves a child holding the OS lock; the populate test runs a second time under an autocrlf=true global config so the stat re-record is proven on every OS (it fails with the re-record removed); the consumer scan compares POSIX path spellings. Docs: the populate sentence names the stat re-record; chapter budget raised. Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
515 lines
25 KiB
Python
515 lines
25 KiB
Python
"""The worktree ops lock guards shared metadata only (#1241).
|
|
|
|
One delegated snapshot of a project with 67,692 untracked files held the
|
|
machine-wide ``.worktree_ops.lock`` for 40 minutes, and every other mutating
|
|
``delegate_start`` on the machine waited 120 s and was refused. Listing,
|
|
classifying, hashing, populating, copying and deleting a snapshot's files now run
|
|
OUTSIDE the lock; the registry row, the baseline pin and the worktree admin dir
|
|
are the only things written under it — row first, so everything after it is
|
|
nameable by the startup GC. A held lock is a typed refusal that names its holder,
|
|
a dead holder is evicted at once, and a pre-POST refusal is definitely unrun.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import pathlib
|
|
import subprocess
|
|
import sys
|
|
import threading
|
|
import time
|
|
|
|
import pytest
|
|
|
|
from ouroboros import artifacts, subagent_worktrees as wt, workspace_patch_capture as capture
|
|
from ouroboros.platform_layer import _lock_identity, pid_is_alive
|
|
from tests._delegated_transport_shared import _owned_gateway_uses_each_test_transport # noqa: F401
|
|
from tests.test_delegated_full_access import full_run # noqa: F401
|
|
from tests.test_delegated_run_isolation import _git, _nanny_ctx, _seed_target
|
|
|
|
REPO = pathlib.Path(__file__).resolve().parents[1]
|
|
|
|
|
|
def _lock_held(snaps: pathlib.Path) -> bool:
|
|
return bool(_lock_identity(pathlib.Path(snaps) / wt._LOCK_NAME))
|
|
|
|
|
|
def _provision(target, snaps, data, snapshot_id="snap1", task_id="t1"):
|
|
return wt.provision_execution_snapshot(
|
|
target_root=target, task_id=task_id, snapshot_id=snapshot_id, worktree_root=snaps, data_dir=data)
|
|
|
|
|
|
def _phase_spies(monkeypatch, snaps):
|
|
"""Record whether the lock was held when each phase ran."""
|
|
seen: dict = {}
|
|
real_verdicts, real_copy, real_git, real_git_env, real_save = (
|
|
capture.untracked_binary_verdicts, artifacts.copy_artifact_file, wt._git, wt._git_env, wt._save_registry)
|
|
|
|
def spy_git_env(repo_dir, *args, **kw): # baseline staging goes through _git_env, not _git
|
|
for marker in ("update-index", "write-tree", "commit-tree"):
|
|
if marker in args:
|
|
seen[marker] = _lock_held(snaps)
|
|
return real_git_env(repo_dir, *args, **kw)
|
|
|
|
def spy_verdicts(root, rels, **kw):
|
|
seen["classify"] = _lock_held(snaps)
|
|
return real_verdicts(root, rels, **kw)
|
|
|
|
def spy_copy(source, destination, **kw):
|
|
seen["copy"] = _lock_held(snaps)
|
|
return real_copy(source, destination, **kw)
|
|
|
|
def spy_git(repo_dir, *args, **kw):
|
|
for marker in ("update-index", "commit-tree", "reset", "update-ref", "worktree"):
|
|
if marker in args:
|
|
seen[marker + ("_add" if marker == "worktree" and "add" in args else "")] = _lock_held(snaps)
|
|
return real_git(repo_dir, *args, **kw)
|
|
|
|
def spy_save(entries, data_dir=None):
|
|
seen.setdefault("save_registry", []).append(_lock_held(snaps))
|
|
return real_save(entries, data_dir)
|
|
|
|
monkeypatch.setattr(capture, "untracked_binary_verdicts", spy_verdicts)
|
|
monkeypatch.setattr(artifacts, "copy_artifact_file", spy_copy)
|
|
monkeypatch.setattr(wt, "_git", spy_git)
|
|
monkeypatch.setattr(wt, "_git_env", spy_git_env)
|
|
monkeypatch.setattr(wt, "_save_registry", spy_save)
|
|
return seen
|
|
|
|
|
|
def test_tree_walk_runs_outside_the_lock_and_shared_metadata_inside(tmp_path, monkeypatch):
|
|
target = _seed_target(tmp_path)
|
|
snaps, data = tmp_path / "snaps", tmp_path / "data"
|
|
seen = _phase_spies(monkeypatch, snaps)
|
|
|
|
handle = _provision(target, snaps, data)
|
|
|
|
assert seen["classify"] is False and seen["copy"] is False and seen["reset"] is False
|
|
assert seen["update-index"] is False and seen["write-tree"] is False and seen["commit-tree"] is False
|
|
assert seen["update-ref"] is True and seen["worktree_add"] is True
|
|
assert seen["save_registry"] == [True, True] # provisional row, then the final row
|
|
assert not _lock_held(snaps)
|
|
assert handle.provisioning_sec >= 0 and wt.find_execution_snapshot("snap1", data_dir=data)["file_baseline"] == {}
|
|
|
|
|
|
@pytest.mark.parametrize("same_target", [False, True])
|
|
def test_a_parked_provision_does_not_block_another(tmp_path, monkeypatch, same_target):
|
|
target_a = _seed_target(tmp_path)
|
|
(tmp_path / "other").mkdir()
|
|
target_b = target_a if same_target else _seed_target(tmp_path / "other")
|
|
snaps, data = tmp_path / "snaps", tmp_path / "data"
|
|
parked, release = threading.Event(), threading.Event()
|
|
real_verdicts = capture.untracked_binary_verdicts
|
|
observed: dict = {}
|
|
|
|
def spy_verdicts(root, rels, **kw):
|
|
if threading.current_thread().name == "provision-a":
|
|
parked.set()
|
|
assert release.wait(timeout=60), "the parked provision was never released"
|
|
else:
|
|
observed["b_saw_lock_held"] = _lock_held(snaps)
|
|
return real_verdicts(root, rels, **kw)
|
|
|
|
monkeypatch.setattr(capture, "untracked_binary_verdicts", spy_verdicts)
|
|
errors: list = []
|
|
|
|
def run_a():
|
|
try:
|
|
_provision(target_a, snaps, data, snapshot_id="snapA", task_id="ta")
|
|
except BaseException as exc: # pragma: no cover - surfaced by the assertion below
|
|
errors.append(exc)
|
|
|
|
thread_a = threading.Thread(target=run_a, name="provision-a")
|
|
thread_a.start()
|
|
assert parked.wait(timeout=60)
|
|
index_before = (target_b / ".git" / "index").read_bytes()
|
|
head_before = _git(target_b, "rev-parse", "HEAD").stdout
|
|
|
|
b_done = threading.Event()
|
|
|
|
def run_b():
|
|
_provision(target_b, snaps, data, snapshot_id="snapB", task_id="tb")
|
|
b_done.set()
|
|
|
|
thread_b = threading.Thread(target=run_b, name="provision-b")
|
|
thread_b.start()
|
|
# With the old whole-function lock B would sit behind A forever; A is still parked.
|
|
assert b_done.wait(timeout=120), "provision B did not finish while A was parked"
|
|
assert observed["b_saw_lock_held"] is False
|
|
release.set()
|
|
thread_a.join(timeout=120)
|
|
assert not errors and not thread_a.is_alive()
|
|
assert (target_b / ".git" / "index").read_bytes() == index_before
|
|
assert _git(target_b, "rev-parse", "HEAD").stdout == head_before
|
|
rows = {row["snapshot_id"]: row for row in wt.list_worktrees(data_dir=data)}
|
|
assert set(rows) == {"snapA", "snapB"}
|
|
assert all(pathlib.Path(rows[s]["path"]).is_dir() for s in rows)
|
|
|
|
|
|
_HOLDER = """
|
|
import os, pathlib, sys
|
|
sys.path.insert(0, sys.argv[2])
|
|
from ouroboros.platform_layer import acquire_exclusive_file_lock
|
|
fd = acquire_exclusive_file_lock(
|
|
pathlib.Path(sys.argv[1]), timeout_sec=5, stale_sec=600,
|
|
metadata=f"pid={os.getpid()} task=t-holder op=provision since=2026-09-24T00:00:00Z target=/tmp/with space")
|
|
assert fd is not None
|
|
print("HELD", os.getpid(), flush=True)
|
|
sys.stdin.readline() # hold until the test closes our stdin (or kills us)
|
|
"""
|
|
|
|
|
|
def _hold_lock(snaps: pathlib.Path) -> "tuple[subprocess.Popen, int]":
|
|
"""A live lock holder in another process; returns it with the pid the holder
|
|
itself wrote into the lock (on Windows a venv ``python.exe`` is a launcher whose
|
|
CHILD is the interpreter, so ``proc.pid`` is not that pid)."""
|
|
snaps.mkdir(parents=True, exist_ok=True)
|
|
proc = subprocess.Popen(
|
|
[sys.executable, "-c", _HOLDER, str(snaps / wt._LOCK_NAME), str(REPO)],
|
|
stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True)
|
|
banner = proc.stdout.readline().split()
|
|
assert banner[:1] == ["HELD"], banner
|
|
return proc, int(banner[1])
|
|
|
|
|
|
def _release_holder(proc: subprocess.Popen, holder_pid: int) -> None:
|
|
proc.kill()
|
|
proc.stdin.close() # the interpreter behind a launcher exits on EOF too
|
|
proc.wait()
|
|
# Wait for the HOLDER (not only the launcher) to be gone: it may still hold
|
|
# the OS-level lock for a moment after EOF, and the dead-holder phase below
|
|
# rewrites the lock file by hand.
|
|
deadline = time.monotonic() + 10
|
|
while pid_is_alive(holder_pid) and time.monotonic() < deadline:
|
|
time.sleep(0.05)
|
|
assert not pid_is_alive(holder_pid)
|
|
|
|
|
|
def test_a_live_holder_is_a_typed_refusal_and_a_dead_holder_is_evicted(tmp_path, monkeypatch):
|
|
target = _seed_target(tmp_path)
|
|
snaps, data = tmp_path / "snaps", tmp_path / "data"
|
|
monkeypatch.setattr(wt, "_LOCK_TIMEOUT_SEC", 0.5)
|
|
holder, holder_pid = _hold_lock(snaps)
|
|
try:
|
|
started = time.monotonic()
|
|
with pytest.raises(wt.WorktreeOpsLockBusy) as info:
|
|
_provision(target, snaps, data)
|
|
# A busy FIRST section registered nothing, so nothing is discarded: the typed
|
|
# refusal arrives after the one wait, not after a second discard wait.
|
|
assert time.monotonic() - started < 3
|
|
finally:
|
|
_release_holder(holder, holder_pid)
|
|
busy = info.value
|
|
assert busy.holder == {"pid": str(holder_pid), "task": "t-holder", "op": "provision",
|
|
"since": "2026-09-24T00:00:00Z", "target": "/tmp/with space"}
|
|
assert busy.waited_sec > 0 and "t-holder" in str(busy)
|
|
# Nothing was registered, pinned or checked out for the refused attempt.
|
|
assert wt.find_execution_snapshot("snap1", data_dir=data) is None
|
|
assert _git(target, "for-each-ref", "refs/ouroboros/").stdout == ""
|
|
assert not list(snaps.glob("dlg_*"))
|
|
|
|
# A holder that died (SIGKILL, panic) left its lock file behind: the owner-aware
|
|
# stale check evicts it at once instead of refusing for _LOCK_STALE_SEC.
|
|
lock_path = snaps / wt._LOCK_NAME
|
|
lock_path.write_text(f"pid={holder.pid} task=t-dead op=provision since=x target=y", encoding="utf-8")
|
|
started = time.monotonic()
|
|
handle = _provision(target, snaps, data)
|
|
assert time.monotonic() - started < 30 and pathlib.Path(handle.path).is_dir() # far below _LOCK_STALE_SEC=600
|
|
assert not _lock_held(snaps)
|
|
|
|
|
|
def test_a_provisioning_refusal_is_definitely_unrun_and_names_the_holder(tmp_path, monkeypatch):
|
|
from ouroboros import delegate_custody as custody
|
|
from ouroboros.delegate_shared import delegate_payload
|
|
from ouroboros.subagent_bootstrap import _startup_refusal_definite
|
|
from ouroboros.tools.delegate_integration import _provision_snapshot
|
|
|
|
target = _seed_target(tmp_path)
|
|
ctx = _nanny_ctx(tmp_path, target, monkeypatch)
|
|
drive = custody.custody_root(ctx)
|
|
|
|
def busy(**kw):
|
|
raise wt.WorktreeOpsLockBusy(tmp_path / "lock", 120.0, {"pid": "4242", "task": "t-x", "op": "provision"})
|
|
|
|
monkeypatch.setattr(wt, "provision_execution_snapshot", busy)
|
|
handle, refusal = _provision_snapshot(ctx, drive, str(target), "inv-busy")
|
|
payload = delegate_payload(refusal)
|
|
assert handle is None and payload["reason"] == "execution_snapshot_failed"
|
|
assert payload["definitely_unrun"] is True and payload["cause"] == "lock_busy"
|
|
assert payload["holder"]["pid"] == "4242" and payload["waited_sec"] == 120.0 and payload["retryable"] is True
|
|
assert "t-x" in payload["detail"] and _startup_refusal_definite(payload)
|
|
|
|
def broken(**kw):
|
|
raise RuntimeError("git exploded")
|
|
|
|
monkeypatch.setattr(wt, "provision_execution_snapshot", broken)
|
|
_handle, refusal = _provision_snapshot(ctx, drive, str(target), "inv-broken")
|
|
payload = delegate_payload(refusal)
|
|
assert payload["definitely_unrun"] is True and "cause" not in payload
|
|
assert _startup_refusal_definite(payload)
|
|
|
|
|
|
def test_a_provisioning_refusal_leaves_a_durable_start_failed_row(full_run, monkeypatch): # noqa: F811
|
|
"""The incident's two bootstrap refusals existed only inside the child's first
|
|
prompt: no custody row, no event. A refused provision now settles its
|
|
invocation durably, and never reaches the daemon."""
|
|
from ouroboros import delegate_custody as custody
|
|
from ouroboros.delegate_shared import delegate_payload
|
|
from ouroboros.tools import delegate
|
|
|
|
ctx, _target, facts = full_run
|
|
|
|
def refused(**_kw):
|
|
raise wt.WorktreeOpsLockBusy(pathlib.Path("/lock"), 120.0, {"pid": "7", "task": "t-other", "op": "provision"})
|
|
|
|
monkeypatch.setattr(wt, "provision_execution_snapshot", refused)
|
|
payload = delegate_payload(delegate._delegate_start(ctx, "Implement the fixture."))
|
|
|
|
assert payload["reason"] == "execution_snapshot_failed" and payload["definitely_unrun"] is True
|
|
assert payload["cause"] == "lock_busy" and payload["holder"]["task"] == "t-other"
|
|
assert facts["requests"] == [], "a refused provision must never POST a run"
|
|
rows = [json.loads(line) for line in custody.event_log_path(custody.custody_root(ctx))
|
|
.read_text(encoding="utf-8").splitlines() if line.strip()]
|
|
failed = [row for row in rows if row.get("type") == custody.START_FAILED]
|
|
assert len(failed) == 1 and failed[0]["definite"] is True and failed[0]["invocation_id"]
|
|
assert failed[0]["reason"] == "execution_snapshot_failed" and failed[0]["run_id"] == ""
|
|
assert failed[0]["cause"] == "lock_busy" and failed[0]["holder"]["task"] == "t-other" # the facts ride the row
|
|
assert "t-other" in failed[0]["detail"] # and so does the producer's own sentence
|
|
|
|
|
|
def test_configured_child_bootstrap_keeps_the_refusal_facts(tmp_path, monkeypatch):
|
|
"""The host pre-start of a configured leaf (the incident's first refusal) ends the
|
|
child at $0 AND keeps the producer's facts: the $0 terminal names the lock holder,
|
|
the availability row and the acceptance evidence carry the detail."""
|
|
from ouroboros import subagent_bootstrap, subagent_runtime
|
|
from ouroboros.agent_dispatch import executor_blocked_outcome
|
|
from ouroboros.delegate_shared import _fail, lock_busy_facts
|
|
from ouroboros.subagents import SubagentExecutorResolution
|
|
|
|
busy = wt.WorktreeOpsLockBusy(tmp_path / "lock", 120.0, {"pid": "4242", "task": "t-other", "op": "provision"})
|
|
refusal = _fail("delegate_start", "execution_snapshot_failed",
|
|
f"A private execution snapshot could not be provisioned ({busy}).",
|
|
definitely_unrun=True, **lock_busy_facts(busy))
|
|
monkeypatch.setattr(subagent_runtime, "delegate_start_entry", lambda ctx, prompt, **kw: refusal)
|
|
monkeypatch.setattr(subagent_runtime, "current_subagent_alternatives", lambda selected: [])
|
|
|
|
class _Ctx:
|
|
task_id = "t-child"
|
|
|
|
ctx = _Ctx()
|
|
task = {"configured_subagent": {"selected_subagent_id": "codex=gpt-6-astra/xhigh"}}
|
|
wake = subagent_bootstrap._pre_start_leaf(ctx, task, {})
|
|
|
|
assert wake == "" # a definite refusal: the child ends unrun at $0, no model round
|
|
stash = ctx._configured_startup_refusal
|
|
assert stash["reason"] == "execution_snapshot_failed" and stash["cause"] == "lock_busy"
|
|
assert stash["holder"]["task"] == "t-other" and "t-other" in stash["detail"]
|
|
availability = task["subagent_availability"]
|
|
assert availability["status"] == "unavailable" and availability["holder"]["pid"] == "4242"
|
|
text, usage = executor_blocked_outcome(
|
|
SubagentExecutorResolution(requested="harness", executor="blocked", reason=stash["reason"]),
|
|
availability=availability)
|
|
assert "t-other" in text and usage["reason_code"] == "subagent_executor_unavailable"
|
|
|
|
|
|
def test_acting_worktree_add_and_remove_keep_tree_work_outside_the_lock(tmp_path, monkeypatch):
|
|
"""The acting self_worktree lane follows the same split: admin dir + branch under
|
|
the lock, the checkout populated and deleted outside it."""
|
|
target = _seed_target(tmp_path)
|
|
snaps, data = tmp_path / "snaps", tmp_path / "data"
|
|
seen: dict = {}
|
|
real_git, real_rmtree = wt._git, wt._force_rmtree
|
|
|
|
def spy_git(repo_dir, *args, **kw):
|
|
if "worktree" in args and "add" in args:
|
|
seen["worktree_add"] = (_lock_held(snaps), "--no-checkout" in args)
|
|
if "reset" in args:
|
|
seen["reset"] = _lock_held(snaps)
|
|
return real_git(repo_dir, *args, **kw)
|
|
|
|
def spy_rmtree(path):
|
|
seen["rmtree"] = _lock_held(snaps)
|
|
return real_rmtree(path)
|
|
|
|
monkeypatch.setattr(wt, "_git", spy_git)
|
|
monkeypatch.setattr(wt, "_force_rmtree", spy_rmtree)
|
|
handle = wt.provision_worktree(repo_dir=target, task_id="acting1", worktree_root=snaps, data_dir=data)
|
|
assert seen["worktree_add"] == (True, True) and seen["reset"] is False
|
|
assert (pathlib.Path(handle.path) / "tracked.txt").read_text(encoding="utf-8") == "one\n" # HEAD content
|
|
assert _git(target, "rev-parse", "--verify", handle.branch).returncode == 0
|
|
assert any(row.get("task_id") == "acting1" for row in wt.list_worktrees(data_dir=data))
|
|
|
|
assert wt.remove_worktree(task_id="acting1", worktree_root=snaps, data_dir=data)
|
|
assert seen["rmtree"] is False
|
|
assert not pathlib.Path(handle.path).exists()
|
|
assert _git(target, "rev-parse", "--verify", handle.branch, check=False).returncode != 0
|
|
assert not any(row.get("task_id") == "acting1" for row in wt.list_worktrees(data_dir=data))
|
|
assert not _lock_held(snaps)
|
|
|
|
|
|
def test_a_row_naming_the_snapshot_root_itself_deletes_nothing(tmp_path):
|
|
"""The registry is durable state; a malformed row whose path IS the root must not
|
|
wipe every live sibling snapshot (the root holds them all)."""
|
|
target = _seed_target(tmp_path)
|
|
snaps, data = tmp_path / "snaps", tmp_path / "data"
|
|
keep = _provision(target, snaps, data, snapshot_id="keep")
|
|
rows = wt._load_registry(data)
|
|
rows.append({**rows[0], "snapshot_id": "bad", "path": str(snaps.resolve())})
|
|
wt._save_registry(rows, data)
|
|
assert wt.remove_execution_snapshot("bad", worktree_root=snaps, data_dir=data)
|
|
assert pathlib.Path(keep.path).is_dir() and wt.find_execution_snapshot("keep", data_dir=data) is not None
|
|
assert wt.find_execution_snapshot("bad", data_dir=data) is None
|
|
assert not wt._deletable(snaps, snaps) and not wt._deletable(pathlib.Path("/"), snaps)
|
|
assert wt._deletable(pathlib.Path(keep.path), snaps)
|
|
|
|
|
|
def test_removal_deletes_files_outside_the_lock_and_forgets_metadata_inside(tmp_path, monkeypatch):
|
|
target = _seed_target(tmp_path)
|
|
snaps, data = tmp_path / "snaps", tmp_path / "data"
|
|
handle = _provision(target, snaps, data)
|
|
seen: dict = {}
|
|
real_rmtree, real_git = wt._force_rmtree, wt._git
|
|
|
|
def spy_rmtree(path):
|
|
seen["rmtree"] = _lock_held(snaps)
|
|
return real_rmtree(path)
|
|
|
|
def spy_git(repo_dir, *args, **kw):
|
|
if "update-ref" in args and "-d" in args:
|
|
seen["unpin"] = _lock_held(snaps)
|
|
return real_git(repo_dir, *args, **kw)
|
|
|
|
monkeypatch.setattr(wt, "_force_rmtree", spy_rmtree)
|
|
monkeypatch.setattr(wt, "_git", spy_git)
|
|
assert wt.remove_execution_snapshot("snap1", worktree_root=snaps, data_dir=data)
|
|
assert seen == {"rmtree": False, "unpin": True}
|
|
assert not pathlib.Path(handle.path).exists()
|
|
assert wt.find_execution_snapshot("snap1", data_dir=data) is None
|
|
assert _git(target, "rev-parse", handle.baseline_ref, check=False).returncode != 0
|
|
assert not _lock_held(snaps)
|
|
|
|
|
|
@pytest.mark.parametrize("failing", ["update-ref", "worktree"])
|
|
def test_a_failure_inside_the_first_lock_section_after_the_row_leaves_nothing(tmp_path, monkeypatch, failing):
|
|
"""The provisional row is written first; a failed pin or admin-dir creation right
|
|
after it (still inside the lock) must discard the row too, not only later phases."""
|
|
target = _seed_target(tmp_path)
|
|
snaps, data = tmp_path / "snaps", tmp_path / "data"
|
|
real_git = wt._git
|
|
|
|
def failing_git(repo_dir, *args, **kw):
|
|
if failing in args and (failing != "worktree" or "add" in args):
|
|
raise subprocess.CalledProcessError(128, ["git", *args])
|
|
return real_git(repo_dir, *args, **kw)
|
|
|
|
monkeypatch.setattr(wt, "_git", failing_git)
|
|
with pytest.raises(subprocess.CalledProcessError):
|
|
_provision(target, snaps, data, snapshot_id="snapLockB")
|
|
assert wt.find_execution_snapshot("snapLockB", data_dir=data) is None
|
|
assert _git(target, "for-each-ref", "refs/ouroboros/").stdout == ""
|
|
assert not list(snaps.glob("dlg_*")) and not list((target / ".git" / "worktrees").glob("dlg_*"))
|
|
assert not _lock_held(snaps)
|
|
|
|
|
|
def test_a_failure_after_the_provisional_row_leaves_nothing_and_a_crash_is_gc_reclaimable(tmp_path, monkeypatch):
|
|
target = _seed_target(tmp_path)
|
|
snaps, data = tmp_path / "snaps", tmp_path / "data"
|
|
|
|
def boom(*_a, **_k):
|
|
raise OSError("copy failed")
|
|
|
|
monkeypatch.setattr(artifacts, "copy_artifact_file", boom)
|
|
with pytest.raises(OSError, match="copy failed"):
|
|
_provision(target, snaps, data, snapshot_id="snapFail")
|
|
assert wt.find_execution_snapshot("snapFail", data_dir=data) is None
|
|
assert _git(target, "for-each-ref", "refs/ouroboros/").stdout == ""
|
|
assert not list(snaps.glob("dlg_*"))
|
|
|
|
# A worker killed between the provisional row and the final row: the row is
|
|
# registered, custody never opened -> the startup GC removes checkout, ref, row.
|
|
monkeypatch.setattr(wt, "_discard_snapshot_checkout", lambda *a, **k: None)
|
|
with pytest.raises(OSError, match="copy failed"):
|
|
_provision(target, snaps, data, snapshot_id="snapCrash")
|
|
assert wt.find_execution_snapshot("snapCrash", data_dir=data) is not None
|
|
assert "refs/ouroboros/delegated/snapCrash" in _git(target, "for-each-ref", "refs/ouroboros/").stdout
|
|
report = wt.prune_execution_snapshots(set(), worktree_root=snaps, data_dir=data)
|
|
assert report["removed"] == ["snapCrash"]
|
|
assert wt.find_execution_snapshot("snapCrash", data_dir=data) is None
|
|
assert _git(target, "for-each-ref", "refs/ouroboros/").stdout == ""
|
|
assert not list(snaps.glob("dlg_*"))
|
|
|
|
|
|
@pytest.mark.parametrize("autocrlf", [False, True])
|
|
def test_populate_matches_worktree_add_and_runs_no_target_hook(tmp_path, monkeypatch, autocrlf):
|
|
"""``worktree add --no-checkout`` + ``reset --hard --no-recurse-submodules`` is what
|
|
git's own ``worktree add`` runs — a target with ``submodule.recurse=true`` must
|
|
still snapshot — minus the target's post-checkout hook, which no longer executes
|
|
project-authored code at provision. Under ``core.autocrlf=true`` (Git for
|
|
Windows' default) the checkout writes CRLF and the raw-bytes copy restores the
|
|
source's LF, so the copied entries' stat must be re-recorded or every such file
|
|
reads as modified in the child's ``git status`` (CI on windows-latest, #1247)."""
|
|
# Both legs pin the setting explicitly: on windows-latest the system config
|
|
# already says true, so an unset "False" leg would not be the working side.
|
|
config = tmp_path / "gitconfig"
|
|
config.write_text(f"[core]\n\tautocrlf = {'true' if autocrlf else 'false'}\n", encoding="utf-8")
|
|
monkeypatch.setenv("GIT_CONFIG_GLOBAL", str(config))
|
|
sub = tmp_path / "sub"
|
|
sub.mkdir()
|
|
_git(sub, "init", "-q")
|
|
(sub / "s.txt").write_text("s\n", encoding="utf-8")
|
|
_git(sub, "add", "s.txt")
|
|
_git(sub, "-c", "user.email=t@t", "-c", "user.name=t", "commit", "-qm", "sub")
|
|
target = _seed_target(tmp_path)
|
|
_git(target, "-c", "protocol.file.allow=always", "submodule", "add", "-q", str(sub), "vendored")
|
|
_git(target, "-c", "user.email=t@t", "-c", "user.name=t", "commit", "-qm", "vendored")
|
|
_git(target, "config", "submodule.recurse", "true")
|
|
hooks = tmp_path / "hooks"
|
|
hooks.mkdir()
|
|
(hooks / "post-checkout").write_text("#!/bin/sh\ntouch .hook_ran\n", encoding="utf-8")
|
|
(hooks / "post-checkout").chmod(0o755)
|
|
_git(target, "config", "core.hooksPath", str(hooks))
|
|
snaps, data = tmp_path / "snaps", tmp_path / "data"
|
|
# Positive control: the hook DOES fire on a plain `worktree add`, so its absence
|
|
# below is the populate path's doing, not a dead fixture.
|
|
control = tmp_path / "control-wt"
|
|
_git(target, "-c", "submodule.recurse=false", "worktree", "add", "--detach", str(control), "HEAD")
|
|
assert (control / ".hook_ran").exists()
|
|
_git(target, "worktree", "remove", "--force", str(control))
|
|
|
|
handle = _provision(target, snaps, data)
|
|
|
|
exec_root = pathlib.Path(handle.path)
|
|
assert (exec_root / "vendored").is_dir() and (exec_root / "tracked.txt").read_text(encoding="utf-8") == "one\ntwo\n"
|
|
assert not (exec_root / ".hook_ran").exists() and not (target / ".hook_ran").exists()
|
|
assert _git(exec_root, "status", "--porcelain").stdout == ""
|
|
assert (exec_root / ".gitmodules").read_bytes() == (target / ".gitmodules").read_bytes()
|
|
assert "160000" in _git(exec_root, "ls-files", "-s", "vendored").stdout
|
|
|
|
# The guard: without --no-recurse-submodules the populate fails on this target.
|
|
real_git = wt._git
|
|
|
|
def recursing_git(repo_dir, *args, **kw):
|
|
return real_git(repo_dir, *tuple(a for a in args if a != "--no-recurse-submodules"), **kw)
|
|
|
|
monkeypatch.setattr(wt, "_git", recursing_git)
|
|
with pytest.raises(subprocess.CalledProcessError):
|
|
_provision(target, snaps, data, snapshot_id="snapRecurse")
|
|
|
|
|
|
def test_snapshot_facts_ride_the_start_receipt_as_disclosure_only(tmp_path):
|
|
from ouroboros.tools.delegate import _snapshot_facts
|
|
|
|
target = _seed_target(tmp_path)
|
|
(target / "blob.bin").write_bytes(b"\x7fELF\x00" * 10)
|
|
handle = _provision(target, tmp_path / "snaps", tmp_path / "data")
|
|
facts = _snapshot_facts(handle)
|
|
assert set(facts) == {"entries", "untracked_files", "file_input_bytes", "provisioning_sec"}
|
|
assert facts["entries"] == handle.entry_count and facts["untracked_files"] == 2 # untracked.txt + blob.bin
|
|
assert facts["file_input_bytes"] == 50 and facts["provisioning_sec"] == handle.provisioning_sec
|
|
assert _snapshot_facts(None) == {}
|
|
# Facts, not a threshold: no runtime module consumes them to refuse or truncate.
|
|
consumers = sorted(
|
|
path.relative_to(REPO).as_posix() for path in (REPO / "ouroboros").rglob("*.py")
|
|
if "provisioning_sec" in path.read_text(encoding="utf-8"))
|
|
assert consumers == ["ouroboros/subagent_worktrees.py", "ouroboros/tools/delegate.py",
|
|
"ouroboros/tools/delegate_integration.py"]
|