@jesdi/skills 0.0.50 → 0.0.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/package.json +2 -2
  2. package/skills/test-prune/SKILL.md +156 -0
  3. package/skills/test-prune/prune.py +202 -0
  4. package/skills/test-prune/prune_repo.py +319 -0
  5. package/skills/test-prune/prune_review.py +363 -0
  6. package/skills/test-prune/prune_runs.py +240 -0
  7. package/skills/test-prune/prune_source.py +237 -0
  8. package/skills/test-prune/prune_strengthen.py +301 -0
  9. package/skills/test-prune/tests/conftest.py +15 -0
  10. package/skills/test-prune/tests/prune_harness.py +205 -0
  11. package/skills/test-prune/tests/test_prune_guards.py +871 -0
  12. package/skills/test-prune/tests/test_prune_inputs.py +97 -0
  13. package/skills/test-prune/tests/test_prune_review.py +113 -0
  14. package/skills/test-prune/tests/test_prune_source.py +84 -0
  15. package/skills/test-prune/tests/test_prune_strengthen.py +183 -0
  16. package/skills/test-prune/tests/test_prune_unattended.py +225 -0
  17. package/skills/test-prune/tests/testdata/gh +16 -0
  18. package/skills/test-prune/tests/testdata/repo/Makefile +25 -0
  19. package/skills/test-prune/tests/testdata/repo/backend/mutation_baseline.json +16 -0
  20. package/skills/test-prune/tests/testdata/repo/backend/services/concentration.py +2 -0
  21. package/skills/test-prune/tests/testdata/repo/backend/services/rebalancer.py +11 -0
  22. package/skills/test-prune/tests/testdata/repo/backend/tests/conftest.py +6 -0
  23. package/skills/test-prune/tests/testdata/repo/backend/tests/test_concentration.py +73 -0
  24. package/skills/test-prune/tests/testdata/repo/backend/tests/test_rebalancer.py +77 -0
  25. package/skills/test-prune/tests/testdata/repo/fake_engine.py +289 -0
  26. package/skills/test-prune/tests/testdata/repo/kill_map.json +131 -0
  27. package/skills-manifest.json +7 -1
@@ -0,0 +1,205 @@
1
+ """Test harness for the test-prune black-box tests: a fixture target repo plus agent stand-ins.
2
+
3
+ `target` builds, per test, a git repo shaped like portfolio_evaluator (backend/tests,
4
+ backend/mutation_baseline.json, make targets) whose make targets call testdata/repo/fake_engine.py,
5
+ plus a bare `origin` with `main` pushed and a fake `gh` first on PATH. Logs land outside the repo.
6
+ """
7
+ import json
8
+ import os
9
+ import shutil
10
+ import stat
11
+ import subprocess
12
+ import sys
13
+ from pathlib import Path
14
+
15
+ HERE = Path(__file__).resolve().parent
16
+ PRUNE = HERE.parent / "prune.py" # the skill is one folder up
17
+ TESTDATA = HERE / "testdata"
18
+ DATE = "2026-10-17"
19
+ PRUNE_BRANCH = f"chore/test-prune-{DATE}"
20
+ STRENGTHEN_BRANCH = f"chore/test-strengthen-{DATE}"
21
+ R = "tests/test_rebalancer.py::"
22
+ C = "tests/test_concentration.py::"
23
+ REFERENCE = C + "test_hhi_reference"
24
+
25
+
26
+ def case(i):
27
+ return f"{C}test_hhi_case_{i:02d}"
28
+
29
+
30
+ def cand(test_id, into=REFERENCE, unprotected=False):
31
+ entry = {"test_id": test_id, "arc_count": 3, "contained_by": [into]}
32
+ if unprotected:
33
+ entry["unprotected"] = True
34
+ return entry
35
+
36
+
37
+ def git_cmd(root, *args, check=True):
38
+ return subprocess.run(["git", *args], cwd=root, check=check, capture_output=True, text=True)
39
+
40
+
41
+ class Target:
42
+ def __init__(self, tmp):
43
+ self.tmp = tmp
44
+ self.root = tmp / "repo"
45
+ self.origin = tmp / "origin.git"
46
+ self.logs = tmp / "logs"
47
+ self.state = tmp / "state"
48
+ self.bin = tmp / "bin"
49
+ self.logs.mkdir()
50
+ self.bin.mkdir()
51
+ shutil.copytree(TESTDATA / "repo", self.root)
52
+ subst = {"@PYTHON@": sys.executable, "@LOGS@": str(self.logs)}
53
+ for path in (self.root / "Makefile", self.bin / "gh"):
54
+ text = (TESTDATA / "gh").read_text() if path.name == "gh" else path.read_text()
55
+ for k, v in subst.items():
56
+ text = text.replace(k, v)
57
+ path.write_text(text)
58
+ (self.bin / "gh").chmod((self.bin / "gh").stat().st_mode | stat.S_IEXEC)
59
+ git_cmd(tmp, "init", "-q", "--bare", "-b", "main", str(self.origin))
60
+ self.git("init", "-q", "-b", "main")
61
+ for k, v in {"user.name": "t", "user.email": "t@example.com", "commit.gpgsign": "false",
62
+ "core.hooksPath": "/dev/null"}.items():
63
+ self.git("config", k, v)
64
+ self.git("add", ".")
65
+ self.git("commit", "-q", "-m", "base")
66
+ self.git("remote", "add", "origin", str(self.origin))
67
+ self.git("push", "-q", "-u", "origin", "main")
68
+ self.base = self.git("rev-parse", "HEAD").strip()
69
+
70
+ # --- driving the CLI -----------------------------------------------------------
71
+ def env(self, extra=None):
72
+ env = {k: v for k, v in os.environ.items()
73
+ if k not in ("GITHUB_STEP_SUMMARY", "KEEP_CACHE", "MAKEFLAGS", "MFLAGS", "MAKELEVEL")
74
+ and not k.startswith("FAKE_")}
75
+ env["PATH"] = f"{self.bin}{os.pathsep}{env.get('PATH', '')}"
76
+ env.update(extra or {})
77
+ return env
78
+
79
+ def run(self, *args, env=None, cwd=None):
80
+ """python3 prune.py <args> --state <state> --date DATE, from inside the repo."""
81
+ return subprocess.run([sys.executable, str(PRUNE), *args, "--state", str(self.state), "--date", DATE],
82
+ cwd=cwd or self.root / "backend", env=self.env(env), capture_output=True, text=True)
83
+
84
+ def ok(self, *args, env=None):
85
+ proc = self.run(*args, env=env)
86
+ assert proc.returncode == 0, f"prune.py {' '.join(args)} exited {proc.returncode}\n{proc.stdout}\n{proc.stderr}"
87
+ return proc
88
+
89
+ # --- playing the agents ----------------------------------------------------------
90
+ def set_candidates(self, candidates):
91
+ (self.logs / "candidates.json").write_text(json.dumps(sorted(candidates, key=lambda c: c["test_id"])))
92
+
93
+ def proposals(self, deletions=None, strengthen=None, groups=None):
94
+ """Fixer output: every fixer-input candidate and group gets one verdict.
95
+
96
+ deletions {test_id: into}, strengthen {test_id: assertion}, groups {group: {verdict, mutant_ids,
97
+ test, reason}}; other candidates are kept; other groups get a strengthen proposal.
98
+ """
99
+ deletions, strengthen, groups = deletions or {}, strengthen or {}, groups or {}
100
+ fixer_input = self.read_state("fixer-input.json")
101
+ cands = []
102
+ for c in fixer_input["candidates"]:
103
+ tid = c["test_id"]
104
+ if tid in deletions:
105
+ cands.append({"test_id": tid, "verdict": "delete", "into": deletions[tid],
106
+ "reason": f"fixer: covered by {deletions[tid]}"})
107
+ elif tid in strengthen:
108
+ cands.append({"test_id": tid, "verdict": "strengthen", "assertion": strengthen[tid],
109
+ "reason": "fixer: weak assertion"})
110
+ else:
111
+ cands.append({"test_id": tid, "verdict": "keep", "reason": "fixer: distinct behaviour"})
112
+ spots = []
113
+ for w in fixer_input["weak_spots"]:
114
+ spots.append(dict({"group": w["group"], "verdict": "strengthen", "mutant_ids": w["mutant_ids"],
115
+ "test": "tests/test_rebalancer.py", "reason": "fixer: add an assertion"},
116
+ **groups.get(w["group"], {})))
117
+ return {"candidates": cands, "weak_spots": spots}
118
+
119
+ def review_input(self, proposals, env=None):
120
+ path = self.tmp / "proposals.json"
121
+ path.write_text(json.dumps(proposals))
122
+ return self.run("review-input", "--proposals", str(path), env=env)
123
+
124
+ def reviews(self, accept=(), reasons=None, ids=None):
125
+ """Reviewer output over every review-input proposal (or `ids`): accepted iff in `accept`."""
126
+ reasons = reasons or {}
127
+ ids = ids if ids is not None else [p["id"] for p in self.read_state("review-input.json")["proposals"]]
128
+ path = self.tmp / f"reviews-{len(list(self.tmp.glob('reviews-*')))}.json"
129
+ path.write_text(json.dumps({"reviews": [
130
+ {"id": i, "verdict": "accept" if i in accept else "reject",
131
+ "reason": reasons.get(i, f"reviewer: {'agreed' if i in accept else 'not this run'} for {i}")}
132
+ for i in ids]}))
133
+ return path
134
+
135
+ def prepare(self, candidates, unattended=True, accept=(), reasons=None, env=None, before=None, **proposal_kw):
136
+ """collect → review-input → batch (each preceded by `before()`); returns the batch run."""
137
+ before = before or (lambda: None)
138
+ self.set_candidates(candidates)
139
+ before()
140
+ self.ok("collect", *(["--unattended"] if unattended else []), env=env)
141
+ before()
142
+ proc = self.review_input(self.proposals(**proposal_kw), env=env)
143
+ assert proc.returncode == 0, proc.stdout + proc.stderr
144
+ before()
145
+ return self.ok("batch", "--reviews", str(self.reviews(accept, reasons)), env=env)
146
+
147
+ def commit_test(self, item, test_source, file="backend/tests/test_rebalancer.py"):
148
+ """Strengthen agent: append a test and commit it with the item trailer."""
149
+ path = self.root / file
150
+ path.write_text(path.read_text() + "\n\n" + test_source.strip("\n") + "\n")
151
+ self.git("add", file)
152
+ self.git("commit", "-q", "-m", f"test: strengthen {item}\n\nTest-Prune-Item: {item}")
153
+
154
+ # --- observing ---------------------------------------------------------------------
155
+ def git(self, *args):
156
+ return git_cmd(self.root, *args).stdout
157
+
158
+ def read_state(self, name):
159
+ return json.loads((self.state / name).read_text())
160
+
161
+ def log(self, name):
162
+ path = self.logs / f"{name}.log"
163
+ return [json.loads(line) for line in path.read_text().splitlines()] if path.exists() else []
164
+
165
+ def pr_creates(self):
166
+ return [c for c in self.log("gh") if c["argv"][:2] == ["pr", "create"]]
167
+
168
+ def remote_branches(self):
169
+ out = git_cmd(self.tmp, "--git-dir", str(self.origin), "for-each-ref", "--format=%(refname:short)",
170
+ "refs/heads").stdout
171
+ return set(out.split())
172
+
173
+ def remote_sha(self, branch):
174
+ return git_cmd(self.tmp, "--git-dir", str(self.origin), "rev-parse", f"refs/heads/{branch}").stdout.strip()
175
+
176
+ def show(self, ref, path):
177
+ return self.git("show", f"{ref}:{path}")
178
+
179
+ def ids_at(self, ref):
180
+ """Test ids collected at ref, by the fake engine's collector (pytest's shape)."""
181
+ sys.path.insert(0, str(self.root))
182
+ try:
183
+ import fake_engine
184
+ finally:
185
+ sys.path.pop(0)
186
+ return set(fake_engine.collect_ref(self.root, ref))
187
+
188
+ def junk_mutants(self):
189
+ """A stale cache claiming every mutant killed, as a kept nightly cache would."""
190
+ mutants = self.root / "backend" / "mutants"
191
+ (mutants / "services").mkdir(parents=True, exist_ok=True)
192
+ (mutants / "junk.txt").write_text("stale\n")
193
+ kill_map = json.loads((self.root / "kill_map.json").read_text())
194
+ meta = {"exit_code_by_key": {f"services.rebalancer.{m}": 1 for m in kill_map["services/rebalancer.py"]},
195
+ "hash_by_function_name": {}}
196
+ (mutants / "services" / "rebalancer.py.meta").write_text(json.dumps(meta))
197
+
198
+
199
+ def arg(argv, flag):
200
+ """Value of `flag` in an argv list (None when absent)."""
201
+ return argv[argv.index(flag) + 1] if flag in argv else None
202
+
203
+
204
+ def lines_with(text, needle):
205
+ return "\n".join(line for line in text.splitlines() if needle in line)