shipwright-cli 1.10.0 → 2.1.0

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 (121) hide show
  1. package/README.md +221 -55
  2. package/completions/_shipwright +264 -32
  3. package/completions/shipwright.bash +118 -26
  4. package/completions/shipwright.fish +80 -2
  5. package/dashboard/server.ts +208 -0
  6. package/docs/strategy/01-market-research.md +619 -0
  7. package/docs/strategy/02-mission-and-brand.md +587 -0
  8. package/docs/strategy/03-gtm-and-roadmap.md +759 -0
  9. package/docs/strategy/QUICK-START.txt +289 -0
  10. package/docs/strategy/README.md +172 -0
  11. package/docs/tmux-research/TMUX-ARCHITECTURE.md +567 -0
  12. package/docs/tmux-research/TMUX-AUDIT.md +925 -0
  13. package/docs/tmux-research/TMUX-BEST-PRACTICES-2025-2026.md +829 -0
  14. package/docs/tmux-research/TMUX-QUICK-REFERENCE.md +543 -0
  15. package/docs/tmux-research/TMUX-RESEARCH-INDEX.md +438 -0
  16. package/package.json +4 -2
  17. package/scripts/lib/helpers.sh +7 -0
  18. package/scripts/sw +323 -2
  19. package/scripts/sw-activity.sh +500 -0
  20. package/scripts/sw-adaptive.sh +925 -0
  21. package/scripts/sw-adversarial.sh +1 -1
  22. package/scripts/sw-architecture-enforcer.sh +1 -1
  23. package/scripts/sw-auth.sh +613 -0
  24. package/scripts/sw-autonomous.sh +754 -0
  25. package/scripts/sw-changelog.sh +704 -0
  26. package/scripts/sw-checkpoint.sh +1 -1
  27. package/scripts/sw-ci.sh +602 -0
  28. package/scripts/sw-cleanup.sh +1 -1
  29. package/scripts/sw-code-review.sh +698 -0
  30. package/scripts/sw-connect.sh +1 -1
  31. package/scripts/sw-context.sh +605 -0
  32. package/scripts/sw-cost.sh +44 -3
  33. package/scripts/sw-daemon.sh +568 -138
  34. package/scripts/sw-dashboard.sh +1 -1
  35. package/scripts/sw-db.sh +1380 -0
  36. package/scripts/sw-decompose.sh +539 -0
  37. package/scripts/sw-deps.sh +551 -0
  38. package/scripts/sw-developer-simulation.sh +1 -1
  39. package/scripts/sw-discovery.sh +412 -0
  40. package/scripts/sw-docs-agent.sh +539 -0
  41. package/scripts/sw-docs.sh +1 -1
  42. package/scripts/sw-doctor.sh +107 -1
  43. package/scripts/sw-dora.sh +615 -0
  44. package/scripts/sw-durable.sh +710 -0
  45. package/scripts/sw-e2e-orchestrator.sh +535 -0
  46. package/scripts/sw-eventbus.sh +393 -0
  47. package/scripts/sw-feedback.sh +479 -0
  48. package/scripts/sw-fix.sh +1 -1
  49. package/scripts/sw-fleet-discover.sh +567 -0
  50. package/scripts/sw-fleet-viz.sh +404 -0
  51. package/scripts/sw-fleet.sh +8 -1
  52. package/scripts/sw-github-app.sh +596 -0
  53. package/scripts/sw-github-checks.sh +4 -4
  54. package/scripts/sw-github-deploy.sh +1 -1
  55. package/scripts/sw-github-graphql.sh +1 -1
  56. package/scripts/sw-guild.sh +569 -0
  57. package/scripts/sw-heartbeat.sh +1 -1
  58. package/scripts/sw-hygiene.sh +559 -0
  59. package/scripts/sw-incident.sh +656 -0
  60. package/scripts/sw-init.sh +237 -24
  61. package/scripts/sw-instrument.sh +699 -0
  62. package/scripts/sw-intelligence.sh +1 -1
  63. package/scripts/sw-jira.sh +1 -1
  64. package/scripts/sw-launchd.sh +363 -28
  65. package/scripts/sw-linear.sh +1 -1
  66. package/scripts/sw-logs.sh +1 -1
  67. package/scripts/sw-loop.sh +267 -21
  68. package/scripts/sw-memory.sh +18 -1
  69. package/scripts/sw-mission-control.sh +487 -0
  70. package/scripts/sw-model-router.sh +545 -0
  71. package/scripts/sw-otel.sh +596 -0
  72. package/scripts/sw-oversight.sh +764 -0
  73. package/scripts/sw-pipeline-composer.sh +1 -1
  74. package/scripts/sw-pipeline-vitals.sh +1 -1
  75. package/scripts/sw-pipeline.sh +947 -35
  76. package/scripts/sw-pm.sh +758 -0
  77. package/scripts/sw-pr-lifecycle.sh +522 -0
  78. package/scripts/sw-predictive.sh +8 -1
  79. package/scripts/sw-prep.sh +1 -1
  80. package/scripts/sw-ps.sh +1 -1
  81. package/scripts/sw-public-dashboard.sh +798 -0
  82. package/scripts/sw-quality.sh +595 -0
  83. package/scripts/sw-reaper.sh +1 -1
  84. package/scripts/sw-recruit.sh +2248 -0
  85. package/scripts/sw-regression.sh +642 -0
  86. package/scripts/sw-release-manager.sh +736 -0
  87. package/scripts/sw-release.sh +706 -0
  88. package/scripts/sw-remote.sh +1 -1
  89. package/scripts/sw-replay.sh +520 -0
  90. package/scripts/sw-retro.sh +691 -0
  91. package/scripts/sw-scale.sh +444 -0
  92. package/scripts/sw-security-audit.sh +505 -0
  93. package/scripts/sw-self-optimize.sh +1 -1
  94. package/scripts/sw-session.sh +1 -1
  95. package/scripts/sw-setup.sh +263 -127
  96. package/scripts/sw-standup.sh +712 -0
  97. package/scripts/sw-status.sh +44 -2
  98. package/scripts/sw-strategic.sh +806 -0
  99. package/scripts/sw-stream.sh +450 -0
  100. package/scripts/sw-swarm.sh +620 -0
  101. package/scripts/sw-team-stages.sh +511 -0
  102. package/scripts/sw-templates.sh +4 -4
  103. package/scripts/sw-testgen.sh +566 -0
  104. package/scripts/sw-tmux-pipeline.sh +554 -0
  105. package/scripts/sw-tmux-role-color.sh +58 -0
  106. package/scripts/sw-tmux-status.sh +128 -0
  107. package/scripts/sw-tmux.sh +1 -1
  108. package/scripts/sw-trace.sh +485 -0
  109. package/scripts/sw-tracker-github.sh +188 -0
  110. package/scripts/sw-tracker-jira.sh +172 -0
  111. package/scripts/sw-tracker-linear.sh +251 -0
  112. package/scripts/sw-tracker.sh +117 -2
  113. package/scripts/sw-triage.sh +627 -0
  114. package/scripts/sw-upgrade.sh +1 -1
  115. package/scripts/sw-ux.sh +677 -0
  116. package/scripts/sw-webhook.sh +627 -0
  117. package/scripts/sw-widgets.sh +530 -0
  118. package/scripts/sw-worktree.sh +1 -1
  119. package/templates/pipelines/autonomous.json +2 -2
  120. package/tmux/shipwright-overlay.conf +35 -17
  121. package/tmux/tmux.conf +23 -21
@@ -0,0 +1,566 @@
1
+ #!/usr/bin/env bash
2
+ # ╔═══════════════════════════════════════════════════════════════════════════╗
3
+ # ║ shipwright testgen — Autonomous test generation and coverage maintenance ║
4
+ # ║ Analyze coverage · Generate tests · Maintain thresholds · Score quality ║
5
+ # ╚═══════════════════════════════════════════════════════════════════════════╝
6
+ set -euo pipefail
7
+ trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
8
+
9
+ VERSION="2.1.0"
10
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
+
12
+ # ─── Handle subcommands ───────────────────────────────────────────────────────
13
+ if [[ "${1:-}" == "test" ]]; then
14
+ shift
15
+ exec "$SCRIPT_DIR/sw-testgen-test.sh" "$@"
16
+ fi
17
+
18
+ # ─── Colors (matches Seth's tmux theme) ─────────────────────────────────────
19
+ CYAN='\033[38;2;0;212;255m' # #00d4ff — primary accent
20
+ PURPLE='\033[38;2;124;58;237m' # #7c3aed — secondary
21
+ BLUE='\033[38;2;0;102;255m' # #0066ff — tertiary
22
+ GREEN='\033[38;2;74;222;128m' # success
23
+ YELLOW='\033[38;2;250;204;21m' # warning
24
+ RED='\033[38;2;248;113;113m' # error
25
+ DIM='\033[2m'
26
+ BOLD='\033[1m'
27
+ RESET='\033[0m'
28
+
29
+ # ─── Cross-platform compatibility ──────────────────────────────────────────
30
+ # shellcheck source=lib/compat.sh
31
+ [[ -f "$SCRIPT_DIR/lib/compat.sh" ]] && source "$SCRIPT_DIR/lib/compat.sh"
32
+
33
+ # ─── Output Helpers ─────────────────────────────────────────────────────────
34
+ info() { echo -e "${CYAN}${BOLD}▸${RESET} $*"; }
35
+ success() { echo -e "${GREEN}${BOLD}✓${RESET} $*"; }
36
+ warn() { echo -e "${YELLOW}${BOLD}⚠${RESET} $*"; }
37
+ error() { echo -e "${RED}${BOLD}✗${RESET} $*" >&2; }
38
+
39
+ emit_event() {
40
+ local type="$1"
41
+ shift
42
+ local json_data="{\"timestamp\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"type\":\"$type\""
43
+ for pair in "$@"; do
44
+ json_data="$json_data,\"$pair\""
45
+ done
46
+ json_data="$json_data}"
47
+ echo "$json_data" >> "${EVENTS_FILE:-.shipwright-events.jsonl}"
48
+ }
49
+
50
+ now_iso() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
51
+
52
+ # ─── Configuration ───────────────────────────────────────────────────────────
53
+ PROJECT_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
54
+ SCRIPTS_DIR="$PROJECT_ROOT/scripts"
55
+ TEST_SUITES_DIR="$PROJECT_ROOT/scripts"
56
+ COVERAGE_THRESHOLD=70
57
+ TESTGEN_DIR="${TESTGEN_DIR:-.claude/testgen}"
58
+ COVERAGE_DB="$TESTGEN_DIR/coverage.json"
59
+ QUALITY_DB="$TESTGEN_DIR/quality.json"
60
+ REGRESSION_DB="$TESTGEN_DIR/regressions.json"
61
+
62
+ # ─── Help ───────────────────────────────────────────────────────────────────
63
+ show_help() {
64
+ echo -e "${CYAN}${BOLD}shipwright testgen${RESET} ${DIM}v${VERSION}${RESET} — Test generation and coverage maintenance"
65
+ echo ""
66
+ echo -e "${BOLD}USAGE${RESET}"
67
+ echo -e " ${CYAN}shipwright testgen${RESET} <command> [options]"
68
+ echo ""
69
+ echo -e "${BOLD}COMMANDS${RESET}"
70
+ echo -e " ${CYAN}coverage${RESET} Show test coverage analysis"
71
+ echo -e " ${CYAN}generate${RESET} Generate tests for uncovered functions"
72
+ echo -e " ${CYAN}gaps${RESET} Show specific untested code paths"
73
+ echo -e " ${CYAN}quality${RESET} Score existing test quality"
74
+ echo -e " ${CYAN}maintain${RESET} Check if tests need updating after code changes"
75
+ echo -e " ${CYAN}threshold${RESET} Set/check coverage threshold"
76
+ echo -e " ${CYAN}regression${RESET} Compare test results across runs"
77
+ echo -e " ${CYAN}help${RESET} Show this help message"
78
+ echo ""
79
+ echo -e "${BOLD}OPTIONS${RESET}"
80
+ echo -e " ${CYAN}--target${RESET} <script> Target script for analysis"
81
+ echo -e " ${CYAN}--threshold${RESET} <num> Set minimum coverage % (default: 70)"
82
+ echo -e " ${CYAN}--json${RESET} Output JSON format"
83
+ echo -e " ${CYAN}--verbose${RESET} Detailed output"
84
+ echo ""
85
+ echo -e "${BOLD}EXAMPLES${RESET}"
86
+ echo -e " ${DIM}shipwright testgen coverage${RESET} # Overall coverage"
87
+ echo -e " ${DIM}shipwright testgen coverage --target sw-daemon.sh${RESET} # Target script"
88
+ echo -e " ${DIM}shipwright testgen generate --threshold 75${RESET} # Generate with threshold"
89
+ echo -e " ${DIM}shipwright testgen quality sw-pipeline-test.sh${RESET} # Score test quality"
90
+ echo ""
91
+ echo -e "${DIM}Docs: https://sethdford.github.io/shipwright${RESET}"
92
+ }
93
+
94
+ # ═══════════════════════════════════════════════════════════════════════════════
95
+ # COVERAGE ANALYSIS
96
+ # ═══════════════════════════════════════════════════════════════════════════════
97
+
98
+ analyze_coverage() {
99
+ local target_script="${1:-.}"
100
+ local output_format="${2:-text}"
101
+
102
+ mkdir -p "$TESTGEN_DIR"
103
+
104
+ # Extract all function definitions from target
105
+ local total_functions=0
106
+ local tested_functions=0
107
+ local function_names=""
108
+
109
+ if [[ -f "$target_script" ]]; then
110
+ # Parse function definitions
111
+ function_names=$(grep -E '^[a-zA-Z_][a-zA-Z0-9_]*\(\)' "$target_script" | sed 's/().*//' || echo "")
112
+ total_functions=$(echo "$function_names" | grep -c . || echo "0")
113
+ fi
114
+
115
+ # Find existing tests that call these functions
116
+ if [[ -n "$function_names" ]]; then
117
+ local test_file
118
+ while IFS= read -r func; do
119
+ [[ -z "$func" ]] && continue
120
+ for test_file in "$TEST_SUITES_DIR"/*-test.sh; do
121
+ [[ -f "$test_file" ]] || continue
122
+ if grep -q "$func" "$test_file" 2>/dev/null; then
123
+ tested_functions=$((tested_functions + 1))
124
+ break
125
+ fi
126
+ done
127
+ done << EOF
128
+ $function_names
129
+ EOF
130
+ fi
131
+
132
+ local coverage_pct=0
133
+ if [[ $total_functions -gt 0 ]]; then
134
+ coverage_pct=$((tested_functions * 100 / total_functions))
135
+ fi
136
+
137
+ if [[ "$output_format" == "json" ]]; then
138
+ jq -n \
139
+ --arg target "$target_script" \
140
+ --argjson total "$total_functions" \
141
+ --argjson tested "$tested_functions" \
142
+ --argjson pct "$coverage_pct" \
143
+ '{target: $target, total_functions: $total, tested_functions: $tested, coverage_percent: $pct}'
144
+ else
145
+ info "Coverage Analysis"
146
+ echo ""
147
+ echo -e " ${CYAN}Target:${RESET} $target_script"
148
+ echo -e " ${CYAN}Functions:${RESET} $total_functions total"
149
+ echo -e " ${CYAN}Tested:${RESET} $tested_functions"
150
+ echo -e " ${CYAN}Coverage:${RESET} ${coverage_pct}%"
151
+ echo ""
152
+
153
+ if [[ $coverage_pct -lt $COVERAGE_THRESHOLD ]]; then
154
+ warn "Coverage below threshold ($COVERAGE_THRESHOLD%)"
155
+ else
156
+ success "Coverage meets threshold"
157
+ fi
158
+ fi
159
+ }
160
+
161
+ # ═══════════════════════════════════════════════════════════════════════════════
162
+ # TEST GENERATION
163
+ # ═══════════════════════════════════════════════════════════════════════════════
164
+
165
+ generate_tests() {
166
+ local target_script="${1:-.}"
167
+ local threshold="${2:-$COVERAGE_THRESHOLD}"
168
+
169
+ mkdir -p "$TESTGEN_DIR"
170
+
171
+ info "Generating tests for $target_script"
172
+
173
+ # Extract untested functions
174
+ local all_functions=""
175
+ all_functions=$(grep -E '^[a-zA-Z_][a-zA-Z0-9_]*\(\)' "$target_script" | sed 's/().*//' || echo "")
176
+
177
+ local untested_functions=""
178
+ if [[ -n "$all_functions" ]]; then
179
+ while IFS= read -r func_name; do
180
+ [[ -z "$func_name" ]] && continue
181
+ local found=false
182
+ for test_file in "$TEST_SUITES_DIR"/*-test.sh; do
183
+ [[ -f "$test_file" ]] || continue
184
+ if grep -q "$func_name" "$test_file" 2>/dev/null; then
185
+ found=true
186
+ break
187
+ fi
188
+ done
189
+ [[ "$found" == "false" ]] && untested_functions="${untested_functions}${func_name}"$'\n'
190
+ done << EOF
191
+ $all_functions
192
+ EOF
193
+ fi
194
+
195
+ local untested_count=0
196
+ [[ -n "$untested_functions" ]] && untested_count=$(echo "$untested_functions" | grep -c . || echo "0")
197
+
198
+ if [[ $untested_count -eq 0 ]]; then
199
+ success "All functions have tests"
200
+ return 0
201
+ fi
202
+
203
+ echo ""
204
+ info "Untested functions: $untested_count"
205
+ echo "$untested_functions" | while IFS= read -r func; do
206
+ [[ -z "$func" ]] && continue
207
+ echo -e " ${YELLOW}•${RESET} $func"
208
+ done
209
+ echo ""
210
+
211
+ # Generate test template; use Claude for real assertions when available
212
+ local test_template_file="$TESTGEN_DIR/generated-tests.sh"
213
+ local use_claude="false"
214
+ command -v claude &>/dev/null && use_claude="true"
215
+ [[ "${TESTGEN_USE_CLAUDE:-true}" == "false" ]] && use_claude="false"
216
+
217
+ {
218
+ echo "#!/usr/bin/env bash"
219
+ echo "# Generated tests for $target_script"
220
+ echo "set -euo pipefail"
221
+ echo "trap 'echo \"ERROR: \$BASH_SOURCE:\$LINENO exited with status \$?\" >&2' ERR"
222
+ echo ""
223
+ echo "SCRIPT_DIR=\"\$(cd \"\$(dirname \"\${BASH_SOURCE[0]}\")\" && pwd)\""
224
+ echo "REPO_DIR=\"\$(cd \"\$SCRIPT_DIR/..\" && pwd)\""
225
+ echo ""
226
+ echo "# Helpers: assert equal (or contains) so tests fail when behavior is wrong"
227
+ echo "assert_equal() { local e=\"\$1\" a=\"\$2\"; if [[ \"\$a\" != \"\$e\" ]]; then echo \"Expected: \$e\"; echo \"Actual: \$a\"; exit 1; fi; }"
228
+ echo "assert_contains() { local sub=\"\$1\" full=\"\$2\"; if [[ \"\$full\" != *\"\$sub\"* ]]; then echo \"Expected to contain: \$sub\"; echo \"In: \$full\"; exit 1; fi; }"
229
+ echo ""
230
+ echo "PASS=0"
231
+ echo "FAIL=0"
232
+ echo ""
233
+ } > "$test_template_file"
234
+
235
+ local func_count=0
236
+ while IFS= read -r func; do
237
+ [[ -z "$func" ]] && continue
238
+ func_count=$((func_count + 1))
239
+ {
240
+ if [[ "$use_claude" == "true" ]]; then
241
+ local func_snippet
242
+ func_snippet=$(awk "/^${func}\(\\)/,/^[a-zA-Z_][a-zA-Z0-9_]*\(\)|^$/" "$target_script" 2>/dev/null | head -40 || true)
243
+ local prompt_file
244
+ prompt_file=$(mktemp "${TMPDIR:-/tmp}/sw-testgen-prompt.XXXXXX")
245
+ {
246
+ echo "Generate a bash test function for the following shell function. Use real assertions (assert_equal, assert_contains, or test exit code). Test happy path and at least one edge or error case. Output only the bash function body."
247
+ echo "Function name: ${func}"
248
+ echo "Function body:"
249
+ echo "$func_snippet"
250
+ } > "$prompt_file"
251
+ local claude_out
252
+ # Read prompt through pipe to avoid shell expansion of $vars in function body
253
+ claude_out=$(cat "$prompt_file" | claude -p --max-turns 2 2>/dev/null || true)
254
+ rm -f "$prompt_file"
255
+ if [[ -n "$claude_out" ]]; then
256
+ local code_block
257
+ code_block=$(echo "$claude_out" | sed -n '/^test_'"${func}"'()/,/^}/p' || echo "$claude_out" | sed -n '/^test_/,/^}/p' || true)
258
+ [[ -z "$code_block" ]] && code_block="$claude_out"
259
+ if echo "$code_block" | grep -qE 'assert_equal|assert_contains|\[\[.*\]\]|exit 1'; then
260
+ echo "test_${func}() {"
261
+ echo "$code_block" | sed 's/^test_'"${func}"'()//' | sed 's/^{//' | sed 's/^}//' | head -50
262
+ echo " ((PASS++))"
263
+ echo "}"
264
+ else
265
+ echo "test_${func}() {"
266
+ echo " # Claude-generated; review assertions"
267
+ echo "$code_block" | head -30 | sed 's/^/ /'
268
+ echo " ((PASS++))"
269
+ echo "}"
270
+ fi
271
+ else
272
+ echo "test_${func}() { # TODO: Claude unavailable"
273
+ echo " ((PASS++))"
274
+ echo "}"
275
+ fi
276
+ else
277
+ echo "test_${func}() {"
278
+ echo " # TODO: Implement test for $func"
279
+ echo " ((PASS++))"
280
+ echo "}"
281
+ fi
282
+ echo ""
283
+ } >> "$test_template_file"
284
+ done << EOF
285
+ $untested_functions
286
+ EOF
287
+
288
+ {
289
+ echo "# Run all tests"
290
+ echo "$untested_functions" | while IFS= read -r func; do
291
+ [[ -z "$func" ]] && continue
292
+ echo "test_${func}"
293
+ done
294
+ echo ""
295
+ echo "echo \"Results: \$PASS passed, \$FAIL failed\""
296
+ } >> "$test_template_file"
297
+
298
+ chmod +x "$test_template_file"
299
+ success "Generated test template: $test_template_file"
300
+ [[ "$use_claude" == "true" ]] && info "Used Claude for assertions; review and run tests to validate"
301
+ }
302
+
303
+ # ═══════════════════════════════════════════════════════════════════════════════
304
+ # GAP DETECTION
305
+ # ═══════════════════════════════════════════════════════════════════════════════
306
+
307
+ show_gaps() {
308
+ local target_script="${1:-.}"
309
+
310
+ info "Finding test gaps in $target_script"
311
+
312
+ # Extract functions and their line numbers
313
+ local gap_found=0
314
+ while IFS=: read -r line_num func_name; do
315
+ [[ -z "$func_name" ]] && continue
316
+ # Check if tested
317
+ local tested=false
318
+ for test_file in "$TEST_SUITES_DIR"/*-test.sh; do
319
+ [[ -f "$test_file" ]] || continue
320
+ if grep -q "$func_name" "$test_file" 2>/dev/null; then
321
+ tested=true
322
+ break
323
+ fi
324
+ done
325
+
326
+ if [[ "$tested" == "false" ]]; then
327
+ gap_found=1
328
+ echo -e " ${YELLOW}Gap at line $line_num:${RESET} $func_name()"
329
+ fi
330
+ done < <(grep -En '^[a-zA-Z_][a-zA-Z0-9_]*\(\)' "$target_script" | sed 's/:.*\([a-zA-Z_][a-zA-Z0-9_]*\)().*/:\1/' || true)
331
+
332
+ [[ $gap_found -eq 0 ]] && success "No test gaps found"
333
+ }
334
+
335
+ # ═══════════════════════════════════════════════════════════════════════════════
336
+ # TEST QUALITY SCORING
337
+ # ═══════════════════════════════════════════════════════════════════════════════
338
+
339
+ score_quality() {
340
+ local test_file="$1"
341
+
342
+ [[ -f "$test_file" ]] || {
343
+ error "Test file not found: $test_file"
344
+ return 1
345
+ }
346
+
347
+ info "Scoring test quality: $(basename "$test_file")"
348
+
349
+ # Count assertions
350
+ local assertion_count=0
351
+ assertion_count=$(grep -c -E '(assert_|test_|expect_)' "$test_file" || echo "0")
352
+
353
+ # Count edge case patterns
354
+ local edge_case_count=0
355
+ edge_case_count=$(grep -c -E '(empty|null|nil|missing|invalid|error|fail)' "$test_file" || echo "0")
356
+
357
+ # Count error path tests
358
+ local error_path_count=0
359
+ error_path_count=$(grep -c -E '(exit|return 1|error|ERROR)' "$test_file" || echo "0")
360
+
361
+ # Calculate score (0-100)
362
+ local quality_score=0
363
+ quality_score=$((assertion_count * 10 + edge_case_count * 5 + error_path_count * 5))
364
+ [[ $quality_score -gt 100 ]] && quality_score=100
365
+
366
+ echo ""
367
+ echo -e " ${CYAN}Assertions:${RESET} $assertion_count"
368
+ echo -e " ${CYAN}Edge cases:${RESET} $edge_case_count"
369
+ echo -e " ${CYAN}Error paths:${RESET} $error_path_count"
370
+ echo -e " ${CYAN}Quality score:${RESET} $quality_score/100"
371
+ echo ""
372
+
373
+ if [[ $quality_score -ge 80 ]]; then
374
+ success "Excellent test quality"
375
+ elif [[ $quality_score -ge 60 ]]; then
376
+ warn "Good test quality, could improve"
377
+ else
378
+ warn "Low test quality, needs improvement"
379
+ fi
380
+ }
381
+
382
+ # ═══════════════════════════════════════════════════════════════════════════════
383
+ # TEST MAINTENANCE
384
+ # ═══════════════════════════════════════════════════════════════════════════════
385
+
386
+ maintain_tests() {
387
+ local source_file="${1:-.}"
388
+
389
+ info "Checking test maintenance for $source_file"
390
+
391
+ # Check for modified functions
392
+ local modified_count=0
393
+ local test_files=("$TEST_SUITES_DIR"/*-test.sh)
394
+
395
+ for test_file in "${test_files[@]}"; do
396
+ [[ -f "$test_file" ]] || continue
397
+
398
+ # Extract functions tested by this test
399
+ while IFS= read -r func_name; do
400
+ [[ -z "$func_name" ]] && continue
401
+
402
+ # Check if function signature changed
403
+ if git diff --no-index "$source_file" "$source_file" 2>/dev/null | grep -q "$func_name"; then
404
+ modified_count=$((modified_count + 1))
405
+ warn "Function $func_name may need test updates"
406
+ fi
407
+ done < <(grep -E "$func_name\(" "$test_file" | sed 's/.*\([a-zA-Z_][a-zA-Z0-9_]*\)(.*/\1/' | sort -u || true)
408
+ done
409
+
410
+ if [[ $modified_count -eq 0 ]]; then
411
+ success "All tests up to date with source"
412
+ fi
413
+ }
414
+
415
+ # ═══════════════════════════════════════════════════════════════════════════════
416
+ # THRESHOLD MANAGEMENT
417
+ # ═══════════════════════════════════════════════════════════════════════════════
418
+
419
+ manage_threshold() {
420
+ local action="${1:-show}"
421
+ local value="${2:-}"
422
+
423
+ case "$action" in
424
+ set)
425
+ [[ -z "$value" ]] && {
426
+ error "Threshold value required"
427
+ return 1
428
+ }
429
+ COVERAGE_THRESHOLD="$value"
430
+ success "Coverage threshold set to $value%"
431
+ ;;
432
+ show)
433
+ info "Current coverage threshold: $COVERAGE_THRESHOLD%"
434
+ ;;
435
+ check)
436
+ # Compare current coverage against threshold
437
+ local coverage_pct
438
+ coverage_pct=$(analyze_coverage "${value:-.}" json 2>/dev/null | jq -r '.coverage_percent // 0')
439
+
440
+ if [[ $coverage_pct -lt $COVERAGE_THRESHOLD ]]; then
441
+ warn "Coverage ${coverage_pct}% below threshold ($COVERAGE_THRESHOLD%)"
442
+ return 1
443
+ else
444
+ success "Coverage ${coverage_pct}% meets threshold ($COVERAGE_THRESHOLD%)"
445
+ fi
446
+ ;;
447
+ *)
448
+ error "Unknown threshold action: $action"
449
+ return 1
450
+ ;;
451
+ esac
452
+ }
453
+
454
+ # ═══════════════════════════════════════════════════════════════════════════════
455
+ # REGRESSION DETECTION
456
+ # ═══════════════════════════════════════════════════════════════════════════════
457
+
458
+ detect_regressions() {
459
+ mkdir -p "$TESTGEN_DIR"
460
+
461
+ info "Scanning for test regressions"
462
+
463
+ # Run all test suites and capture results
464
+ local current_results=()
465
+ local test_file
466
+ for test_file in "$TEST_SUITES_DIR"/*-test.sh; do
467
+ [[ -f "$test_file" ]] || continue
468
+ [[ "$(basename "$test_file")" == "sw-testgen-test.sh" ]] && continue
469
+
470
+ local result
471
+ if bash "$test_file" &>/dev/null; then
472
+ result="PASS"
473
+ else
474
+ result="FAIL"
475
+ fi
476
+ current_results+=("$(basename "$test_file"):$result")
477
+ done
478
+
479
+ # Compare with previous results
480
+ if [[ -f "$REGRESSION_DB" ]]; then
481
+ info "Comparing with previous run..."
482
+
483
+ local regression_found=0
484
+ for entry in "${current_results[@]}"; do
485
+ local test_name="${entry%:*}"
486
+ local current_status="${entry##*:}"
487
+
488
+ local previous_status
489
+ previous_status=$(jq -r ".\"$test_name\" // \"UNKNOWN\"" "$REGRESSION_DB" 2>/dev/null || echo "UNKNOWN")
490
+
491
+ if [[ "$previous_status" == "PASS" && "$current_status" == "FAIL" ]]; then
492
+ warn "Regression detected: $test_name (was PASS, now FAIL)"
493
+ regression_found=$((regression_found + 1))
494
+ fi
495
+ done
496
+
497
+ if [[ $regression_found -eq 0 ]]; then
498
+ success "No regressions detected"
499
+ fi
500
+ fi
501
+
502
+ # Save current results for future comparison
503
+ {
504
+ jq -n '.' > "$REGRESSION_DB"
505
+ for entry in "${current_results[@]}"; do
506
+ local test_name="${entry%:*}"
507
+ local status="${entry##*:}"
508
+ jq --arg name "$test_name" --arg status "$status" '.[$name] = $status' "$REGRESSION_DB" > "$REGRESSION_DB.tmp"
509
+ mv "$REGRESSION_DB.tmp" "$REGRESSION_DB"
510
+ done
511
+ }
512
+
513
+ success "Regression detection saved to $REGRESSION_DB"
514
+ }
515
+
516
+ # ═══════════════════════════════════════════════════════════════════════════════
517
+ # MAIN COMMAND ROUTER
518
+ # ═══════════════════════════════════════════════════════════════════════════════
519
+
520
+ main() {
521
+ local cmd="${1:-help}"
522
+
523
+ case "$cmd" in
524
+ coverage)
525
+ shift || true
526
+ analyze_coverage "$@"
527
+ ;;
528
+ generate)
529
+ shift || true
530
+ generate_tests "$@"
531
+ ;;
532
+ gaps)
533
+ shift || true
534
+ show_gaps "$@"
535
+ ;;
536
+ quality)
537
+ shift || true
538
+ score_quality "$@"
539
+ ;;
540
+ maintain)
541
+ shift || true
542
+ maintain_tests "$@"
543
+ ;;
544
+ threshold)
545
+ shift || true
546
+ manage_threshold "$@"
547
+ ;;
548
+ regression)
549
+ shift || true
550
+ detect_regressions "$@"
551
+ ;;
552
+ help|--help|-h)
553
+ show_help
554
+ ;;
555
+ *)
556
+ error "Unknown command: $cmd"
557
+ echo ""
558
+ show_help
559
+ exit 1
560
+ ;;
561
+ esac
562
+ }
563
+
564
+ if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
565
+ main "$@"
566
+ fi