@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.
- package/package.json +2 -2
- package/skills/test-prune/SKILL.md +156 -0
- package/skills/test-prune/prune.py +202 -0
- package/skills/test-prune/prune_repo.py +319 -0
- package/skills/test-prune/prune_review.py +363 -0
- package/skills/test-prune/prune_runs.py +240 -0
- package/skills/test-prune/prune_source.py +237 -0
- package/skills/test-prune/prune_strengthen.py +301 -0
- package/skills/test-prune/tests/conftest.py +15 -0
- package/skills/test-prune/tests/prune_harness.py +205 -0
- package/skills/test-prune/tests/test_prune_guards.py +871 -0
- package/skills/test-prune/tests/test_prune_inputs.py +97 -0
- package/skills/test-prune/tests/test_prune_review.py +113 -0
- package/skills/test-prune/tests/test_prune_source.py +84 -0
- package/skills/test-prune/tests/test_prune_strengthen.py +183 -0
- package/skills/test-prune/tests/test_prune_unattended.py +225 -0
- package/skills/test-prune/tests/testdata/gh +16 -0
- package/skills/test-prune/tests/testdata/repo/Makefile +25 -0
- package/skills/test-prune/tests/testdata/repo/backend/mutation_baseline.json +16 -0
- package/skills/test-prune/tests/testdata/repo/backend/services/concentration.py +2 -0
- package/skills/test-prune/tests/testdata/repo/backend/services/rebalancer.py +11 -0
- package/skills/test-prune/tests/testdata/repo/backend/tests/conftest.py +6 -0
- package/skills/test-prune/tests/testdata/repo/backend/tests/test_concentration.py +73 -0
- package/skills/test-prune/tests/testdata/repo/backend/tests/test_rebalancer.py +77 -0
- package/skills/test-prune/tests/testdata/repo/fake_engine.py +289 -0
- package/skills/test-prune/tests/testdata/repo/kill_map.json +131 -0
- 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)
|