prizmkit 1.0.45 → 1.0.66

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/bundled/VERSION.json +3 -3
  2. package/bundled/adapters/claude/agent-adapter.js +2 -1
  3. package/bundled/adapters/claude/command-adapter.js +3 -3
  4. package/bundled/agents/prizm-dev-team-dev.md +1 -1
  5. package/bundled/dev-pipeline/README.md +6 -8
  6. package/bundled/dev-pipeline/assets/prizm-dev-team-integration.md +24 -19
  7. package/bundled/dev-pipeline/launch-bugfix-daemon.sh +2 -2
  8. package/bundled/dev-pipeline/launch-daemon.sh +2 -2
  9. package/bundled/dev-pipeline/lib/branch.sh +76 -0
  10. package/bundled/dev-pipeline/run-bugfix.sh +58 -149
  11. package/bundled/dev-pipeline/run.sh +60 -153
  12. package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +17 -4
  13. package/bundled/dev-pipeline/scripts/parse-stream-progress.py +2 -2
  14. package/bundled/dev-pipeline/templates/bootstrap-tier1.md +16 -27
  15. package/bundled/dev-pipeline/templates/bootstrap-tier2.md +20 -32
  16. package/bundled/dev-pipeline/templates/bootstrap-tier3.md +32 -53
  17. package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +29 -41
  18. package/bundled/dev-pipeline/templates/session-status-schema.json +1 -1
  19. package/bundled/dev-pipeline/tests/conftest.py +19 -126
  20. package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +207 -0
  21. package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +128 -141
  22. package/bundled/dev-pipeline/tests/test_utils.py +51 -110
  23. package/bundled/rules/prizm/prizm-commit-workflow.md +3 -3
  24. package/bundled/skills/_metadata.json +15 -16
  25. package/bundled/skills/app-planner/SKILL.md +8 -7
  26. package/bundled/skills/bug-fix-workflow/SKILL.md +171 -0
  27. package/bundled/skills/bug-planner/SKILL.md +25 -33
  28. package/bundled/skills/bug-planner/scripts/validate-bug-list.py +156 -0
  29. package/bundled/skills/bugfix-pipeline-launcher/SKILL.md +5 -7
  30. package/bundled/skills/dev-pipeline-launcher/SKILL.md +4 -6
  31. package/bundled/skills/feature-workflow/SKILL.md +25 -42
  32. package/bundled/skills/prizm-kit/SKILL.md +61 -23
  33. package/bundled/skills/prizm-kit/assets/{claude-md-template.md → project-memory-template.md} +3 -3
  34. package/bundled/skills/prizmkit-analyze/SKILL.md +44 -33
  35. package/bundled/skills/prizmkit-clarify/SKILL.md +40 -30
  36. package/bundled/skills/prizmkit-code-review/SKILL.md +58 -45
  37. package/bundled/skills/prizmkit-committer/SKILL.md +30 -68
  38. package/bundled/skills/prizmkit-implement/SKILL.md +60 -28
  39. package/bundled/skills/prizmkit-init/SKILL.md +57 -66
  40. package/bundled/skills/prizmkit-plan/SKILL.md +60 -23
  41. package/bundled/skills/prizmkit-prizm-docs/SKILL.md +74 -19
  42. package/bundled/skills/prizmkit-prizm-docs/assets/PRIZM-SPEC.md +23 -23
  43. package/bundled/skills/prizmkit-retrospective/SKILL.md +142 -65
  44. package/bundled/skills/prizmkit-retrospective/assets/retrospective-template.md +13 -0
  45. package/bundled/skills/prizmkit-specify/SKILL.md +69 -15
  46. package/bundled/skills/refactor-workflow/SKILL.md +116 -52
  47. package/bundled/team/prizm-dev-team.json +2 -2
  48. package/package.json +1 -1
  49. package/src/scaffold.js +4 -4
  50. package/bundled/dev-pipeline/lib/worktree.sh +0 -164
  51. package/bundled/dev-pipeline/tests/__init__.py +0 -0
  52. package/bundled/dev-pipeline/tests/test_check_session.py +0 -131
  53. package/bundled/dev-pipeline/tests/test_cleanup_logs.py +0 -119
  54. package/bundled/dev-pipeline/tests/test_detect_stuck.py +0 -207
  55. package/bundled/dev-pipeline/tests/test_generate_prompt.py +0 -190
  56. package/bundled/dev-pipeline/tests/test_init_bugfix_pipeline.py +0 -153
  57. package/bundled/dev-pipeline/tests/test_init_pipeline.py +0 -241
  58. package/bundled/dev-pipeline/tests/test_update_bug_status.py +0 -142
  59. package/bundled/dev-pipeline/tests/test_update_feature_status.py +0 -338
  60. package/bundled/dev-pipeline/tests/test_worktree.py +0 -236
  61. package/bundled/dev-pipeline/tests/test_worktree_integration.py +0 -796
  62. package/bundled/skills/prizm-kit/assets/codebuddy-md-template.md +0 -35
  63. package/bundled/skills/prizm-kit/assets/hooks/prizm-commit-hook.json +0 -15
  64. package/bundled/skills/prizmkit-summarize/SKILL.md +0 -51
  65. package/bundled/skills/prizmkit-summarize/assets/registry-template.md +0 -18
  66. package/bundled/templates/hooks/commit-intent-claude.json +0 -26
  67. /package/bundled/templates/hooks/{commit-intent-codebuddy.json → commit-intent.json} +0 -0
@@ -1,338 +0,0 @@
1
- """Tests for update-feature-status.py."""
2
-
3
- import json
4
- import os
5
- import re
6
- import sys
7
- import pytest
8
- from types import SimpleNamespace
9
-
10
-
11
- def _import_update_feature_status():
12
- import importlib.util
13
- path = os.path.join(
14
- os.path.dirname(__file__), "..", "scripts", "update-feature-status.py"
15
- )
16
- spec = importlib.util.spec_from_file_location("update_feature_status", path)
17
- mod = importlib.util.module_from_spec(spec)
18
- sys.modules["update_feature_status"] = mod
19
- spec.loader.exec_module(mod)
20
- return mod
21
-
22
-
23
- ufs = _import_update_feature_status()
24
- now_iso = ufs.now_iso
25
- load_feature_status = ufs.load_feature_status
26
- save_feature_status = ufs.save_feature_status
27
- _build_feature_slug = ufs._build_feature_slug
28
- _format_duration = ufs._format_duration
29
- _calc_feature_duration = ufs._calc_feature_duration
30
-
31
-
32
- class TestSessionStatusValues:
33
- def test_contains_commit_missing(self):
34
- assert "commit_missing" in ufs.SESSION_STATUS_VALUES
35
-
36
- def test_contains_docs_missing(self):
37
- assert "docs_missing" in ufs.SESSION_STATUS_VALUES
38
-
39
- def test_contains_merge_conflict(self):
40
- assert "merge_conflict" in ufs.SESSION_STATUS_VALUES
41
-
42
-
43
- class TestNowIso:
44
- def test_returns_valid_iso_format(self):
45
- result = now_iso()
46
- # Should match YYYY-MM-DDTHH:MM:SSZ
47
- pattern = r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$"
48
- assert re.match(pattern, result) is not None
49
-
50
- def test_returns_string(self):
51
- assert isinstance(now_iso(), str)
52
-
53
-
54
- class TestLoadAndSaveFeatureStatus:
55
- def test_round_trip(self, state_dir):
56
- fid = "F-001"
57
- # Create the feature directory
58
- fdir = os.path.join(state_dir, "features", fid)
59
- os.makedirs(fdir, exist_ok=True)
60
-
61
- status_data = {
62
- "feature_id": fid,
63
- "status": "in_progress",
64
- "retry_count": 1,
65
- "max_retries": 3,
66
- "sessions": ["s-001"],
67
- "last_session_id": "s-001",
68
- "resume_from_phase": "3",
69
- "created_at": "2024-01-01T00:00:00Z",
70
- "updated_at": "2024-01-01T01:00:00Z",
71
- }
72
- err = save_feature_status(state_dir, fid, status_data)
73
- assert err is None
74
-
75
- loaded = load_feature_status(state_dir, fid)
76
- assert loaded["feature_id"] == fid
77
- assert loaded["status"] == "in_progress"
78
- assert loaded["retry_count"] == 1
79
-
80
- def test_load_missing_returns_default(self, state_dir):
81
- result = load_feature_status(state_dir, "F-999")
82
- assert result["feature_id"] == "F-999"
83
- assert result["status"] == "pending"
84
- assert result["retry_count"] == 0
85
-
86
- def test_load_invalid_json_returns_default(self, state_dir):
87
- fid = "F-BAD"
88
- fdir = os.path.join(state_dir, "features", fid)
89
- os.makedirs(fdir, exist_ok=True)
90
- with open(os.path.join(fdir, "status.json"), "w") as f:
91
- f.write("{invalid json")
92
- result = load_feature_status(state_dir, fid)
93
- assert result["status"] == "pending"
94
-
95
-
96
- class TestBuildFeatureSlug:
97
- def test_basic(self):
98
- assert _build_feature_slug("F-001", "Project Setup") == "001-project-setup"
99
-
100
- def test_special_chars(self):
101
- result = _build_feature_slug("F-002", "Auth (OAuth2.0)")
102
- assert result == "002-auth-oauth20"
103
-
104
- def test_empty_title(self):
105
- result = _build_feature_slug("F-003", "")
106
- assert result == "003-feature"
107
-
108
- def test_none_title(self):
109
- result = _build_feature_slug("F-003", None)
110
- assert result == "003-feature"
111
-
112
- def test_lowercase_f(self):
113
- result = _build_feature_slug("f-5", "Test")
114
- assert result == "005-test"
115
-
116
- def test_numeric_padding(self):
117
- result = _build_feature_slug("F-1", "A")
118
- assert result.startswith("001-")
119
-
120
-
121
- class TestFormatDuration:
122
- def test_none(self):
123
- assert _format_duration(None) == "N/A"
124
-
125
- def test_seconds(self):
126
- assert _format_duration(45) == "45s"
127
-
128
- def test_minutes(self):
129
- assert _format_duration(125) == "2m5s"
130
-
131
- def test_hours(self):
132
- assert _format_duration(3661) == "1h1m"
133
-
134
- def test_zero(self):
135
- assert _format_duration(0) == "0s"
136
-
137
- def test_exact_minute(self):
138
- assert _format_duration(60) == "1m0s"
139
-
140
- def test_exact_hour(self):
141
- assert _format_duration(3600) == "1h0m"
142
-
143
-
144
- class TestCalcFeatureDuration:
145
- def test_valid_duration(self, state_dir):
146
- fid = "F-001"
147
- fdir = os.path.join(state_dir, "features", fid)
148
- os.makedirs(fdir, exist_ok=True)
149
- status = {
150
- "created_at": "2024-01-01T00:00:00Z",
151
- "updated_at": "2024-01-01T00:05:00Z", # 300 seconds
152
- }
153
- with open(os.path.join(fdir, "status.json"), "w") as f:
154
- json.dump(status, f)
155
- result = _calc_feature_duration(state_dir, fid)
156
- assert result == 300.0
157
-
158
- def test_too_short_duration(self, state_dir):
159
- fid = "F-002"
160
- fdir = os.path.join(state_dir, "features", fid)
161
- os.makedirs(fdir, exist_ok=True)
162
- status = {
163
- "created_at": "2024-01-01T00:00:00Z",
164
- "updated_at": "2024-01-01T00:00:05Z", # 5 seconds - below 10s threshold
165
- }
166
- with open(os.path.join(fdir, "status.json"), "w") as f:
167
- json.dump(status, f)
168
- result = _calc_feature_duration(state_dir, fid)
169
- assert result is None
170
-
171
- def test_missing_file(self, state_dir):
172
- result = _calc_feature_duration(state_dir, "F-MISSING")
173
- assert result is None
174
-
175
- def test_missing_timestamps(self, state_dir):
176
- fid = "F-003"
177
- fdir = os.path.join(state_dir, "features", fid)
178
- os.makedirs(fdir, exist_ok=True)
179
- with open(os.path.join(fdir, "status.json"), "w") as f:
180
- json.dump({"status": "pending"}, f)
181
- result = _calc_feature_duration(state_dir, fid)
182
- assert result is None
183
-
184
-
185
- class TestActionUpdateDegradedStatuses:
186
- def test_commit_missing_updates_feature_state_without_cleanup(
187
- self, feature_list_file, state_dir, monkeypatch, capsys
188
- ):
189
- called = {"cleanup": 0}
190
-
191
- def _fake_cleanup(**_kwargs):
192
- called["cleanup"] += 1
193
- return []
194
-
195
- monkeypatch.setattr(ufs, "cleanup_feature_artifacts", _fake_cleanup)
196
-
197
- args = SimpleNamespace(
198
- feature_id="F-001",
199
- session_status="commit_missing",
200
- session_id="s-commit-missing",
201
- max_retries=3,
202
- project_root=None,
203
- )
204
-
205
- ufs.action_update(args, feature_list_file, state_dir)
206
- out = capsys.readouterr().out
207
- summary = json.loads(out)
208
-
209
- fs = ufs.load_feature_status(state_dir, "F-001")
210
- assert fs["status"] == "commit_missing"
211
- assert fs["retry_count"] == 1
212
- assert summary["new_status"] == "commit_missing"
213
- assert summary["degraded_reason"] == "commit_missing"
214
- assert summary["restart_policy"] == "finalization_retry"
215
- assert called["cleanup"] == 0
216
-
217
- with open(feature_list_file, "r", encoding="utf-8") as f:
218
- data = json.load(f)
219
- f1 = next(x for x in data["features"] if x["id"] == "F-001")
220
- assert f1["status"] == "commit_missing"
221
-
222
- def test_docs_missing_reaches_failed_when_retry_exhausted(
223
- self, feature_list_file, state_dir, monkeypatch, capsys
224
- ):
225
- called = {"cleanup": 0}
226
-
227
- def _fake_cleanup(**_kwargs):
228
- called["cleanup"] += 1
229
- return []
230
-
231
- monkeypatch.setattr(ufs, "cleanup_feature_artifacts", _fake_cleanup)
232
-
233
- args = SimpleNamespace(
234
- feature_id="F-001",
235
- session_status="docs_missing",
236
- session_id="s-docs-missing",
237
- max_retries=1,
238
- project_root=None,
239
- )
240
-
241
- ufs.action_update(args, feature_list_file, state_dir)
242
- out = capsys.readouterr().out
243
- summary = json.loads(out)
244
-
245
- fs = ufs.load_feature_status(state_dir, "F-001")
246
- assert fs["status"] == "failed"
247
- assert fs["retry_count"] == 1
248
- assert summary["new_status"] == "failed"
249
- assert summary["degraded_reason"] == "docs_missing"
250
- assert summary["restart_policy"] == "finalization_retry"
251
- assert called["cleanup"] == 0
252
-
253
- with open(feature_list_file, "r", encoding="utf-8") as f:
254
- data = json.load(f)
255
- f1 = next(x for x in data["features"] if x["id"] == "F-001")
256
- assert f1["status"] == "failed"
257
-
258
- def test_merge_conflict_updates_feature_state_without_cleanup(
259
- self, feature_list_file, state_dir, monkeypatch, capsys
260
- ):
261
- """merge_conflict is treated like commit_missing: retryable, no artifact cleanup."""
262
- called = {"cleanup": 0}
263
-
264
- def _fake_cleanup(**_kwargs):
265
- called["cleanup"] += 1
266
- return []
267
-
268
- monkeypatch.setattr(ufs, "cleanup_feature_artifacts", _fake_cleanup)
269
-
270
- args = SimpleNamespace(
271
- feature_id="F-001",
272
- session_status="merge_conflict",
273
- session_id="s-merge-conflict",
274
- max_retries=3,
275
- project_root=None,
276
- )
277
-
278
- ufs.action_update(args, feature_list_file, state_dir)
279
- out = capsys.readouterr().out
280
- summary = json.loads(out)
281
-
282
- fs = ufs.load_feature_status(state_dir, "F-001")
283
- assert fs["status"] == "merge_conflict"
284
- assert fs["retry_count"] == 1
285
- assert summary["new_status"] == "merge_conflict"
286
- assert summary["degraded_reason"] == "merge_conflict"
287
- assert summary["restart_policy"] == "finalization_retry"
288
- assert called["cleanup"] == 0
289
-
290
- with open(feature_list_file, "r", encoding="utf-8") as f:
291
- data = json.load(f)
292
- f1 = next(x for x in data["features"] if x["id"] == "F-001")
293
- assert f1["status"] == "merge_conflict"
294
-
295
- def test_merge_conflict_reaches_failed_when_retry_exhausted(
296
- self, feature_list_file, state_dir, monkeypatch, capsys
297
- ):
298
- """merge_conflict with max_retries=1 should fail the feature."""
299
- called = {"cleanup": 0}
300
-
301
- def _fake_cleanup(**_kwargs):
302
- called["cleanup"] += 1
303
- return []
304
-
305
- monkeypatch.setattr(ufs, "cleanup_feature_artifacts", _fake_cleanup)
306
-
307
- args = SimpleNamespace(
308
- feature_id="F-001",
309
- session_status="merge_conflict",
310
- session_id="s-merge-conflict-fail",
311
- max_retries=1,
312
- project_root=None,
313
- )
314
-
315
- ufs.action_update(args, feature_list_file, state_dir)
316
- out = capsys.readouterr().out
317
- summary = json.loads(out)
318
-
319
- fs = ufs.load_feature_status(state_dir, "F-001")
320
- assert fs["status"] == "failed"
321
- assert fs["retry_count"] == 1
322
- assert summary["new_status"] == "failed"
323
- assert called["cleanup"] == 0
324
-
325
-
326
- class TestActionStatusForDegradedStates:
327
- def test_status_output_contains_degraded_counters(self, feature_list_file, state_dir, capsys):
328
- # Update feature-list.json to set degraded statuses (single source of truth)
329
- with open(feature_list_file, "r", encoding="utf-8") as f:
330
- feature_list_data = json.load(f)
331
-
332
- feature_list_data["features"][0]["status"] = "commit_missing"
333
- feature_list_data["features"][1]["status"] = "docs_missing"
334
-
335
- ufs.action_status(feature_list_data, state_dir)
336
- out = capsys.readouterr().out
337
-
338
- assert "Commit Missing: 1 | Docs Missing: 1" in out
@@ -1,236 +0,0 @@
1
- """Tests for dev-pipeline/lib/worktree.sh.
2
-
3
- Uses subprocess to invoke the worktree functions in a real (temporary) git repo
4
- to verify create/merge/cleanup/prune lifecycle.
5
- """
6
-
7
- import json
8
- import os
9
- import subprocess
10
- import pytest
11
-
12
-
13
- SCRIPT_DIR = os.path.join(os.path.dirname(__file__), "..")
14
- LIB_DIR = os.path.join(SCRIPT_DIR, "lib")
15
-
16
-
17
- def _run_worktree_script(script_body, env=None):
18
- """Run a bash script that sources worktree.sh (with stub log functions), then executes body."""
19
- # We define stub log functions instead of sourcing common.sh to avoid
20
- # CLI/platform detection which requires cbc/claude in PATH.
21
- worktree_sh_path = os.path.join(os.path.abspath(LIB_DIR), "worktree.sh")
22
- full_script = "\n".join([
23
- "#!/usr/bin/env bash",
24
- "set -euo pipefail",
25
- # Stub log functions (worktree.sh uses log_info, log_warn, log_error, log_success)
26
- 'log_info() { echo "[INFO] $*"; }',
27
- 'log_warn() { echo "[WARN] $*"; }',
28
- 'log_error() { echo "[ERROR] $*"; }',
29
- 'log_success() { echo "[SUCCESS] $*"; }',
30
- 'source "%s"' % worktree_sh_path,
31
- script_body,
32
- ])
33
- merged_env = dict(os.environ)
34
- if env:
35
- merged_env.update(env)
36
- result = subprocess.run(
37
- ["bash", "-c", full_script],
38
- capture_output=True,
39
- text=True,
40
- env=merged_env,
41
- )
42
- return result
43
-
44
-
45
- def _init_git_repo(path):
46
- """Initialize a bare-bones git repo with an initial commit on 'main' branch."""
47
- subprocess.run(["git", "init", str(path)], capture_output=True, check=True)
48
- subprocess.run(
49
- ["git", "-C", str(path), "config", "user.email", "test@test.com"],
50
- capture_output=True,
51
- check=True,
52
- )
53
- subprocess.run(
54
- ["git", "-C", str(path), "config", "user.name", "Test"],
55
- capture_output=True,
56
- check=True,
57
- )
58
- # Create an initial commit so we have a branch
59
- readme = os.path.join(str(path), "README.md")
60
- with open(readme, "w") as f:
61
- f.write("# Test Repo\n")
62
- subprocess.run(
63
- ["git", "-C", str(path), "add", "."], capture_output=True, check=True
64
- )
65
- subprocess.run(
66
- ["git", "-C", str(path), "commit", "-m", "Initial commit"],
67
- capture_output=True,
68
- check=True,
69
- )
70
- # Ensure the branch is named 'main' regardless of git defaults
71
- subprocess.run(
72
- ["git", "-C", str(path), "branch", "-M", "main"],
73
- capture_output=True,
74
- check=True,
75
- )
76
-
77
-
78
- class TestWorktreeCreate:
79
- def test_creates_worktree_and_branch(self, tmp_path):
80
- repo = tmp_path / "repo"
81
- repo.mkdir()
82
- _init_git_repo(repo)
83
- wt_base = tmp_path / "worktrees"
84
-
85
- script = (
86
- 'worktree_create "{repo}" "{wt_base}" "test-session-001" "main"\n'
87
- 'echo "PATH=$_WORKTREE_PATH"\n'
88
- 'echo "BRANCH=$_WORKTREE_BRANCH"\n'
89
- ).format(repo=repo, wt_base=wt_base)
90
-
91
- result = _run_worktree_script(script)
92
- assert result.returncode == 0, f"stderr: {result.stderr}"
93
-
94
- lines = result.stdout.strip().split("\n")
95
- path_line = [l for l in lines if l.startswith("PATH=")]
96
- branch_line = [l for l in lines if l.startswith("BRANCH=")]
97
-
98
- assert len(path_line) == 1
99
- assert len(branch_line) == 1
100
-
101
- wt_path = path_line[0].split("=", 1)[1]
102
- wt_branch = branch_line[0].split("=", 1)[1]
103
-
104
- assert os.path.isdir(wt_path)
105
- assert wt_branch == "worktree/test-session-001"
106
-
107
- # Verify git branch exists
108
- branch_check = subprocess.run(
109
- ["git", "-C", str(repo), "rev-parse", "--verify", wt_branch],
110
- capture_output=True,
111
- )
112
- assert branch_check.returncode == 0
113
-
114
- def test_fails_when_worktree_already_exists(self, tmp_path):
115
- repo = tmp_path / "repo"
116
- repo.mkdir()
117
- _init_git_repo(repo)
118
- wt_base = tmp_path / "worktrees"
119
-
120
- # Create the worktree directory manually to trigger failure
121
- wt_path = wt_base / "dup-session"
122
- wt_path.mkdir(parents=True)
123
-
124
- script = (
125
- 'worktree_create "{repo}" "{wt_base}" "dup-session" "main"\n'
126
- 'echo "RC=$?"\n'
127
- ).format(repo=repo, wt_base=wt_base)
128
-
129
- # Since set -e is on and worktree_create returns 1, the script exits with 1
130
- result = _run_worktree_script(script)
131
- assert result.returncode != 0 or "RC=1" in result.stdout
132
-
133
-
134
- class TestWorktreeMerge:
135
- def test_merge_success(self, tmp_path):
136
- repo = tmp_path / "repo"
137
- repo.mkdir()
138
- _init_git_repo(repo)
139
- wt_base = tmp_path / "worktrees"
140
-
141
- # Create worktree, add a file, commit, then merge
142
- script = (
143
- 'worktree_create "{repo}" "{wt_base}" "merge-test" "main"\n'
144
- 'echo "hello" > "$_WORKTREE_PATH/newfile.txt"\n'
145
- 'git -C "$_WORKTREE_PATH" add newfile.txt\n'
146
- 'git -C "$_WORKTREE_PATH" commit -m "Add newfile"\n'
147
- 'worktree_merge "{repo}" "$_WORKTREE_BRANCH" "main" "F-001" "merge-test"\n'
148
- 'echo "MERGE=$_MERGE_RESULT"\n'
149
- ).format(repo=repo, wt_base=wt_base)
150
-
151
- result = _run_worktree_script(script)
152
- assert result.returncode == 0, f"stderr: {result.stderr}"
153
- assert "MERGE=success" in result.stdout
154
-
155
- # Verify the file exists on main
156
- assert os.path.isfile(str(repo / "newfile.txt"))
157
-
158
- def test_merge_conflict(self, tmp_path):
159
- repo = tmp_path / "repo"
160
- repo.mkdir()
161
- _init_git_repo(repo)
162
- wt_base = tmp_path / "worktrees"
163
-
164
- # Create worktree, modify README on both main and worktree to cause conflict
165
- script = (
166
- # Create worktree
167
- 'worktree_create "{repo}" "{wt_base}" "conflict-test" "main"\n'
168
- # Modify file in worktree and commit
169
- 'echo "worktree change" > "$_WORKTREE_PATH/README.md"\n'
170
- 'git -C "$_WORKTREE_PATH" add README.md\n'
171
- 'git -C "$_WORKTREE_PATH" commit -m "Worktree change"\n'
172
- # Modify same file in main and commit
173
- 'echo "main change" > "{repo}/README.md"\n'
174
- 'git -C "{repo}" add README.md\n'
175
- 'git -C "{repo}" commit -m "Main change"\n'
176
- # Attempt merge (should conflict)
177
- 'worktree_merge "{repo}" "worktree/conflict-test" "main" "F-001" "conflict-test" || true\n'
178
- 'echo "MERGE=$_MERGE_RESULT"\n'
179
- ).format(repo=repo, wt_base=wt_base)
180
-
181
- result = _run_worktree_script(script)
182
- # Script may exit non-zero due to merge conflict detection
183
- assert "MERGE=conflict" in result.stdout
184
-
185
-
186
- class TestWorktreeCleanup:
187
- def test_cleanup_removes_worktree_and_branch(self, tmp_path):
188
- repo = tmp_path / "repo"
189
- repo.mkdir()
190
- _init_git_repo(repo)
191
- wt_base = tmp_path / "worktrees"
192
-
193
- script = (
194
- 'worktree_create "{repo}" "{wt_base}" "cleanup-test" "main"\n'
195
- 'local_path="$_WORKTREE_PATH"\n'
196
- 'local_branch="$_WORKTREE_BRANCH"\n'
197
- 'worktree_cleanup "{repo}" "$local_path" "$local_branch"\n'
198
- '[ -d "$local_path" ] && echo "DIR_EXISTS" || echo "DIR_GONE"\n'
199
- 'git -C "{repo}" rev-parse --verify "$local_branch" 2>/dev/null && echo "BRANCH_EXISTS" || echo "BRANCH_GONE"\n'
200
- ).format(repo=repo, wt_base=wt_base)
201
-
202
- result = _run_worktree_script(script)
203
- assert result.returncode == 0, f"stderr: {result.stderr}"
204
- assert "DIR_GONE" in result.stdout
205
- assert "BRANCH_GONE" in result.stdout
206
-
207
- def test_cleanup_idempotent(self, tmp_path):
208
- repo = tmp_path / "repo"
209
- repo.mkdir()
210
- _init_git_repo(repo)
211
-
212
- # Call cleanup on non-existent worktree/branch
213
- script = (
214
- 'worktree_cleanup "{repo}" "/nonexistent/path" "nonexistent-branch"\n'
215
- 'echo "OK"\n'
216
- ).format(repo=repo)
217
-
218
- result = _run_worktree_script(script)
219
- assert result.returncode == 0
220
- assert "OK" in result.stdout
221
-
222
-
223
- class TestWorktreePruneStale:
224
- def test_prune_stale(self, tmp_path):
225
- repo = tmp_path / "repo"
226
- repo.mkdir()
227
- _init_git_repo(repo)
228
-
229
- script = (
230
- 'worktree_prune_stale "{repo}"\n'
231
- 'echo "OK"\n'
232
- ).format(repo=repo)
233
-
234
- result = _run_worktree_script(script)
235
- assert result.returncode == 0
236
- assert "OK" in result.stdout