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.
- package/bundled/VERSION.json +3 -3
- package/bundled/adapters/claude/agent-adapter.js +2 -1
- package/bundled/adapters/claude/command-adapter.js +3 -3
- package/bundled/agents/prizm-dev-team-dev.md +1 -1
- package/bundled/dev-pipeline/README.md +6 -8
- package/bundled/dev-pipeline/assets/prizm-dev-team-integration.md +24 -19
- package/bundled/dev-pipeline/launch-bugfix-daemon.sh +2 -2
- package/bundled/dev-pipeline/launch-daemon.sh +2 -2
- package/bundled/dev-pipeline/lib/branch.sh +76 -0
- package/bundled/dev-pipeline/run-bugfix.sh +58 -149
- package/bundled/dev-pipeline/run.sh +60 -153
- package/bundled/dev-pipeline/scripts/generate-bootstrap-prompt.py +17 -4
- package/bundled/dev-pipeline/scripts/parse-stream-progress.py +2 -2
- package/bundled/dev-pipeline/templates/bootstrap-tier1.md +16 -27
- package/bundled/dev-pipeline/templates/bootstrap-tier2.md +20 -32
- package/bundled/dev-pipeline/templates/bootstrap-tier3.md +32 -53
- package/bundled/dev-pipeline/templates/bugfix-bootstrap-prompt.md +29 -41
- package/bundled/dev-pipeline/templates/session-status-schema.json +1 -1
- package/bundled/dev-pipeline/tests/conftest.py +19 -126
- package/bundled/dev-pipeline/tests/test_generate_bootstrap_prompt.py +207 -0
- package/bundled/dev-pipeline/tests/test_generate_bugfix_prompt.py +128 -141
- package/bundled/dev-pipeline/tests/test_utils.py +51 -110
- package/bundled/rules/prizm/prizm-commit-workflow.md +3 -3
- package/bundled/skills/_metadata.json +15 -16
- package/bundled/skills/app-planner/SKILL.md +8 -7
- package/bundled/skills/bug-fix-workflow/SKILL.md +171 -0
- package/bundled/skills/bug-planner/SKILL.md +25 -33
- package/bundled/skills/bug-planner/scripts/validate-bug-list.py +156 -0
- package/bundled/skills/bugfix-pipeline-launcher/SKILL.md +5 -7
- package/bundled/skills/dev-pipeline-launcher/SKILL.md +4 -6
- package/bundled/skills/feature-workflow/SKILL.md +25 -42
- package/bundled/skills/prizm-kit/SKILL.md +61 -23
- package/bundled/skills/prizm-kit/assets/{claude-md-template.md → project-memory-template.md} +3 -3
- package/bundled/skills/prizmkit-analyze/SKILL.md +44 -33
- package/bundled/skills/prizmkit-clarify/SKILL.md +40 -30
- package/bundled/skills/prizmkit-code-review/SKILL.md +58 -45
- package/bundled/skills/prizmkit-committer/SKILL.md +30 -68
- package/bundled/skills/prizmkit-implement/SKILL.md +60 -28
- package/bundled/skills/prizmkit-init/SKILL.md +57 -66
- package/bundled/skills/prizmkit-plan/SKILL.md +60 -23
- package/bundled/skills/prizmkit-prizm-docs/SKILL.md +74 -19
- package/bundled/skills/prizmkit-prizm-docs/assets/PRIZM-SPEC.md +23 -23
- package/bundled/skills/prizmkit-retrospective/SKILL.md +142 -65
- package/bundled/skills/prizmkit-retrospective/assets/retrospective-template.md +13 -0
- package/bundled/skills/prizmkit-specify/SKILL.md +69 -15
- package/bundled/skills/refactor-workflow/SKILL.md +116 -52
- package/bundled/team/prizm-dev-team.json +2 -2
- package/package.json +1 -1
- package/src/scaffold.js +4 -4
- package/bundled/dev-pipeline/lib/worktree.sh +0 -164
- package/bundled/dev-pipeline/tests/__init__.py +0 -0
- package/bundled/dev-pipeline/tests/test_check_session.py +0 -131
- package/bundled/dev-pipeline/tests/test_cleanup_logs.py +0 -119
- package/bundled/dev-pipeline/tests/test_detect_stuck.py +0 -207
- package/bundled/dev-pipeline/tests/test_generate_prompt.py +0 -190
- package/bundled/dev-pipeline/tests/test_init_bugfix_pipeline.py +0 -153
- package/bundled/dev-pipeline/tests/test_init_pipeline.py +0 -241
- package/bundled/dev-pipeline/tests/test_update_bug_status.py +0 -142
- package/bundled/dev-pipeline/tests/test_update_feature_status.py +0 -338
- package/bundled/dev-pipeline/tests/test_worktree.py +0 -236
- package/bundled/dev-pipeline/tests/test_worktree_integration.py +0 -796
- package/bundled/skills/prizm-kit/assets/codebuddy-md-template.md +0 -35
- package/bundled/skills/prizm-kit/assets/hooks/prizm-commit-hook.json +0 -15
- package/bundled/skills/prizmkit-summarize/SKILL.md +0 -51
- package/bundled/skills/prizmkit-summarize/assets/registry-template.md +0 -18
- package/bundled/templates/hooks/commit-intent-claude.json +0 -26
- /package/bundled/templates/hooks/{commit-intent-codebuddy.json → commit-intent.json} +0 -0
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
"""Tests for check-session-status.py."""
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
import os
|
|
5
|
-
import sys
|
|
6
|
-
import pytest
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def _import_check_session():
|
|
10
|
-
import importlib.util
|
|
11
|
-
path = os.path.join(
|
|
12
|
-
os.path.dirname(__file__), "..", "scripts", "check-session-status.py"
|
|
13
|
-
)
|
|
14
|
-
spec = importlib.util.spec_from_file_location("check_session_status", path)
|
|
15
|
-
mod = importlib.util.module_from_spec(spec)
|
|
16
|
-
sys.modules["check_session_status"] = mod
|
|
17
|
-
spec.loader.exec_module(mod)
|
|
18
|
-
return mod
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
css = _import_check_session()
|
|
22
|
-
validate_required_fields = css.validate_required_fields
|
|
23
|
-
determine_status = css.determine_status
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
class TestValidateRequiredFields:
|
|
27
|
-
def test_all_present(self):
|
|
28
|
-
data = {
|
|
29
|
-
"session_id": "s-001",
|
|
30
|
-
"feature_id": "F-001",
|
|
31
|
-
"status": "success",
|
|
32
|
-
"timestamp": "2024-01-01T00:00:00Z",
|
|
33
|
-
}
|
|
34
|
-
missing = validate_required_fields(data)
|
|
35
|
-
assert missing == []
|
|
36
|
-
|
|
37
|
-
def test_missing_session_id(self):
|
|
38
|
-
data = {
|
|
39
|
-
"feature_id": "F-001",
|
|
40
|
-
"status": "success",
|
|
41
|
-
"timestamp": "2024-01-01T00:00:00Z",
|
|
42
|
-
}
|
|
43
|
-
missing = validate_required_fields(data)
|
|
44
|
-
assert "session_id" in missing
|
|
45
|
-
|
|
46
|
-
def test_missing_multiple(self):
|
|
47
|
-
data = {"feature_id": "F-001"}
|
|
48
|
-
missing = validate_required_fields(data)
|
|
49
|
-
assert "session_id" in missing
|
|
50
|
-
assert "status" in missing
|
|
51
|
-
assert "timestamp" in missing
|
|
52
|
-
|
|
53
|
-
def test_empty_string_field(self):
|
|
54
|
-
data = {
|
|
55
|
-
"session_id": "",
|
|
56
|
-
"feature_id": "F-001",
|
|
57
|
-
"status": "success",
|
|
58
|
-
"timestamp": "2024-01-01T00:00:00Z",
|
|
59
|
-
}
|
|
60
|
-
missing = validate_required_fields(data)
|
|
61
|
-
assert "session_id" in missing
|
|
62
|
-
|
|
63
|
-
def test_whitespace_only_field(self):
|
|
64
|
-
data = {
|
|
65
|
-
"session_id": " ",
|
|
66
|
-
"feature_id": "F-001",
|
|
67
|
-
"status": "success",
|
|
68
|
-
"timestamp": "2024-01-01T00:00:00Z",
|
|
69
|
-
}
|
|
70
|
-
missing = validate_required_fields(data)
|
|
71
|
-
assert "session_id" in missing
|
|
72
|
-
|
|
73
|
-
def test_non_string_field(self):
|
|
74
|
-
data = {
|
|
75
|
-
"session_id": 123,
|
|
76
|
-
"feature_id": "F-001",
|
|
77
|
-
"status": "success",
|
|
78
|
-
"timestamp": "2024-01-01T00:00:00Z",
|
|
79
|
-
}
|
|
80
|
-
missing = validate_required_fields(data)
|
|
81
|
-
assert "session_id" in missing
|
|
82
|
-
|
|
83
|
-
def test_empty_data(self):
|
|
84
|
-
missing = validate_required_fields({})
|
|
85
|
-
assert len(missing) == 4
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
class TestDetermineStatus:
|
|
89
|
-
def test_success(self):
|
|
90
|
-
data = {"status": "success"}
|
|
91
|
-
assert determine_status(data) == "success"
|
|
92
|
-
|
|
93
|
-
def test_failed(self):
|
|
94
|
-
data = {"status": "failed"}
|
|
95
|
-
assert determine_status(data) == "failed"
|
|
96
|
-
|
|
97
|
-
def test_partial_resumable(self):
|
|
98
|
-
data = {"status": "partial", "can_resume": True}
|
|
99
|
-
assert determine_status(data) == "partial_resumable"
|
|
100
|
-
|
|
101
|
-
def test_partial_not_resumable(self):
|
|
102
|
-
data = {"status": "partial", "can_resume": False}
|
|
103
|
-
assert determine_status(data) == "partial_not_resumable"
|
|
104
|
-
|
|
105
|
-
def test_partial_no_resume_key(self):
|
|
106
|
-
data = {"status": "partial"}
|
|
107
|
-
assert determine_status(data) == "partial_not_resumable"
|
|
108
|
-
|
|
109
|
-
def test_commit_missing(self):
|
|
110
|
-
data = {"status": "commit_missing"}
|
|
111
|
-
assert determine_status(data) == "commit_missing"
|
|
112
|
-
|
|
113
|
-
def test_docs_missing(self):
|
|
114
|
-
data = {"status": "docs_missing"}
|
|
115
|
-
assert determine_status(data) == "docs_missing"
|
|
116
|
-
|
|
117
|
-
def test_merge_conflict(self):
|
|
118
|
-
data = {"status": "merge_conflict"}
|
|
119
|
-
assert determine_status(data) == "merge_conflict"
|
|
120
|
-
|
|
121
|
-
def test_unknown_status(self):
|
|
122
|
-
data = {"status": "something_weird"}
|
|
123
|
-
assert determine_status(data) == "crashed"
|
|
124
|
-
|
|
125
|
-
def test_empty_status(self):
|
|
126
|
-
data = {"status": ""}
|
|
127
|
-
assert determine_status(data) == "crashed"
|
|
128
|
-
|
|
129
|
-
def test_missing_status(self):
|
|
130
|
-
data = {}
|
|
131
|
-
assert determine_status(data) == "crashed"
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
"""Tests for cleanup-logs.py."""
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
import os
|
|
5
|
-
import subprocess
|
|
6
|
-
import sys
|
|
7
|
-
import time
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def _script_path():
|
|
11
|
-
return os.path.join(
|
|
12
|
-
os.path.dirname(__file__), "..", "scripts", "cleanup-logs.py"
|
|
13
|
-
)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def _write_log_file(path, content):
|
|
17
|
-
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
18
|
-
with open(path, "w", encoding="utf-8") as f:
|
|
19
|
-
f.write(content)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def test_iter_log_files_only_sessions_logs(state_dir):
|
|
23
|
-
# Targeted log path under sessions/*/logs/
|
|
24
|
-
valid_log = os.path.join(
|
|
25
|
-
state_dir, "features", "F-001", "sessions", "S-001", "logs", "session.log"
|
|
26
|
-
)
|
|
27
|
-
_write_log_file(valid_log, "ok")
|
|
28
|
-
|
|
29
|
-
# logs/ not under sessions should be ignored
|
|
30
|
-
ignored_log = os.path.join(state_dir, "features", "F-001", "logs", "misc.log")
|
|
31
|
-
_write_log_file(ignored_log, "ignore me")
|
|
32
|
-
|
|
33
|
-
# Import script module for direct function test
|
|
34
|
-
import importlib.util
|
|
35
|
-
|
|
36
|
-
path = _script_path()
|
|
37
|
-
spec = importlib.util.spec_from_file_location("cleanup_logs", path)
|
|
38
|
-
mod = importlib.util.module_from_spec(spec)
|
|
39
|
-
sys.modules["cleanup_logs"] = mod
|
|
40
|
-
spec.loader.exec_module(mod)
|
|
41
|
-
|
|
42
|
-
files = list(mod.iter_log_files(state_dir))
|
|
43
|
-
assert valid_log in files
|
|
44
|
-
assert ignored_log not in files
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def test_cleanup_removes_old_logs_by_retention(state_dir):
|
|
48
|
-
old_log = os.path.join(
|
|
49
|
-
state_dir, "features", "F-001", "sessions", "S-001", "logs", "session.log"
|
|
50
|
-
)
|
|
51
|
-
new_log = os.path.join(
|
|
52
|
-
state_dir, "features", "F-001", "sessions", "S-002", "logs", "session.log"
|
|
53
|
-
)
|
|
54
|
-
|
|
55
|
-
_write_log_file(old_log, "old")
|
|
56
|
-
_write_log_file(new_log, "new")
|
|
57
|
-
|
|
58
|
-
# Make old log 20 days old
|
|
59
|
-
old_ts = time.time() - (20 * 86400)
|
|
60
|
-
os.utime(old_log, (old_ts, old_ts))
|
|
61
|
-
|
|
62
|
-
result = subprocess.run(
|
|
63
|
-
[
|
|
64
|
-
sys.executable,
|
|
65
|
-
_script_path(),
|
|
66
|
-
"--state-dir",
|
|
67
|
-
state_dir,
|
|
68
|
-
"--retention-days",
|
|
69
|
-
"14",
|
|
70
|
-
"--max-total-mb",
|
|
71
|
-
"1024",
|
|
72
|
-
],
|
|
73
|
-
check=True,
|
|
74
|
-
capture_output=True,
|
|
75
|
-
text=True,
|
|
76
|
-
)
|
|
77
|
-
|
|
78
|
-
report = json.loads(result.stdout)
|
|
79
|
-
assert report["deleted_by_reason"]["retention"] >= 1
|
|
80
|
-
assert not os.path.exists(old_log)
|
|
81
|
-
assert os.path.exists(new_log)
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
def test_cleanup_enforces_total_size_cap(state_dir):
|
|
85
|
-
log1 = os.path.join(
|
|
86
|
-
state_dir, "features", "F-001", "sessions", "S-001", "logs", "session.log"
|
|
87
|
-
)
|
|
88
|
-
log2 = os.path.join(
|
|
89
|
-
state_dir, "features", "F-001", "sessions", "S-002", "logs", "session.log"
|
|
90
|
-
)
|
|
91
|
-
|
|
92
|
-
_write_log_file(log1, "a" * 1024)
|
|
93
|
-
_write_log_file(log2, "b" * 1024)
|
|
94
|
-
|
|
95
|
-
# Ensure deterministic ordering: log1 older than log2
|
|
96
|
-
old_ts = time.time() - 120
|
|
97
|
-
new_ts = time.time() - 60
|
|
98
|
-
os.utime(log1, (old_ts, old_ts))
|
|
99
|
-
os.utime(log2, (new_ts, new_ts))
|
|
100
|
-
|
|
101
|
-
result = subprocess.run(
|
|
102
|
-
[
|
|
103
|
-
sys.executable,
|
|
104
|
-
_script_path(),
|
|
105
|
-
"--state-dir",
|
|
106
|
-
state_dir,
|
|
107
|
-
"--retention-days",
|
|
108
|
-
"999",
|
|
109
|
-
"--max-total-mb",
|
|
110
|
-
"0",
|
|
111
|
-
],
|
|
112
|
-
check=True,
|
|
113
|
-
capture_output=True,
|
|
114
|
-
text=True,
|
|
115
|
-
)
|
|
116
|
-
|
|
117
|
-
report = json.loads(result.stdout)
|
|
118
|
-
assert report["deleted_by_reason"]["size"] >= 1
|
|
119
|
-
assert report["final_total_bytes"] == 0
|
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
"""Tests for detect-stuck.py."""
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
import os
|
|
5
|
-
import sys
|
|
6
|
-
import pytest
|
|
7
|
-
from datetime import datetime, timezone, timedelta
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def _import_detect_stuck():
|
|
11
|
-
import importlib.util
|
|
12
|
-
path = os.path.join(
|
|
13
|
-
os.path.dirname(__file__), "..", "scripts", "detect-stuck.py"
|
|
14
|
-
)
|
|
15
|
-
spec = importlib.util.spec_from_file_location("detect_stuck", path)
|
|
16
|
-
mod = importlib.util.module_from_spec(spec)
|
|
17
|
-
sys.modules["detect_stuck"] = mod
|
|
18
|
-
spec.loader.exec_module(mod)
|
|
19
|
-
return mod
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
ds = _import_detect_stuck()
|
|
23
|
-
parse_iso_timestamp = ds.parse_iso_timestamp
|
|
24
|
-
check_max_retries = ds.check_max_retries
|
|
25
|
-
check_stuck_checkpoint = ds.check_stuck_checkpoint
|
|
26
|
-
check_dependency_deadlock = ds.check_dependency_deadlock
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
class TestParseIsoTimestamp:
|
|
30
|
-
def test_standard_z_format(self):
|
|
31
|
-
result = parse_iso_timestamp("2024-01-15T10:30:00Z")
|
|
32
|
-
assert result is not None
|
|
33
|
-
assert result.year == 2024
|
|
34
|
-
assert result.month == 1
|
|
35
|
-
assert result.day == 15
|
|
36
|
-
assert result.hour == 10
|
|
37
|
-
assert result.minute == 30
|
|
38
|
-
|
|
39
|
-
def test_plus_zero_offset(self):
|
|
40
|
-
result = parse_iso_timestamp("2024-01-15T10:30:00+00:00")
|
|
41
|
-
assert result is not None
|
|
42
|
-
assert result.year == 2024
|
|
43
|
-
|
|
44
|
-
def test_with_microseconds_z(self):
|
|
45
|
-
result = parse_iso_timestamp("2024-01-15T10:30:00.123456Z")
|
|
46
|
-
assert result is not None
|
|
47
|
-
|
|
48
|
-
def test_with_microseconds_offset(self):
|
|
49
|
-
result = parse_iso_timestamp("2024-01-15T10:30:00.123456+00:00")
|
|
50
|
-
assert result is not None
|
|
51
|
-
|
|
52
|
-
def test_invalid_string(self):
|
|
53
|
-
result = parse_iso_timestamp("not a timestamp")
|
|
54
|
-
assert result is None
|
|
55
|
-
|
|
56
|
-
def test_none_input(self):
|
|
57
|
-
result = parse_iso_timestamp(None)
|
|
58
|
-
assert result is None
|
|
59
|
-
|
|
60
|
-
def test_integer_input(self):
|
|
61
|
-
result = parse_iso_timestamp(12345)
|
|
62
|
-
assert result is None
|
|
63
|
-
|
|
64
|
-
def test_empty_string(self):
|
|
65
|
-
result = parse_iso_timestamp("")
|
|
66
|
-
assert result is None
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
class TestCheckMaxRetries:
|
|
70
|
-
def test_below_max(self):
|
|
71
|
-
status = {"retry_count": 1}
|
|
72
|
-
result = check_max_retries(status, 3)
|
|
73
|
-
assert result is None
|
|
74
|
-
|
|
75
|
-
def test_at_max(self):
|
|
76
|
-
status = {"retry_count": 3}
|
|
77
|
-
result = check_max_retries(status, 3)
|
|
78
|
-
assert result is not None
|
|
79
|
-
assert result["reason"] == "max_retries_exceeded"
|
|
80
|
-
|
|
81
|
-
def test_above_max(self):
|
|
82
|
-
status = {"retry_count": 5}
|
|
83
|
-
result = check_max_retries(status, 3)
|
|
84
|
-
assert result is not None
|
|
85
|
-
|
|
86
|
-
def test_zero_retries(self):
|
|
87
|
-
status = {"retry_count": 0}
|
|
88
|
-
result = check_max_retries(status, 3)
|
|
89
|
-
assert result is None
|
|
90
|
-
|
|
91
|
-
def test_missing_retry_count(self):
|
|
92
|
-
status = {}
|
|
93
|
-
result = check_max_retries(status, 3)
|
|
94
|
-
assert result is None # defaults to 0
|
|
95
|
-
|
|
96
|
-
def test_non_int_retry_count(self):
|
|
97
|
-
status = {"retry_count": "not a number"}
|
|
98
|
-
result = check_max_retries(status, 3)
|
|
99
|
-
assert result is None
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
class TestCheckStuckCheckpoint:
|
|
103
|
-
def _setup_sessions(self, tmp_path, checkpoints):
|
|
104
|
-
"""Create a feature dir with sessions having given checkpoints."""
|
|
105
|
-
feature_dir = tmp_path / "feature"
|
|
106
|
-
sessions_dir = feature_dir / "sessions"
|
|
107
|
-
for i, cp in enumerate(checkpoints):
|
|
108
|
-
session_dir = sessions_dir / "session-{:03d}".format(i)
|
|
109
|
-
session_dir.mkdir(parents=True)
|
|
110
|
-
data = {"checkpoint_reached": cp}
|
|
111
|
-
(session_dir / "session-status.json").write_text(
|
|
112
|
-
json.dumps(data), encoding="utf-8"
|
|
113
|
-
)
|
|
114
|
-
return str(feature_dir)
|
|
115
|
-
|
|
116
|
-
def test_stuck_at_same_checkpoint(self, tmp_path):
|
|
117
|
-
feature_dir = self._setup_sessions(tmp_path, ["CP-3", "CP-3", "CP-3"])
|
|
118
|
-
result = check_stuck_checkpoint(feature_dir)
|
|
119
|
-
assert result is not None
|
|
120
|
-
assert result["reason"] == "stuck_at_checkpoint"
|
|
121
|
-
assert "CP-3" in result["details"]
|
|
122
|
-
|
|
123
|
-
def test_different_checkpoints(self, tmp_path):
|
|
124
|
-
feature_dir = self._setup_sessions(tmp_path, ["CP-1", "CP-2", "CP-3"])
|
|
125
|
-
result = check_stuck_checkpoint(feature_dir)
|
|
126
|
-
assert result is None
|
|
127
|
-
|
|
128
|
-
def test_fewer_than_three_sessions(self, tmp_path):
|
|
129
|
-
feature_dir = self._setup_sessions(tmp_path, ["CP-3", "CP-3"])
|
|
130
|
-
result = check_stuck_checkpoint(feature_dir)
|
|
131
|
-
assert result is None
|
|
132
|
-
|
|
133
|
-
def test_no_sessions(self, tmp_path):
|
|
134
|
-
feature_dir = tmp_path / "feature"
|
|
135
|
-
feature_dir.mkdir()
|
|
136
|
-
result = check_stuck_checkpoint(str(feature_dir))
|
|
137
|
-
assert result is None
|
|
138
|
-
|
|
139
|
-
def test_none_checkpoints(self, tmp_path):
|
|
140
|
-
feature_dir = self._setup_sessions(tmp_path, [None, None, None])
|
|
141
|
-
result = check_stuck_checkpoint(feature_dir)
|
|
142
|
-
assert result is None # None checkpoints don't trigger stuck
|
|
143
|
-
|
|
144
|
-
def test_mixed_with_last_three_same(self, tmp_path):
|
|
145
|
-
feature_dir = self._setup_sessions(
|
|
146
|
-
tmp_path, ["CP-1", "CP-2", "CP-3", "CP-3", "CP-3"]
|
|
147
|
-
)
|
|
148
|
-
result = check_stuck_checkpoint(feature_dir)
|
|
149
|
-
assert result is not None
|
|
150
|
-
assert "CP-3" in result["details"]
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
class TestCheckDependencyDeadlock:
|
|
154
|
-
def test_dependency_failed(self, state_dir):
|
|
155
|
-
# Create a failed dependency
|
|
156
|
-
dep_dir = os.path.join(state_dir, "features", "F-001")
|
|
157
|
-
os.makedirs(dep_dir, exist_ok=True)
|
|
158
|
-
with open(os.path.join(dep_dir, "status.json"), "w") as f:
|
|
159
|
-
json.dump({"status": "failed"}, f)
|
|
160
|
-
|
|
161
|
-
feature_list_data = {
|
|
162
|
-
"features": [
|
|
163
|
-
{"id": "F-001", "dependencies": []},
|
|
164
|
-
{"id": "F-002", "dependencies": ["F-001"]},
|
|
165
|
-
]
|
|
166
|
-
}
|
|
167
|
-
result = check_dependency_deadlock("F-002", feature_list_data, state_dir)
|
|
168
|
-
assert result is not None
|
|
169
|
-
assert result["reason"] == "dependency_failed"
|
|
170
|
-
assert "F-001" in result["details"]
|
|
171
|
-
|
|
172
|
-
def test_dependency_completed(self, state_dir):
|
|
173
|
-
dep_dir = os.path.join(state_dir, "features", "F-001")
|
|
174
|
-
os.makedirs(dep_dir, exist_ok=True)
|
|
175
|
-
with open(os.path.join(dep_dir, "status.json"), "w") as f:
|
|
176
|
-
json.dump({"status": "completed"}, f)
|
|
177
|
-
|
|
178
|
-
feature_list_data = {
|
|
179
|
-
"features": [
|
|
180
|
-
{"id": "F-001", "dependencies": []},
|
|
181
|
-
{"id": "F-002", "dependencies": ["F-001"]},
|
|
182
|
-
]
|
|
183
|
-
}
|
|
184
|
-
result = check_dependency_deadlock("F-002", feature_list_data, state_dir)
|
|
185
|
-
assert result is None
|
|
186
|
-
|
|
187
|
-
def test_no_dependencies(self, state_dir):
|
|
188
|
-
feature_list_data = {
|
|
189
|
-
"features": [
|
|
190
|
-
{"id": "F-001", "dependencies": []},
|
|
191
|
-
]
|
|
192
|
-
}
|
|
193
|
-
result = check_dependency_deadlock("F-001", feature_list_data, state_dir)
|
|
194
|
-
assert result is None
|
|
195
|
-
|
|
196
|
-
def test_none_feature_list(self, state_dir):
|
|
197
|
-
result = check_dependency_deadlock("F-001", None, state_dir)
|
|
198
|
-
assert result is None
|
|
199
|
-
|
|
200
|
-
def test_feature_not_found(self, state_dir):
|
|
201
|
-
feature_list_data = {
|
|
202
|
-
"features": [
|
|
203
|
-
{"id": "F-001", "dependencies": []},
|
|
204
|
-
]
|
|
205
|
-
}
|
|
206
|
-
result = check_dependency_deadlock("F-999", feature_list_data, state_dir)
|
|
207
|
-
assert result is None
|
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
"""Tests for generate-bootstrap-prompt.py."""
|
|
2
|
-
|
|
3
|
-
import os
|
|
4
|
-
import sys
|
|
5
|
-
import pytest
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def _import_generate_prompt():
|
|
9
|
-
import importlib.util
|
|
10
|
-
path = os.path.join(
|
|
11
|
-
os.path.dirname(__file__), "..", "scripts", "generate-bootstrap-prompt.py"
|
|
12
|
-
)
|
|
13
|
-
spec = importlib.util.spec_from_file_location("generate_bootstrap_prompt", path)
|
|
14
|
-
mod = importlib.util.module_from_spec(spec)
|
|
15
|
-
sys.modules["generate_bootstrap_prompt"] = mod
|
|
16
|
-
spec.loader.exec_module(mod)
|
|
17
|
-
return mod
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
gen_prompt = _import_generate_prompt()
|
|
21
|
-
compute_feature_slug = gen_prompt.compute_feature_slug
|
|
22
|
-
format_acceptance_criteria = gen_prompt.format_acceptance_criteria
|
|
23
|
-
format_global_context = gen_prompt.format_global_context
|
|
24
|
-
process_conditional_blocks = gen_prompt.process_conditional_blocks
|
|
25
|
-
process_mode_blocks = gen_prompt.process_mode_blocks
|
|
26
|
-
determine_pipeline_mode = gen_prompt.determine_pipeline_mode
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
class TestComputeFeatureSlug:
|
|
30
|
-
def test_basic(self):
|
|
31
|
-
result = compute_feature_slug("F-001", "Project Infrastructure Setup")
|
|
32
|
-
assert result == "001-project-infrastructure-setup"
|
|
33
|
-
|
|
34
|
-
def test_special_characters(self):
|
|
35
|
-
result = compute_feature_slug("F-002", "User Auth (OAuth2.0)")
|
|
36
|
-
assert result == "002-user-auth-oauth20"
|
|
37
|
-
|
|
38
|
-
def test_uppercase_id(self):
|
|
39
|
-
result = compute_feature_slug("F-042", "My Feature")
|
|
40
|
-
assert result == "042-my-feature"
|
|
41
|
-
|
|
42
|
-
def test_lowercase_f(self):
|
|
43
|
-
result = compute_feature_slug("f-5", "Quick Fix")
|
|
44
|
-
assert result == "005-quick-fix"
|
|
45
|
-
|
|
46
|
-
def test_extra_spaces(self):
|
|
47
|
-
result = compute_feature_slug("F-010", " Extra Spaces ")
|
|
48
|
-
assert result == "010-extra-spaces"
|
|
49
|
-
|
|
50
|
-
def test_empty_title(self):
|
|
51
|
-
result = compute_feature_slug("F-001", "")
|
|
52
|
-
# Numeric part only, no trailing hyphen
|
|
53
|
-
assert result.startswith("001")
|
|
54
|
-
|
|
55
|
-
def test_numeric_padding(self):
|
|
56
|
-
result = compute_feature_slug("F-1", "Test")
|
|
57
|
-
assert result.startswith("001-")
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
class TestFormatAcceptanceCriteria:
|
|
61
|
-
def test_single_item(self):
|
|
62
|
-
result = format_acceptance_criteria(["Must work"])
|
|
63
|
-
assert result == "- Must work"
|
|
64
|
-
|
|
65
|
-
def test_multiple_items(self):
|
|
66
|
-
result = format_acceptance_criteria(["A", "B", "C"])
|
|
67
|
-
assert result == "- A\n- B\n- C"
|
|
68
|
-
|
|
69
|
-
def test_empty_list(self):
|
|
70
|
-
result = format_acceptance_criteria([])
|
|
71
|
-
assert "none specified" in result
|
|
72
|
-
|
|
73
|
-
def test_none_input(self):
|
|
74
|
-
result = format_acceptance_criteria(None)
|
|
75
|
-
assert "none specified" in result
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
class TestFormatGlobalContext:
|
|
79
|
-
def test_basic(self):
|
|
80
|
-
result = format_global_context({"language": "Python", "framework": "FastAPI"})
|
|
81
|
-
assert "**framework**" in result
|
|
82
|
-
assert "**language**" in result
|
|
83
|
-
assert "Python" in result
|
|
84
|
-
assert "FastAPI" in result
|
|
85
|
-
|
|
86
|
-
def test_sorted_keys(self):
|
|
87
|
-
result = format_global_context({"z_key": "z", "a_key": "a"})
|
|
88
|
-
lines = result.split("\n")
|
|
89
|
-
assert "a_key" in lines[0]
|
|
90
|
-
assert "z_key" in lines[1]
|
|
91
|
-
|
|
92
|
-
def test_empty_context(self):
|
|
93
|
-
result = format_global_context({})
|
|
94
|
-
assert "none specified" in result
|
|
95
|
-
|
|
96
|
-
def test_none_context(self):
|
|
97
|
-
result = format_global_context(None)
|
|
98
|
-
assert "none specified" in result
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
class TestProcessConditionalBlocks:
|
|
102
|
-
def test_fresh_start_keeps_fresh_block(self):
|
|
103
|
-
content = "before\n{{IF_FRESH_START}}\nfresh content\n{{END_IF_FRESH_START}}\nafter"
|
|
104
|
-
result = process_conditional_blocks(content, "null")
|
|
105
|
-
assert "fresh content" in result
|
|
106
|
-
assert "{{IF_FRESH_START}}" not in result
|
|
107
|
-
assert "after" in result
|
|
108
|
-
|
|
109
|
-
def test_fresh_start_removes_resume_block(self):
|
|
110
|
-
content = "before\n{{IF_RESUME}}\nresume content\n{{END_IF_RESUME}}\nafter"
|
|
111
|
-
result = process_conditional_blocks(content, "null")
|
|
112
|
-
assert "resume content" not in result
|
|
113
|
-
assert "after" in result
|
|
114
|
-
|
|
115
|
-
def test_resume_keeps_resume_block(self):
|
|
116
|
-
content = "before\n{{IF_RESUME}}\nresume content\n{{END_IF_RESUME}}\nafter"
|
|
117
|
-
result = process_conditional_blocks(content, "3")
|
|
118
|
-
assert "resume content" in result
|
|
119
|
-
assert "{{IF_RESUME}}" not in result
|
|
120
|
-
|
|
121
|
-
def test_resume_removes_fresh_block(self):
|
|
122
|
-
content = "before\n{{IF_FRESH_START}}\nfresh content\n{{END_IF_FRESH_START}}\nafter"
|
|
123
|
-
result = process_conditional_blocks(content, "3")
|
|
124
|
-
assert "fresh content" not in result
|
|
125
|
-
assert "after" in result
|
|
126
|
-
|
|
127
|
-
def test_no_conditional_blocks(self):
|
|
128
|
-
content = "just plain text"
|
|
129
|
-
result = process_conditional_blocks(content, "null")
|
|
130
|
-
assert result == "just plain text"
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
class TestProcessModeBlocks:
|
|
134
|
-
def test_keeps_matching_mode(self):
|
|
135
|
-
content = "before\n{{IF_MODE_LITE}}\nlite content\n{{END_IF_MODE_LITE}}\nafter"
|
|
136
|
-
result = process_mode_blocks(content, "lite", False)
|
|
137
|
-
assert "lite content" in result
|
|
138
|
-
assert "{{IF_MODE_LITE}}" not in result
|
|
139
|
-
|
|
140
|
-
def test_removes_non_matching_mode(self):
|
|
141
|
-
content = "{{IF_MODE_FULL}}\nfull content\n{{END_IF_MODE_FULL}}\nrest"
|
|
142
|
-
result = process_mode_blocks(content, "lite", False)
|
|
143
|
-
assert "full content" not in result
|
|
144
|
-
assert "rest" in result
|
|
145
|
-
|
|
146
|
-
def test_init_done_keeps_init_done_block(self):
|
|
147
|
-
content = "{{IF_INIT_DONE}}\ninit done\n{{END_IF_INIT_DONE}}\n"
|
|
148
|
-
result = process_mode_blocks(content, "standard", True)
|
|
149
|
-
assert "init done" in result
|
|
150
|
-
|
|
151
|
-
def test_init_done_removes_init_needed_block(self):
|
|
152
|
-
content = "{{IF_INIT_NEEDED}}\nneed init\n{{END_IF_INIT_NEEDED}}\n"
|
|
153
|
-
result = process_mode_blocks(content, "standard", True)
|
|
154
|
-
assert "need init" not in result
|
|
155
|
-
|
|
156
|
-
def test_init_not_done_keeps_init_needed_block(self):
|
|
157
|
-
content = "{{IF_INIT_NEEDED}}\nneed init\n{{END_IF_INIT_NEEDED}}\n"
|
|
158
|
-
result = process_mode_blocks(content, "standard", False)
|
|
159
|
-
assert "need init" in result
|
|
160
|
-
|
|
161
|
-
def test_multiple_modes(self):
|
|
162
|
-
content = (
|
|
163
|
-
"{{IF_MODE_LITE}}\nlite\n{{END_IF_MODE_LITE}}\n"
|
|
164
|
-
"{{IF_MODE_STANDARD}}\nstandard\n{{END_IF_MODE_STANDARD}}\n"
|
|
165
|
-
"{{IF_MODE_FULL}}\nfull\n{{END_IF_MODE_FULL}}\n"
|
|
166
|
-
)
|
|
167
|
-
result = process_mode_blocks(content, "standard", False)
|
|
168
|
-
assert "lite" not in result
|
|
169
|
-
assert "standard" in result
|
|
170
|
-
assert "full" not in result
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
class TestDeterminePipelineMode:
|
|
174
|
-
def test_low_complexity(self):
|
|
175
|
-
assert determine_pipeline_mode("low") == "lite"
|
|
176
|
-
|
|
177
|
-
def test_medium_complexity(self):
|
|
178
|
-
assert determine_pipeline_mode("medium") == "standard"
|
|
179
|
-
|
|
180
|
-
def test_high_complexity(self):
|
|
181
|
-
assert determine_pipeline_mode("high") == "full"
|
|
182
|
-
|
|
183
|
-
def test_critical_complexity(self):
|
|
184
|
-
assert determine_pipeline_mode("critical") == "full"
|
|
185
|
-
|
|
186
|
-
def test_unknown_complexity(self):
|
|
187
|
-
assert determine_pipeline_mode("unknown") == "standard"
|
|
188
|
-
|
|
189
|
-
def test_none_complexity(self):
|
|
190
|
-
assert determine_pipeline_mode(None) == "standard"
|