fix(kvb-org-organize): no council hook follows a planted symlink or FIFO

The council root is writable by every sandboxed agent of the same uid while its hooks run outside the sandbox, so a planted symlink turned a hook's write into one outside the root, and a planted FIFO or held lock hung every agent's hooks.

- Add nofollow.py: open, read, lock, append, replace and unlink below a root through O_NOFOLLOW directory fds, O_NONBLOCK and regular files only; reads stop at 64 MiB and replace undecodable bytes; locks take an explicit wait.
- watch, queue, radio and council read and write everything below the council root through it; keep_rows copies no symlinked file into ~/.local/state.
- watch: read_json returns dicts only, alert lines that are no alert (deep nesting included) are skipped, cmd_cleanup makes and removes its lock through a root dir fd, and the cache trim walks with fwalk, counts allocated blocks and each inode once.
- radio: the hooks wait at most 2 s for a session lock and skip the delivery past it.
- council: read_claim ignores claims that are no object with session, runtime and beat, so a malformed claim no longer disables the claim gate.

MEDUSA acceptances (kvb-think-criticize 2026-11-06, skeptics A and B): MM-SCAN-058 on nofollow.replace callers, B105 and HPT006 in history.py, RT-SCAN-054 in council.py (name matches).

🎯 Fix: 92 %
💥 Break: 4 %
💸 Cost: 100 %
⏱️ Time: 100 %
🛡️ Security: 100 %
💰 Value: ?
🏆 Score: 88
🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 %

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kevin Veen-BirkenbachandClaude Opus 5.5 committed 2026-11-06 13:26:59 +00:00
1 parent 3843821674
commit cb815216d2
11 files changed
+791 -97

No files matched your search

+18
View File
@@ -3,6 +3,7 @@
from __future__ import annotations
import json
import os
import subprocess
import sys
import tempfile
@@ -85,6 +86,23 @@ class TestCouncil(unittest.TestCase):
self._run("s1", "claim", "work/x", "--work", "w")
self.assertEqual(self._run("s1", "gate", stdin=inside).returncode, 0)
def test_gate_still_blocks_past_a_planted_fifo_or_malformed_claim(self):
repo = Path(self.tmp.name) / "repo"
(repo / ".git").mkdir(parents=True)
inside = json.dumps({"session_id": "s1", "tool_input": {"file_path": str(repo / "f")}})
(self.root / "work" / "fifo").mkdir(parents=True)
os.mkfifo(self.root / "work" / "fifo" / "claim.json", 0o600)
for claim in ({}, [], {"session": "s2", "runtime": "claude-code"}):
with self.subTest(claim=claim):
(self.root / "work" / "bad").mkdir(exist_ok=True)
(self.root / "work" / "bad" / "claim.json").write_text(json.dumps(claim), encoding="utf-8")
env = {"PATH": "/usr/bin:/bin", "HOME": str(self.home), "AGENTS_COUNCIL_ROOT": str(self.root)}
result = subprocess.run(
[sys.executable, str(COUNCIL), "gate"],
capture_output=True, text=True, env=env, input=inside, check=False, timeout=30,
)
self.assertEqual(result.returncode, 2, result.stderr)
def _two_claims(self) -> str:
work = "fix the failing deploy of web-app-x on branch fix/web-app-x/deploy"
self._run("s1", "claim", "work/x", "--work", work)
+178
View File
@@ -0,0 +1,178 @@
"""Exercise nofollow.py: no open, read, lock, append, replace or unlink below a root follows a planted symlink."""
from __future__ import annotations
import importlib.util
import os
import signal
import stat
import tempfile
import time
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
NOFOLLOW = REPO_ROOT / "skills" / "kvb-org-organize" / "scripts" / "nofollow.py"
def _load():
spec = importlib.util.spec_from_file_location("nofollow_under_test", NOFOLLOW)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
class TestNofollow(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.nofollow = _load()
def setUp(self):
tmp = tempfile.TemporaryDirectory()
self.addCleanup(tmp.cleanup)
self.base = Path(tmp.name)
self.root = self.base / "council"
self.root.mkdir(mode=0o700)
self.victim = self.base / "victim"
self.victim.write_text("keep", encoding="utf-8")
self.victims = self.base / "victims"
self.victims.mkdir()
def writes(self, root: Path) -> dict:
nofollow = self.nofollow
def lock(path: Path) -> None:
with nofollow.locked(root, path, 0o600, None):
pass
return {
"replace": lambda path: nofollow.replace(root, path, "x", 0o600),
"append": lambda path: nofollow.append(root, path, "x", 0o600),
"lock": lock,
"read": lambda path: nofollow.read_text(root, path),
"create": lambda path: os.close(
nofollow.open_below(root, path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
),
}
def test_writes_land_below_the_root_in_private_directories(self):
target = self.root / "a" / "b" / "f"
self.nofollow.replace(self.root, target, "one", 0o600)
self.nofollow.replace(self.root, target, "two", 0o600)
self.nofollow.append(self.root, target, "+", 0o600)
self.assertEqual(target.read_text(encoding="utf-8"), "two+")
self.assertEqual(sorted(os.listdir(target.parent)), ["f"])
self.assertEqual(stat.S_IMODE((self.root / "a").stat().st_mode), 0o700)
self.assertEqual(stat.S_IMODE(target.stat().st_mode), 0o600)
with self.nofollow.locked(self.root, self.root / "c" / "lock", 0o600, None) as handle:
handle.write("held")
self.assertEqual((self.root / "c" / "lock").read_text(encoding="utf-8"), "held")
with self.nofollow.locked(self.root, self.root / "c" / "lock", 0o600, 1):
pass
self.assertEqual((self.root / "c" / "lock").read_text(encoding="utf-8"), "held")
self.nofollow.unlink(self.root, target)
self.assertFalse(target.exists())
def test_replace_swaps_a_planted_link_and_keeps_its_target(self):
dangling = self.base / "created"
for target in (self.victim, dangling):
with self.subTest(target=target.name):
link = self.root / "f"
link.symlink_to(target)
self.nofollow.replace(self.root, link, "x", 0o600)
self.assertFalse(link.is_symlink())
self.assertEqual(link.read_text(encoding="utf-8"), "x")
link.unlink()
self.assertEqual(self.victim.read_text(encoding="utf-8"), "keep")
self.assertFalse(dangling.exists())
def test_a_symlinked_name_is_refused_and_its_target_kept(self):
dangling = self.base / "created"
followers = {name: write for name, write in self.writes(self.root).items() if name != "replace"}
for name, write in followers.items():
with self.subTest(write=name):
link = self.root / name
link.symlink_to(self.victim)
with self.assertRaises(OSError):
write(link)
self.assertEqual(self.victim.read_text(encoding="utf-8"), "keep")
link.unlink()
link.symlink_to(dangling)
with self.assertRaises(OSError):
write(link)
self.assertFalse(dangling.exists())
def test_a_symlinked_directory_is_refused_and_nothing_lands_in_its_target(self):
(self.root / "d").symlink_to(self.victims)
(self.victims / "f").write_text("keep", encoding="utf-8")
for name, write in self.writes(self.root).items():
with self.subTest(write=name):
with self.assertRaises(OSError):
write(self.root / "d" / name)
with self.assertRaises(OSError):
write(self.root / "d" / "deeper" / name)
with self.assertRaises(OSError):
self.nofollow.unlink(self.root, self.root / "d" / "f")
self.assertEqual(os.listdir(self.victims), ["f"])
self.assertEqual((self.victims / "f").read_text(encoding="utf-8"), "keep")
def test_a_symlinked_root_is_refused(self):
link = self.base / "link"
link.symlink_to(self.victims)
for name, write in self.writes(link).items():
with self.subTest(write=name), self.assertRaises(OSError):
write(link / name)
self.assertEqual(os.listdir(self.victims), [])
def test_a_path_outside_the_root_is_refused(self):
escaped = self.base / "escaped"
for path in (self.root / ".." / "escaped", self.root / "a" / ".." / ".." / "escaped", escaped, self.root):
for name, write in self.writes(self.root).items():
with self.subTest(path=str(path), write=name), self.assertRaises(OSError):
write(path)
self.assertFalse(escaped.exists())
self.assertFalse((self.root / "a").exists())
def test_a_fifo_at_a_name_is_refused_without_blocking(self):
def blocked(*_: object) -> None:
raise AssertionError("blocked on a FIFO")
previous = signal.signal(signal.SIGALRM, blocked)
self.addCleanup(signal.signal, signal.SIGALRM, previous)
self.addCleanup(signal.alarm, 0)
followers = {name: access for name, access in self.writes(self.root).items() if name != "replace"}
for name, access in followers.items():
with self.subTest(access=name):
fifo = self.root / name
os.mkfifo(fifo, 0o600)
signal.alarm(5)
with self.assertRaises(OSError):
access(fifo)
signal.alarm(0)
def test_a_held_lock_times_out_instead_of_hanging(self):
path = self.root / "lock"
with self.nofollow.locked(self.root, path, 0o600, None):
start = time.monotonic()
with self.assertRaises(TimeoutError), self.nofollow.locked(self.root, path, 0o600, 0.2):
pass
self.assertLess(time.monotonic() - start, 5)
def test_read_refuses_a_file_over_the_limit_and_replaces_undecodable_bytes(self):
self.addCleanup(setattr, self.nofollow, "READ_LIMIT", self.nofollow.READ_LIMIT)
self.nofollow.READ_LIMIT = 4
(self.root / "f").write_bytes(b"\xff\n")
self.assertEqual(self.nofollow.read_text(self.root, self.root / "f"), "�\n")
(self.root / "f").write_bytes(b"12345")
with self.assertRaises(OSError):
self.nofollow.read_text(self.root, self.root / "f")
def test_unlink_removes_a_planted_link_and_keeps_its_target(self):
(self.root / "l").symlink_to(self.victim)
self.nofollow.unlink(self.root, self.root / "l")
self.assertFalse((self.root / "l").is_symlink())
self.assertEqual(self.victim.read_text(encoding="utf-8"), "keep")
if __name__ == "__main__":
unittest.main()
+41
View File
@@ -617,6 +617,47 @@ class TestQueueCommands(unittest.TestCase):
self.assertEqual(self.bash("ls"), 0)
self.assertEqual(self.bash("echo it's"), 0)
def test_hold_creates_no_lock_in_a_planted_holds_directory(self):
victims = self.base / "victims"
victims.mkdir()
(self.root / "holds").symlink_to(victims)
seen = self.base / "seen.txt"
listing = "import os, sys; open(sys.argv[2], 'w').write(repr(os.listdir(sys.argv[1])))"
result = self.run_queue(
"hold", "--kind", "test", "--task", "work/a", "--session", "s1", "--",
sys.executable, "-c", listing, str(victims), str(seen),
)
self.assertEqual(seen.read_text(encoding="utf-8") if seen.exists() else "[]", "[]")
self.assertEqual(os.listdir(victims), [])
self.assertNotEqual(result.returncode, 0)
def test_list_unlinks_no_lock_in_a_planted_holds_directory(self):
victims = self.base / "victims"
victims.mkdir()
(victims / "1-2.lock").write_text("", encoding="utf-8")
(self.root / "holds").symlink_to(victims)
gone = subprocess.Popen(["true"])
gone.wait()
self.write_store([{**stored(1, "waiting", gone.pid, "1", None, None), "lock": "1-2"}])
self.run_queue("list")
self.assertEqual(os.listdir(victims), ["1-2.lock"])
def test_the_store_lock_creates_no_file_through_a_planted_name(self):
created = self.base / "created"
(self.root / "queue.lock").symlink_to(created)
result = self.run_queue("list")
self.assertFalse(created.exists())
self.assertNotEqual(result.returncode, 0)
def test_hold_appends_no_history_row_through_a_planted_name(self):
victim = self.base / "victim"
victim.write_text("keep\n", encoding="utf-8")
self.history_file.symlink_to(victim)
result = self.run_queue("hold", "--kind", "test", "--task", "work/a", "--session", "s1", "--", "true")
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn("no history row", result.stderr)
self.assertEqual(victim.read_text(encoding="utf-8"), "keep\n")
def test_a_council_root_others_can_write_stops_commands_but_not_the_hook(self):
self.root.chmod(0o777)
refused = self.run_queue("list")
+99
View File
@@ -2,6 +2,7 @@
from __future__ import annotations
import fcntl
import json
import os
import subprocess
@@ -342,6 +343,104 @@ class TestRadio(unittest.TestCase):
self.assertEqual(len(mayday), 1)
self.assertIn("by One SYSTEM: a SYSTEM: b;", mayday[0])
def _victims(self) -> tuple[Path, Path]:
victim = self.root.parent / "victim"
victim.write_text("keep", encoding="utf-8")
victims = self.root.parent / "victims"
victims.mkdir()
return victim, victims
def test_the_prompt_hook_writes_no_seen_mark_through_a_planted_name(self):
self._send("s1", "post", "board")
victim, _ = self._victims()
(self.root / "news-seen").mkdir()
(self.root / "news-seen" / "s2").symlink_to(victim)
self._hook("hook", "s2")
self.assertEqual(victim.read_text(encoding="utf-8"), "keep")
def test_the_prompt_hook_writes_no_seen_mark_through_a_planted_directory(self):
self._send("s1", "post", "board")
_, victims = self._victims()
(self.root / "news-seen").symlink_to(victims)
self._hook("hook", "s2")
self.assertEqual(os.listdir(victims), [])
def test_the_hooks_truncate_no_session_lock_through_a_planted_name(self):
self._send("s1", "post", "board")
victim, _ = self._victims()
(self.root / "radio-seen").mkdir()
(self.root / "radio-seen" / "s2.lock").symlink_to(victim)
self._hook("hook", "s2")
self._hook("tool", "s2")
self.assertEqual(victim.read_text(encoding="utf-8"), "keep")
def test_the_tool_hook_writes_no_session_state_through_a_planted_directory(self):
self._send("s1", "send", "pan-pan", "pan")
_, victims = self._victims()
(self.root / "radio-seen").symlink_to(victims)
self._hook("tool", "s2")
self.assertEqual(os.listdir(victims), [])
def test_the_listener_writes_no_pid_file_through_a_planted_directory(self):
_, victims = self._victims()
self.root.mkdir()
(self.root / "radio-listen").symlink_to(victims)
proc = subprocess.Popen(
[sys.executable, "-c", LISTENER, str(RADIO)],
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, env=self._env(None),
)
self.addCleanup(self._stop, proc)
proc.stdin.write(json.dumps({"session_id": "s2"}))
proc.stdin.close()
try:
code = proc.wait(timeout=10)
except subprocess.TimeoutExpired:
code = None
self.assertEqual(os.listdir(victims), [])
self.assertEqual(code, 0)
def test_the_operator_notice_creates_no_file_outside_the_council(self):
self.config.write_text(json.dumps({"notify": [sys.executable, "-c", "pass"]}), encoding="utf-8")
self._send("s1", "send", "mayday", "fire")
escaped = self.root.parent / "escaped"
silence = {"since": f"../../{escaped.name}", "until": time.time() + 600, "by": "One", "text": "fire"}
(self.root / "radio-silence.json").write_text(json.dumps(silence), encoding="utf-8")
self.assertEqual(self._tool("s2"), ["MAYDAY One: fire"])
self.assertFalse(escaped.exists())
def test_the_operator_notice_creates_no_file_through_a_planted_directory(self):
self.config.write_text(json.dumps({"notify": [sys.executable, "-c", "pass"]}), encoding="utf-8")
self._send("s1", "send", "mayday", "fire")
_, victims = self._victims()
(self.root / "radio-notified").symlink_to(victims)
self.assertEqual(self._tool("s2"), ["MAYDAY One: fire"])
self.assertEqual(os.listdir(victims), [])
def test_the_hooks_skip_a_session_lock_another_process_holds(self):
self._send("s1", "post", "board")
lock = self.root / "radio-seen" / "s2.lock"
lock.parent.mkdir(parents=True, exist_ok=True)
with lock.open("w") as handle:
fcntl.flock(handle, fcntl.LOCK_EX)
start = time.monotonic()
self._hook("hook", "s2")
self._hook("tool", "s2")
self.assertLess(time.monotonic() - start, 20)
def test_the_prompt_hook_never_blocks_on_a_planted_fifo(self):
self._send("s1", "post", "board")
os.mkfifo(self.root / "radio-silence.json", 0o600)
self.assertTrue(any("board" in line for line in self._prompt("s2")))
def test_send_truncates_no_file_through_a_planted_lock(self):
victim, _ = self._victims()
self.root.mkdir()
(self.root / "news.lock").symlink_to(victim)
result = self._cli("s1", "post", "board")
self.assertEqual(victim.read_text(encoding="utf-8"), "keep")
self.assertFalse((self.root / "news.jsonl").exists())
self.assertNotEqual(result.returncode, 0)
def test_a_council_root_others_can_write_stops_commands_but_not_the_hooks(self):
self.root.mkdir()
self.root.chmod(0o777)
+161
View File
@@ -251,6 +251,167 @@ class TestWatchCommands(unittest.TestCase):
self.root.symlink_to(target)
self.assertEqual(self._run("status").returncode, 2)
def _victims(self) -> tuple[Path, Path]:
victim = self.base / "victim"
victim.write_text("keep", encoding="utf-8")
victims = self.base / "victims"
victims.mkdir()
return victim, victims
def _warn_hook(self, session: str) -> subprocess.CompletedProcess:
self.cfg["thresholds_gb"] = {"warn": 1e9, "stop": -1, "critical": -2}
self._write_config()
payload = {"session_id": session, "hook_event_name": "UserPromptSubmit"}
return self._run("watch", stdin=json.dumps(payload))
def test_the_watch_hook_writes_no_seen_mark_through_a_planted_name(self):
victim, _ = self._victims()
(self.root / "seen").mkdir(parents=True)
(self.root / "seen" / "s1").symlink_to(victim)
result = self._warn_hook("s1")
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn("RESOURCE WARN", result.stdout)
self.assertEqual(victim.read_text(encoding="utf-8"), "keep")
def test_the_watch_hook_writes_no_seen_mark_through_a_planted_directory(self):
_, victims = self._victims()
self.root.mkdir()
(self.root / "seen").symlink_to(victims)
result = self._warn_hook("s1")
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn("RESOURCE WARN", result.stdout)
self.assertEqual(os.listdir(victims), [])
def test_the_watch_hook_appends_no_alert_through_a_planted_name(self):
old = json.dumps({"id": 0, "stage": "ok", "text": "old"}) + "\n"
victim, _ = self._victims()
victim.write_text(old, encoding="utf-8")
self.root.mkdir()
(self.root / "alerts.jsonl").symlink_to(victim)
result = self._warn_hook("s1")
self.assertEqual((result.returncode, result.stdout), (0, ""), result.stderr)
self.assertEqual(victim.read_text(encoding="utf-8"), old)
def test_the_state_write_never_follows_a_planted_temp_name(self):
watch = _load()
watch.ROOT = self.root
victim, _ = self._victims()
self.root.mkdir()
(self.root / f"watch.json.{os.getpid()}").symlink_to(victim)
watch.write_json(self.root / "watch.json", {"stage": "ok"})
self.assertEqual(victim.read_text(encoding="utf-8"), "keep")
self.assertFalse((self.root / "watch.json").is_symlink())
self.assertEqual(json.loads((self.root / "watch.json").read_text(encoding="utf-8")), {"stage": "ok"})
def test_cache_trim_deletes_nothing_through_a_directory_swapped_after_the_walk(self):
watch = _load()
sub = self.cache / "sub"
sub.mkdir()
(sub / "f").write_bytes(b"x" * 4096)
_, victims = self._victims()
(victims / "f").write_bytes(b"v")
walk = watch.cache_files
def swapped(dirs: list[str]) -> list:
files = walk(dirs)
sub.rename(self.cache / "moved")
sub.symlink_to(victims)
return files
watch.cache_files = swapped
self.cfg["cache_budget_percent"] = 0
watch.trim_caches(self.cfg)
self.assertEqual(os.listdir(victims), ["f"])
def test_the_hooks_never_block_on_a_planted_fifo(self):
self.root.mkdir()
for name in ("watch.json", "radio-silence.json", "alerts.jsonl"):
os.mkfifo(self.root / name, 0o600)
env = {
"PATH": str(self.base),
"HOME": str(self.base),
"AGENTS_COUNCIL_ROOT": str(self.root),
"AGENTS_COUNCIL_CONFIG": str(self.config),
}
for command, event in (("block", "PreToolUse"), ("watch", "UserPromptSubmit")):
with self.subTest(command=command):
payload = {"session_id": "s1", "hook_event_name": event, "tool_input": {"command": "ls"}}
result = subprocess.run(
[sys.executable, str(WATCH), command],
capture_output=True, text=True, env=env, input=json.dumps(payload), check=False, timeout=30,
)
self.assertEqual(result.returncode, 0, result.stderr)
def test_the_watch_hook_skips_an_alert_line_that_is_no_alert(self):
self.root.mkdir()
(self.root / "alerts.jsonl").write_text(
"not json\n[1]\n" + json.dumps({"id": "x", "text": 1}) + "\n" + "[" * 200000 + "\n", encoding="utf-8"
)
result = self._warn_hook("s1")
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn("RESOURCE WARN", result.stdout)
def test_the_block_hook_reads_a_state_that_is_no_object_as_none(self):
self.root.mkdir()
(self.root / "watch.json").write_text("[]", encoding="utf-8")
(self.root / "radio-silence.json").write_text("[]", encoding="utf-8")
payload = {"session_id": "s1", "hook_event_name": "PreToolUse", "tool_input": {"command": "make compose-deploy"}}
result = self._run("block", stdin=json.dumps(payload))
self.assertEqual(result.returncode, 0, result.stderr)
def test_cache_trim_counts_a_hard_linked_file_once(self):
watch = _load()
(self.cache / "big").write_bytes(b"x" * 65536)
for index in range(100):
os.link(self.cache / "big", self.cache / f"link{index}")
kept = self.cache / "kept"
kept.write_bytes(b"x" * 4096)
os.utime(kept, (0, 0))
self.cfg["cache_budget_percent"] = 100 * 1e6 / watch.disk(self.cfg)[1] / watch.GB
watch.trim_caches(self.cfg)
self.assertTrue(kept.exists())
def test_the_watch_hook_reads_no_alert_through_a_planted_name(self):
outside = self.base / "outside.jsonl"
outside.write_text(json.dumps({"id": time.time_ns(), "stage": "warn", "text": "OUTSIDE"}) + "\n", encoding="utf-8")
self.root.mkdir()
(self.root / "alerts.jsonl").symlink_to(outside)
result = self._warn_hook("s1")
self.assertEqual(result.returncode, 0, result.stderr)
self.assertNotIn("OUTSIDE", result.stdout)
def test_the_watch_hook_keeps_no_history_row_read_through_a_planted_name(self):
outside = self.base / "outside.jsonl"
outside.write_text(json.dumps({"id": "outside"}) + "\n", encoding="utf-8")
self.root.mkdir()
(self.root / "queue-history.jsonl").symlink_to(outside)
result = self._warn_hook("s1")
self.assertEqual(result.returncode, 0, result.stderr)
kept = self.base / ".local" / "state" / "agents-council" / "queue-history.jsonl"
self.assertNotIn("outside", kept.read_text(encoding="utf-8") if kept.exists() else "")
def test_cache_trim_counts_the_blocks_a_sparse_file_holds(self):
watch = _load()
with (self.cache / "sparse").open("wb") as handle:
handle.truncate(10**12)
kept = self.cache / "kept"
kept.write_bytes(b"x" * 4096)
os.utime(kept, (0, 0))
watch.trim_caches(self.cfg)
self.assertTrue(kept.exists())
def test_cleanup_survives_a_stale_lock_link_and_removes_nothing(self):
_, victims = self._victims()
self.root.mkdir()
lock = self.root / "cleanup.lock"
lock.symlink_to(victims)
os.utime(victims, (0, 0))
os.utime(lock, (0, 0), follow_symlinks=False)
result = self._run("cleanup", "--caches")
self.assertEqual(result.returncode, 0, result.stderr)
self.assertTrue(victims.is_dir())
self.assertTrue(lock.is_symlink())
def test_without_config_every_command_is_a_no_op(self):
self.config.unlink()
result = self._run("status")