@stylusnexus/work-plan 2026.6.11 → 2026.6.13-2
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/README.md +38 -6
- package/VERSION +1 -1
- package/package.json +1 -1
- package/skills/work-plan/SKILL.md +3 -0
- package/skills/work-plan/commands/auth_status.py +35 -0
- package/skills/work-plan/commands/close_issue.py +82 -0
- package/skills/work-plan/commands/export.py +72 -3
- package/skills/work-plan/commands/group.py +5 -1
- package/skills/work-plan/commands/init_repo.py +84 -14
- package/skills/work-plan/commands/list_open_issues.py +52 -0
- package/skills/work-plan/commands/new_track.py +8 -2
- package/skills/work-plan/commands/plan_ack.py +71 -0
- package/skills/work-plan/commands/plan_baseline.py +85 -0
- package/skills/work-plan/commands/plan_branch.py +314 -0
- package/skills/work-plan/commands/plan_confirm.py +83 -0
- package/skills/work-plan/commands/plan_status.py +140 -9
- package/skills/work-plan/commands/push_track.py +156 -0
- package/skills/work-plan/commands/reconcile.py +49 -34
- package/skills/work-plan/commands/refresh_md.py +49 -1
- package/skills/work-plan/commands/remove_repo.py +69 -0
- package/skills/work-plan/commands/set_field.py +22 -3
- package/skills/work-plan/lib/export_model.py +27 -4
- package/skills/work-plan/lib/git_state.py +22 -0
- package/skills/work-plan/lib/github_state.py +63 -0
- package/skills/work-plan/lib/manifest.py +28 -0
- package/skills/work-plan/lib/plan_fm.py +71 -0
- package/skills/work-plan/lib/plan_worktree.py +288 -0
- package/skills/work-plan/lib/status_header.py +6 -2
- package/skills/work-plan/lib/tracks.py +6 -2
- package/skills/work-plan/lib/verdict.py +1 -0
- package/skills/work-plan/tests/test_auth_status.py +98 -0
- package/skills/work-plan/tests/test_close_issue.py +121 -0
- package/skills/work-plan/tests/test_export.py +65 -0
- package/skills/work-plan/tests/test_export_command.py +95 -0
- package/skills/work-plan/tests/test_init_repo.py +100 -1
- package/skills/work-plan/tests/test_list_open_issues.py +83 -0
- package/skills/work-plan/tests/test_manifest.py +30 -1
- package/skills/work-plan/tests/test_notes_vcs_command.py +77 -0
- package/skills/work-plan/tests/test_plan_ack.py +104 -0
- package/skills/work-plan/tests/test_plan_baseline.py +86 -0
- package/skills/work-plan/tests/test_plan_branch.py +279 -0
- package/skills/work-plan/tests/test_plan_confirm.py +109 -0
- package/skills/work-plan/tests/test_plan_status_override.py +145 -0
- package/skills/work-plan/tests/test_plan_status_stalled.py +219 -0
- package/skills/work-plan/tests/test_plan_worktree.py +378 -0
- package/skills/work-plan/tests/test_push_track.py +131 -0
- package/skills/work-plan/tests/test_reconcile_dup_slug.py +138 -0
- package/skills/work-plan/tests/test_refresh_md.py +75 -0
- package/skills/work-plan/tests/test_remove_repo.py +77 -0
- package/skills/work-plan/tests/test_set_field.py +60 -0
- package/skills/work-plan/work_plan.py +125 -6
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"""Tests for the plan-branch command (#260 Phase 3). All git + config writes
|
|
2
|
+
are mocked — offline, no real repo or yq touched.
|
|
3
|
+
"""
|
|
4
|
+
import io
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
import unittest
|
|
8
|
+
from contextlib import redirect_stdout
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from unittest.mock import patch, MagicMock
|
|
11
|
+
|
|
12
|
+
SKILL_ROOT = Path(__file__).resolve().parents[1]
|
|
13
|
+
sys.path.insert(0, str(SKILL_ROOT))
|
|
14
|
+
|
|
15
|
+
from commands import plan_branch as pb
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _cfg(plan_branch=None, local="/tmp/repo", github="o/r", visibility=None):
|
|
19
|
+
entry = {"github": github}
|
|
20
|
+
if local is not None:
|
|
21
|
+
entry["local"] = local
|
|
22
|
+
if plan_branch is not None:
|
|
23
|
+
entry["plan_branch"] = plan_branch
|
|
24
|
+
return {"notes_root": "/tmp/notes", "repos": {"myrepo": entry}}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _run(args, *, cfg, pw=None, needs=False, valid=True, repo_ok=True,
|
|
28
|
+
set_ok=True):
|
|
29
|
+
"""Drive plan_branch.run with the lib + guards mocked. `pw` is a dict of
|
|
30
|
+
plan_worktree function-name → MagicMock/return overrides."""
|
|
31
|
+
pwmod = MagicMock()
|
|
32
|
+
# sensible defaults
|
|
33
|
+
pwmod._branch_exists.return_value = False
|
|
34
|
+
pwmod.local_branch_exists.return_value = True
|
|
35
|
+
pwmod.is_published.return_value = False
|
|
36
|
+
pwmod.unpushed_oneline.return_value = []
|
|
37
|
+
pwmod.fetch_branch.return_value = True
|
|
38
|
+
pwmod.ensure_worktree.return_value = Path("/wt")
|
|
39
|
+
pwmod.create_orphan_worktree.return_value = Path("/wt")
|
|
40
|
+
pwmod.dirty_work_plan_paths.return_value = [".work-plan/README.md"]
|
|
41
|
+
pwmod.commit_shared_tier.return_value = "abc1234"
|
|
42
|
+
pwmod.push_plan_branch.return_value = MagicMock(returncode=0, stderr="")
|
|
43
|
+
# Overrides set each function's return_value (the function stays a MagicMock
|
|
44
|
+
# so call-count assertions still work).
|
|
45
|
+
for k, v in (pw or {}).items():
|
|
46
|
+
getattr(pwmod, k).return_value = v
|
|
47
|
+
with patch("commands.plan_branch.load_config", return_value=cfg), \
|
|
48
|
+
patch("commands.plan_branch.is_valid_git_repo", return_value=repo_ok), \
|
|
49
|
+
patch("commands.plan_branch.seed_readme", return_value=True), \
|
|
50
|
+
patch("commands.plan_branch.needs_confirm", return_value=needs), \
|
|
51
|
+
patch("commands.plan_branch.valid_token", return_value=valid), \
|
|
52
|
+
patch("commands.plan_branch.make_token", return_value="tok123"), \
|
|
53
|
+
patch("commands.plan_branch._set_plan_branch", return_value=set_ok), \
|
|
54
|
+
patch("commands.plan_branch.pw", pwmod):
|
|
55
|
+
buf = io.StringIO()
|
|
56
|
+
with redirect_stdout(buf):
|
|
57
|
+
rc = pb.run(args)
|
|
58
|
+
return rc, buf.getvalue(), pwmod
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class UsageTest(unittest.TestCase):
|
|
62
|
+
def test_bad_action_rc2(self):
|
|
63
|
+
rc, out, _ = _run(["frobnicate", "myrepo"], cfg=_cfg())
|
|
64
|
+
self.assertEqual(rc, 2)
|
|
65
|
+
self.assertIn("usage", out)
|
|
66
|
+
|
|
67
|
+
def test_no_action_rc2(self):
|
|
68
|
+
rc, out, _ = _run([], cfg=_cfg())
|
|
69
|
+
self.assertEqual(rc, 2)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class ResolveRepoTest(unittest.TestCase):
|
|
73
|
+
def test_unconfigured_repo_errs(self):
|
|
74
|
+
rc, out, _ = _run(["status", "nope"], cfg=_cfg())
|
|
75
|
+
self.assertEqual(rc, 1)
|
|
76
|
+
self.assertIn("not a configured repo", out)
|
|
77
|
+
|
|
78
|
+
def test_missing_local_path_errs(self):
|
|
79
|
+
rc, out, _ = _run(["status", "myrepo"], cfg=_cfg(local=None))
|
|
80
|
+
self.assertEqual(rc, 1)
|
|
81
|
+
self.assertIn("no local clone path", out)
|
|
82
|
+
|
|
83
|
+
def test_not_a_git_repo_errs(self):
|
|
84
|
+
rc, out, _ = _run(["status", "myrepo"], cfg=_cfg(), repo_ok=False)
|
|
85
|
+
self.assertEqual(rc, 1)
|
|
86
|
+
self.assertIn("not a git repository", out)
|
|
87
|
+
|
|
88
|
+
def test_single_repo_inferred_when_arg_omitted(self):
|
|
89
|
+
rc, out, _ = _run(["status"], cfg=_cfg(plan_branch="work-plan/plan"))
|
|
90
|
+
self.assertEqual(rc, 0)
|
|
91
|
+
|
|
92
|
+
def test_ambiguous_repo_requires_arg(self):
|
|
93
|
+
cfg = _cfg(plan_branch="x")
|
|
94
|
+
cfg["repos"]["other"] = {"github": "o/o", "local": "/tmp/o"}
|
|
95
|
+
rc, out, _ = _run(["status"], cfg=cfg)
|
|
96
|
+
self.assertEqual(rc, 1)
|
|
97
|
+
self.assertIn("specify which repo", out)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class InitTest(unittest.TestCase):
|
|
101
|
+
def test_create_orphan_when_branch_absent(self):
|
|
102
|
+
rc, out, pwmod = _run(["init", "myrepo"], cfg=_cfg(),
|
|
103
|
+
pw={"_branch_exists": False})
|
|
104
|
+
self.assertEqual(rc, 0)
|
|
105
|
+
pwmod.create_orphan_worktree.assert_called_once()
|
|
106
|
+
pwmod.commit_shared_tier.assert_called_once()
|
|
107
|
+
pwmod.ensure_worktree.assert_not_called()
|
|
108
|
+
self.assertIn("Created orphan branch 'work-plan/plan'", out)
|
|
109
|
+
self.assertIn("Recorded plan_branch", out)
|
|
110
|
+
|
|
111
|
+
def test_connect_when_branch_exists(self):
|
|
112
|
+
rc, out, pwmod = _run(["init", "myrepo"], cfg=_cfg(),
|
|
113
|
+
pw={"_branch_exists": True})
|
|
114
|
+
self.assertEqual(rc, 0)
|
|
115
|
+
pwmod.ensure_worktree.assert_called_once()
|
|
116
|
+
pwmod.create_orphan_worktree.assert_not_called()
|
|
117
|
+
self.assertIn("Connected", out)
|
|
118
|
+
|
|
119
|
+
def test_custom_branch_name(self):
|
|
120
|
+
rc, out, pwmod = _run(["init", "myrepo", "--branch=wp/custom"],
|
|
121
|
+
cfg=_cfg(), pw={"_branch_exists": False})
|
|
122
|
+
self.assertEqual(rc, 0)
|
|
123
|
+
self.assertIn("wp/custom", out)
|
|
124
|
+
|
|
125
|
+
def test_invalid_branch_rc2(self):
|
|
126
|
+
for bad in ["--branch=-evil", "--branch=a..b", "--branch=/lead",
|
|
127
|
+
"--branch=trail/", "--branch=a//b"]:
|
|
128
|
+
rc, out, _ = _run(["init", "myrepo", bad], cfg=_cfg())
|
|
129
|
+
self.assertEqual(rc, 2, bad)
|
|
130
|
+
self.assertIn("not a valid branch", out)
|
|
131
|
+
|
|
132
|
+
def test_refuses_switching_existing_plan_branch(self):
|
|
133
|
+
rc, out, _ = _run(["init", "myrepo", "--branch=work-plan/other"],
|
|
134
|
+
cfg=_cfg(plan_branch="work-plan/plan"))
|
|
135
|
+
self.assertEqual(rc, 1)
|
|
136
|
+
self.assertIn("already has plan_branch", out)
|
|
137
|
+
|
|
138
|
+
def test_commit_failure_errs(self):
|
|
139
|
+
rc, out, _ = _run(["init", "myrepo"], cfg=_cfg(),
|
|
140
|
+
pw={"_branch_exists": False, "commit_shared_tier": None})
|
|
141
|
+
self.assertEqual(rc, 1)
|
|
142
|
+
self.assertIn("initial commit failed", out)
|
|
143
|
+
|
|
144
|
+
def test_orphan_creation_failure_errs(self):
|
|
145
|
+
rc, out, _ = _run(["init", "myrepo"], cfg=_cfg(),
|
|
146
|
+
pw={"_branch_exists": False,
|
|
147
|
+
"create_orphan_worktree": None})
|
|
148
|
+
self.assertEqual(rc, 1)
|
|
149
|
+
self.assertIn("could not create the plan worktree", out)
|
|
150
|
+
|
|
151
|
+
def test_public_repo_push_warning_shown(self):
|
|
152
|
+
rc, out, _ = _run(["init", "myrepo"], cfg=_cfg(),
|
|
153
|
+
pw={"_branch_exists": False}, needs=True)
|
|
154
|
+
self.assertEqual(rc, 0)
|
|
155
|
+
self.assertIn("public", out.lower())
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class StatusTest(unittest.TestCase):
|
|
159
|
+
def test_no_plan_branch(self):
|
|
160
|
+
rc, out, _ = _run(["status", "myrepo"], cfg=_cfg())
|
|
161
|
+
self.assertEqual(rc, 0)
|
|
162
|
+
self.assertIn("no plan_branch configured", out)
|
|
163
|
+
|
|
164
|
+
def test_no_plan_branch_json(self):
|
|
165
|
+
rc, out, _ = _run(["status", "myrepo", "--json"], cfg=_cfg())
|
|
166
|
+
self.assertEqual(rc, 0)
|
|
167
|
+
self.assertEqual(json.loads(out)["configured"], False)
|
|
168
|
+
|
|
169
|
+
def test_published_with_unpushed(self):
|
|
170
|
+
rc, out, _ = _run(["status", "myrepo"], cfg=_cfg(plan_branch="work-plan/plan"),
|
|
171
|
+
pw={"is_published": True,
|
|
172
|
+
"unpushed_oneline": ["a1 one", "b2 two"]})
|
|
173
|
+
self.assertEqual(rc, 0)
|
|
174
|
+
self.assertIn("on origin", out)
|
|
175
|
+
self.assertIn("2 commit(s)", out)
|
|
176
|
+
|
|
177
|
+
def test_local_only_json_shape(self):
|
|
178
|
+
rc, out, _ = _run(["status", "myrepo", "--json"],
|
|
179
|
+
cfg=_cfg(plan_branch="work-plan/plan"),
|
|
180
|
+
pw={"is_published": False, "local_branch_exists": True,
|
|
181
|
+
"unpushed_oneline": ["a1 x"]})
|
|
182
|
+
d = json.loads(out)
|
|
183
|
+
self.assertEqual(d["plan_branch"], "work-plan/plan")
|
|
184
|
+
self.assertTrue(d["configured"])
|
|
185
|
+
self.assertFalse(d["published"])
|
|
186
|
+
self.assertEqual(d["unpushed_count"], 1)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class PushTest(unittest.TestCase):
|
|
190
|
+
def test_no_plan_branch_errs(self):
|
|
191
|
+
rc, out, _ = _run(["push", "myrepo"], cfg=_cfg())
|
|
192
|
+
self.assertEqual(rc, 1)
|
|
193
|
+
self.assertIn("no plan_branch", out)
|
|
194
|
+
|
|
195
|
+
def test_local_branch_missing_errs(self):
|
|
196
|
+
rc, out, _ = _run(["push", "myrepo"], cfg=_cfg(plan_branch="work-plan/plan"),
|
|
197
|
+
pw={"local_branch_exists": False})
|
|
198
|
+
self.assertEqual(rc, 1)
|
|
199
|
+
self.assertIn("doesn't exist locally", out)
|
|
200
|
+
|
|
201
|
+
def test_nothing_to_push(self):
|
|
202
|
+
rc, out, pwmod = _run(["push", "myrepo"],
|
|
203
|
+
cfg=_cfg(plan_branch="work-plan/plan"),
|
|
204
|
+
pw={"unpushed_oneline": []})
|
|
205
|
+
self.assertEqual(rc, 0)
|
|
206
|
+
self.assertIn("Nothing to push", out)
|
|
207
|
+
pwmod.push_plan_branch.assert_not_called()
|
|
208
|
+
|
|
209
|
+
def test_dry_run_lists_without_pushing(self):
|
|
210
|
+
rc, out, pwmod = _run(["push", "myrepo", "--dry-run"],
|
|
211
|
+
cfg=_cfg(plan_branch="work-plan/plan"),
|
|
212
|
+
pw={"unpushed_oneline": ["a1 one", "b2 two"]})
|
|
213
|
+
self.assertEqual(rc, 0)
|
|
214
|
+
self.assertIn("Would push 2 commit(s)", out)
|
|
215
|
+
pwmod.push_plan_branch.assert_not_called()
|
|
216
|
+
|
|
217
|
+
def test_public_repo_blocks_without_confirm(self):
|
|
218
|
+
rc, out, pwmod = _run(["push", "myrepo"],
|
|
219
|
+
cfg=_cfg(plan_branch="work-plan/plan"),
|
|
220
|
+
pw={"unpushed_oneline": ["a1 x"]},
|
|
221
|
+
needs=True, valid=False)
|
|
222
|
+
self.assertEqual(rc, 0)
|
|
223
|
+
d = json.loads(out)
|
|
224
|
+
self.assertTrue(d["needs_confirm"])
|
|
225
|
+
self.assertEqual(d["token"], "tok123")
|
|
226
|
+
self.assertIn("visible to anyone on the internet", d["reason"])
|
|
227
|
+
pwmod.push_plan_branch.assert_not_called()
|
|
228
|
+
|
|
229
|
+
def test_public_repo_pushes_with_valid_confirm(self):
|
|
230
|
+
rc, out, pwmod = _run(["push", "myrepo", "--confirm=tok123"],
|
|
231
|
+
cfg=_cfg(plan_branch="work-plan/plan"),
|
|
232
|
+
pw={"unpushed_oneline": ["a1 x"]},
|
|
233
|
+
needs=True, valid=True)
|
|
234
|
+
self.assertEqual(rc, 0)
|
|
235
|
+
pwmod.push_plan_branch.assert_called_once()
|
|
236
|
+
self.assertIn("Pushed", out)
|
|
237
|
+
|
|
238
|
+
def test_empty_github_still_gated(self):
|
|
239
|
+
# Regression: a config entry with an empty/unknown github must NOT skip
|
|
240
|
+
# the gate. needs_confirm fails CLOSED on unknown visibility; the old
|
|
241
|
+
# `github and needs_confirm(...)` short-circuit defeated that and pushed
|
|
242
|
+
# unguarded. With the guard removed the gate fires.
|
|
243
|
+
rc, out, pwmod = _run(["push", "myrepo"],
|
|
244
|
+
cfg=_cfg(plan_branch="work-plan/plan", github=""),
|
|
245
|
+
pw={"unpushed_oneline": ["a1 x"]},
|
|
246
|
+
needs=True, valid=False)
|
|
247
|
+
self.assertEqual(rc, 0)
|
|
248
|
+
self.assertIn("needs_confirm", out)
|
|
249
|
+
pwmod.push_plan_branch.assert_not_called()
|
|
250
|
+
|
|
251
|
+
def test_private_repo_pushes_without_gate(self):
|
|
252
|
+
rc, out, pwmod = _run(["push", "myrepo"],
|
|
253
|
+
cfg=_cfg(plan_branch="work-plan/plan"),
|
|
254
|
+
pw={"unpushed_oneline": ["a1 x"]}, needs=False)
|
|
255
|
+
self.assertEqual(rc, 0)
|
|
256
|
+
pwmod.push_plan_branch.assert_called_once()
|
|
257
|
+
|
|
258
|
+
def test_protected_branch_failure_actionable(self):
|
|
259
|
+
proc = MagicMock(returncode=1, stderr="remote: protected branch hook declined")
|
|
260
|
+
rc, out, _ = _run(["push", "myrepo"],
|
|
261
|
+
cfg=_cfg(plan_branch="work-plan/plan"),
|
|
262
|
+
pw={"unpushed_oneline": ["a1 x"],
|
|
263
|
+
"push_plan_branch": proc}, needs=False)
|
|
264
|
+
self.assertEqual(rc, 1)
|
|
265
|
+
self.assertIn("protected", out.lower())
|
|
266
|
+
self.assertIn("work-plan/**", out)
|
|
267
|
+
|
|
268
|
+
def test_generic_push_failure(self):
|
|
269
|
+
proc = MagicMock(returncode=1, stderr="fatal: unable to access")
|
|
270
|
+
rc, out, _ = _run(["push", "myrepo"],
|
|
271
|
+
cfg=_cfg(plan_branch="work-plan/plan"),
|
|
272
|
+
pw={"unpushed_oneline": ["a1 x"],
|
|
273
|
+
"push_plan_branch": proc}, needs=False)
|
|
274
|
+
self.assertEqual(rc, 1)
|
|
275
|
+
self.assertIn("push failed", out)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
if __name__ == "__main__":
|
|
279
|
+
unittest.main()
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""plan-confirm (#286): frontmatter-only verdict_override writes, with the
|
|
2
|
+
public-repo confirm-token gate. Uses a real temp repo so the frontmatter write
|
|
3
|
+
round-trips through real yq; config + visibility are mocked (offline)."""
|
|
4
|
+
import io
|
|
5
|
+
import json
|
|
6
|
+
import unittest
|
|
7
|
+
import sys
|
|
8
|
+
import tempfile
|
|
9
|
+
from contextlib import redirect_stdout, redirect_stderr
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from unittest import mock
|
|
12
|
+
|
|
13
|
+
SKILL_ROOT = Path(__file__).resolve().parents[1]
|
|
14
|
+
sys.path.insert(0, str(SKILL_ROOT))
|
|
15
|
+
|
|
16
|
+
from commands import plan_confirm
|
|
17
|
+
from lib import frontmatter
|
|
18
|
+
from lib.write_guard import make_token
|
|
19
|
+
|
|
20
|
+
REL = "docs/superpowers/plans/p.md"
|
|
21
|
+
BODY = "# Idea Mode UI\n\nbody text the writer must never touch\n"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class PlanConfirmTest(unittest.TestCase):
|
|
25
|
+
def _repo(self, d, doc_text=BODY):
|
|
26
|
+
root = Path(d)
|
|
27
|
+
(root / "docs/superpowers/plans").mkdir(parents=True)
|
|
28
|
+
(root / REL).write_text(doc_text)
|
|
29
|
+
return root
|
|
30
|
+
|
|
31
|
+
def _drive(self, root, args, slug=None, vis="PRIVATE"):
|
|
32
|
+
cfg = {"notes_root": str(root), "repos": {}}
|
|
33
|
+
with mock.patch("commands.plan_confirm.config_mod.load_config", return_value=cfg), \
|
|
34
|
+
mock.patch("commands.plan_confirm.config_mod.resolve_local_path_for_folder",
|
|
35
|
+
return_value=root), \
|
|
36
|
+
mock.patch("commands.plan_confirm.config_mod.resolve_github_for_folder",
|
|
37
|
+
return_value=slug), \
|
|
38
|
+
mock.patch("lib.write_guard.repo_visibility", return_value=vis):
|
|
39
|
+
out, err = io.StringIO(), io.StringIO()
|
|
40
|
+
with redirect_stdout(out), redirect_stderr(err):
|
|
41
|
+
rc = plan_confirm.run(args)
|
|
42
|
+
# stdout carries the machine surfaces (needs_confirm JSON, success line);
|
|
43
|
+
# stderr carries usage/validation errors. Tests assert on either.
|
|
44
|
+
return rc, out.getvalue(), err.getvalue()
|
|
45
|
+
|
|
46
|
+
def test_writes_override_to_frontmatter_preserving_body(self):
|
|
47
|
+
with tempfile.TemporaryDirectory() as d:
|
|
48
|
+
root = self._repo(d)
|
|
49
|
+
rc, out, err = self._drive(root, ["--repo=k", "--verdict=shipped", "--", REL])
|
|
50
|
+
self.assertEqual(rc, 0)
|
|
51
|
+
meta, body = frontmatter.parse_file(root / REL)
|
|
52
|
+
self.assertEqual(meta["verdict_override"], "shipped")
|
|
53
|
+
self.assertEqual(body, BODY) # body byte-preserved
|
|
54
|
+
self.assertIn("frontmatter only", out)
|
|
55
|
+
|
|
56
|
+
def test_clear_removes_override(self):
|
|
57
|
+
with tempfile.TemporaryDirectory() as d:
|
|
58
|
+
root = self._repo(d, f"---\nverdict_override: shipped\n---\n{BODY}")
|
|
59
|
+
rc, out, err = self._drive(root, ["--repo=k", "--clear", "--", REL])
|
|
60
|
+
self.assertEqual(rc, 0)
|
|
61
|
+
meta, body = frontmatter.parse_file(root / REL)
|
|
62
|
+
self.assertNotIn("verdict_override", meta)
|
|
63
|
+
self.assertEqual(body, BODY)
|
|
64
|
+
|
|
65
|
+
def test_public_repo_no_token_returns_needs_confirm(self):
|
|
66
|
+
with tempfile.TemporaryDirectory() as d:
|
|
67
|
+
root = self._repo(d)
|
|
68
|
+
rc, out, err = self._drive(root, ["--repo=k", "--verdict=shipped", "--", REL],
|
|
69
|
+
slug="org/pub", vis="PUBLIC")
|
|
70
|
+
self.assertEqual(rc, 0)
|
|
71
|
+
data = json.loads(out)
|
|
72
|
+
self.assertTrue(data["needs_confirm"])
|
|
73
|
+
self.assertEqual(data["token"], make_token("org/pub", REL))
|
|
74
|
+
# NO write happened.
|
|
75
|
+
meta, _ = frontmatter.parse_file(root / REL)
|
|
76
|
+
self.assertNotIn("verdict_override", meta)
|
|
77
|
+
|
|
78
|
+
def test_public_repo_with_valid_token_writes(self):
|
|
79
|
+
with tempfile.TemporaryDirectory() as d:
|
|
80
|
+
root = self._repo(d)
|
|
81
|
+
token = make_token("org/pub", REL)
|
|
82
|
+
rc, out, err = self._drive(
|
|
83
|
+
root, ["--repo=k", "--verdict=shipped", f"--confirm={token}", "--", REL],
|
|
84
|
+
slug="org/pub", vis="PUBLIC")
|
|
85
|
+
self.assertEqual(rc, 0)
|
|
86
|
+
meta, _ = frontmatter.parse_file(root / REL)
|
|
87
|
+
self.assertEqual(meta["verdict_override"], "shipped")
|
|
88
|
+
|
|
89
|
+
def test_path_escape_rejected(self):
|
|
90
|
+
with tempfile.TemporaryDirectory() as d:
|
|
91
|
+
root = self._repo(d)
|
|
92
|
+
rc, out, err = self._drive(root, ["--repo=k", "--verdict=shipped", "--",
|
|
93
|
+
"../../etc/passwd"])
|
|
94
|
+
self.assertEqual(rc, 1)
|
|
95
|
+
self.assertIn("not a file inside", err)
|
|
96
|
+
|
|
97
|
+
def test_invalid_verdict_rejected(self):
|
|
98
|
+
with tempfile.TemporaryDirectory() as d:
|
|
99
|
+
root = self._repo(d)
|
|
100
|
+
rc, out, err = self._drive(root, ["--repo=k", "--verdict=done", "--", REL])
|
|
101
|
+
self.assertEqual(rc, 2)
|
|
102
|
+
|
|
103
|
+
def test_missing_repo_flag_rejected(self):
|
|
104
|
+
rc, out, err = self._drive(Path("/tmp"), ["--verdict=shipped", "--", REL])
|
|
105
|
+
self.assertEqual(rc, 2)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
if __name__ == "__main__":
|
|
109
|
+
unittest.main()
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""verdict_override (#286): a frontmatter override pins the verdict and silences
|
|
2
|
+
the lie-gap on plan-status. Offline — frontmatter parse shells to real yq."""
|
|
3
|
+
import io
|
|
4
|
+
import json
|
|
5
|
+
import unittest
|
|
6
|
+
import sys
|
|
7
|
+
import tempfile
|
|
8
|
+
from contextlib import redirect_stdout
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from unittest import mock
|
|
11
|
+
|
|
12
|
+
SKILL_ROOT = Path(__file__).resolve().parents[1]
|
|
13
|
+
sys.path.insert(0, str(SKILL_ROOT))
|
|
14
|
+
|
|
15
|
+
from commands import plan_status
|
|
16
|
+
|
|
17
|
+
# A plan whose ONE declared file exists (→ mechanical "shipped", file score 100%)
|
|
18
|
+
# but whose two phase checkboxes are unticked → the classic lie-gap shape.
|
|
19
|
+
BODY = (
|
|
20
|
+
"# Idea Mode UI\n\n"
|
|
21
|
+
"**Files:**\n"
|
|
22
|
+
"- Create: `src/new.ts`\n"
|
|
23
|
+
"- [ ] Step 1: do the thing\n"
|
|
24
|
+
"- [ ] Step 2: do the other thing\n"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _frontmattered(override: str) -> str:
|
|
29
|
+
return f"---\nverdict_override: {override}\n---\n{BODY}"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _frontmattered_kv(key: str, value: str) -> str:
|
|
33
|
+
return f"---\n{key}: {value}\n---\n{BODY}"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class OverrideTest(unittest.TestCase):
|
|
37
|
+
def _row(self, root):
|
|
38
|
+
with mock.patch("commands.plan_status.git_state.path_last_commit_date",
|
|
39
|
+
return_value=None), \
|
|
40
|
+
mock.patch("commands.plan_status.Path.cwd", return_value=root):
|
|
41
|
+
buf = io.StringIO()
|
|
42
|
+
with redirect_stdout(buf):
|
|
43
|
+
rc = plan_status.run(["--json"])
|
|
44
|
+
self.assertEqual(rc, 0)
|
|
45
|
+
return json.loads(buf.getvalue())["docs"][0]
|
|
46
|
+
|
|
47
|
+
def _repo(self, d, doc_text):
|
|
48
|
+
root = Path(d)
|
|
49
|
+
(root / "docs/superpowers/plans").mkdir(parents=True)
|
|
50
|
+
(root / "docs/superpowers/plans/p.md").write_text(doc_text)
|
|
51
|
+
(root / "src").mkdir()
|
|
52
|
+
(root / "src/new.ts").write_text("export const x = 1") # 1/1 declared present
|
|
53
|
+
return root
|
|
54
|
+
|
|
55
|
+
def test_baseline_lie_gap_without_override(self):
|
|
56
|
+
with tempfile.TemporaryDirectory() as d:
|
|
57
|
+
row = self._row(self._repo(d, BODY))
|
|
58
|
+
self.assertEqual(row["verdict"], "shipped")
|
|
59
|
+
self.assertTrue(row["lie_gap"]) # 0/2 boxes -> lie-gap fires
|
|
60
|
+
self.assertIsNone(row["override"])
|
|
61
|
+
|
|
62
|
+
def test_override_shipped_silences_lie_gap(self):
|
|
63
|
+
with tempfile.TemporaryDirectory() as d:
|
|
64
|
+
row = self._row(self._repo(d, _frontmattered("shipped")))
|
|
65
|
+
self.assertEqual(row["verdict"], "shipped")
|
|
66
|
+
self.assertEqual(row["override"], "shipped")
|
|
67
|
+
self.assertFalse(row["lie_gap"]) # human-confirmed -> no longer a lie
|
|
68
|
+
self.assertIn("human-confirmed", row["rationale"])
|
|
69
|
+
|
|
70
|
+
def test_override_pins_verdict_over_mechanical(self):
|
|
71
|
+
# Declared file MISSING -> mechanical would be partial; override pins dead.
|
|
72
|
+
with tempfile.TemporaryDirectory() as d:
|
|
73
|
+
root = Path(d)
|
|
74
|
+
(root / "docs/superpowers/plans").mkdir(parents=True)
|
|
75
|
+
(root / "docs/superpowers/plans/p.md").write_text(_frontmattered("dead"))
|
|
76
|
+
# no src/new.ts -> 0/1 files
|
|
77
|
+
row = self._row(root)
|
|
78
|
+
self.assertEqual(row["verdict"], "dead")
|
|
79
|
+
self.assertEqual(row["override"], "dead")
|
|
80
|
+
self.assertEqual(row["glyph"], "💀")
|
|
81
|
+
|
|
82
|
+
def test_invalid_override_value_ignored(self):
|
|
83
|
+
with tempfile.TemporaryDirectory() as d:
|
|
84
|
+
row = self._row(self._repo(d, _frontmattered("done"))) # not a valid verdict
|
|
85
|
+
self.assertIsNone(row["override"])
|
|
86
|
+
self.assertTrue(row["lie_gap"]) # falls back to mechanical
|
|
87
|
+
|
|
88
|
+
def test_acknowledged_frontmatter_emitted(self):
|
|
89
|
+
with tempfile.TemporaryDirectory() as d:
|
|
90
|
+
row = self._row(self._repo(d, f"---\nacknowledged: true\n---\n{BODY}"))
|
|
91
|
+
self.assertTrue(row["acknowledged"])
|
|
92
|
+
|
|
93
|
+
def test_acknowledged_false_without_frontmatter(self):
|
|
94
|
+
with tempfile.TemporaryDirectory() as d:
|
|
95
|
+
row = self._row(self._repo(d, BODY))
|
|
96
|
+
self.assertFalse(row["acknowledged"])
|
|
97
|
+
|
|
98
|
+
def test_baseline_matching_live_verdict_no_drift(self):
|
|
99
|
+
# File present → live "shipped"; baseline shipped → no drift.
|
|
100
|
+
with tempfile.TemporaryDirectory() as d:
|
|
101
|
+
row = self._row(self._repo(d, _frontmattered_kv("verdict_baseline", "shipped")))
|
|
102
|
+
self.assertEqual(row["verdict_baseline"], "shipped")
|
|
103
|
+
self.assertFalse(row["verdict_drift"])
|
|
104
|
+
|
|
105
|
+
def test_baseline_diverged_flags_drift(self):
|
|
106
|
+
# File MISSING → live "partial"; baseline shipped → drift (regressed).
|
|
107
|
+
with tempfile.TemporaryDirectory() as d:
|
|
108
|
+
root = Path(d)
|
|
109
|
+
(root / "docs/superpowers/plans").mkdir(parents=True)
|
|
110
|
+
(root / "docs/superpowers/plans/p.md").write_text(
|
|
111
|
+
f"---\nverdict_baseline: shipped\n---\n{BODY}") # no src/new.ts
|
|
112
|
+
row = self._row(root)
|
|
113
|
+
self.assertEqual(row["verdict"], "partial")
|
|
114
|
+
self.assertTrue(row["verdict_drift"])
|
|
115
|
+
|
|
116
|
+
def test_override_suppresses_drift(self):
|
|
117
|
+
# baseline partial, but a human override pins shipped → drift suppressed.
|
|
118
|
+
with tempfile.TemporaryDirectory() as d:
|
|
119
|
+
row = self._row(self._repo(
|
|
120
|
+
d, "---\nverdict_baseline: partial\nverdict_override: shipped\n---\n" + BODY))
|
|
121
|
+
self.assertEqual(row["verdict"], "shipped")
|
|
122
|
+
self.assertFalse(row["verdict_drift"])
|
|
123
|
+
|
|
124
|
+
def test_no_baseline_no_drift(self):
|
|
125
|
+
with tempfile.TemporaryDirectory() as d:
|
|
126
|
+
row = self._row(self._repo(d, BODY))
|
|
127
|
+
self.assertIsNone(row["verdict_baseline"])
|
|
128
|
+
self.assertFalse(row["verdict_drift"])
|
|
129
|
+
|
|
130
|
+
def test_offtree_paths_flagged(self):
|
|
131
|
+
# A plan declaring an out-of-tree path surfaces it read-only (#286 slice 3).
|
|
132
|
+
body = ("# P\n\n**Files:**\n- Create: `src/new.ts`\n"
|
|
133
|
+
"- Create: `../sibling/escape.ts`\n- [ ] Step 1\n")
|
|
134
|
+
with tempfile.TemporaryDirectory() as d:
|
|
135
|
+
row = self._row(self._repo(d, body))
|
|
136
|
+
self.assertEqual(row["offtree_paths"], ["../sibling/escape.ts"])
|
|
137
|
+
|
|
138
|
+
def test_offtree_paths_empty_for_clean_manifest(self):
|
|
139
|
+
with tempfile.TemporaryDirectory() as d:
|
|
140
|
+
row = self._row(self._repo(d, BODY))
|
|
141
|
+
self.assertEqual(row["offtree_paths"], [])
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
if __name__ == "__main__":
|
|
145
|
+
unittest.main()
|