-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathrun_tests.py
More file actions
462 lines (372 loc) · 15.9 KB
/
Copy pathrun_tests.py
File metadata and controls
462 lines (372 loc) · 15.9 KB
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
#!/usr/bin/env python3
"""
Unified test runner for CAT plugin (no pytest required).
Runs all tests and reports results. Exit code 0 = all pass, 1 = failures.
"""
import sys
import os
import traceback
from pathlib import Path
from typing import Callable, Any
from tempfile import TemporaryDirectory
# Add paths for imports
PROJECT_ROOT = Path(__file__).parent
sys.path.insert(0, str(PROJECT_ROOT / "plugin" / "hooks"))
sys.path.insert(0, str(PROJECT_ROOT / "plugin" / "scripts"))
class TestRunner:
"""Simple test runner."""
def __init__(self):
self.passed = 0
self.failed = 0
self.errors = []
def test(self, name: str, condition: bool, message: str = ""):
"""Record a test result."""
if condition:
print(f" ✓ {name}")
self.passed += 1
else:
print(f" ✗ {name}: {message}")
self.failed += 1
self.errors.append(f"{name}: {message}")
def run_test_func(self, name: str, func: Callable[[], bool]):
"""Run a test function that returns True/False."""
try:
result = func()
self.test(name, result)
except Exception as e:
self.test(name, False, f"Exception: {e}")
def section(self, name: str):
"""Print a section header."""
print(f"\n{'='*60}")
print(f" {name}")
print(f"{'='*60}")
def summary(self) -> bool:
"""Print summary and return True if all passed."""
print(f"\n{'='*60}")
print(f" SUMMARY: {self.passed} passed, {self.failed} failed")
print(f"{'='*60}")
return self.failed == 0
runner = TestRunner()
# =============================================================================
# BASH HANDLERS TESTS
# =============================================================================
def test_validate_commit_type():
"""Test validate_commit_type handler."""
runner.section("bash_handlers/validate_commit_type")
from bash_handlers.validate_commit_type import ValidateCommitTypeHandler
handler = ValidateCommitTypeHandler()
# Test HEREDOC format with valid types
cmd = '''git commit -m "$(cat <<'EOF'
feature: add new functionality
EOF
)"'''
result = handler.check(cmd, {})
runner.test("HEREDOC: valid 'feature' type", result is None)
cmd = '''git commit -m "$(cat <<'EOF'
bugfix(hooks): fix validation
EOF
)"'''
result = handler.check(cmd, {})
runner.test("HEREDOC: valid 'bugfix' with scope", result is None)
# Test HEREDOC with invalid types
cmd = '''git commit -m "$(cat <<'EOF'
feat: add feature
EOF
)"'''
result = handler.check(cmd, {})
runner.test("HEREDOC: invalid 'feat' blocked",
result is not None and result.get("decision") == "block")
cmd = '''git commit -m "$(cat <<'EOF'
fix: resolve bug
EOF
)"'''
result = handler.check(cmd, {})
runner.test("HEREDOC: invalid 'fix' blocked",
result is not None and result.get("decision") == "block")
# Test simple format
cmd = 'git commit -m "feature: add login"'
result = handler.check(cmd, {})
runner.test("Simple: valid 'feature' type", result is None)
cmd = 'git commit -m "feat: add feature"'
result = handler.check(cmd, {})
runner.test("Simple: invalid 'feat' blocked",
result is not None and result.get("decision") == "block")
# Test unparseable formats
cmd = 'git commit -m $(echo "test")'
result = handler.check(cmd, {})
runner.test("Unparseable -m format blocked",
result is not None and result.get("decision") == "block")
# Test non-commit commands pass through
cmd = 'git status'
result = handler.check(cmd, {})
runner.test("Non-commit command allowed", result is None)
cmd = 'git commit'
result = handler.check(cmd, {})
runner.test("Interactive commit allowed", result is None)
# Test all valid types
for t in ["feature", "bugfix", "docs", "style", "refactor",
"performance", "test", "config", "planning", "revert"]:
cmd = f'''git commit -m "$(cat <<'EOF'
{t}: test
EOF
)"'''
result = handler.check(cmd, {})
runner.test(f"Valid type '{t}' allowed", result is None)
# Test all invalid types
for t in ["feat", "fix", "chore", "build", "ci", "perf"]:
cmd = f'''git commit -m "$(cat <<'EOF'
{t}: test
EOF
)"'''
result = handler.check(cmd, {})
runner.test(f"Invalid type '{t}' blocked",
result is not None and result.get("decision") == "block")
# =============================================================================
# SKILL HANDLERS TESTS
# =============================================================================
def test_add_handler():
"""Test add_handler."""
runner.section("skill_handlers/add_handler")
from skill_handlers.add_handler import AddHandler
handler = AddHandler()
# Test returns None without item_type
result = handler.handle({})
runner.test("Returns None without item_type", result is None)
# Test returns None for invalid item_type
result = handler.handle({"item_type": "invalid"})
runner.test("Returns None for invalid item_type", result is None)
# Test task display
task_context = {
"item_type": "task",
"item_name": "parse-tokens",
"version": "2.0",
"task_type": "Feature",
"dependencies": [],
}
result = handler.handle(task_context)
runner.test("Task returns string", isinstance(result, str))
runner.test("Task contains PRE-COMPUTED marker", "PRE-COMPUTED ADD DISPLAY" in result)
runner.test("Task contains task name", "parse-tokens" in result)
runner.test("Task contains version", "Version: 2.0" in result)
runner.test("Task contains checkmark", "✅" in result)
runner.test("Task has box structure", "╭" in result and "╯" in result)
# Test version display
version_context = {
"item_type": "version",
"item_name": "New Features",
"version": "2.1",
"version_type": "minor",
"parent_info": "v2",
"path": ".claude/cat/issues/v2/v2.1",
}
result = handler.handle(version_context)
runner.test("Version returns string", isinstance(result, str))
runner.test("Version contains PRE-COMPUTED marker", "PRE-COMPUTED ADD DISPLAY" in result)
runner.test("Version contains version name", "v2.1" in result)
def test_display_utils():
"""Test display utility functions in status_handler."""
runner.section("skill_handlers/status_handler display utils")
from skill_handlers.status_handler import (
display_width, build_line, build_border, build_progress_bar, build_inner_box
)
# Test display_width
runner.test("display_width: empty string", display_width("") == 0)
runner.test("display_width: ASCII", display_width("hello") == 5)
runner.test("display_width: single emoji", display_width("🐱") == 2)
runner.test("display_width: emoji with text", display_width("🐱 cat") == 6)
runner.test("display_width: box chars", display_width("╭╮") == 2)
# Test build_line
result = build_line("hello", 10)
runner.test("build_line: starts with │", result.startswith("│ "))
runner.test("build_line: ends with │", result.endswith(" │"))
runner.test("build_line: contains content", "hello" in result)
result = build_line("hi", 10)
expected = "│ hi" + " " * 8 + " │"
runner.test("build_line: correct padding", result == expected)
# Test build_border
top = build_border(10, is_top=True)
runner.test("build_border: top starts with ╭", top.startswith("╭"))
runner.test("build_border: top ends with ╮", top.endswith("╮"))
runner.test("build_border: top has dashes", "─" * 12 in top)
bottom = build_border(10, is_top=False)
runner.test("build_border: bottom starts with ╰", bottom.startswith("╰"))
runner.test("build_border: bottom ends with ╯", bottom.endswith("╯"))
# Test build_progress_bar
runner.test("progress_bar: 0%", build_progress_bar(0, width=10) == "░" * 10)
runner.test("progress_bar: 100%", build_progress_bar(100, width=10) == "█" * 10)
runner.test("progress_bar: 50%", build_progress_bar(50, width=10) == "█" * 5 + "░" * 5)
runner.test("progress_bar: default width", len(build_progress_bar(0)) == 25)
# Test build_inner_box
result = build_inner_box("Header", ["Item 1", "Item 2"])
runner.test("build_inner_box: returns list", isinstance(result, list))
runner.test("build_inner_box: correct line count", len(result) == 4)
runner.test("build_inner_box: header in first line", "Header" in result[0])
runner.test("build_inner_box: top starts with ╭─", result[0].startswith("╭─"))
runner.test("build_inner_box: bottom starts with ╰", result[-1].startswith("╰"))
def test_status_handler():
"""Test status_handler."""
runner.section("skill_handlers/status_handler")
from skill_handlers.status_handler import StatusHandler, collect_status_data
handler = StatusHandler()
# Test returns None without project_root
result = handler.handle({})
runner.test("Returns None without project_root", result is None)
# Test returns None with non-existent path
result = handler.handle({"project_root": "/nonexistent/path"})
runner.test("Returns None with invalid path", result is None)
# Regression test for M223: must use issues/ subdirectory
# Create temp structure with issues/ subdirectory
with TemporaryDirectory() as tmp_dir:
tmp_path = Path(tmp_dir)
cat_dir = tmp_path / ".claude" / "cat"
cat_dir.mkdir(parents=True)
issues_dir = cat_dir / "issues"
issues_dir.mkdir()
# Create config files in cat_dir
(cat_dir / "PROJECT.md").write_text("# Test Project\n")
(cat_dir / "ROADMAP.md").write_text("# Roadmap\n")
# Create version structure in issues_dir
v1_dir = issues_dir / "v1"
v1_dir.mkdir()
v10_dir = v1_dir / "v1.0"
v10_dir.mkdir()
task_dir = v10_dir / "test-task"
task_dir.mkdir()
(task_dir / "STATE.md").write_text("Status: pending\n")
(task_dir / "PLAN.md").write_text("# Test Task\n")
# Test collect_status_data finds tasks in issues/
data = collect_status_data(issues_dir)
runner.test("M223 regression: finds tasks in issues/ dir",
"error" not in data and data.get("overall", {}).get("total", 0) == 1)
# Test handler uses issues/ subdirectory
result = handler.handle({"project_root": str(tmp_path)})
runner.test("M223 regression: handler uses issues/ dir",
result is not None and "test-task" in result)
def test_help_handler():
"""Test help_handler."""
runner.section("skill_handlers/help_handler")
from skill_handlers.help_handler import HelpHandler
handler = HelpHandler()
# Test returns string
result = handler.handle({})
runner.test("Returns string", isinstance(result, str))
runner.test("Contains PRE-COMPUTED marker", "PRE-COMPUTED" in result)
runner.test("Contains commands", "/cat:" in result)
def test_work_handler():
"""Test work_handler."""
runner.section("skill_handlers/work_handler")
from skill_handlers.work_handler import WorkHandler
handler = WorkHandler()
# Test with task context - returns template format
context = {
"task_id": "2.0-parse-tokens",
"task_name": "parse-tokens",
"task_status": "pending",
}
result = handler.handle(context)
runner.test("Returns string with task", isinstance(result, str))
if result:
runner.test("Contains PRE-COMPUTED marker", "PRE-COMPUTED" in result)
runner.test("Contains progress info", "Progress" in result or "Template" in result)
def test_cleanup_handler():
"""Test cleanup_handler."""
runner.section("skill_handlers/cleanup_handler")
from skill_handlers.cleanup_handler import CleanupHandler
handler = CleanupHandler()
# Test returns None without phase
result = handler.handle({})
runner.test("Returns None without phase", result is None)
# Test survey phase
context = {
"phase": "survey",
"worktrees": [
{"path": "/workspace/.worktrees/1.0-task", "branch": "1.0-task", "state": ""}
],
"locks": [],
"branches": ["1.0-old-branch"],
"stale_remotes": [],
}
result = handler.handle(context)
runner.test("Returns string for survey phase", isinstance(result, str))
if result:
runner.test("Contains PRE-COMPUTED marker", "PRE-COMPUTED" in result)
def test_config_loader():
"""Test unified config loader."""
runner.section("lib/config loader")
# Import the config module
import sys
from pathlib import Path
lib_path = Path(__file__).parent / "plugin" / "hooks" / "lib"
if str(lib_path) not in sys.path:
sys.path.insert(0, str(lib_path))
from config import load_config, get_config_value, DEFAULTS
# Test defaults
runner.test("DEFAULTS has autoRemoveWorktrees", "autoRemoveWorktrees" in DEFAULTS)
runner.test("DEFAULTS has trust", DEFAULTS.get("trust") == "medium")
runner.test("DEFAULTS has terminalWidth", DEFAULTS.get("terminalWidth") == 120)
# Test with temp directory
with TemporaryDirectory() as tmp_dir:
tmp_path = Path(tmp_dir)
# Test: no config returns defaults
result = load_config(tmp_path)
runner.test("No config returns defaults", result == DEFAULTS)
# Create cat dir
cat_dir = tmp_path / ".claude" / "cat"
cat_dir.mkdir(parents=True)
# Test: empty cat dir returns defaults
result = load_config(tmp_path)
runner.test("Empty cat dir returns defaults", result == DEFAULTS)
# Test: base config overrides defaults
base_config = cat_dir / "cat-config.json"
base_config.write_text('{"trust": "high"}')
result = load_config(tmp_path)
runner.test("Base config overrides defaults", result["trust"] == "high")
runner.test("Unset keys use defaults", result["verify"] == "changed")
# Test: local config overrides base
local_config = cat_dir / "cat-config.local.json"
local_config.write_text('{"trust": "low", "license": "key123"}')
result = load_config(tmp_path)
runner.test("Local config overrides base", result["trust"] == "low")
runner.test("Local config adds keys", result.get("license") == "key123")
# Test: invalid local JSON doesn't break loading
local_config.write_text('{ invalid json }')
result = load_config(tmp_path)
runner.test("Invalid local JSON uses base", result["trust"] == "high")
# Test: get_config_value helper
local_config.write_text('{"customKey": "customValue"}')
result = get_config_value("customKey", tmp_path)
runner.test("get_config_value finds key", result == "customValue")
result = get_config_value("missing", tmp_path, default="fallback")
runner.test("get_config_value uses default", result == "fallback")
# =============================================================================
# MAIN
# =============================================================================
def main():
"""Run all tests."""
print("\n" + "=" * 60)
print(" CAT PLUGIN TEST SUITE")
print("=" * 60)
# Run all test functions
test_functions = [
test_validate_commit_type,
test_add_handler,
test_display_utils,
test_status_handler,
test_help_handler,
test_work_handler,
test_cleanup_handler,
test_config_loader,
]
for test_func in test_functions:
try:
test_func()
except Exception as e:
runner.section(f"ERROR in {test_func.__name__}")
print(f" Exception: {e}")
traceback.print_exc()
runner.failed += 1
# Print summary
success = runner.summary()
sys.exit(0 if success else 1)
if __name__ == "__main__":
main()