#!/usr/bin/env python3 """ Where the issue store is: the project the operator marked, never the plugin. python3 -m unittest discover -s tests -v Stdlib unittest, no third-party anything — the same rule the scripts under test live by. `skills/*/scripts/` are not packages, so the domain module is imported by path. These tests build a throwaway project in a temp directory — a `.tea/` marker, a store with two issues — and run the real scripts inside it as subprocesses with different working directories. **The scripts are deliberately NOT copied into the fixture.** They stay where they really live, several directories away from the project under test, because that separation IS the thing being tested: a plugin is installed in one place and used on projects in another, and the store belongs to the project. The suite used to copy both script layers in, which made the two locations the same directory and hid the bug completely — issues written from a project landed in `~/.claude/plugins/cache/tea/tea//tmp/issues` and vanished on the next version bump. `CLAUDE_PROJECT_DIR` is stripped from the child environment except where a test is about it: it is the first anchor, so leaving the harness's own value in place would point every fixture at this repository. """ import os import shutil import subprocess import sys import tempfile import unittest REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) ISSUE_SCRIPTS = os.path.join(REPO, "skills", "issue", "scripts") SYNC_SCRIPTS = os.path.join(REPO, "skills", "sync", "scripts") sys.path.insert(0, ISSUE_SCRIPTS) import issue # noqa: E402 ALPHA = """\ --- id: alpha-issue state: open labels: [type/task] assignees: [] milestone: none depends: [] origin: local --- # Alpha issue ## Summary Первый issue фикстуры. ## Spec none ## Motivation Нужен, чтобы в store что-то лежало. ## Acceptance criteria - [ ] проверяемое условие """ BETA = """\ --- id: beta-issue state: open labels: [type/task] assignees: [] milestone: none depends: [alpha-issue] origin: local --- # Beta issue ## Summary Второй issue фикстуры, зависит от первого. ## Spec none ## Depends on - alpha-issue ## Motivation Нужен, чтобы у графа было ребро. ## Acceptance criteria - [ ] проверяемое условие """ def run(script, *args, **kw): """Run one of the plugin's real scripts and return (rc, stdout, stderr). `project_dir` sets CLAUDE_PROJECT_DIR for the child; by default the variable is removed, so cwd alone decides which project answers.""" cwd = kw.pop("cwd") project_dir = kw.pop("project_dir", None) env = dict(os.environ) env.pop("PYTHONPATH", None) # no leakage from the harness into the child env.pop("CLAUDE_PROJECT_DIR", None) if project_dir: env["CLAUDE_PROJECT_DIR"] = project_dir p = subprocess.run([sys.executable, script] + list(args), cwd=cwd, env=env, capture_output=True, text=True) return p.returncode, p.stdout, p.stderr def script(layer, name): """A script at its real installed path — never a copy inside a fixture.""" return os.path.join(REPO, "skills", layer, "scripts", name) class FakeProject(object): """An initialized project in a temp directory, far from the scripts.""" def __init__(self, with_store=True, marker=True, issues=(ALPHA, BETA)): self._tmp = tempfile.TemporaryDirectory() # realpath: on macOS $TMPDIR is a symlink, and a child process reporting # its own cwd would otherwise disagree with the path we handed it. self.root = os.path.realpath(self._tmp.name) if marker: os.makedirs(os.path.join(self.root, issue.MARKER)) os.makedirs(self.path("sub", "deeper")) if with_store: os.makedirs(self.store) for text in issues: id = text.split("id: ", 1)[1].split("\n", 1)[0] with open(os.path.join(self.store, "%s.md" % id), "w") as f: f.write(text) def cleanup(self): self._tmp.cleanup() def path(self, *parts): return os.path.join(self.root, *parts) @property def store(self): return self.path(*issue.STORE_PARTS) def everywhere(self): """Working directories that must all produce the same answer: the project root, a plain subdirectory, a deeper one, and — the case from the original bug report — inside the store itself.""" return [self.root, self.path("sub"), self.path("sub", "deeper"), self.store] # -------------------------------------------------------------------------- # resolution, in isolation # -------------------------------------------------------------------------- class TestResolution(unittest.TestCase): def setUp(self): self.project = FakeProject() self.addCleanup(self.project.cleanup) def test_the_marker_is_found_from_any_depth(self): for start in self.project.everywhere(): self.assertEqual(issue.project_root(start), self.project.root, start) def test_git_alone_is_not_a_marker(self): """The whole point of an explicit marker. `.git` is in every clone, including this plugin's own — inferring the root from one is how the plugin came to answer with itself. An uninitialized repository is not a project this tool knows about, and it says so instead of guessing.""" plain = FakeProject(marker=False, with_store=False) self.addCleanup(plain.cleanup) os.makedirs(plain.path(".git")) open(plain.path("AGENTS.md"), "w").close() self.assertIsNone(issue.project_root(plain.path("sub", "deeper"))) self.assertIsNone(issue.store_root(plain.path("sub", "deeper"))) def test_nearest_marker_wins(self): """A project inside a project (a vendored copy, a nested checkout) resolves to the inner one, not the outer.""" inner = self.project.path("sub", "inner") os.makedirs(os.path.join(inner, issue.MARKER)) self.assertEqual(issue.project_root(inner), inner) self.assertEqual(issue.project_root(self.project.root), self.project.root) def test_store_root_is_project_root_plus_marker(self): self.assertEqual(issue.store_root(self.project.path("sub", "deeper")), self.project.store) self.assertTrue(os.path.isabs(issue.store_root(self.project.root))) def test_no_marker_anywhere_resolves_to_nothing(self): """Not a default, not cwd, not the script's own directory: None. A wrong directory that looks like it worked is the failure this replaces.""" plain = FakeProject(marker=False, with_store=False) self.addCleanup(plain.cleanup) self.assertIsNone(issue.store_root(plain.path("sub", "deeper"))) def test_the_error_names_the_directories_it_searched(self): msg = issue.no_project_error("/nowhere-at-all") self.assertIn(issue.MARKER, msg) self.assertIn("/nowhere-at-all", msg) self.assertIn("issue_init.py", msg) # -------------------------------------------------------------------------- # the regression: an installed plugin never answers with itself # -------------------------------------------------------------------------- class TestTheStoreIsNeverThePlugin(unittest.TestCase): """The bug this contract exists for. Anchored on `__file__`, every one of these commands resolved the store inside the plugin — a versioned cache directory — so work written from a project was invisible from it and disappeared on the next plugin update.""" def setUp(self): self.project = FakeProject() self.addCleanup(self.project.cleanup) self.before = self._plugin_tree() def _plugin_tree(self): out = set() for dirpath, dirnames, filenames in os.walk(REPO): dirnames[:] = [d for d in dirnames if d != "__pycache__"] for f in filenames: out.add(os.path.join(dirpath, f)) return out def test_scripts_run_from_a_project_write_only_into_that_project(self): for d in self.project.everywhere(): for name in ("issue_index.py", "issue_check.py", "issue_tree.py"): run(script("issue", name), cwd=d) rc, out, err = run(script("issue", "issue_new.py"), "--type", "task", "--title", "Written from a project", cwd=self.project.path("sub", "deeper")) self.assertEqual(rc, 0, err) self.assertTrue(os.path.isfile( os.path.join(self.project.store, "written-from-a-project.md"))) new = self._plugin_tree() - self.before self.assertEqual(new, set(), "these commands wrote into the plugin: %s" % ", ".join(sorted(new))) def test_the_plugins_own_marker_does_not_leak_into_a_project(self): """Should this repository ever be initialized for its own issues, that marker must not become the answer for a project that has one.""" for d in self.project.everywhere(): self.assertEqual(issue.project_root(d), self.project.root, d) # -------------------------------------------------------------------------- # the acceptance criterion: same answer from any subdirectory # -------------------------------------------------------------------------- class TestSameFromAnywhere(unittest.TestCase): def setUp(self): self.project = FakeProject() self.addCleanup(self.project.cleanup) def assertSameEverywhere(self, layer, name, *args): """Run the script from the project root and from every other directory; every result must be byte-identical to the one from the root.""" dirs = self.project.everywhere() base = run(script(layer, name), *args, cwd=dirs[0]) self.assertEqual(base[0], 0, "%s failed at the project root:\n%s" % (name, base[2])) for d in dirs[1:]: self.assertEqual(run(script(layer, name), *args, cwd=d), base, "%s disagrees when run from %s" % (name, d)) return base def test_issue_check(self): rc, out, _ = self.assertSameEverywhere("issue", "issue_check.py") self.assertIn("ok alpha-issue", out) self.assertIn("2 issue(s) checked, 0 with errors", out) def test_issue_tree(self): _, out, _ = self.assertSameEverywhere("issue", "issue_tree.py") self.assertIn("beta-issue", out) self.assertIn("alpha-issue", out) def test_issue_index(self): _, out, _ = self.assertSameEverywhere("issue", "issue_index.py") self.assertIn("2 issue(s)", out) self.assertIn(os.path.join(self.project.store, "INDEX.md"), out) def test_no_second_store_is_ever_created(self): """The old bug's worst symptom: `issue_index.py` run from inside the store used to leave tmp/issues/tmp/issues/ behind, silently.""" for d in self.project.everywhere(): for name in ("issue_index.py", "issue_check.py", "issue_tree.py"): run(script("issue", name), cwd=d) found = [] for dirpath, dirnames, filenames in os.walk(self.project.root): if "__pycache__" in dirnames: dirnames.remove("__pycache__") if "INDEX.md" in filenames: found.append(dirpath) self.assertEqual(found, [self.project.store], "a second store appeared: %s" % found) def test_a_cd_into_another_project_answers_with_that_project(self): """Walking up is not cwd-independence for its own sake: two projects are two stores, and the one you are standing in is the one you meant.""" other = FakeProject(issues=(ALPHA,)) self.addCleanup(other.cleanup) _, mine, _ = run(script("issue", "issue_check.py"), cwd=self.project.root) _, theirs, _ = run(script("issue", "issue_check.py"), cwd=other.root) self.assertIn("2 issue(s) checked", mine) self.assertIn("1 issue(s) checked", theirs) # -------------------------------------------------------------------------- # which anchor wins # -------------------------------------------------------------------------- class TestAnchorOrder(unittest.TestCase): def setUp(self): self.project = FakeProject() self.other = FakeProject(issues=(ALPHA,)) self.addCleanup(self.project.cleanup) self.addCleanup(self.other.cleanup) def test_claude_project_dir_is_asked_before_cwd(self): """The editor's project is the project, even when a command happens to run from somewhere else — the same order the login pin uses, so the two cannot disagree about which project this is.""" _, out, err = run(script("issue", "issue_check.py"), cwd=self.other.root, project_dir=self.project.root) self.assertIn("2 issue(s) checked", out, err) def test_an_unmarked_claude_project_dir_falls_through_to_cwd(self): """First hit wins, not first anchor tried: a project dir with no marker above it is no answer at all, and cwd still gets its turn.""" plain = FakeProject(marker=False, with_store=False) self.addCleanup(plain.cleanup) _, out, err = run(script("issue", "issue_check.py"), cwd=self.other.root, project_dir=plain.root) self.assertIn("1 issue(s) checked", out, err) # -------------------------------------------------------------------------- # no project at all # -------------------------------------------------------------------------- class TestNoProject(unittest.TestCase): def setUp(self): self.plain = FakeProject(marker=False, with_store=False) self.addCleanup(self.plain.cleanup) def test_readers_report_it_and_name_where_they_looked(self): for name in ("issue_check.py", "issue_tree.py", "issue_index.py"): rc, out, err = run(script("issue", name), cwd=self.plain.root) msg = out + err self.assertNotEqual(rc, 0, "%s should fail with no project" % name) self.assertIn("no %s/ found" % issue.MARKER, msg, name) self.assertIn(self.plain.root, msg, name) def test_no_domain_script_ever_shows_a_traceback(self): """"No project" is an ordinary answer, not a crash. A TypeError on a None path is how an unresolved root announced itself while this was being written — every entry point is swept, so a new one cannot quietly reintroduce it.""" args = {"issue_ac.py": ["alpha-issue"], "issue_evict.py": ["--dry-run"], "issue_new.py": ["--type", "task", "--title", "Nowhere"]} entries = [n for n in sorted(os.listdir(ISSUE_SCRIPTS)) if n.endswith(".py") and n not in ("issue.py", "issue_init.py")] self.assertTrue(entries) for name in entries: rc, out, err = run(script("issue", name), *args.get(name, []), cwd=self.plain.path("sub", "deeper")) self.assertNotIn("Traceback", err, "%s crashed:\n%s" % (name, err)) self.assertNotEqual(rc, 0, name) self.assertIn("no %s/ found" % issue.MARKER, out + err, name) def test_a_writer_refuses_to_invent_a_project(self): rc, out, err = run(script("issue", "issue_new.py"), "--type", "task", "--title", "Nowhere to put this", cwd=self.plain.path("sub", "deeper")) self.assertNotEqual(rc, 0) self.assertIn("no %s/ found" % issue.MARKER, out + err) self.assertFalse(os.path.exists(self.plain.path(issue.MARKER))) self.assertFalse(os.path.exists(self.plain.path("sub", "deeper", issue.MARKER))) # -------------------------------------------------------------------------- # missing is not empty # -------------------------------------------------------------------------- class TestMissingVersusEmpty(unittest.TestCase): def test_missing_store_says_missing(self): project = FakeProject(with_store=False) self.addCleanup(project.cleanup) for name in ("issue_check.py", "issue_tree.py", "issue_index.py"): rc, out, err = run(script("issue", name), cwd=project.root) msg = out + err self.assertNotEqual(rc, 0, "%s should fail on a missing store" % name) self.assertIn("does not exist", msg, name) self.assertNotIn("is empty", msg, name) def test_empty_store_says_empty(self): project = FakeProject(issues=()) self.addCleanup(project.cleanup) for name in ("issue_check.py", "issue_tree.py"): rc, out, err = run(script("issue", name), cwd=project.root) msg = out + err self.assertNotEqual(rc, 0, name) self.assertIn("is empty", msg, name) self.assertNotIn("does not exist", msg, name) def test_index_of_an_empty_store_is_legitimate(self): """An existing store with nothing in it gets an index saying so. Only a missing directory is an error.""" project = FakeProject(issues=()) self.addCleanup(project.cleanup) rc, out, err = run(script("issue", "issue_index.py"), cwd=project.root) self.assertEqual(rc, 0, err) self.assertIn("0 issue(s)", out) with open(os.path.join(project.store, "INDEX.md")) as f: self.assertIn("_empty_", f.read()) # -------------------------------------------------------------------------- # nothing conjures a store # -------------------------------------------------------------------------- class TestNoSilentCreation(unittest.TestCase): def setUp(self): self.project = FakeProject(with_store=False) self.addCleanup(self.project.cleanup) def test_readers_and_the_indexer_create_nothing(self): for d in (self.project.root, self.project.path("sub")): for name in ("issue_check.py", "issue_tree.py", "issue_index.py"): run(script("issue", name), cwd=d) self.assertFalse(os.path.exists(self.project.store), "the store was created by a read") self.assertFalse(os.path.exists(self.project.path("sub", issue.MARKER)), "a store was created relative to cwd") def test_explicit_out_pointing_nowhere_is_an_error_not_a_mkdir(self): target = self.project.path("sub", "nowhere") rc, out, err = run(script("issue", "issue_index.py"), "--out", target, cwd=self.project.root) self.assertNotEqual(rc, 0) self.assertIn("does not exist", out + err) self.assertFalse(os.path.exists(target)) def test_issue_new_creates_the_store_and_says_so(self): """Creating the first issue in a fresh project must still work — but out loud, and at the project root, not below whatever cwd happens to be.""" rc, out, err = run(script("issue", "issue_new.py"), "--type", "task", "--title", "Bootstrap the store", cwd=self.project.path("sub", "deeper")) self.assertEqual(rc, 0, err) self.assertIn("created store", err) self.assertIn(self.project.store, err) self.assertTrue(os.path.isfile( os.path.join(self.project.store, "bootstrap-the-store.md"))) self.assertFalse( os.path.exists(self.project.path("sub", "deeper", issue.MARKER)), "a store was created relative to cwd") # -------------------------------------------------------------------------- # an explicit --out is the operator's, not ours to rewrite # -------------------------------------------------------------------------- class TestExplicitOutWins(unittest.TestCase): def setUp(self): self.project = FakeProject() self.addCleanup(self.project.cleanup) def test_absolute_out_is_honored(self): other = self.project.path("sub", "other-store") os.makedirs(other) shutil.copy(os.path.join(self.project.store, "alpha-issue.md"), other) rc, out, err = run(script("issue", "issue_check.py"), "--out", other, cwd=self.project.root) self.assertEqual(rc, 0, err) self.assertIn("1 issue(s) checked", out) def test_relative_out_stays_relative_to_cwd(self): """`--out .tea/issues` typed from a subdirectory means that subdirectory's `.tea/issues` — which is not there. Auto-resolution must not step in and "fix" what the operator typed.""" rel = os.path.join(*issue.STORE_PARTS) rc, out, err = run(script("issue", "issue_check.py"), "--out", rel, cwd=self.project.path("sub")) self.assertNotEqual(rc, 0) self.assertIn("does not exist", out + err) # the same relative path from the root does resolve, by cwd alone rc, out, err = run(script("issue", "issue_check.py"), "--out", rel, cwd=self.project.root) self.assertEqual(rc, 0, err) self.assertIn("2 issue(s) checked", out) def test_relative_out_can_climb(self): rc, out, err = run(script("issue", "issue_check.py"), "--out", os.path.join("..", *issue.STORE_PARTS), cwd=self.project.path("sub")) self.assertEqual(rc, 0, err) self.assertIn("2 issue(s) checked", out) # -------------------------------------------------------------------------- # both layers, one root # -------------------------------------------------------------------------- class TestSyncLayerAgrees(unittest.TestCase): def setUp(self): self.project = FakeProject() self.addCleanup(self.project.cleanup) def _probe(self, layer, cwd): """Ask one layer, from `cwd`, which module defines the store and where it lands. The sync scripts put the issue scripts on sys.path themselves — `import map` is how they do it — so each layer is asked its own way. """ scripts = SYNC_SCRIPTS if layer == "sync" else ISSUE_SCRIPTS entry = "import map, issue" if layer == "sync" else "import issue" code = ("import sys; sys.path.insert(0, %r)\n%s\n" "print(issue.__file__)\nprint(issue.ISSUE_ROOT)\n") % (scripts, entry) env = dict(os.environ) env.pop("PYTHONPATH", None) env.pop("CLAUDE_PROJECT_DIR", None) p = subprocess.run([sys.executable, "-c", code], cwd=cwd, env=env, capture_output=True, text=True) self.assertEqual(p.returncode, 0, p.stderr) return p.stdout.strip().splitlines() def test_both_layers_resolve_the_same_store_from_anywhere(self): for d in self.project.everywhere(): mod_i, root_i = self._probe("issue", d) mod_s, root_s = self._probe("sync", d) # sync does not redefine the store; it imports the domain module self.assertEqual(os.path.realpath(mod_i), os.path.realpath(mod_s), d) self.assertEqual(root_i, self.project.store, d) self.assertEqual(root_s, self.project.store, d) def test_the_payload_root_is_a_sibling_of_the_store(self): """One marker, one walk: the transport's scratchpad and the domain's store cannot end up in different projects, and the scratchpad is never inside the store.""" code = ("import sys; sys.path.insert(0, %r)\n" "import _gitea\nprint(_gitea.PAYLOAD_ROOT)\n") % SYNC_SCRIPTS env = dict(os.environ) env.pop("PYTHONPATH", None) env.pop("CLAUDE_PROJECT_DIR", None) for d in self.project.everywhere(): p = subprocess.run([sys.executable, "-c", code], cwd=d, env=env, capture_output=True, text=True) self.assertEqual(p.returncode, 0, p.stderr) payload = p.stdout.strip() self.assertEqual(payload, self.project.path(issue.MARKER, "payload"), d) self.assertFalse(payload.startswith(self.project.store + os.sep), d) def test_every_out_flag_defers_to_the_domain_layer(self): """Both layers agree by construction, not by coincidence: no script spells the default out for itself.""" for layer, names in (("issue", ("issue_new.py", "issue_check.py", "issue_tree.py", "issue_index.py", "issue_evict.py")), ("sync", ("pull.py", "push.py", "remote.py", "comment.py", "evict.py"))): for name in names: with open(os.path.join(REPO, "skills", layer, "scripts", name)) as f: src = f.read() self.assertIn('"--out", default=issue.ISSUE_ROOT', src, "%s/%s does not take its --out default from the " "domain layer" % (layer, name)) # -------------------------------------------------------------------------- # the layering rule, mechanically # -------------------------------------------------------------------------- class TestLayering(unittest.TestCase): def test_domain_layer_is_stdlib_only(self): """skills/issue must keep working with skills/sync deleted — so no transport, and above all no subprocess, in the domain layer.""" imported = set() for name in sorted(os.listdir(ISSUE_SCRIPTS)): if not name.endswith(".py"): continue with open(os.path.join(ISSUE_SCRIPTS, name)) as f: for line in f: if line.startswith(("import ", "from ")): imported.add(line.split()[1].split(".")[0]) local = {"issue", "issue_ac", "issue_index"} foreign = imported - local - sys.stdlib_module_names self.assertEqual(foreign, set(), "non-stdlib import in the domain layer: %s" % ", ".join(sorted(foreign))) self.assertNotIn("subprocess", imported) if __name__ == "__main__": unittest.main()