Files
marketplace/plugins/tea/tests/test_payload_root.py
T
naudachu fb5445915f fix: resolve the issue store from the project, not the plugin
`issue.store_root` and `_gitea.PAYLOAD_ROOT` were anchored on `__file__`, on
the reasoning that where an installation keeps its files is a fact about the
installation. That holds for an installation and not for a store.

Installed, the plugin therefore resolved every project's issues inside its own
directory — and a plugin cache is versioned, so the store moved on each
update:

    ~/.claude/plugins/cache/tea/tea/2.0.0/tmp/issues   5 files, 2 origin: local
    ~/.claude/plugins/cache/tea/tea/2.1.0/tmp/issues   12 files
    ~/.claude/plugins/cache/claude-skills/tea/2.2.0/   empty, the current one

Issues written from one project were invisible from the next, and an `origin:
local` file — which IS the issue, the only copy — was stranded a version bump
at a time. Two of them were.

The store is a fact about the project, exactly as the login pin is. So the
anchor is now an explicit marker an operator creates, `.tea/`, searched for up
from $CLAUDE_PROJECT_DIR and then cwd — the pin's order, so the two cannot
disagree about which project this is. Inferred markers were tried and are worse
than useless here: `.git` is in every clone including this plugin's own, and
the agents-sync hook writes an AGENTS.md next to every AGENTS.md, so the plugin
root always carried one and cwd never got a turn.

With no marker anywhere, `store_root()` is None and every entry point reports
which directories it searched. A store in a plausible-looking directory is the
failure this replaces, so nothing falls back to one.

- `.tea/` holds the store and the transport's scratchpad: `.tea/issues`,
  `.tea/payload`. One marker, one walk, one gitignore line.
- `issue_init.py` creates it, moves an old `tmp/issues` store in rather than
  copying, adds `.tea/` to `.gitignore`, and refuses to pick a winner when both
  sides hold the same file name.
- A linked worktree has no marker — it is gitignored — and reaches the main
  checkout's store by the hop the pin already took.
- `parents`, `gitdir_of` and `main_worktree` move from `pin.py` into the domain
  and `pin.py` imports them. The domain depends on nothing, so it is the layer
  all three callers can borrow from, and the walk stays written once: the
  guard, the transport and the store cannot disagree about a directory.

The suite stopped copying the script layers into its fixtures. That is what hid
this: with the scripts inside the fixture, the installation and the project
were the same directory. They are now deliberately far apart, and a regression
test asserts the plugin tree gains no files when commands run against a project
somewhere else.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 13:38:39 +05:00

284 lines
12 KiB
Python

#!/usr/bin/env python3
"""
Where request bodies land, and that writing one never conjures a store.
python3 -m unittest discover -s tests -v
Stdlib unittest, no third-party anything. The bug these tests pin down:
`labels.py --bootstrap` on a fresh checkout left `tmp/issues/.payload/` behind,
because the only place `_gitea.api` had to put a request file was whatever root
the caller handed it — and the label bootstrap, which touches no issue at all,
handed it the issue store. A store materialized as a side effect of an
operation that has nothing to do with issues.
Every run here is against a throwaway repository with a FAKE `tea` first on
PATH, so nothing reaches the network and the developer's own store is never in
the blast radius.
"""
import json
import os
import shutil
import stat
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")
AUTH_SCRIPTS = os.path.join(REPO, "skills", "auth", "scripts")
sys.path.insert(0, SYNC_SCRIPTS)
sys.path.insert(0, ISSUE_SCRIPTS)
import _gitea # noqa: E402
import issue # noqa: E402
# A `tea` that answers without a network: an empty list for every GET (so the
# repository looks like it has no labels yet) and a created object for every
# write. It also records its own argv, which is how a test can tell that the
# payload file the script wrote is the one the call actually referenced.
FAKE_TEA = '''#!%s
import json, os, sys
with open(os.path.join(os.environ["TEA_CALL_LOG"], "calls.txt"), "a") as f:
f.write("\\t".join(sys.argv[1:]) + "\\n")
sys.stdout.write(json.dumps({"id": 1, "name": "created"})
if "-X" in sys.argv else "[]")
'''
class FakeRepo(object):
"""An initialized project with no store and nothing under `.tea/` yet.
The scripts are NOT copied in: they stay at their real installed path, so
what these tests exercise is a plugin operating on somebody else's project
— which is every use of it but this repository's own."""
def __init__(self):
self._tmp = tempfile.TemporaryDirectory()
# realpath: on macOS $TMPDIR is a symlink, and a child reporting its
# own cwd would otherwise disagree with the path we handed it.
self.root = os.path.realpath(self._tmp.name)
os.makedirs(os.path.join(self.root, issue.MARKER)) # the project marker
os.makedirs(self.path("sub", "deeper"))
# the login pin the transport insists on, local to this fixture
os.makedirs(self.path(".claude"))
with open(self.path(".claude", "settings.local.json"), "w") as f:
json.dump({"env": {"GITEA_LOGIN": "fixture/user"}}, f)
self.bin = self.path("fakebin")
os.makedirs(self.bin)
tea = os.path.join(self.bin, "tea")
with open(tea, "w") as f:
f.write(FAKE_TEA % sys.executable)
os.chmod(tea, os.stat(tea).st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)
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)
@property
def payloads(self):
return self.path(*_gitea.PAYLOAD_PARTS)
def script(self, layer, name):
"""The real script, several directories away from this fixture."""
return os.path.join(REPO, "skills", layer, "scripts", name)
def run(self, script, *args, **kw):
env = dict(os.environ)
env.pop("PYTHONPATH", None) # no leakage from the harness into the child
# the first anchor of both walks: left in place, every fixture would
# resolve to this repository instead of itself
env.pop("CLAUDE_PROJECT_DIR", None)
env["PATH"] = self.bin + os.pathsep + env["PATH"]
env["TEA_CALL_LOG"] = self.root
p = subprocess.run([sys.executable, script] + list(args),
cwd=kw.pop("cwd", self.root), env=env,
capture_output=True, text=True)
return p.returncode, p.stdout, p.stderr
def calls(self):
p = os.path.join(self.root, "calls.txt")
if not os.path.isfile(p):
return []
with open(p) as f:
return [line.rstrip("\n").split("\t") for line in f if line.strip()]
# --------------------------------------------------------------------------
# resolution
# --------------------------------------------------------------------------
class TestPayloadRoot(unittest.TestCase):
def setUp(self):
self.repo = FakeRepo()
self.addCleanup(self.repo.cleanup)
def test_root_is_absolute_and_project_anchored(self):
root = _gitea.payload_root(self.repo.path("sub", "deeper"))
self.assertTrue(os.path.isabs(root), root)
self.assertEqual(root, self.repo.payloads)
def test_it_is_not_the_issue_store_and_not_inside_one(self):
"""The acceptance criterion, as a path fact: a request body is not
store content, so it may not live in a store or under one."""
start = self.repo.path("sub", "deeper")
payload = _gitea.payload_root(start)
store = issue.store_root(start)
self.assertNotEqual(payload, store)
self.assertFalse(payload.startswith(store + os.sep))
self.assertFalse(store.startswith(payload + os.sep))
def test_the_name_says_what_it_holds(self):
"""Named so the distinction is visible: `payload`, a sibling of the
store under the marker, not a dotdir hiding among an issue's files."""
payload = _gitea.payload_root(self.repo.root)
self.assertEqual(os.path.basename(payload), "payload")
self.assertEqual(os.path.dirname(payload),
self.repo.path(issue.MARKER))
def test_no_project_means_no_payload_root(self):
"""Same answer as the store gives: nothing, rather than a directory
picked because it was the only one at hand."""
plain = tempfile.TemporaryDirectory()
self.addCleanup(plain.cleanup)
self.assertIsNone(_gitea.payload_root(os.path.realpath(plain.name)))
def test_gitignore_covers_it(self):
"""The rule is that the marker is ignored, not which file says so: this
plugin lives under `plugins/` in a marketplace repo, and git reads every
.gitignore on the way up. So walk up the same way git does."""
ignored = set()
d = REPO
while True:
p = os.path.join(d, ".gitignore")
if os.path.isfile(p):
with open(p) as f:
ignored |= {line.strip() for line in f}
parent = os.path.dirname(d)
if parent == d or os.path.isdir(os.path.join(d, ".git")):
break
d = parent
self.assertEqual(_gitea.PAYLOAD_PARTS[0], issue.MARKER)
self.assertIn(issue.MARKER + "/", ignored,
"the payload directory is not covered by .gitignore")
def test_resolution_follows_the_project_not_the_module(self):
"""The bug, as a path fact: the scripts live somewhere else entirely,
and the answer is still this project's directory."""
self.assertEqual(_gitea.payload_root(self.repo.path("sub", "deeper")),
self.repo.payloads)
self.assertFalse(self.repo.payloads.startswith(REPO + os.sep))
# --------------------------------------------------------------------------
# the bug: a label bootstrap that materialized the store
# --------------------------------------------------------------------------
class TestLabelsTouchesNoStore(unittest.TestCase):
def setUp(self):
self.repo = FakeRepo()
self.addCleanup(self.repo.cleanup)
def bootstrap(self, *args, **kw):
rc, out, err = self.repo.run(self.repo.script("sync", "labels.py"),
"--repo", "fixture/repo", *args, **kw)
self.assertEqual(rc, 0, "labels.py failed:\n%s%s" % (out, err))
return out, err
def test_bootstrap_creates_no_store(self):
"""The reproduction from the report, run for real: no tmp/issues, and
no complaint about one either."""
out, _ = self.bootstrap()
self.assertIn("created", out)
self.assertFalse(os.path.exists(self.repo.store),
"labels.py created the issue store")
def test_bootstrap_writes_its_payloads_to_the_payload_root(self):
self.bootstrap()
self.assertTrue(os.path.isdir(self.repo.payloads),
"no payload directory: %s" % self.repo.payloads)
written = os.listdir(self.repo.payloads)
self.assertIn("label-type-bug.json", written)
for name in written:
self.assertTrue(name.startswith("label-"), name)
# and the file named on the command line is the one that was written
sent = [a[a.index("-d") + 1][1:] for a in self.repo.calls() if "-d" in a]
self.assertTrue(sent)
for path in sent:
self.assertEqual(os.path.dirname(path), self.repo.payloads)
self.assertTrue(os.path.isfile(path), path)
def test_the_payload_is_the_request_body(self):
self.bootstrap()
with open(os.path.join(self.repo.payloads, "label-type-bug.json")) as f:
body = json.load(f)
self.assertEqual(body.get("name"), "type/bug")
self.assertTrue(body.get("color"))
def test_a_dry_run_writes_nothing_at_all(self):
out, _ = self.bootstrap("--dry-run")
self.assertIn("nothing was written", out)
self.assertEqual(os.listdir(self.repo.path(issue.MARKER)), [],
"a dry run left something behind under the marker")
def test_the_directory_does_not_follow_cwd(self):
"""Run from a subdirectory: still one payload root, at the project
root. A cwd-relative directory is how the store ended up with a second
copy of itself, and this one is resolved the same way to avoid the same
class of bug."""
self.bootstrap(cwd=self.repo.path("sub", "deeper"))
self.assertTrue(os.path.isdir(self.repo.payloads))
self.assertFalse(os.path.exists(
self.repo.path("sub", "deeper", issue.MARKER)))
self.assertFalse(os.path.exists(self.repo.store))
# --------------------------------------------------------------------------
# one place, every caller
# --------------------------------------------------------------------------
class TestOnePlaceForEveryCaller(unittest.TestCase):
def hits(self, needle, skip_transport=False):
"""Every `layer/script.py:line` mentioning `needle`."""
out = []
d = SYNC_SCRIPTS
layer = os.path.basename(os.path.dirname(d))
for name in sorted(os.listdir(d)):
if not name.endswith(".py") or (skip_transport and name == "_gitea.py"):
continue
with open(os.path.join(d, name)) as f:
for n, line in enumerate(f, 1):
if needle in line:
out.append("%s/%s:%d" % (layer, name, n))
return out
def test_no_caller_chooses_where_its_payload_goes(self):
"""Whatever the answer is, it has to be the same for all of them —
payload files scattered across the stores of whichever command wrote
them is the state this replaced."""
self.assertEqual(self.hits("out_root"), [],
"a caller still picks a payload directory of its own")
def test_only_the_transport_names_the_directory(self):
self.assertEqual(self.hits("PAYLOAD", skip_transport=True), [],
"the payload directory is named outside the transport")
if __name__ == "__main__":
unittest.main()