|
| 1 | +"""Stress test: Team Lifecycle Edge Cases. |
| 2 | +
|
| 3 | +Calls the same functions the MCP server tools delegate to, |
| 4 | +exercising identical validation and I/O code paths. |
| 5 | +""" |
| 6 | +import json |
| 7 | +import tempfile |
| 8 | +import traceback |
| 9 | +from pathlib import Path |
| 10 | + |
| 11 | +from claude_teams import teams |
| 12 | + |
| 13 | +SESSION_ID = "stress-test-session-001" |
| 14 | + |
| 15 | +# Use an isolated temp directory so we don't touch ~/.claude |
| 16 | +tmp_base = Path(tempfile.mkdtemp(prefix="stress_test_")) |
| 17 | +(tmp_base / "teams").mkdir() |
| 18 | +(tmp_base / "tasks").mkdir() |
| 19 | + |
| 20 | +results = [] |
| 21 | + |
| 22 | +def run_test(num, name, fn, expected): |
| 23 | + actual = "" |
| 24 | + passed = False |
| 25 | + try: |
| 26 | + ret = fn() |
| 27 | + actual = repr(ret) if not isinstance(ret, str) else ret |
| 28 | + # If we got here without exception, result is "success" |
| 29 | + if "success" in expected.lower() or "created" in expected.lower() or "returns" in expected.lower() or "valid" in expected.lower(): |
| 30 | + passed = True |
| 31 | + else: |
| 32 | + passed = False |
| 33 | + except Exception as e: |
| 34 | + actual = f"{type(e).__name__}: {e}" |
| 35 | + if "error" in expected.lower() or "fail" in expected.lower() or "reject" in expected.lower(): |
| 36 | + passed = True |
| 37 | + else: |
| 38 | + passed = False |
| 39 | + results.append((num, name, expected, actual, "PASS" if passed else "FAIL")) |
| 40 | + print(f" {'PASS' if passed else 'FAIL'} | Test {num}: {name}") |
| 41 | + print(f" Expected: {expected}") |
| 42 | + print(f" Actual: {actual}") |
| 43 | + print() |
| 44 | + |
| 45 | + |
| 46 | +# ── Test 1: Create team with empty name ── |
| 47 | +run_test(1, "Create team with empty name", |
| 48 | + lambda: teams.create_team(name="", session_id=SESSION_ID, base_dir=tmp_base), |
| 49 | + "Error/reject: invalid team name") |
| 50 | + |
| 51 | +# ── Test 2: Create team with special characters ── |
| 52 | +run_test(2, "Create team with special characters", |
| 53 | + lambda: teams.create_team(name="test!@#$%^&*()", session_id=SESSION_ID, base_dir=tmp_base), |
| 54 | + "Error/reject: invalid team name") |
| 55 | + |
| 56 | +# ── Test 3: Create team with spaces ── |
| 57 | +run_test(3, "Create team with spaces", |
| 58 | + lambda: teams.create_team(name="my team name", session_id=SESSION_ID, base_dir=tmp_base), |
| 59 | + "Error/reject: invalid team name") |
| 60 | + |
| 61 | +# ── Test 4: Create team with very long name (500 chars) ── |
| 62 | +run_test(4, "Create team with very long name (500 chars)", |
| 63 | + lambda: teams.create_team(name="a" * 500, session_id=SESSION_ID, base_dir=tmp_base), |
| 64 | + "Success or error depending on filesystem limits") |
| 65 | + |
| 66 | +# ── Test 5: Create valid team ── |
| 67 | +run_test(5, "Create valid team stress-test-lifecycle-1", |
| 68 | + lambda: teams.create_team(name="stress-test-lifecycle-1", session_id=SESSION_ID, base_dir=tmp_base), |
| 69 | + "Success: team created") |
| 70 | + |
| 71 | +# ── Test 6: Create duplicate team ── |
| 72 | +run_test(6, "Create duplicate team stress-test-lifecycle-1", |
| 73 | + lambda: teams.create_team(name="stress-test-lifecycle-1", session_id=SESSION_ID, base_dir=tmp_base), |
| 74 | + "Success or silent overwrite (no uniqueness check in create_team)") |
| 75 | + |
| 76 | +# ── Test 7: Read config of non-existent team ── |
| 77 | +run_test(7, "Read config of non-existent team", |
| 78 | + lambda: teams.read_config(name="nonexistent-team-xyz", base_dir=tmp_base), |
| 79 | + "Error/fail: team not found or FileNotFoundError") |
| 80 | + |
| 81 | +# ── Test 8: Read config of valid team ── |
| 82 | +run_test(8, "Read config of valid team stress-test-lifecycle-1", |
| 83 | + lambda: teams.read_config(name="stress-test-lifecycle-1", base_dir=tmp_base), |
| 84 | + "Success: returns TeamConfig") |
| 85 | + |
| 86 | +# ── Test 9: Delete non-existent team ── |
| 87 | +run_test(9, "Delete non-existent team", |
| 88 | + lambda: teams.delete_team(name="nonexistent-team-xyz", base_dir=tmp_base), |
| 89 | + "Error/fail: team not found") |
| 90 | + |
| 91 | +# ── Test 10: Delete valid team ── |
| 92 | +run_test(10, "Delete valid team stress-test-lifecycle-1", |
| 93 | + lambda: teams.delete_team(name="stress-test-lifecycle-1", base_dir=tmp_base), |
| 94 | + "Success: team deleted") |
| 95 | + |
| 96 | +# ── Test 11: Double delete ── |
| 97 | +run_test(11, "Double delete stress-test-lifecycle-1", |
| 98 | + lambda: teams.delete_team(name="stress-test-lifecycle-1", base_dir=tmp_base), |
| 99 | + "Error/fail: team already deleted") |
| 100 | + |
| 101 | +# ── Test 12: Create team with unicode ── |
| 102 | +run_test(12, "Create team with unicode emoji", |
| 103 | + lambda: teams.create_team(name="test-unicode-\U0001f680", session_id=SESSION_ID, base_dir=tmp_base), |
| 104 | + "Error/reject: invalid team name (regex rejects unicode)") |
| 105 | + |
| 106 | +# ── Test 13: Create team with dots ── |
| 107 | +run_test(13, "Create team with dots", |
| 108 | + lambda: teams.create_team(name="test.dotted.name", session_id=SESSION_ID, base_dir=tmp_base), |
| 109 | + "Error/reject: dots not in allowed charset") |
| 110 | + |
| 111 | +# ── Test 14: Create team with leading hyphen ── |
| 112 | +run_test(14, "Create team with leading hyphen", |
| 113 | + lambda: teams.create_team(name="-leading-hyphen", session_id=SESSION_ID, base_dir=tmp_base), |
| 114 | + "Success: hyphens are allowed by regex ^[A-Za-z0-9_-]+$") |
| 115 | + |
| 116 | +# ── Test 15: Create team with only numbers ── |
| 117 | +run_test(15, "Create team with only numbers", |
| 118 | + lambda: teams.create_team(name="12345", session_id=SESSION_ID, base_dir=tmp_base), |
| 119 | + "Success: digits are allowed by regex") |
| 120 | + |
| 121 | + |
| 122 | +# ── Cleanup ── |
| 123 | +print("=" * 60) |
| 124 | +print("CLEANUP: removing teams created during tests") |
| 125 | +cleanup_teams = [] |
| 126 | +# Test 4 may have created a long-name team |
| 127 | +long_name = "a" * 500 |
| 128 | +for tname in [long_name, "-leading-hyphen", "12345"]: |
| 129 | + try: |
| 130 | + teams.delete_team(name=tname, base_dir=tmp_base) |
| 131 | + cleanup_teams.append(tname[:40]) |
| 132 | + except Exception: |
| 133 | + pass |
| 134 | +if cleanup_teams: |
| 135 | + print(f" Deleted: {cleanup_teams}") |
| 136 | +else: |
| 137 | + print(" Nothing to clean up.") |
| 138 | + |
| 139 | +import shutil |
| 140 | +shutil.rmtree(tmp_base, ignore_errors=True) |
| 141 | +print(f" Removed temp dir: {tmp_base}") |
| 142 | + |
| 143 | +# ── Summary Table ── |
| 144 | +print() |
| 145 | +print("=" * 120) |
| 146 | +print(f"| {'#':>2} | {'Test':<50} | {'Expected':<55} | {'Pass/Fail':<9} |") |
| 147 | +print(f"|{'-'*4}|{'-'*52}|{'-'*57}|{'-'*11}|") |
| 148 | +for num, tname, expected, actual, verdict in results: |
| 149 | + print(f"| {num:>2} | {tname:<50} | {expected:<55} | {verdict:<9} |") |
| 150 | +print("=" * 120) |
| 151 | + |
| 152 | +total = len(results) |
| 153 | +passed = sum(1 for r in results if r[4] == "PASS") |
| 154 | +print(f"\nTotal: {total} Passed: {passed} Failed: {total - passed}") |
| 155 | + |
| 156 | +# ── Detailed Actual Results ── |
| 157 | +print() |
| 158 | +print("DETAILED ACTUAL RESULTS:") |
| 159 | +print("-" * 120) |
| 160 | +for num, tname, expected, actual, verdict in results: |
| 161 | + # Truncate long actual results for readability |
| 162 | + actual_display = actual if len(actual) < 200 else actual[:200] + "..." |
| 163 | + print(f" Test {num:>2} [{verdict}]: {actual_display}") |
0 commit comments