Files
marketplace/plugins/tea/tests/test_store_path.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

622 lines
26 KiB
Python

#!/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/<version>/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()