tools/context-pack.py: carry cut_spec anchors to the worktree #8
5 changed files with 836 additions and 0 deletions
|
|
@ -13,3 +13,4 @@ gets a Soul pass before merge.
|
|||
| `tools/mutation-table.sh` | One verify job that runs a list of hand mutants (file, before, after, test) against a revision and returns a kill table: CAUGHT with the killing test, SURVIVED, UNVIABLE, TIMEOUT, NOAPPLY, ERROR. Refuses a red or zero-test baseline and a red BUILD; rows print live. Isolation is Docker's: the baselines run once in a container that holds the revision, that container is committed to a throwaway image, and every mutant runs in a fresh `--rm` container from it, so nothing a run writes (tracked, untracked or ignored, such as `target/`) reaches the next mutant; the image is removed when the job ends, killed or not. The shared registry and sccache volumes and the network are not isolated. A file-level load failure (vitest `FAIL <file> [ <file> ]`, jest `FAIL <file>` plus `Test suite failed to run`) reads UNVIABLE. Replaces per-agent runmut/mutrun/strykrun scripts. | Soul, Hands (scoped run) | `references/briefs.md` (Hands verification, Soul reruns) |
|
||||
| `tools/testparse.sh` | Sourced library: failing test names and pass/fail/skip counts from cargo, dotnet, vitest, jest and pytest logs, and test files that failed to load (`LOADFAIL`). Shared by the summary and the kill table. | the two scripts above | not named by a brief; it lives and dies with them |
|
||||
| `tools/test-tools.sh` | Local fixture check of the parsers and the summary over `tools/fixtures/logs` (real cargo and mutation logs, plus hand-written dotnet, vitest, jest, pytest, exit-0-but-failed and no-verdict logs), and of the mutation runner's host driver and container half under a mock docker. Run it before merging a change to the scripts above. | whoever changes those scripts | `SKILL.md` (Shared tools) |
|
||||
| `tools/context-pack.py` | `py -3 tools/context-pack.py <cut_spec id> <worktree> [--since <sha>] [--only <k>[,<k>...]] [--spec-json <file>] [--max-lines <n>]`. Carries a cut_spec's `file_changes` anchors from the spec's base to the worktree by `git diff` hunks (the working tree included) and prints one header, a status table (SAME, MOVED, TOUCHED, RENAMED, NEW, GONE, OUT_OF_RANGE, UNRESOLVED) and windows of at most 100 lines numbered in worktree lines, within `--max-lines` (800). Exit 3 when an anchor cannot be carried, 4 when the spec is unreadable (fetched by id through the eureka-state binding, retried once on Unavailable; `--spec-json` takes a saved view). `--since <sha>` re-prints only the windows edited since that commit. Standard library plus git; writes nothing. | Hands, Soul, Imagination | the briefs that cut `context-pack-briefs` edits; not yet named |
|
||||
|
|
|
|||
475
tools/context-pack.py
Normal file
475
tools/context-pack.py
Normal file
|
|
@ -0,0 +1,475 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Carry a cut_spec's file_changes anchors from the spec's base to a worktree.
|
||||
|
||||
Usage: context-pack.py <cut_spec id> <worktree> [--since <sha>] [--only <k>[,<k>...]]
|
||||
[--spec-json <file>] [--max-lines <n>]
|
||||
|
||||
Prints a header, a status table (one line per anchor) and bounded windows of the
|
||||
code each anchor names, numbered in the worktree's lines. Exit: 0 every anchor
|
||||
carried; 3 an anchor cannot be carried (OUT_OF_RANGE, UNRESOLVED, GONE outside
|
||||
the spec's deletes); 2 usage; 4 spec unreadable. Standard library plus git.
|
||||
Errors name a field and a code, never an input value.
|
||||
--max-lines bounds the windows, not the table: the table always lists every
|
||||
anchor, so a budget smaller than the table prints the table and no window.
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import queue
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
|
||||
WINDOW_CAP = 100
|
||||
CONTEXT = 3
|
||||
DEFAULT_MAX_LINES = 800
|
||||
ERROR_CODES = ("Unavailable", "Rejected", "Misconfigured", "TooLarge",
|
||||
"Unencodable", "InvalidInput", "Internal")
|
||||
HUNK = re.compile(r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@")
|
||||
HEX = re.compile(r"^[0-9a-fA-F]{7,40}$")
|
||||
|
||||
|
||||
class Fail(Exception):
|
||||
def __init__(self, code, msg):
|
||||
super().__init__(msg)
|
||||
self.code, self.msg = code, msg
|
||||
|
||||
|
||||
class Parser(argparse.ArgumentParser):
|
||||
def error(self, message): # argparse would echo the offending value
|
||||
sys.stderr.write("context-pack: usage error\n" + self.format_usage())
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
def git(wt, *args):
|
||||
"""The one git call: stdout bytes, or None when git failed; stderr is always
|
||||
discarded. Paths are literal pathspecs here, so a spec path holding [ ] * ?
|
||||
names that file and no sibling."""
|
||||
try:
|
||||
p = subprocess.run(["git", "-C", wt, "--literal-pathspecs", *args], stdout=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL)
|
||||
except OSError:
|
||||
return None
|
||||
return p.stdout if p.returncode == 0 else None
|
||||
|
||||
|
||||
def text_lines(raw):
|
||||
s = raw.decode("utf-8", "replace").replace("\r\n", "\n")
|
||||
parts = s.split("\n")
|
||||
if parts[-1] == "":
|
||||
parts.pop()
|
||||
return parts
|
||||
|
||||
|
||||
# ---- the spec ------------------------------------------------------------
|
||||
|
||||
def spec_from_json(obj):
|
||||
if isinstance(obj, dict) and "document" in obj:
|
||||
obj = obj["document"]
|
||||
if isinstance(obj, dict) and "kind" in obj and "value" in obj:
|
||||
obj = obj["value"]
|
||||
if not isinstance(obj, dict) or "base" not in obj or "file_changes" not in obj:
|
||||
raise Fail(4, "spec: not a cut_spec value")
|
||||
return check_spec_shape(obj)
|
||||
|
||||
|
||||
def is_int(v):
|
||||
return isinstance(v, int) and not isinstance(v, bool)
|
||||
|
||||
|
||||
def check_spec_shape(spec):
|
||||
"""A wrong-typed field is a typed refusal naming the field and InvalidInput,
|
||||
never the value."""
|
||||
def bad(field):
|
||||
raise Fail(4, "spec: %s: InvalidInput" % field)
|
||||
if not isinstance(spec["base"], str):
|
||||
bad("base")
|
||||
fcs = spec["file_changes"]
|
||||
if not isinstance(fcs, list):
|
||||
bad("file_changes")
|
||||
for k, fc in enumerate(fcs, 1):
|
||||
at = "file_changes[%d]" % k
|
||||
if not isinstance(fc, dict):
|
||||
bad(at)
|
||||
loc = fc.get("location")
|
||||
if not isinstance(loc, dict):
|
||||
bad(at + ".location")
|
||||
if not isinstance(loc.get("path"), str):
|
||||
bad(at + ".location.path")
|
||||
if not is_int(loc.get("line")):
|
||||
bad(at + ".location.line")
|
||||
if loc.get("end_line") is not None and not is_int(loc["end_line"]):
|
||||
bad(at + ".location.end_line")
|
||||
if "change" in fc and not isinstance(fc["change"], str):
|
||||
bad(at + ".change")
|
||||
dels = spec.get("deletes", [])
|
||||
if not isinstance(dels, list):
|
||||
bad("deletes")
|
||||
for k, d in enumerate(dels, 1):
|
||||
if not isinstance(d, dict) or not isinstance(d.get("path"), str):
|
||||
bad("deletes[%d].path" % k)
|
||||
return spec
|
||||
|
||||
|
||||
def mcp_view_once(spec_id):
|
||||
exe = os.environ.get("EUREKA_STATE_EXE")
|
||||
env = dict(os.environ)
|
||||
args = []
|
||||
try:
|
||||
with open(os.path.join(os.path.expanduser("~"), ".claude.json"), encoding="utf-8") as f:
|
||||
binding = json.load(f)["mcpServers"]["eureka-state"]
|
||||
exe = exe or binding.get("command")
|
||||
args = list(binding.get("args", []))
|
||||
for k, v in (binding.get("env") or {}).items():
|
||||
env.setdefault(k, v)
|
||||
except (OSError, ValueError, KeyError, TypeError):
|
||||
if not exe:
|
||||
raise Fail(4, "binding: eureka-state is not configured")
|
||||
if not exe:
|
||||
raise Fail(4, "binding: eureka-state has no command")
|
||||
try:
|
||||
proc = subprocess.Popen([exe, *args], stdin=subprocess.PIPE, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL, env=env)
|
||||
except OSError:
|
||||
raise Fail(4, "transport: Unavailable")
|
||||
lines = queue.Queue()
|
||||
|
||||
def pump():
|
||||
for ln in proc.stdout:
|
||||
lines.put(ln)
|
||||
lines.put(None)
|
||||
threading.Thread(target=pump, daemon=True).start()
|
||||
|
||||
def send(obj):
|
||||
proc.stdin.write((json.dumps(obj) + "\n").encode("utf-8"))
|
||||
proc.stdin.flush()
|
||||
|
||||
def reply(want):
|
||||
while True:
|
||||
try:
|
||||
ln = lines.get(timeout=30)
|
||||
except queue.Empty:
|
||||
raise Fail(4, "transport: Unavailable")
|
||||
if ln is None:
|
||||
raise Fail(4, "transport: Unavailable")
|
||||
try:
|
||||
msg = json.loads(ln)
|
||||
except ValueError:
|
||||
continue
|
||||
if isinstance(msg, dict) and msg.get("id") == want:
|
||||
return msg
|
||||
|
||||
try:
|
||||
send({"jsonrpc": "2.0", "id": 1, "method": "initialize",
|
||||
"params": {"protocolVersion": "2024-11-05", "capabilities": {},
|
||||
"clientInfo": {"name": "context-pack", "version": "1"}}})
|
||||
reply(1)
|
||||
send({"jsonrpc": "2.0", "method": "notifications/initialized"})
|
||||
send({"jsonrpc": "2.0", "id": 2, "method": "tools/call",
|
||||
"params": {"name": "view", "arguments": {"id": {"kind": "CutSpec", "id": spec_id}}}})
|
||||
res = reply(2).get("result") or {}
|
||||
except OSError:
|
||||
raise Fail(4, "transport: Unavailable")
|
||||
finally:
|
||||
try:
|
||||
proc.stdin.close()
|
||||
proc.terminate()
|
||||
except OSError:
|
||||
pass
|
||||
body = "".join(c.get("text", "") for c in res.get("content", []) if isinstance(c, dict))
|
||||
if res.get("isError"):
|
||||
code = next((c for c in ERROR_CODES if c in body), "error")
|
||||
raise Fail(4, "view: " + code)
|
||||
try:
|
||||
view = json.loads(body)
|
||||
except ValueError:
|
||||
raise Fail(4, "view: unparsable answer")
|
||||
if view is None:
|
||||
raise Fail(4, "view: no such cut_spec")
|
||||
return spec_from_json(view)
|
||||
|
||||
|
||||
def mcp_view(spec_id):
|
||||
if ":cut_spec:" not in spec_id:
|
||||
raise Fail(2, "spec id: not a cut_spec id")
|
||||
try:
|
||||
return mcp_view_once(spec_id)
|
||||
except Fail as first:
|
||||
if first.msg.split(": ")[-1] != "Unavailable":
|
||||
raise
|
||||
return mcp_view_once(spec_id)
|
||||
|
||||
|
||||
def load_spec(spec_id, spec_json):
|
||||
try:
|
||||
if spec_json:
|
||||
with open(spec_json, encoding="utf-8") as f:
|
||||
return spec_from_json(json.load(f))
|
||||
return mcp_view(spec_id)
|
||||
except Fail as e:
|
||||
if e.code == 4:
|
||||
raise Fail(4, e.msg + "; --spec-json reads a saved view")
|
||||
raise
|
||||
except (OSError, ValueError):
|
||||
raise Fail(4, "--spec-json: unreadable; --spec-json reads a saved view")
|
||||
|
||||
|
||||
# ---- carrying an anchor --------------------------------------------------
|
||||
|
||||
def carry(s, e, hunks):
|
||||
"""Map old lines s..e through -U0 hunks (a,b,c,d). Returns (ns, ne, ins, dele, touched)."""
|
||||
shift = net_in = ins = dele = 0
|
||||
touched = False
|
||||
lo = hi = None
|
||||
for a, b, c, d in hunks:
|
||||
if b == 0: # insertion after old line a
|
||||
if a < s:
|
||||
shift += d
|
||||
continue
|
||||
if a >= e: # at the last line or after: outside
|
||||
continue
|
||||
touched = True
|
||||
ins += d
|
||||
net_in += d
|
||||
else:
|
||||
last = a + b - 1
|
||||
if last < s:
|
||||
shift += d - b
|
||||
continue
|
||||
if a > e:
|
||||
continue
|
||||
touched = True
|
||||
ins += d
|
||||
dele += b
|
||||
net_in += d - b
|
||||
if a < s: # the first surviving line: c, or c+1 after a pure deletion
|
||||
lo = c if d > 0 else c + 1
|
||||
if last > e: # the last surviving line: c+d-1, or c after a pure deletion
|
||||
hi = c + d - 1 if d > 0 else c
|
||||
ns, ne = s + shift, e + shift + net_in
|
||||
if lo is not None:
|
||||
ns = min(ns, lo)
|
||||
if hi is not None:
|
||||
ne = max(ne, hi)
|
||||
return ns, max(ne, ns), ins, dele, touched
|
||||
|
||||
|
||||
class Tree:
|
||||
def __init__(self, wt, base):
|
||||
self.wt, self.base = wt, base
|
||||
self.changes = {}
|
||||
self._hunks, self._basefile, self._wtfile = {}, {}, {}
|
||||
|
||||
def load_changes(self):
|
||||
out = git(self.wt, "diff", "-M", "--name-status", "-z", "--no-color", "--no-ext-diff", self.base)
|
||||
if out is None:
|
||||
return False
|
||||
parts = out.decode("utf-8", "replace").split("\0")
|
||||
i = 0
|
||||
while i < len(parts) and parts[i]:
|
||||
code = parts[i]
|
||||
if code[0] in "RC" and i + 2 < len(parts):
|
||||
self.changes[parts[i + 1]] = (code[0], parts[i + 2])
|
||||
i += 3
|
||||
else:
|
||||
self.changes[parts[i + 1]] = (code[0], None)
|
||||
i += 2
|
||||
return True
|
||||
|
||||
def hunks(self, since, *paths):
|
||||
key = (since, paths)
|
||||
if key not in self._hunks:
|
||||
out = git(self.wt, "diff", "-M", "-U0", "--no-color", "--no-ext-diff", since, "--", *paths)
|
||||
hs = None
|
||||
if out is not None:
|
||||
hs = []
|
||||
for ln in out.decode("utf-8", "replace").split("\n"):
|
||||
m = HUNK.match(ln)
|
||||
if m:
|
||||
a, b, c, d = m.groups()
|
||||
hs.append((int(a), 1 if b is None else int(b), int(c), 1 if d is None else int(d)))
|
||||
self._hunks[key] = hs
|
||||
return self._hunks[key]
|
||||
|
||||
def base_lines(self, path):
|
||||
"""None when the path is absent at base."""
|
||||
if path not in self._basefile:
|
||||
raw = git(self.wt, "show", self.base + ":" + path)
|
||||
self._basefile[path] = None if raw is None else text_lines(raw)
|
||||
return self._basefile[path]
|
||||
|
||||
def wt_lines(self, path):
|
||||
if path not in self._wtfile:
|
||||
try:
|
||||
with open(os.path.join(self.wt, *path.split("/")), "rb") as f:
|
||||
self._wtfile[path] = text_lines(f.read())
|
||||
except OSError:
|
||||
self._wtfile[path] = None
|
||||
return self._wtfile[path]
|
||||
|
||||
|
||||
def safe_path(path):
|
||||
return bool(path) and not path.startswith(("/", "\\")) and ":" not in path \
|
||||
and ".." not in path.replace("\\", "/").split("/")
|
||||
|
||||
|
||||
def resolve(tree, k, fc, deleted_ok, base_ok, listed):
|
||||
loc = fc["location"]
|
||||
path, s = loc["path"], int(loc["line"])
|
||||
e = int(loc.get("end_line") or s)
|
||||
a = {"k": k, "path": path, "s": s, "e": e, "change": str(fc.get("change", "")),
|
||||
"rank": 1, "exit": 0, "status": "", "now": None, "window": None}
|
||||
if not base_ok:
|
||||
a.update(rank=0, exit=3, status="UNRESOLVED base not a commit here: git fetch origin " + tree.base)
|
||||
return a
|
||||
if not listed or not safe_path(path):
|
||||
a.update(rank=0, exit=3, status="UNRESOLVED path or git diff unusable")
|
||||
return a
|
||||
base = tree.base_lines(path)
|
||||
if base is None:
|
||||
sib = [p for p in tree.siblings() if p.rsplit("/", 1)[-1] == path.rsplit("/", 1)[-1]][:3]
|
||||
a.update(rank=0, status="NEW" + (" (at base: " + ", ".join(sib) + ")" if sib else ""))
|
||||
a["now"] = (path, s, e)
|
||||
if tree.wt_lines(path) is not None:
|
||||
a["window"] = ("wt", path, s, e)
|
||||
return a
|
||||
code, newpath = tree.changes.get(path, ("", None))
|
||||
if code == "D":
|
||||
ok = path in deleted_ok
|
||||
a.update(rank=0, exit=0 if ok else 3, status="GONE" + ("" if ok else " (not named by the spec's deletes)"))
|
||||
a["window"] = ("base", path, s, e)
|
||||
return a
|
||||
if s > len(base) or e > len(base):
|
||||
a.update(rank=0, exit=3, status="OUT_OF_RANGE (base file has %d lines)" % len(base))
|
||||
return a
|
||||
now = newpath or path
|
||||
hs = tree.hunks(tree.base, *dict.fromkeys([path, now]))
|
||||
if hs is None:
|
||||
a.update(rank=0, exit=3, status="UNRESOLVED git diff failed")
|
||||
return a
|
||||
ns, ne, ins, dele, touched = carry(s, e, hs)
|
||||
if touched:
|
||||
st = "TOUCHED(+%d/-%d inside)" % (ins, dele)
|
||||
elif ns != s:
|
||||
st = "MOVED(%+d)" % (ns - s)
|
||||
else:
|
||||
st = "SAME"
|
||||
if newpath:
|
||||
st = "RENAMED -> " + newpath + " " + st
|
||||
a.update(status=st, now=(now, ns, ne))
|
||||
a["window"] = ("wt", now, ns, ne)
|
||||
return a
|
||||
|
||||
|
||||
def table_line(a):
|
||||
line = "[%d] %s:%d-%d %s" % (a["k"], a["path"], a["s"], a["e"], a["status"])
|
||||
if a["now"]:
|
||||
line += " -> %s:%d-%d" % a["now"]
|
||||
return line + " # " + " ".join(a["change"].split())[:70]
|
||||
|
||||
|
||||
def window_body(tree, a):
|
||||
kind, path, s, e = a["window"]
|
||||
lines = tree.base_lines(path) if kind == "base" else tree.wt_lines(path)
|
||||
cap = "== [%d] %s:%d-%d%s %s" % (a["k"], path, s, e, " AT BASE" if kind == "base" else "", a["status"])
|
||||
if lines is None:
|
||||
return cap, ["(file not readable in the worktree)"]
|
||||
lo, hi = max(1, s - CONTEXT), min(len(lines), e + CONTEXT)
|
||||
body = ["%6d %s" % (n, lines[n - 1]) for n in range(lo, hi + 1)]
|
||||
return cap, body or ["(no lines: file has %d lines)" % len(lines)]
|
||||
|
||||
|
||||
def overlaps_since(tree, since, a):
|
||||
kind, path, s, e = a["window"]
|
||||
if kind != "wt":
|
||||
return False
|
||||
hs = tree.hunks(since, path)
|
||||
if hs is None:
|
||||
return False
|
||||
for _, _, c, d in hs:
|
||||
if d == 0:
|
||||
if s - 1 <= c <= e:
|
||||
return True
|
||||
elif c <= e and c + d - 1 >= s:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def main(argv):
|
||||
p = Parser(prog="context-pack.py")
|
||||
p.add_argument("spec_id")
|
||||
p.add_argument("worktree")
|
||||
p.add_argument("--since")
|
||||
p.add_argument("--only")
|
||||
p.add_argument("--spec-json")
|
||||
p.add_argument("--max-lines", type=int, default=DEFAULT_MAX_LINES)
|
||||
ns = p.parse_args(argv)
|
||||
if ns.max_lines < 1:
|
||||
p.error("")
|
||||
only = None
|
||||
if ns.only is not None:
|
||||
try:
|
||||
only = {int(x) for x in ns.only.split(",")}
|
||||
except ValueError:
|
||||
p.error("")
|
||||
wt = ns.worktree
|
||||
if git(wt, "rev-parse", "--git-dir") is None:
|
||||
raise Fail(2, "worktree: not a git worktree")
|
||||
spec = load_spec(ns.spec_id, ns.spec_json)
|
||||
fcs = spec["file_changes"]
|
||||
if only is not None and not only <= set(range(1, len(fcs) + 1)):
|
||||
raise Fail(2, "--only: index outside file_changes")
|
||||
base = str(spec["base"])
|
||||
resolved = HEX.match(base) and git(wt, "rev-parse", "--verify", "--quiet", base + "^{commit}")
|
||||
tree = Tree(wt, base)
|
||||
listed = bool(resolved) and tree.load_changes()
|
||||
tree.siblings = lambda: [x for x in (git(wt, "ls-tree", "-r", "--name-only", base) or b"")
|
||||
.decode("utf-8", "replace").split("\n") if x]
|
||||
if ns.since is not None and (ns.since.startswith("-") or
|
||||
git(wt, "rev-parse", "--verify", "--quiet", ns.since + "^{commit}") is None):
|
||||
raise Fail(2, "--since: not a commit in this worktree")
|
||||
deleted_ok = {d["path"] for d in spec.get("deletes", [])}
|
||||
anchors = [resolve(tree, i + 1, fc, deleted_ok, bool(resolved), listed) for i, fc in enumerate(fcs)]
|
||||
|
||||
head = (git(wt, "rev-parse", "--short", "HEAD") or b"none").decode().strip()
|
||||
out = ["context-pack %s base %s HEAD %s modified %d" % (
|
||||
ns.spec_id, base[:12] if HEX.match(base) else "unresolvable", head, len(tree.changes))]
|
||||
out += [table_line(a) for a in sorted(anchors, key=lambda a: a["rank"])]
|
||||
out.append("")
|
||||
want = [a for a in anchors if a["window"] and (only is None or a["k"] in only)
|
||||
and (ns.since is None or overlaps_since(tree, ns.since, a))]
|
||||
table_len = len(fcs)
|
||||
skipped = []
|
||||
left = ns.max_lines - len(out)
|
||||
for idx, a in enumerate(want):
|
||||
cap, body = window_body(tree, a)
|
||||
full = len(body)
|
||||
if only is None and full > WINDOW_CAP:
|
||||
body = body[:WINDOW_CAP]
|
||||
reserve = 1 if idx < len(want) - 1 or skipped else 0 # room for the not-expanded line
|
||||
need = 1 + len(body) + (1 if len(body) < full else 0)
|
||||
if only is not None and need > left - reserve and left - reserve >= 4:
|
||||
body = body[:left - reserve - 2]
|
||||
need = 1 + len(body) + 1
|
||||
if need > left - reserve:
|
||||
skipped.append(a["k"])
|
||||
continue
|
||||
out.append(cap)
|
||||
out += body
|
||||
if len(body) < full:
|
||||
out.append("%d more lines: --only %d --max-lines %d" % (full - len(body), a["k"], table_len + full + 3))
|
||||
left -= need
|
||||
if skipped:
|
||||
out.append("not expanded: --only " + ",".join(map(str, skipped)))
|
||||
sys.stdout.reconfigure(encoding="utf-8", errors="replace", newline="\n")
|
||||
sys.stdout.write("\n".join(out) + "\n")
|
||||
return max([a["exit"] for a in anchors] or [0])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
sys.exit(main(sys.argv[1:]))
|
||||
except Fail as f:
|
||||
sys.stderr.write("context-pack: %s\n" % f.msg)
|
||||
sys.exit(f.code)
|
||||
42
tools/fixtures/context-pack/fake-eureka-state.py
Normal file
42
tools/fixtures/context-pack/fake-eureka-state.py
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
#!/usr/bin/env python3
|
||||
"""A stand-in for eureka-state's stdio MCP: answers initialize and tools/call view.
|
||||
FAKE_SPEC: the view result to return. FAKE_MODE=unavailable answers isError
|
||||
Unavailable (naming HUGINN_ENDPOINT, as the real daemon's text might). FAKE_COUNT:
|
||||
a file that gains one line per view call. FAKE_MODE=needenv answers the spec only when
|
||||
EUREKA_INSTANCE is "t" and HUGINN_ENDPOINT equals FAKE_NEED, else isError Misconfigured."""
|
||||
import json, os, sys
|
||||
|
||||
inited = False
|
||||
for ln in sys.stdin:
|
||||
msg = json.loads(ln)
|
||||
if "id" not in msg:
|
||||
inited = inited or msg.get("method") == "notifications/initialized"
|
||||
continue
|
||||
if msg["method"] == "initialize":
|
||||
p = msg.get("params", {})
|
||||
if msg["id"] != 1 or not p.get("protocolVersion") or not p.get("clientInfo", {}).get("name") or "capabilities" not in p:
|
||||
sys.exit(1)
|
||||
res = {"protocolVersion": "2024-11-05", "capabilities": {}, "serverInfo": {"name": "fake", "version": "0"}}
|
||||
else:
|
||||
p = msg.get("params", {})
|
||||
if (not inited or msg["method"] != "tools/call" or p.get("name") != "view" or msg["id"] != 2
|
||||
or p.get("arguments") != {"id": {"kind": "CutSpec", "id": os.environ["FAKE_WANT"]}}):
|
||||
sys.exit(1)
|
||||
with open(os.environ["FAKE_COUNT"], "a") as f:
|
||||
f.write("view\n")
|
||||
if os.environ.get("FAKE_MODE") == "null":
|
||||
res = {"content": [{"type": "text", "text": "null"}]}
|
||||
elif os.environ.get("FAKE_MODE") == "garbage":
|
||||
res = {"content": [{"type": "text", "text": "not json"}]}
|
||||
elif os.environ.get("FAKE_MODE") == "invalid":
|
||||
res = {"isError": True, "content": [{"type": "text", "text": os.environ.get("FAKE_CODE", "InvalidInput") + ": " + os.environ.get("HUGINN_ENDPOINT", "")}]}
|
||||
elif os.environ.get("FAKE_MODE") == "needenv" and not (
|
||||
os.environ.get("EUREKA_INSTANCE") == "t" and os.environ.get("HUGINN_ENDPOINT") == os.environ["FAKE_NEED"]):
|
||||
res = {"isError": True, "content": [{"type": "text", "text": "Misconfigured"}]}
|
||||
elif os.environ.get("FAKE_MODE") == "unavailable":
|
||||
res = {"isError": True, "content": [{"type": "text",
|
||||
"text": "Unavailable: " + os.environ.get("HUGINN_ENDPOINT", "")}]}
|
||||
else:
|
||||
res = {"content": [{"type": "text", "text": open(os.environ["FAKE_SPEC"], encoding="utf-8").read()}]}
|
||||
sys.stdout.write(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": res}) + "\n")
|
||||
sys.stdout.flush()
|
||||
30
tools/fixtures/context-pack/spec.json
Normal file
30
tools/fixtures/context-pack/spec.json
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
{"id": {"id": "t:cut_spec:cut-x.r1", "kind": "CutSpec"},
|
||||
"status": "InForce",
|
||||
"document": {"kind": "cut_spec", "value": {
|
||||
"base": "BASE", "deletes": [],
|
||||
"file_changes": [
|
||||
{"change": "moved below the insertion", "location": {"path": "a.txt", "line": 20, "end_line": 22}},
|
||||
{"change": "insertion after the last line is outside", "location": {"path": "a.txt", "line": 3, "end_line": 5}},
|
||||
{"change": "insertion just before the range", "location": {"path": "a.txt", "line": 6, "end_line": 7}},
|
||||
{"change": "insertion inside the range", "location": {"path": "a.txt", "line": 4, "end_line": 8}},
|
||||
{"change": "delete above and edit inside", "location": {"path": "b.txt", "line": 8, "end_line": 12}},
|
||||
{"change": "moved up by a deletion", "location": {"path": "b.txt", "line": 30, "end_line": 31}},
|
||||
{"change": "deleted file", "location": {"path": "c.txt", "line": 2, "end_line": 3}},
|
||||
{"change": "renamed file", "location": {"path": "d.txt", "line": 4, "end_line": 5}},
|
||||
{"change": "path absent at base", "location": {"path": "sub/a.txt", "line": 1, "end_line": 2}},
|
||||
{"change": "range past EOF", "location": {"path": "e.txt", "line": 5, "end_line": 50}},
|
||||
{"change": "uncommitted edit", "location": {"path": "f.txt", "line": 5, "end_line": 6}},
|
||||
{"change": "last line is in range", "location": {"path": "e.txt", "line": 10, "end_line": 10}},
|
||||
{"change": "one past the last line", "location": {"path": "e.txt", "line": 10, "end_line": 11}},
|
||||
{"change": "CRLF file", "location": {"path": "h.txt", "line": 2, "end_line": 3}},
|
||||
{"change": "straddles the head of a replaced block", "location": {"path": "m.txt", "line": 12, "end_line": 13}},
|
||||
{"change": "replaced block starts inside", "location": {"path": "m.txt", "line": 8, "end_line": 11}},
|
||||
{"change": "just after a replaced block", "location": {"path": "m.txt", "line": 15, "end_line": 16}},
|
||||
{"change": "deletion follows the range", "location": {"path": "m.txt", "line": 28, "end_line": 28}},
|
||||
{"change": "deletion at the next line", "location": {"path": "m.txt", "line": 29, "end_line": 29}},
|
||||
{"change": "deletion ends just before", "location": {"path": "m.txt", "line": 32, "end_line": 32}},
|
||||
{"change": "after the deletion", "location": {"path": "m.txt", "line": 33, "end_line": 33}},
|
||||
{"change": "insertion at the first line", "location": {"path": "a.txt", "line": 5, "end_line": 7}},
|
||||
{"change": "deletion ends just before the range", "location": {"path": "b.txt", "line": 4, "end_line": 5}},
|
||||
{"change": "untracked file the cut creates", "location": {"path": "n.txt", "line": 1, "end_line": 2}}
|
||||
]}}}
|
||||
|
|
@ -225,4 +225,292 @@ r=$(WM=2 wo log "$T/l.log" '(unclosed'); wchk "invalid regex" 3 "$r"
|
|||
grep -q 'not a valid extended regex' "$T/wo.out" || { echo "FAIL wait-on: no regex message"; fail=1; }
|
||||
r=$(wo bogus); wchk "bad usage" 2 "$r"
|
||||
wait
|
||||
|
||||
# context-pack.py: a spec's anchors carried from its base to the worktree (no network)
|
||||
CP=$here/context-pack.py; CF=$here/fixtures/context-pack
|
||||
if py -3 -c pass >/dev/null 2>&1; then PY="py -3"; elif python3 -c pass >/dev/null 2>&1; then PY=python3; else PY=python; fi
|
||||
wp() { cygpath -m "$1" 2>/dev/null || echo "$1"; }
|
||||
R=$T/cp; mkdir -p "$R"; Rw=$(wp "$R")
|
||||
g() { git -C "$R" -c user.name=t -c user.email=t@t -c core.autocrlf=false "$@" >/dev/null 2>&1; }
|
||||
seqf() { local i; for i in $(seq 1 "$2"); do echo "$1$i"; done; }
|
||||
g init -q
|
||||
seqf a 40 >"$R/a.txt"; seqf b 40 >"$R/b.txt"; seqf c 10 >"$R/c.txt"; seqf d 20 >"$R/d.txt"
|
||||
seqf e 10 >"$R/e.txt"; seqf f 20 >"$R/f.txt"; seqf g 300 >"$R/big.txt"; seqf h 5 | sed 's/$/
/' >"$R/h.txt"; seqf m 40 >"$R/m.txt"
|
||||
g add -A; g commit -q -m base; BASE=$(git -C "$R" rev-parse HEAD)
|
||||
{ sed -n 1,5p "$R/a.txt"; printf 'ins1\nins2\nins3\n'; sed -n '6,$p' "$R/a.txt"; } >"$R/a.new"; mv "$R/a.new" "$R/a.txt"
|
||||
sed -e '2,3d' -e 's/^b10$/b10-EDITED/' "$R/b.txt" >"$R/b.new"; mv "$R/b.new" "$R/b.txt"
|
||||
sed -e '10,14c\X1\nX2' -e '30,31d' "$R/m.txt" >"$R/m.new"; mv "$R/m.new" "$R/m.txt"
|
||||
g rm -q c.txt; g mv d.txt d2.txt; g add -A; g commit -q -m second
|
||||
sed -i 's/^f5$/f5-UNCOMMITTED/' "$R/f.txt"; printf 'n1\nn2\nn3\n' >"$R/n.txt"
|
||||
sed "s/\"BASE\"/\"$BASE\"/" "$CF/spec.json" >"$T/cp-spec.json"; SP=$(wp "$T/cp-spec.json")
|
||||
cpo() { $PY "$CP" t:cut_spec:cut-x.r1 "$Rw" "$@" >"$T/cp.out" 2>"$T/cp.err"; echo $?; }
|
||||
cchk() { if [ "$3" = "$2" ]; then echo "ok context-pack $1: exit $3"; else echo "FAIL context-pack $1: exit $3, want $2"; fail=1; fi; }
|
||||
cin() { grep -qF -- "$2" "$T/cp.out" && echo "ok context-pack $1" || { echo "FAIL context-pack $1: missing: $2"; fail=1; }; }
|
||||
cnot() { if grep -qF -- "$2" "$T/cp.out" "$T/cp.err"; then echo "FAIL context-pack $1: must not contain: $2"; fail=1; else echo "ok context-pack $1"; fi; }
|
||||
r=$(cpo --spec-json "$SP"); cchk "fixture (GONE and OUT_OF_RANGE)" 3 "$r"
|
||||
cin "header" "context-pack t:cut_spec:cut-x.r1 base ${BASE:0:12} HEAD"
|
||||
cin "header counts modified files" " modified 6"
|
||||
cin "MOVED(+3)" "[1] a.txt:20-22 MOVED(+3) -> a.txt:23-25"
|
||||
cin "insertion at the last line is outside" "[2] a.txt:3-5 SAME -> a.txt:3-5"
|
||||
cin "insertion before the range shifts it" "[3] a.txt:6-7 MOVED(+3) -> a.txt:9-10"
|
||||
cin "insertion inside widens" "[4] a.txt:4-8 TOUCHED(+3/-0 inside) -> a.txt:4-11"
|
||||
cin "delete above, edit inside" "[5] b.txt:8-12 TOUCHED(+1/-1 inside) -> b.txt:6-10"
|
||||
cin "MOVED(-2)" "[6] b.txt:30-31 MOVED(-2) -> b.txt:28-29"
|
||||
cin "GONE" "[7] c.txt:2-3 GONE (not named by the spec's deletes)"
|
||||
cin "RENAMED" "[8] d.txt:4-5 RENAMED -> d2.txt SAME -> d2.txt:4-5"
|
||||
cin "NEW with basename candidates" "[9] sub/a.txt:1-2 NEW (at base: a.txt)"
|
||||
cin "OUT_OF_RANGE" "[10] e.txt:5-50 OUT_OF_RANGE (base file has 10 lines)"
|
||||
cin "uncommitted edit is TOUCHED" "[11] f.txt:5-6 TOUCHED(+1/-1 inside)"
|
||||
cin "last line of the file is in range" "[12] e.txt:10-10 SAME"
|
||||
cin "one past the last line is out of range" "[13] e.txt:10-11 OUT_OF_RANGE (base file has 10 lines)"
|
||||
cin "CRLF file reads SAME" "[14] h.txt:2-3 SAME"
|
||||
cin "hunk straddling the head clamps to the new block" "[15] m.txt:12-13 TOUCHED(+2/-5 inside) -> m.txt:10-11"
|
||||
cin "hunk starting inside and ending past the range" "[16] m.txt:8-11 TOUCHED(+2/-5 inside) -> m.txt:8-11"
|
||||
cin "range just after a replaced block shifts" "[17] m.txt:15-16 MOVED(-3) -> m.txt:12-13"
|
||||
cin "deletion after the range moves nothing else" "[18] m.txt:28-28 MOVED(-3) -> m.txt:25-25"
|
||||
cin "deletion at the next line" "[19] m.txt:29-29 MOVED(-3) -> m.txt:26-26"
|
||||
cin "deletion ending just before the range" "[20] m.txt:32-32 MOVED(-5) -> m.txt:27-27"
|
||||
cin "range after both hunks" "[21] m.txt:33-33 MOVED(-5) -> m.txt:28-28"
|
||||
cin "insertion at the first line is inside" "[22] a.txt:5-7 TOUCHED(+3/-0 inside) -> a.txt:5-10"
|
||||
cin "hunk ending just before the range shifts it" "[23] b.txt:4-5 MOVED(-2) -> b.txt:2-3"
|
||||
cin "NEW file present in the worktree gets a window" "== [24] n.txt:1-2"
|
||||
cin "NEW window shows the file" " 2 n2"
|
||||
cin "CRLF window text" " 2 h2"
|
||||
{ tr -d '
' <"$T/cp.out" | cmp -s - "$T/cp.out"; } && echo "ok context-pack CRLF is stripped from windows" || { echo "FAIL context-pack CR in output"; fail=1; }
|
||||
cin "window numbers are worktree lines" " 6 b8"
|
||||
cin "window shows the edit" "b10-EDITED"
|
||||
cin "GONE window is labelled AT BASE" "== [7] c.txt:2-3 AT BASE"
|
||||
cin "GONE window shows base text" " 2 c2"
|
||||
cin "renamed window reads the new path" "== [8] d2.txt:4-5"
|
||||
[ "$(grep -c '^\[' "$T/cp.out")" = 24 ] && echo "ok context-pack table lists every anchor" || { echo "FAIL context-pack table count"; fail=1; }
|
||||
{ sed -n 2p "$T/cp.out" | grep -q '^\[7\]' && sed -n 6p "$T/cp.out" | grep -q '^\[24\]' && sed -n 7p "$T/cp.out" | grep -q '^\[1\]'; } && echo "ok context-pack problem anchors first" || { echo "FAIL context-pack problem anchors first"; fail=1; }
|
||||
# deletes naming the file, and a valid range, make the same spec exit 0
|
||||
sed -e 's/"deletes": \[\]/"deletes": [{"path": "c.txt"}]/' -e 's/"end_line": 50/"end_line": 6/' -e 's/"end_line": 11/"end_line": 10/' "$T/cp-spec.json" >"$T/cp-spec0.json"
|
||||
r=$(cpo --spec-json "$(wp "$T/cp-spec0.json")"); cchk "GONE named by deletes, no OUT_OF_RANGE" 0 "$r"
|
||||
cin "GONE in deletes has no qualifier" "[7] c.txt:2-3 GONE #"
|
||||
# unreachable base
|
||||
sed "s/\"$BASE\"/\"deadbeef\"/" "$T/cp-spec.json" >"$T/cp-specb.json"
|
||||
r=$(cpo --spec-json "$(wp "$T/cp-specb.json")"); cchk "unreachable base" 3 "$r"
|
||||
cin "UNRESOLVED hint" "UNRESOLVED base not a commit here: git fetch origin deadbeef"
|
||||
[ "$(grep -c 'UNRESOLVED' "$T/cp.out")" = 24 ] && echo "ok context-pack every anchor UNRESOLVED" || { echo "FAIL context-pack UNRESOLVED count"; fail=1; }
|
||||
cnot "UNRESOLVED prints no window" "== ["
|
||||
# --since: every anchor stays in the table; only the edited one has a window
|
||||
r=$(cpo --spec-json "$SP" --since HEAD); cchk "--since" 3 "$r"
|
||||
cin "--since keeps the table" "[1] a.txt:20-22 MOVED(+3)"
|
||||
cin "--since prints the edited window" "== [11] f.txt:5-6"
|
||||
cin "--since window shows the edit" "f5-UNCOMMITTED"
|
||||
cnot "--since omits untouched windows" "== [1] "
|
||||
cnot "--since omits the GONE window" "== [7] "
|
||||
r=$(cpo --spec-json "$SP" --since "$BASE"); cin "--since an older commit widens" "== [4] a.txt:4-11"
|
||||
cnot "--since an older commit still omits untouched windows" "== [2] "
|
||||
r=$(cpo --spec-json "$SP" --since "$BASE" --only 15,16,17,18,19,20,21)
|
||||
for k in 15 16 19 20; do cin "--since window $k overlaps a hunk" "== [$k] "; done
|
||||
for k in 17 18 21; do cnot "--since window $k does not overlap" "== [$k] "; done
|
||||
r=$(cpo --spec-json "$SP" --only 2,11); cin "--only prints the named window" "== [2] "
|
||||
cnot "--only omits the others" "== [1] "
|
||||
r=$(cpo --spec-json "$SP" --only 99); cchk "--only outside file_changes" 2 "$r"; grep -qF "context-pack: --only: index outside file_changes" "$T/cp.err" && echo "ok context-pack names --only" || { echo "FAIL context-pack names --only"; fail=1; }
|
||||
r=$(cpo --spec-json "$SP" --since feedface); cchk "--since not a commit" 2 "$r"; grep -qF "context-pack: --since: not a commit in this worktree" "$T/cp.err" && echo "ok context-pack names --since" || { echo "FAIL context-pack names --since"; fail=1; }
|
||||
Z70=$(printf 'z%.0s' $(seq 1 70))
|
||||
echo '{"base": "'$BASE'", "deletes": [], "file_changes": [
|
||||
{"change": "up", "location": {"path": "../a.txt", "line": 1, "end_line": 1}},
|
||||
{"change": "abs", "location": {"path": "/a.txt", "line": 1, "end_line": 1}},
|
||||
{"change": "drive", "location": {"path": "c:a.txt", "line": 1, "end_line": 1}},
|
||||
{"change": "'$Z70'zzzz", "location": {"path": "a.txt", "line": 1, "end_line": 1}}]}' >"$T/cp-specu.json"
|
||||
r=$(cpo --spec-json "$(wp "$T/cp-specu.json")"); cchk "unsafe paths" 3 "$r"
|
||||
[ "$(grep -c 'UNRESOLVED path or git diff unusable' "$T/cp.out")" = 3 ] && echo "ok context-pack unsafe paths are UNRESOLVED" || { echo "FAIL context-pack unsafe paths"; fail=1; }
|
||||
cin "a long change is cut at 70 characters" "# $Z70"
|
||||
cnot "a long change is cut at 70 characters (not 71)" "${Z70}z"
|
||||
# caps
|
||||
{ echo '{"base": "'$BASE'", "deletes": [], "file_changes": ['
|
||||
for i in $(seq 1 30); do echo '{"change": "c'$i'", "location": {"path": "big.txt", "line": '$((i*5))', "end_line": '$((i*5+9))'}}'$([ $i -lt 30 ] && echo ,); done
|
||||
echo ']}'; } >"$T/cp-spec30.json"
|
||||
r=$(cpo --spec-json "$(wp "$T/cp-spec30.json")" --max-lines 200); cchk "30 anchors" 0 "$r"
|
||||
[ "$(wc -l <"$T/cp.out")" -le 200 ] && echo "ok context-pack output within --max-lines" || { echo "FAIL context-pack over 200 lines"; fail=1; }
|
||||
[ "$(grep -c '^\[' "$T/cp.out")" = 30 ] && echo "ok context-pack capped output keeps the full table" || { echo "FAIL context-pack capped table"; fail=1; }
|
||||
[ "$(grep -c '^== \[' "$T/cp.out")" = 9 ] && echo "ok context-pack expands 9 windows of 17 lines" || { echo "FAIL context-pack window count: $(grep -c '^== \[' "$T/cp.out")"; fail=1; }
|
||||
cin "rest listed as not expanded" "not expanded: --only 10,11,12"
|
||||
echo '{"base": "'$BASE'", "deletes": [], "file_changes": [{"change": "long", "location": {"path": "big.txt", "line": 20, "end_line": 169}}]}' >"$T/cp-spec1.json"
|
||||
S1=$(wp "$T/cp-spec1.json")
|
||||
r=$(cpo --spec-json "$S1"); cchk "150-line range" 0 "$r"
|
||||
cin "window cut at 100 lines" "56 more lines: --only 1 --max-lines 160"
|
||||
[ "$(grep -c '^ *[0-9]* g' "$T/cp.out")" = 100 ] && echo "ok context-pack long window prints 100 lines" || { echo "FAIL context-pack long window"; fail=1; }
|
||||
r=$(cpo --spec-json "$S1" --only 1 --max-lines 160); cchk "--only with the suggested --max-lines" 0 "$r"
|
||||
cnot "full window needs no more-lines note" "more lines"
|
||||
[ "$(wc -l <"$T/cp.out" | tr -d ' ')" = 160 ] && echo "ok context-pack suggested --max-lines is exact" || { echo "FAIL context-pack suggested max: $(wc -l <"$T/cp.out")"; fail=1; }
|
||||
# usage and unreadable spec; no error echoes an input value
|
||||
r=$(cpo --spec-json "$SP" --max-lines CANARY-usage); cchk "bad --max-lines" 2 "$r"; cnot "usage error echoes no value" "CANARY-usage"
|
||||
r=$(cpo --spec-json "$T/CANARY-nofile.json"); cchk "unreadable --spec-json" 4 "$r"; grep -qF "context-pack: --spec-json: unreadable; --spec-json reads a saved view" "$T/cp.err" && echo "ok context-pack names the unreadable --spec-json" || { echo "FAIL context-pack names the unreadable --spec-json"; fail=1; }; cnot "unreadable --spec-json echoes no path" "CANARY-nofile"
|
||||
grep -qF -- "--spec-json" "$T/cp.err" && echo "ok context-pack names --spec-json" || { echo "FAIL context-pack error does not name --spec-json"; fail=1; }
|
||||
r=$($PY "$CP" CANARY-id "$Rw" >"$T/cp.out" 2>"$T/cp.err"; echo $?); cchk "spec id that is no cut_spec id" 2 "$r"; grep -qF "context-pack: spec id: not a cut_spec id" "$T/cp.err" && echo "ok context-pack names the spec id field" || { echo "FAIL context-pack names the spec id field"; fail=1; }; cnot "bad id echoes no value" "CANARY-id"
|
||||
r=$($PY "$CP" t:cut_spec:cut-x.r1 "$T/CANARY-notrepo" --spec-json "$SP" >"$T/cp.out" 2>"$T/cp.err"; echo $?); cchk "worktree is not a repo" 2 "$r"; grep -qF "context-pack: worktree: not a git worktree" "$T/cp.err" && echo "ok context-pack names the worktree field" || { echo "FAIL context-pack names the worktree field"; fail=1; }; cnot "worktree error echoes no path" "CANARY-notrepo"
|
||||
# the spec by id through the stdio MCP binding (a fake eureka-state), retried once on Unavailable
|
||||
H=$T/cphome; mkdir -p "$H"; FK=$(wp "$CF/fake-eureka-state.py")
|
||||
$PY - "$(wp "$H")/.claude.json" "$($PY -c 'import sys;print(sys.executable)')" "$FK" <<'PYE'
|
||||
import json, sys
|
||||
json.dump({"mcpServers": {"eureka-state": {"type": "stdio", "command": sys.argv[2], "args": [sys.argv[3]],
|
||||
"env": {"EUREKA_INSTANCE": "t", "HUGINN_ENDPOINT": "rudp://binding"}}}}, open(sys.argv[1], "w"))
|
||||
PYE
|
||||
Hw=$(wp "$H")
|
||||
mcp() { HOME="$Hw" USERPROFILE="$Hw" FAKE_WANT=t:cut_spec:cut-x.r1 FAKE_SPEC="$SP" FAKE_COUNT="$(wp "$T/cp.count")" "$@"; }
|
||||
$PY "$CP" t:cut_spec:cut-x.r1 "$Rw" --spec-json "$SP" >"$T/cp.want" 2>/dev/null
|
||||
: >"$T/cp.count"
|
||||
mcp $PY "$CP" t:cut_spec:cut-x.r1 "$Rw" >"$T/cp.out" 2>"$T/cp.err"; r=$?
|
||||
cchk "spec by id through the binding" 3 "$r"
|
||||
cmp -s "$T/cp.out" "$T/cp.want" && echo "ok context-pack by id equals --spec-json" || { echo "FAIL context-pack by id differs from --spec-json"; fail=1; }
|
||||
: >"$T/cp.count"
|
||||
mcp env FAKE_MODE=unavailable HUGINN_ENDPOINT=rudp://CANARY-endpoint $PY "$CP" t:cut_spec:cut-x.r1 "$Rw" >"$T/cp.out" 2>"$T/cp.err"; r=$?
|
||||
cchk "Unavailable twice" 4 "$r"; cnot "Unavailable echoes no endpoint" "CANARY-endpoint"
|
||||
[ "$(wc -l <"$T/cp.count" | tr -d ' ')" = 2 ] && echo "ok context-pack retried Unavailable exactly once" || { echo "FAIL context-pack view attempts: $(wc -l <"$T/cp.count")"; fail=1; }
|
||||
grep -qF "context-pack: view: Unavailable; --spec-json reads a saved view" "$T/cp.err" && echo "ok context-pack Unavailable names --spec-json" || { echo "FAIL context-pack Unavailable hint"; fail=1; }
|
||||
mcp env EUREKA_STATE_EXE="$T/CANARY-exe" $PY "$CP" t:cut_spec:cut-x.r1 "$Rw" >"$T/cp.out" 2>"$T/cp.err"; r=$?
|
||||
cchk "unspawnable EUREKA_STATE_EXE" 4 "$r"; grep -qF "context-pack: transport: Unavailable; --spec-json reads a saved view" "$T/cp.err" && echo "ok context-pack names the transport failure" || { echo "FAIL context-pack names the transport failure"; fail=1; }; cnot "spawn error echoes no path" "CANARY-exe"
|
||||
: >"$T/cp.count"
|
||||
mcp env FAKE_MODE=invalid HUGINN_ENDPOINT=rudp://CANARY-endpoint2 $PY "$CP" t:cut_spec:cut-x.r1 "$Rw" >"$T/cp.out" 2>"$T/cp.err"; r=$?
|
||||
cchk "a refusal that is not Unavailable" 4 "$r"; [ "$(wc -l <"$T/cp.count" | tr -d " ")" = 1 ] && echo "ok context-pack does not retry a refusal" || { echo "FAIL context-pack retried a refusal"; fail=1; }; cnot "refusal echoes no endpoint" "CANARY-endpoint2"
|
||||
grep -qF "context-pack: view: InvalidInput; --spec-json reads a saved view" "$T/cp.err" && echo "ok context-pack names the refusal code" || { echo "FAIL context-pack refusal code"; fail=1; }
|
||||
for code in Rejected Misconfigured TooLarge Unencodable Internal; do
|
||||
mcp env FAKE_MODE=invalid FAKE_CODE=$code $PY "$CP" t:cut_spec:cut-x.r1 "$Rw" >"$T/cp.out" 2>"$T/cp.err"; r=$?
|
||||
grep -qF "context-pack: view: $code; --spec-json reads a saved view" "$T/cp.err" && [ $r = 4 ] && echo "ok context-pack names the code $code" || { echo "FAIL context-pack code $code"; fail=1; }
|
||||
done
|
||||
mcp env FAKE_MODE=garbage $PY "$CP" t:cut_spec:cut-x.r1 "$Rw" >"$T/cp.out" 2>"$T/cp.err"; r=$?
|
||||
cchk "unparsable answer" 4 "$r"; grep -qF "context-pack: view: unparsable answer; --spec-json reads a saved view" "$T/cp.err" && echo "ok context-pack names the unparsable answer" || { echo "FAIL context-pack unparsable message"; fail=1; }
|
||||
mcp env FAKE_MODE=null $PY "$CP" t:cut_spec:cut-x.r1 "$Rw" >"$T/cp.out" 2>"$T/cp.err"; r=$?
|
||||
cchk "no such cut_spec" 4 "$r"; grep -qF "context-pack: view: no such cut_spec; --spec-json reads a saved view" "$T/cp.err" && echo "ok context-pack names the missing spec" || { echo "FAIL context-pack missing spec message"; fail=1; }
|
||||
mcp env HOME="$(wp "$T")/nohome" USERPROFILE="$(wp "$T")/nohome" $PY "$CP" t:cut_spec:cut-x.r1 "$Rw" >"$T/cp.out" 2>"$T/cp.err"; r=$?
|
||||
cchk "no binding and no EUREKA_STATE_EXE" 4 "$r"; grep -qF "context-pack: binding: eureka-state is not configured; --spec-json reads a saved view" "$T/cp.err" && echo "ok context-pack names the missing binding" || { echo "FAIL context-pack binding message"; fail=1; }
|
||||
echo '{}' >"$T/cp-empty.json"; r=$(cpo --spec-json "$(wp "$T/cp-empty.json")"); cchk "spec-json that is no cut_spec" 4 "$r"
|
||||
grep -qF "context-pack: spec: not a cut_spec value; --spec-json reads a saved view" "$T/cp.err" && echo "ok context-pack names the spec shape" || { echo "FAIL context-pack spec shape message"; fail=1; }
|
||||
grep -qF "usage: " "$T/cp.err" || echo "ok context-pack spec error prints no usage text"
|
||||
r=$(cpo --max-lines 0); cchk "usage: no spec source" 2 "$r"
|
||||
grep -qF "context-pack: usage error" "$T/cp.err" && echo "ok context-pack usage error text" || { echo "FAIL context-pack usage text"; fail=1; }
|
||||
# glob characters in a spec path name that file only; straddling hunks; --since boundaries
|
||||
R2=$T/cp2; mkdir -p "$R2/src/[a]" "$R2/src/a"; R2w=$(wp "$R2")
|
||||
g2() { git -C "$R2" -c user.name=t -c user.email=t@t -c core.autocrlf=false "$@" >/dev/null 2>&1; }
|
||||
g2 init -q
|
||||
seqf x 30 >"$R2/src/[a]/f.txt"; seqf x 30 >"$R2/src/a/f.txt"
|
||||
for n in s t u v; do seqf x 30 >"$R2/$n.txt"; done; seqf w 12 >"$R2/w.txt"; seqf g 200 >"$R2/big.txt"; echo q1 >"$R2/one.txt"
|
||||
g2 add -A; g2 commit -q -m base; B2=$(git -C "$R2" rev-parse HEAD)
|
||||
{ printf 'new1\nnew2\n'; cat "$R2/src/a/f.txt"; } >"$R2/src/a/f.new"; mv "$R2/src/a/f.new" "$R2/src/a/f.txt"
|
||||
sed -i 8,12d "$R2/s.txt"; sed -i 15,25d "$R2/t.txt"
|
||||
sed -i '8,12c\NEW' "$R2/u.txt"; sed -i '15,25c\NEW' "$R2/v.txt"
|
||||
g2 add -A; g2 commit -q -m second; S2=$(git -C "$R2" rev-parse HEAD)
|
||||
sed -i 's/^w5$/w5-EDITED/' "$R2/w.txt"
|
||||
echo '{"base": "'$B2'", "deletes": [], "file_changes": [
|
||||
{"change": "glob path", "location": {"path": "src/[a]/f.txt", "line": 10, "end_line": 12}},
|
||||
{"change": "pure deletion above the start", "location": {"path": "s.txt", "line": 10, "end_line": 20}},
|
||||
{"change": "pure deletion past the end", "location": {"path": "t.txt", "line": 10, "end_line": 20}},
|
||||
{"change": "one-line replacement above the start", "location": {"path": "u.txt", "line": 10, "end_line": 20}},
|
||||
{"change": "one-line replacement past the end", "location": {"path": "v.txt", "line": 10, "end_line": 20}},
|
||||
{"change": "edit on the last line", "location": {"path": "w.txt", "line": 3, "end_line": 5}},
|
||||
{"change": "edit on the first line", "location": {"path": "w.txt", "line": 5, "end_line": 7}},
|
||||
{"change": "after the edit", "location": {"path": "w.txt", "line": 6, "end_line": 8}},
|
||||
{"change": "before the edit", "location": {"path": "w.txt", "line": 1, "end_line": 4}}]}' >"$T/cp-spec2.json"
|
||||
SP2=$(wp "$T/cp-spec2.json")
|
||||
cp2() { $PY "$CP" t:cut_spec:cut-x.r1 "$R2w" "$@" >"$T/cp.out" 2>"$T/cp.err"; echo $?; }
|
||||
r=$(cp2 --spec-json "$SP2"); cchk "straddle fixture" 0 "$r"
|
||||
cin "a glob path reads its own file: SAME" "[1] src/[a]/f.txt:10-12 SAME -> src/[a]/f.txt:10-12"
|
||||
cin "pure deletion starting above the anchor starts at the first survivor" "[2] s.txt:10-20 TOUCHED(+0/-5 inside) -> s.txt:8-15"
|
||||
cin "pure deletion ending past the anchor ends at the last survivor" "[3] t.txt:10-20 TOUCHED(+0/-11 inside) -> t.txt:10-14"
|
||||
cin "one-line replacement above the start begins at the replacement" "[4] u.txt:10-20 TOUCHED(+1/-5 inside) -> u.txt:8-16"
|
||||
cin "one-line replacement past the end ends at the replacement" "[5] v.txt:10-20 TOUCHED(+1/-11 inside) -> v.txt:10-15"
|
||||
cin "straddle window starts at the mapped range" "== [2] s.txt:8-15"
|
||||
r=$(cp2 --spec-json "$SP2" --since "$S2" --only 6,7,8,9)
|
||||
cin "--since: an edit on the anchor's last line is inside" "== [6] w.txt:3-5"
|
||||
cin "--since: an edit on the anchor's first line is inside" "== [7] w.txt:5-7"
|
||||
cnot "--since: an anchor after the edit has no window" "== [8] "
|
||||
cnot "--since: an anchor before the edit has no window" "== [9] "
|
||||
cin "--since keeps every anchor in the table" "[9] w.txt:1-4 SAME"
|
||||
# a --since commit whose tree is unreadable: git diff fails, so no anchor is claimed edited
|
||||
BL=$(echo only | git -C "$R2" hash-object -w --stdin); TR=$(printf '100644 blob %s\tonly.txt\n' "$BL" | git -C "$R2" mktree)
|
||||
CM=$(git -C "$R2" -c user.name=t -c user.email=t@t commit-tree "$TR" -m orphan)
|
||||
chmod u+w "$R2/.git/objects/${TR:0:2}/${TR:2}" 2>/dev/null; rm -f "$R2/.git/objects/${TR:0:2}/${TR:2}"
|
||||
r=$(cp2 --spec-json "$SP2" --since "$CM"); cchk "--since with an unreadable tree" 0 "$r"
|
||||
cnot "--since: a failed git diff claims no edited window" "== ["
|
||||
cin "--since: a failed git diff keeps the table" "[6] w.txt:3-5"
|
||||
# a spec value of the wrong type is a typed refusal naming the field, never a traceback or the value
|
||||
$PY - "$T" "$SP" <<'PYE'
|
||||
import copy, json, sys
|
||||
t, sp = sys.argv[1], sys.argv[2]
|
||||
spec = json.load(open(sp, encoding="utf-8"))
|
||||
cases = {
|
||||
"line": (lambda v: v["file_changes"][1]["location"].update(line="CANARY-line"), "file_changes[2].location.line"),
|
||||
"line-bool": (lambda v: v["file_changes"][0]["location"].update(line=True), "file_changes[1].location.line"),
|
||||
"end_line": (lambda v: v["file_changes"][2]["location"].update(end_line="CANARY-end"), "file_changes[3].location.end_line"),
|
||||
"path": (lambda v: v["file_changes"][0]["location"].update(path=["CANARY-path"]), "file_changes[1].location.path"),
|
||||
"nolocation": (lambda v: v["file_changes"][3].pop("location"), "file_changes[4].location"),
|
||||
"location-str": (lambda v: v["file_changes"][0].update(location="CANARY-loc"), "file_changes[1].location"),
|
||||
"entry-str": (lambda v: v["file_changes"].__setitem__(0, "CANARY-str"), "file_changes[1]"),
|
||||
"change": (lambda v: v["file_changes"][0].update(change={"CANARY": 1}), "file_changes[1].change"),
|
||||
"fcs-str": (lambda v: v.update(file_changes="CANARY-fcs"), "file_changes"),
|
||||
"base": (lambda v: v.update(base=12345), "base"),
|
||||
"deletes-str": (lambda v: v.update(deletes="CANARY-del"), "deletes"),
|
||||
"deletes-nopath": (lambda v: v.update(deletes=[{"note": "CANARY-note"}]), "deletes[1].path"),
|
||||
"deletes-entry-str": (lambda v: v.update(deletes=["CANARY-d"]), "deletes[1].path"),
|
||||
}
|
||||
names = []
|
||||
for name, (edit, field) in cases.items():
|
||||
d = copy.deepcopy(spec)
|
||||
edit(d["document"]["value"])
|
||||
json.dump(d, open("%s/cp-bad-%s.json" % (t, name), "w"))
|
||||
names.append("%s %s" % (name, field))
|
||||
open(t + "/cp-bad.list", "w", newline=chr(10)).write("\n".join(names) + "\n")
|
||||
PYE
|
||||
while read -r nm fld; do
|
||||
r=$(cpo --spec-json "$(wp "$T/cp-bad-$nm.json")"); cchk "wrong-typed spec: $nm" 4 "$r"
|
||||
grep -qF "context-pack: spec: $fld: InvalidInput; --spec-json reads a saved view" "$T/cp.err" && echo "ok context-pack names $fld" || { echo "FAIL context-pack message for $nm: $(cat "$T/cp.err")"; fail=1; }
|
||||
cnot "wrong-typed spec $nm: no value" "CANARY"; cnot "wrong-typed spec $nm: no traceback" "Traceback"
|
||||
done <"$T/cp-bad.list"
|
||||
echo '{"base": "CANARY-base", "deletes": [], "file_changes": [{"change": "c", "location": {"path": "a.txt", "line": 1, "end_line": 2}}]}' >"$T/cp-specnb.json"
|
||||
r=$(cpo --spec-json "$(wp "$T/cp-specnb.json")"); cchk "a base that is no sha" 3 "$r"
|
||||
cin "a base that is no sha reads UNRESOLVED" "UNRESOLVED base not a commit here"
|
||||
cin "a base that is no sha is not echoed in the header" "base unresolvable HEAD"
|
||||
echo '{"file_changes": [{"change": "c", "location": {"path": "a.txt", "line": 1}}]}' >"$T/cp-spec-nobase.json"
|
||||
r=$(cpo --spec-json "$(wp "$T/cp-spec-nobase.json")"); cchk "a spec value without a base" 4 "$r"
|
||||
grep -qF "context-pack: spec: not a cut_spec value; --spec-json reads a saved view" "$T/cp.err" && echo "ok context-pack a missing base is a typed refusal" || { echo "FAIL context-pack missing base: $(cat "$T/cp.err")"; fail=1; }
|
||||
# a --max-lines below the table: the table always lists every anchor and no window prints
|
||||
r=$(cpo --spec-json "$(wp "$T/cp-spec30.json")" --max-lines 5); cchk "--max-lines below the table" 0 "$r"
|
||||
[ "$(grep -c '^\[' "$T/cp.out")" = 30 ] && echo "ok context-pack a small budget keeps the full table" || { echo "FAIL context-pack small budget table"; fail=1; }
|
||||
cnot "a small budget prints no window" "== ["
|
||||
[ -z "$(sed -n 32p "$T/cp.out")" ] && echo "ok context-pack a blank line follows the table" || { echo "FAIL context-pack blank line after the table"; fail=1; }
|
||||
cin "a small budget lists every window as not expanded" "not expanded: --only 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30"
|
||||
# --only truncates a window to the budget and says how to see the rest
|
||||
r=$(cpo --spec-json "$S1" --only 1 --max-lines 50); cchk "--only truncation" 0 "$r"
|
||||
[ "$(wc -l <"$T/cp.out" | tr -d ' ')" = 50 ] && echo "ok context-pack --only fills the budget exactly" || { echo "FAIL context-pack --only budget: $(wc -l <"$T/cp.out")"; fail=1; }
|
||||
[ "$(grep -c '^ *[0-9]* g' "$T/cp.out")" = 45 ] && echo "ok context-pack --only truncated window has 45 lines" || { echo "FAIL context-pack --only window lines"; fail=1; }
|
||||
cin "--only truncation note counts the rest" "111 more lines: --only 1 --max-lines 160"
|
||||
r=$(cpo --spec-json "$S1" --only 1 --max-lines 7); cchk "--only at the smallest truncatable budget" 0 "$r"
|
||||
[ "$(wc -l <"$T/cp.out" | tr -d ' ')" = 7 ] && [ "$(grep -c '^ *[0-9]* g' "$T/cp.out")" = 2 ] && echo "ok context-pack --only budget 7 prints 2 window lines" || { echo "FAIL context-pack --only budget 7"; fail=1; }
|
||||
r=$(cpo --spec-json "$S1" --only 1 --max-lines 6); cin "--only below the smallest truncation is not expanded" "not expanded: --only 1"
|
||||
cnot "--only below the smallest truncation prints no window" "== ["
|
||||
echo '{"base": "'$BASE'", "deletes": [], "file_changes": [
|
||||
{"change": "long", "location": {"path": "big.txt", "line": 20, "end_line": 169}},
|
||||
{"change": "short", "location": {"path": "big.txt", "line": 200, "end_line": 210}}]}' >"$T/cp-spec2w.json"
|
||||
r=$(cpo --spec-json "$(wp "$T/cp-spec2w.json")" --only 1,2 --max-lines 50); cchk "--only two windows" 0 "$r"
|
||||
[ "$(wc -l <"$T/cp.out" | tr -d ' ')" = 50 ] && echo "ok context-pack --only keeps room for the not-expanded line" || { echo "FAIL context-pack --only reserve: $(wc -l <"$T/cp.out")"; fail=1; }
|
||||
cin "--only: the window that no longer fits is listed" "not expanded: --only 2"
|
||||
r=$(cpo --spec-json "$(wp "$T/cp-spec2w.json")" --only 1,2 --max-lines 161); cchk "--only first window needs exactly the remaining budget" 0 "$r"
|
||||
cin "--only: a window that fills the budget minus the reserve is cut, not skipped" "== [1] big.txt:20-169"
|
||||
[ "$(wc -l <"$T/cp.out" | tr -d ' ')" = 161 ] && echo "ok context-pack --only cut window keeps the reserve line" || { echo "FAIL context-pack --only exact need: $(wc -l <"$T/cp.out")"; fail=1; }
|
||||
r=$(cpo --spec-json "$(wp "$T/cp-spec2w.json")" --only 1,2 --max-lines 8); cchk "--only two windows, too small to cut" 0 "$r"
|
||||
cnot "--only: below four lines after the reserve nothing is cut" "== ["
|
||||
cin "--only: both are listed as not expanded" "not expanded: --only 1,2"
|
||||
echo '{"base": "'$B2'", "deletes": [], "file_changes": [
|
||||
{"change": "long", "location": {"path": "big.txt", "line": 20, "end_line": 169}},
|
||||
{"change": "tiny", "location": {"path": "one.txt", "line": 1, "end_line": 1}}]}' >"$T/cp-spec3.json"
|
||||
r=$(cp2 --spec-json "$(wp "$T/cp-spec3.json")" --only 1,2 --max-lines 100); cchk "--only cut window then a tiny one" 0 "$r"
|
||||
[ "$(wc -l <"$T/cp.out" | tr -d ' ')" = 100 ] && echo "ok context-pack a cut window is charged what it prints" || { echo "FAIL context-pack cut window charge: $(wc -l <"$T/cp.out")"; fail=1; }
|
||||
cin "a cut window leaves no room for the next one" "not expanded: --only 2"
|
||||
r=$(cpo --spec-json "$S1" --max-lines 104); cin "a 100-line cut window needs 102 lines of budget" "not expanded: --only 1"
|
||||
r=$(cpo --spec-json "$S1" --max-lines 105); cin "a 100-line cut window fits in 105 lines" "56 more lines: --only 1 --max-lines 160"
|
||||
[ "$(wc -l <"$T/cp.out" | tr -d ' ')" = 105 ] && echo "ok context-pack a 100-line cut window fills 105 lines" || { echo "FAIL context-pack cut window at 105: $(wc -l <"$T/cp.out")"; fail=1; }
|
||||
grep -qxF "56 more lines: --only 1 --max-lines 160" "$T/cp.out" && echo "ok context-pack the more-lines note is its own line" || { echo "FAIL context-pack more-lines note line"; fail=1; }
|
||||
r=$(cpo --spec-json "$(wp "$T/cp-spec30.json")" --max-lines 202); cchk "reserve line at the budget edge" 0 "$r"
|
||||
[ "$(grep -c '^== \[' "$T/cp.out")" = 9 ] && [ "$(wc -l <"$T/cp.out" | tr -d ' ')" -le 202 ] && echo "ok context-pack a window that would crowd out the not-expanded line is skipped" || { echo "FAIL context-pack reserve at 202: $(wc -l <"$T/cp.out")"; fail=1; }
|
||||
# the binding env block reaches the server; the caller environment overrides it
|
||||
mcp env -u HUGINN_ENDPOINT -u EUREKA_INSTANCE FAKE_MODE=needenv FAKE_NEED=rudp://binding $PY "$CP" t:cut_spec:cut-x.r1 "$Rw" >"$T/cp.out" 2>"$T/cp.err"; r=$?
|
||||
cchk "env from the binding reaches the server" 3 "$r"; cmp -s "$T/cp.out" "$T/cp.want" && echo "ok context-pack binding env supplies the server environment" || { echo "FAIL context-pack binding env not applied"; fail=1; }
|
||||
mcp env -u EUREKA_INSTANCE HUGINN_ENDPOINT=rudp://caller FAKE_MODE=needenv FAKE_NEED=rudp://caller $PY "$CP" t:cut_spec:cut-x.r1 "$Rw" >"$T/cp.out" 2>"$T/cp.err"; r=$?
|
||||
cchk "the caller environment overrides the binding" 3 "$r"; cmp -s "$T/cp.out" "$T/cp.want" && echo "ok context-pack caller env wins over binding env" || { echo "FAIL context-pack caller env lost"; fail=1; }
|
||||
# standard library plus git only
|
||||
if grep -qE '^import (requests|yaml|tree_sitter)|^from (requests|yaml|tree_sitter)|ctags|tree-sitter|"rg"' "$CP"; then echo "FAIL context-pack: a dependency beyond git"; fail=1; else echo "ok context-pack: standard library and git only"; fi
|
||||
exit $fail
|
||||
|
|
|
|||
Loading…
Reference in a new issue