autonomous-coding-toolkit 1.0.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 (324) hide show
  1. package/.claude-plugin/marketplace.json +22 -0
  2. package/.claude-plugin/plugin.json +13 -0
  3. package/LICENSE +21 -0
  4. package/Makefile +21 -0
  5. package/README.md +140 -0
  6. package/SECURITY.md +28 -0
  7. package/agents/bash-expert.md +113 -0
  8. package/agents/dependency-auditor.md +138 -0
  9. package/agents/integration-tester.md +120 -0
  10. package/agents/lesson-scanner.md +149 -0
  11. package/agents/python-expert.md +179 -0
  12. package/agents/service-monitor.md +141 -0
  13. package/agents/shell-expert.md +147 -0
  14. package/benchmarks/runner.sh +147 -0
  15. package/benchmarks/tasks/01-rest-endpoint/rubric.sh +29 -0
  16. package/benchmarks/tasks/01-rest-endpoint/task.md +17 -0
  17. package/benchmarks/tasks/02-refactor-module/task.md +8 -0
  18. package/benchmarks/tasks/03-fix-integration-bug/task.md +8 -0
  19. package/benchmarks/tasks/04-add-test-coverage/task.md +8 -0
  20. package/benchmarks/tasks/05-multi-file-feature/task.md +8 -0
  21. package/bin/act.js +238 -0
  22. package/commands/autocode.md +6 -0
  23. package/commands/cancel-ralph.md +18 -0
  24. package/commands/code-factory.md +53 -0
  25. package/commands/create-prd.md +55 -0
  26. package/commands/ralph-loop.md +18 -0
  27. package/commands/run-plan.md +117 -0
  28. package/commands/submit-lesson.md +122 -0
  29. package/docs/ARCHITECTURE.md +630 -0
  30. package/docs/CONTRIBUTING.md +125 -0
  31. package/docs/lessons/0001-bare-exception-swallowing.md +34 -0
  32. package/docs/lessons/0002-async-def-without-await.md +28 -0
  33. package/docs/lessons/0003-create-task-without-callback.md +28 -0
  34. package/docs/lessons/0004-hardcoded-test-counts.md +28 -0
  35. package/docs/lessons/0005-sqlite-without-closing.md +33 -0
  36. package/docs/lessons/0006-venv-pip-path.md +27 -0
  37. package/docs/lessons/0007-runner-state-self-rejection.md +35 -0
  38. package/docs/lessons/0008-quality-gate-blind-spot.md +33 -0
  39. package/docs/lessons/0009-parser-overcount-empty-batches.md +36 -0
  40. package/docs/lessons/0010-local-outside-function-bash.md +33 -0
  41. package/docs/lessons/0011-batch-tests-for-unimplemented-code.md +36 -0
  42. package/docs/lessons/0012-api-markdown-unescaped-chars.md +33 -0
  43. package/docs/lessons/0013-export-prefix-env-parsing.md +33 -0
  44. package/docs/lessons/0014-decorator-registry-import-side-effect.md +43 -0
  45. package/docs/lessons/0015-frontend-backend-schema-drift.md +43 -0
  46. package/docs/lessons/0016-event-driven-cold-start-seeding.md +44 -0
  47. package/docs/lessons/0017-copy-paste-logic-diverges.md +43 -0
  48. package/docs/lessons/0018-layer-passes-pipeline-broken.md +45 -0
  49. package/docs/lessons/0019-systemd-envfile-ignores-export.md +41 -0
  50. package/docs/lessons/0020-persist-state-incrementally.md +44 -0
  51. package/docs/lessons/0021-dual-axis-testing.md +48 -0
  52. package/docs/lessons/0022-jsx-factory-shadowing.md +43 -0
  53. package/docs/lessons/0023-static-analysis-spiral.md +51 -0
  54. package/docs/lessons/0024-shared-pipeline-implementation.md +55 -0
  55. package/docs/lessons/0025-defense-in-depth-all-entry-points.md +65 -0
  56. package/docs/lessons/0026-linter-no-rules-false-enforcement.md +54 -0
  57. package/docs/lessons/0027-jsx-silent-prop-drop.md +64 -0
  58. package/docs/lessons/0028-no-infrastructure-in-client-code.md +49 -0
  59. package/docs/lessons/0029-never-write-secrets-to-files.md +61 -0
  60. package/docs/lessons/0030-cache-merge-not-replace.md +62 -0
  61. package/docs/lessons/0031-verify-units-at-boundaries.md +66 -0
  62. package/docs/lessons/0032-module-lifecycle-subscribe-unsubscribe.md +89 -0
  63. package/docs/lessons/0033-async-iteration-mutable-snapshot.md +72 -0
  64. package/docs/lessons/0034-caller-missing-await-silent-discard.md +65 -0
  65. package/docs/lessons/0035-duplicate-registration-silent-overwrite.md +85 -0
  66. package/docs/lessons/0036-websocket-dirty-disconnect.md +33 -0
  67. package/docs/lessons/0037-parallel-agents-worktree-corruption.md +31 -0
  68. package/docs/lessons/0038-subscribe-no-stored-ref.md +36 -0
  69. package/docs/lessons/0039-fallback-or-default-hides-bugs.md +34 -0
  70. package/docs/lessons/0040-event-firehose-filter-first.md +36 -0
  71. package/docs/lessons/0041-ambiguous-base-dir-path-nesting.md +32 -0
  72. package/docs/lessons/0042-spec-compliance-insufficient.md +36 -0
  73. package/docs/lessons/0043-exact-count-extensible-collections.md +32 -0
  74. package/docs/lessons/0044-relative-file-deps-worktree.md +39 -0
  75. package/docs/lessons/0045-iterative-design-improvement.md +33 -0
  76. package/docs/lessons/0046-plan-assertion-math-bugs.md +38 -0
  77. package/docs/lessons/0047-pytest-single-threaded-default.md +37 -0
  78. package/docs/lessons/0048-integration-wiring-batch.md +40 -0
  79. package/docs/lessons/0049-ab-verification.md +41 -0
  80. package/docs/lessons/0050-editing-sourced-files-during-execution.md +33 -0
  81. package/docs/lessons/0051-infrastructure-fixes-cant-self-heal.md +30 -0
  82. package/docs/lessons/0052-uncommitted-changes-poison-quality-gates.md +31 -0
  83. package/docs/lessons/0053-jq-compact-flag-inconsistency.md +31 -0
  84. package/docs/lessons/0054-parser-matches-inside-code-blocks.md +30 -0
  85. package/docs/lessons/0055-agents-compensate-for-garbled-prompts.md +31 -0
  86. package/docs/lessons/0056-grep-count-exit-code-on-zero.md +42 -0
  87. package/docs/lessons/0057-new-artifacts-break-git-clean-gates.md +42 -0
  88. package/docs/lessons/0058-dead-config-keys-never-consumed.md +49 -0
  89. package/docs/lessons/0059-contract-test-shared-structures.md +53 -0
  90. package/docs/lessons/0060-set-e-silent-death-in-runners.md +53 -0
  91. package/docs/lessons/0061-context-injection-dirty-state.md +50 -0
  92. package/docs/lessons/0062-sibling-bug-neighborhood-scan.md +29 -0
  93. package/docs/lessons/0063-one-flag-two-lifetimes.md +31 -0
  94. package/docs/lessons/0064-test-passes-wrong-reason.md +31 -0
  95. package/docs/lessons/0065-pipefail-grep-count-double-output.md +39 -0
  96. package/docs/lessons/0066-local-keyword-outside-function.md +37 -0
  97. package/docs/lessons/0067-stdin-hang-non-interactive-shell.md +36 -0
  98. package/docs/lessons/0068-agent-builds-wrong-thing-correctly.md +31 -0
  99. package/docs/lessons/0069-plan-quality-dominates-execution.md +30 -0
  100. package/docs/lessons/0070-spec-echo-back-prevents-drift.md +31 -0
  101. package/docs/lessons/0071-positive-instructions-outperform-negative.md +30 -0
  102. package/docs/lessons/0072-lost-in-the-middle-context-placement.md +30 -0
  103. package/docs/lessons/0073-unscoped-lessons-cause-false-positives.md +30 -0
  104. package/docs/lessons/0074-stale-context-injection-wrong-batch.md +32 -0
  105. package/docs/lessons/0075-research-artifacts-must-persist.md +32 -0
  106. package/docs/lessons/0076-wrong-decomposition-contaminates-downstream.md +30 -0
  107. package/docs/lessons/0077-cherry-pick-merges-need-manual-resolution.md +30 -0
  108. package/docs/lessons/0078-static-review-without-live-test.md +30 -0
  109. package/docs/lessons/0079-integration-wiring-batch-required.md +32 -0
  110. package/docs/lessons/FRAMEWORK.md +161 -0
  111. package/docs/lessons/SUMMARY.md +201 -0
  112. package/docs/lessons/TEMPLATE.md +85 -0
  113. package/docs/plans/2026-02-21-code-factory-v2-design.md +204 -0
  114. package/docs/plans/2026-02-21-code-factory-v2-implementation-plan.md +2189 -0
  115. package/docs/plans/2026-02-21-code-factory-v2-phase4-design.md +537 -0
  116. package/docs/plans/2026-02-21-code-factory-v2-phase4-implementation-plan.md +2012 -0
  117. package/docs/plans/2026-02-21-hardening-pass-design.md +108 -0
  118. package/docs/plans/2026-02-21-hardening-pass-plan.md +1378 -0
  119. package/docs/plans/2026-02-21-mab-research-report.md +406 -0
  120. package/docs/plans/2026-02-21-marketplace-restructure-design.md +240 -0
  121. package/docs/plans/2026-02-21-marketplace-restructure-plan.md +832 -0
  122. package/docs/plans/2026-02-21-phase4-completion-plan.md +697 -0
  123. package/docs/plans/2026-02-21-validator-suite-design.md +148 -0
  124. package/docs/plans/2026-02-21-validator-suite-plan.md +540 -0
  125. package/docs/plans/2026-02-22-mab-research-round2.md +556 -0
  126. package/docs/plans/2026-02-22-mab-run-design.md +462 -0
  127. package/docs/plans/2026-02-22-mab-run-plan.md +2046 -0
  128. package/docs/plans/2026-02-22-operations-design-methodology-research.md +681 -0
  129. package/docs/plans/2026-02-22-research-agent-failure-taxonomy.md +532 -0
  130. package/docs/plans/2026-02-22-research-code-guideline-policies.md +886 -0
  131. package/docs/plans/2026-02-22-research-codebase-audit-refactoring.md +908 -0
  132. package/docs/plans/2026-02-22-research-coding-standards-documentation.md +541 -0
  133. package/docs/plans/2026-02-22-research-competitive-landscape.md +687 -0
  134. package/docs/plans/2026-02-22-research-comprehensive-testing.md +1076 -0
  135. package/docs/plans/2026-02-22-research-context-utilization.md +459 -0
  136. package/docs/plans/2026-02-22-research-cost-quality-tradeoff.md +548 -0
  137. package/docs/plans/2026-02-22-research-lesson-transferability.md +508 -0
  138. package/docs/plans/2026-02-22-research-multi-agent-coordination.md +312 -0
  139. package/docs/plans/2026-02-22-research-phase-integration.md +602 -0
  140. package/docs/plans/2026-02-22-research-plan-quality.md +428 -0
  141. package/docs/plans/2026-02-22-research-prompt-engineering.md +558 -0
  142. package/docs/plans/2026-02-22-research-unconventional-perspectives.md +528 -0
  143. package/docs/plans/2026-02-22-research-user-adoption.md +638 -0
  144. package/docs/plans/2026-02-22-research-verification-effectiveness.md +433 -0
  145. package/docs/plans/2026-02-23-agent-suite-design.md +299 -0
  146. package/docs/plans/2026-02-23-agent-suite-plan.md +578 -0
  147. package/docs/plans/2026-02-23-phase3-cost-infrastructure-design.md +148 -0
  148. package/docs/plans/2026-02-23-phase3-cost-infrastructure-plan.md +1062 -0
  149. package/docs/plans/2026-02-23-research-bash-expert-agent.md +543 -0
  150. package/docs/plans/2026-02-23-research-dependency-auditor-agent.md +564 -0
  151. package/docs/plans/2026-02-23-research-improving-existing-agents.md +503 -0
  152. package/docs/plans/2026-02-23-research-integration-tester-agent.md +454 -0
  153. package/docs/plans/2026-02-23-research-python-expert-agent.md +429 -0
  154. package/docs/plans/2026-02-23-research-service-monitor-agent.md +425 -0
  155. package/docs/plans/2026-02-23-research-shell-expert-agent.md +533 -0
  156. package/docs/plans/2026-02-23-roadmap-to-completion.md +530 -0
  157. package/docs/plans/2026-02-24-headless-module-split-design.md +98 -0
  158. package/docs/plans/2026-02-24-headless-module-split.md +443 -0
  159. package/docs/plans/2026-02-24-lesson-scope-metadata-design.md +228 -0
  160. package/docs/plans/2026-02-24-lesson-scope-metadata-plan.md +968 -0
  161. package/docs/plans/2026-02-24-npm-packaging-design.md +841 -0
  162. package/docs/plans/2026-02-24-npm-packaging-plan.md +1965 -0
  163. package/docs/plans/audit-findings.md +186 -0
  164. package/docs/telegram-notification-format.md +98 -0
  165. package/examples/example-plan.md +51 -0
  166. package/examples/example-prd.json +72 -0
  167. package/examples/example-roadmap.md +33 -0
  168. package/examples/quickstart-plan.md +63 -0
  169. package/hooks/hooks.json +26 -0
  170. package/hooks/setup-symlinks.sh +48 -0
  171. package/hooks/stop-hook.sh +135 -0
  172. package/package.json +47 -0
  173. package/policies/bash.md +71 -0
  174. package/policies/python.md +71 -0
  175. package/policies/testing.md +61 -0
  176. package/policies/universal.md +60 -0
  177. package/scripts/analyze-report.sh +97 -0
  178. package/scripts/architecture-map.sh +145 -0
  179. package/scripts/auto-compound.sh +273 -0
  180. package/scripts/batch-audit.sh +42 -0
  181. package/scripts/batch-test.sh +101 -0
  182. package/scripts/entropy-audit.sh +221 -0
  183. package/scripts/failure-digest.sh +51 -0
  184. package/scripts/generate-ast-rules.sh +96 -0
  185. package/scripts/init.sh +112 -0
  186. package/scripts/lesson-check.sh +428 -0
  187. package/scripts/lib/common.sh +61 -0
  188. package/scripts/lib/cost-tracking.sh +153 -0
  189. package/scripts/lib/ollama.sh +60 -0
  190. package/scripts/lib/progress-writer.sh +128 -0
  191. package/scripts/lib/run-plan-context.sh +215 -0
  192. package/scripts/lib/run-plan-echo-back.sh +231 -0
  193. package/scripts/lib/run-plan-headless.sh +396 -0
  194. package/scripts/lib/run-plan-notify.sh +57 -0
  195. package/scripts/lib/run-plan-parser.sh +81 -0
  196. package/scripts/lib/run-plan-prompt.sh +215 -0
  197. package/scripts/lib/run-plan-quality-gate.sh +132 -0
  198. package/scripts/lib/run-plan-routing.sh +315 -0
  199. package/scripts/lib/run-plan-sampling.sh +170 -0
  200. package/scripts/lib/run-plan-scoring.sh +146 -0
  201. package/scripts/lib/run-plan-state.sh +142 -0
  202. package/scripts/lib/run-plan-team.sh +199 -0
  203. package/scripts/lib/telegram.sh +54 -0
  204. package/scripts/lib/thompson-sampling.sh +176 -0
  205. package/scripts/license-check.sh +74 -0
  206. package/scripts/mab-run.sh +575 -0
  207. package/scripts/module-size-check.sh +146 -0
  208. package/scripts/patterns/async-no-await.yml +5 -0
  209. package/scripts/patterns/bare-except.yml +6 -0
  210. package/scripts/patterns/empty-catch.yml +6 -0
  211. package/scripts/patterns/hardcoded-localhost.yml +9 -0
  212. package/scripts/patterns/retry-loop-no-backoff.yml +12 -0
  213. package/scripts/pipeline-status.sh +197 -0
  214. package/scripts/policy-check.sh +226 -0
  215. package/scripts/prior-art-search.sh +133 -0
  216. package/scripts/promote-mab-lessons.sh +126 -0
  217. package/scripts/prompts/agent-a-superpowers.md +29 -0
  218. package/scripts/prompts/agent-b-ralph.md +29 -0
  219. package/scripts/prompts/judge-agent.md +61 -0
  220. package/scripts/prompts/planner-agent.md +44 -0
  221. package/scripts/pull-community-lessons.sh +90 -0
  222. package/scripts/quality-gate.sh +266 -0
  223. package/scripts/research-gate.sh +90 -0
  224. package/scripts/run-plan.sh +329 -0
  225. package/scripts/scope-infer.sh +159 -0
  226. package/scripts/setup-ralph-loop.sh +155 -0
  227. package/scripts/telemetry.sh +230 -0
  228. package/scripts/tests/run-all-tests.sh +52 -0
  229. package/scripts/tests/test-act-cli.sh +46 -0
  230. package/scripts/tests/test-agents-md.sh +87 -0
  231. package/scripts/tests/test-analyze-report.sh +114 -0
  232. package/scripts/tests/test-architecture-map.sh +89 -0
  233. package/scripts/tests/test-auto-compound.sh +169 -0
  234. package/scripts/tests/test-batch-test.sh +65 -0
  235. package/scripts/tests/test-benchmark-runner.sh +25 -0
  236. package/scripts/tests/test-common.sh +168 -0
  237. package/scripts/tests/test-cost-tracking.sh +158 -0
  238. package/scripts/tests/test-echo-back.sh +180 -0
  239. package/scripts/tests/test-entropy-audit.sh +146 -0
  240. package/scripts/tests/test-failure-digest.sh +66 -0
  241. package/scripts/tests/test-generate-ast-rules.sh +145 -0
  242. package/scripts/tests/test-helpers.sh +82 -0
  243. package/scripts/tests/test-init.sh +47 -0
  244. package/scripts/tests/test-lesson-check.sh +278 -0
  245. package/scripts/tests/test-lesson-local.sh +55 -0
  246. package/scripts/tests/test-license-check.sh +109 -0
  247. package/scripts/tests/test-mab-run.sh +182 -0
  248. package/scripts/tests/test-ollama-lib.sh +49 -0
  249. package/scripts/tests/test-ollama.sh +60 -0
  250. package/scripts/tests/test-pipeline-status.sh +198 -0
  251. package/scripts/tests/test-policy-check.sh +124 -0
  252. package/scripts/tests/test-prior-art-search.sh +96 -0
  253. package/scripts/tests/test-progress-writer.sh +140 -0
  254. package/scripts/tests/test-promote-mab-lessons.sh +110 -0
  255. package/scripts/tests/test-pull-community-lessons.sh +149 -0
  256. package/scripts/tests/test-quality-gate.sh +241 -0
  257. package/scripts/tests/test-research-gate.sh +132 -0
  258. package/scripts/tests/test-run-plan-cli.sh +86 -0
  259. package/scripts/tests/test-run-plan-context.sh +305 -0
  260. package/scripts/tests/test-run-plan-e2e.sh +153 -0
  261. package/scripts/tests/test-run-plan-headless.sh +424 -0
  262. package/scripts/tests/test-run-plan-notify.sh +124 -0
  263. package/scripts/tests/test-run-plan-parser.sh +217 -0
  264. package/scripts/tests/test-run-plan-prompt.sh +254 -0
  265. package/scripts/tests/test-run-plan-quality-gate.sh +222 -0
  266. package/scripts/tests/test-run-plan-routing.sh +178 -0
  267. package/scripts/tests/test-run-plan-scoring.sh +148 -0
  268. package/scripts/tests/test-run-plan-state.sh +261 -0
  269. package/scripts/tests/test-run-plan-team.sh +157 -0
  270. package/scripts/tests/test-scope-infer.sh +150 -0
  271. package/scripts/tests/test-setup-ralph-loop.sh +63 -0
  272. package/scripts/tests/test-telegram-env.sh +38 -0
  273. package/scripts/tests/test-telegram.sh +121 -0
  274. package/scripts/tests/test-telemetry.sh +46 -0
  275. package/scripts/tests/test-thompson-sampling.sh +139 -0
  276. package/scripts/tests/test-validate-all.sh +60 -0
  277. package/scripts/tests/test-validate-commands.sh +89 -0
  278. package/scripts/tests/test-validate-hooks.sh +98 -0
  279. package/scripts/tests/test-validate-lessons.sh +150 -0
  280. package/scripts/tests/test-validate-plan-quality.sh +235 -0
  281. package/scripts/tests/test-validate-plans.sh +187 -0
  282. package/scripts/tests/test-validate-plugin.sh +106 -0
  283. package/scripts/tests/test-validate-prd.sh +184 -0
  284. package/scripts/tests/test-validate-skills.sh +134 -0
  285. package/scripts/validate-all.sh +57 -0
  286. package/scripts/validate-commands.sh +67 -0
  287. package/scripts/validate-hooks.sh +89 -0
  288. package/scripts/validate-lessons.sh +98 -0
  289. package/scripts/validate-plan-quality.sh +369 -0
  290. package/scripts/validate-plans.sh +120 -0
  291. package/scripts/validate-plugin.sh +86 -0
  292. package/scripts/validate-policies.sh +42 -0
  293. package/scripts/validate-prd.sh +118 -0
  294. package/scripts/validate-skills.sh +96 -0
  295. package/skills/autocode/SKILL.md +285 -0
  296. package/skills/autocode/ab-verification.md +51 -0
  297. package/skills/autocode/code-quality-standards.md +37 -0
  298. package/skills/autocode/competitive-mode.md +364 -0
  299. package/skills/brainstorming/SKILL.md +97 -0
  300. package/skills/capture-lesson/SKILL.md +187 -0
  301. package/skills/check-lessons/SKILL.md +116 -0
  302. package/skills/dispatching-parallel-agents/SKILL.md +110 -0
  303. package/skills/executing-plans/SKILL.md +85 -0
  304. package/skills/finishing-a-development-branch/SKILL.md +201 -0
  305. package/skills/receiving-code-review/SKILL.md +72 -0
  306. package/skills/requesting-code-review/SKILL.md +59 -0
  307. package/skills/requesting-code-review/code-reviewer.md +82 -0
  308. package/skills/research/SKILL.md +145 -0
  309. package/skills/roadmap/SKILL.md +115 -0
  310. package/skills/subagent-driven-development/SKILL.md +98 -0
  311. package/skills/subagent-driven-development/code-quality-reviewer-prompt.md +18 -0
  312. package/skills/subagent-driven-development/implementer-prompt.md +73 -0
  313. package/skills/subagent-driven-development/spec-reviewer-prompt.md +57 -0
  314. package/skills/systematic-debugging/SKILL.md +134 -0
  315. package/skills/systematic-debugging/condition-based-waiting.md +64 -0
  316. package/skills/systematic-debugging/defense-in-depth.md +32 -0
  317. package/skills/systematic-debugging/root-cause-tracing.md +55 -0
  318. package/skills/test-driven-development/SKILL.md +167 -0
  319. package/skills/using-git-worktrees/SKILL.md +219 -0
  320. package/skills/using-superpowers/SKILL.md +54 -0
  321. package/skills/verification-before-completion/SKILL.md +140 -0
  322. package/skills/verify/SKILL.md +82 -0
  323. package/skills/writing-plans/SKILL.md +128 -0
  324. package/skills/writing-skills/SKILL.md +93 -0
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+ source "$SCRIPT_DIR/test-helpers.sh"
6
+
7
+ # Source the library under test
8
+ source "$SCRIPT_DIR/../lib/thompson-sampling.sh"
9
+
10
+ # --- Test: thompson_sample returns float in [0,1] ---
11
+ sample=$(thompson_sample 10 5)
12
+ TESTS=$((TESTS + 1))
13
+ # Check it's a valid float between 0 and 1 using bc
14
+ is_valid=$(echo "$sample >= 0 && $sample <= 1" | bc -l 2>/dev/null || echo "0")
15
+ if [[ "$is_valid" == "1" ]]; then
16
+ echo "PASS: thompson_sample 10 5 returns float in [0,1] (got $sample)"
17
+ else
18
+ echo "FAIL: thompson_sample 10 5 returned '$sample' (expected float in [0,1])"
19
+ FAILURES=$((FAILURES + 1))
20
+ fi
21
+
22
+ # --- Test: thompson_route returns "mab" when perf file missing ---
23
+ route_missing=$(thompson_route "new-file" "/tmp/nonexistent-perf-$$.json")
24
+ assert_eq "missing perf file → mab" "mab" "$route_missing"
25
+
26
+ # --- Test: thompson_route returns "mab" when type has < 5 data points ---
27
+ TEST_TMPDIR=$(mktemp -d)
28
+ trap 'rm -rf "$TEST_TMPDIR"' EXIT
29
+
30
+ cat > "$TEST_TMPDIR/perf.json" <<'JSON'
31
+ {
32
+ "new-file": {"superpowers": {"wins": 2, "losses": 1}, "ralph": {"wins": 1, "losses": 0}},
33
+ "refactoring": {"superpowers": {"wins": 0, "losses": 0}, "ralph": {"wins": 0, "losses": 0}},
34
+ "integration": {"superpowers": {"wins": 5, "losses": 2}, "ralph": {"wins": 3, "losses": 4}},
35
+ "test-only": {"superpowers": {"wins": 0, "losses": 0}, "ralph": {"wins": 0, "losses": 0}},
36
+ "calibration_count": 0,
37
+ "calibration_complete": false
38
+ }
39
+ JSON
40
+
41
+ route_few=$(thompson_route "new-file" "$TEST_TMPDIR/perf.json")
42
+ assert_eq "< 5 data points → mab" "mab" "$route_few"
43
+
44
+ # --- Test: thompson_route returns "mab" for integration type (always explore) ---
45
+ cat > "$TEST_TMPDIR/perf-int.json" <<'JSON'
46
+ {
47
+ "new-file": {"superpowers": {"wins": 10, "losses": 2}, "ralph": {"wins": 3, "losses": 9}},
48
+ "refactoring": {"superpowers": {"wins": 10, "losses": 2}, "ralph": {"wins": 3, "losses": 9}},
49
+ "integration": {"superpowers": {"wins": 10, "losses": 2}, "ralph": {"wins": 3, "losses": 9}},
50
+ "test-only": {"superpowers": {"wins": 10, "losses": 2}, "ralph": {"wins": 3, "losses": 9}},
51
+ "calibration_count": 10,
52
+ "calibration_complete": true
53
+ }
54
+ JSON
55
+
56
+ route_int=$(thompson_route "integration" "$TEST_TMPDIR/perf-int.json")
57
+ assert_eq "integration type → always mab" "mab" "$route_int"
58
+
59
+ # --- Test: thompson_route returns winning strategy with strong signal ---
60
+ # superpowers: 15 wins / 3 losses = 83% win rate, 18 total points
61
+ # ralph: 3 wins / 15 losses = 17% win rate
62
+ # Run 10 times and check majority goes to superpowers
63
+ superpowers_count=0
64
+ for i in $(seq 1 10); do
65
+ result=$(thompson_route "new-file" "$TEST_TMPDIR/perf-int.json")
66
+ if [[ "$result" == "superpowers" ]]; then
67
+ superpowers_count=$((superpowers_count + 1))
68
+ fi
69
+ done
70
+
71
+ TESTS=$((TESTS + 1))
72
+ if [[ "$superpowers_count" -ge 7 ]]; then
73
+ echo "PASS: strong signal routes to winner (superpowers won $superpowers_count/10)"
74
+ else
75
+ echo "FAIL: strong signal should route to superpowers ≥7/10 times (got $superpowers_count/10)"
76
+ FAILURES=$((FAILURES + 1))
77
+ fi
78
+
79
+ # --- Test: init_strategy_perf creates valid JSON ---
80
+ init_file="$TEST_TMPDIR/init-perf.json"
81
+ init_strategy_perf "$init_file"
82
+
83
+ TESTS=$((TESTS + 1))
84
+ if [[ -f "$init_file" ]] && jq . "$init_file" > /dev/null 2>&1; then
85
+ echo "PASS: init_strategy_perf creates valid JSON"
86
+ else
87
+ echo "FAIL: init_strategy_perf did not create valid JSON"
88
+ FAILURES=$((FAILURES + 1))
89
+ fi
90
+
91
+ # Check all 4 batch types present
92
+ for bt in "new-file" "refactoring" "integration" "test-only"; do
93
+ has_type=$(jq --arg bt "$bt" 'has($bt)' "$init_file" 2>/dev/null)
94
+ assert_eq "init has $bt type" "true" "$has_type"
95
+ done
96
+
97
+ # Check calibration fields
98
+ has_cal_count=$(jq 'has("calibration_count")' "$init_file" 2>/dev/null)
99
+ assert_eq "init has calibration_count" "true" "$has_cal_count"
100
+
101
+ has_cal_complete=$(jq 'has("calibration_complete")' "$init_file" 2>/dev/null)
102
+ assert_eq "init has calibration_complete" "true" "$has_cal_complete"
103
+
104
+ # --- Test: update_strategy_perf increments wins/losses ---
105
+ update_file="$TEST_TMPDIR/update-perf.json"
106
+ init_strategy_perf "$update_file"
107
+
108
+ # Superpowers wins a new-file batch
109
+ update_strategy_perf "$update_file" "new-file" "superpowers"
110
+ sp_wins=$(jq '."new-file".superpowers.wins' "$update_file" 2>/dev/null)
111
+ assert_eq "superpowers wins incremented" "1" "$sp_wins"
112
+
113
+ # Ralph loses (the other side)
114
+ ralph_losses=$(jq '."new-file".ralph.losses' "$update_file" 2>/dev/null)
115
+ assert_eq "ralph losses incremented" "1" "$ralph_losses"
116
+
117
+ # Do it again
118
+ update_strategy_perf "$update_file" "new-file" "superpowers"
119
+ sp_wins2=$(jq '."new-file".superpowers.wins' "$update_file" 2>/dev/null)
120
+ assert_eq "superpowers wins incremented again" "2" "$sp_wins2"
121
+
122
+ # --- Test: thompson_route spread-too-close returns "mab" ---
123
+ # When both strategies have similar win rates (spread < 15%), route to mab.
124
+ # superpowers: 6W/4L = 60%, ralph: 5W/5L = 50% → spread = 10% < 15% → "mab"
125
+ cat > "$TEST_TMPDIR/perf-close.json" <<'JSON'
126
+ {
127
+ "new-file": {"superpowers": {"wins": 6, "losses": 4}, "ralph": {"wins": 5, "losses": 5}},
128
+ "refactoring": {"superpowers": {"wins": 6, "losses": 4}, "ralph": {"wins": 5, "losses": 5}},
129
+ "integration": {"superpowers": {"wins": 6, "losses": 4}, "ralph": {"wins": 5, "losses": 5}},
130
+ "test-only": {"superpowers": {"wins": 6, "losses": 4}, "ralph": {"wins": 5, "losses": 5}},
131
+ "calibration_count": 20,
132
+ "calibration_complete": true
133
+ }
134
+ JSON
135
+
136
+ route_close=$(thompson_route "new-file" "$TEST_TMPDIR/perf-close.json")
137
+ assert_eq "spread < 15% → mab (too close to call)" "mab" "$route_close"
138
+
139
+ report_results
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env bash
2
+ # Test validate-all.sh
3
+ set -euo pipefail
4
+
5
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
+ source "$SCRIPT_DIR/test-helpers.sh"
7
+
8
+ VALIDATOR="$SCRIPT_DIR/../validate-all.sh"
9
+
10
+ # === Test: Runs on actual toolkit with --warn and reports summary ===
11
+ output=$(bash "$VALIDATOR" --warn 2>&1 || echo "EXIT:$?")
12
+ assert_contains "actual toolkit --warn: shows summary" "validators passed" "$output"
13
+ assert_not_contains "actual toolkit --warn: exit 0" "EXIT:" "$output"
14
+
15
+ # === Test: Reports individual validator names ===
16
+ output=$(bash "$VALIDATOR" --warn 2>&1 || echo "EXIT:$?")
17
+ assert_contains "shows lessons" "validate-lessons" "$output"
18
+ assert_contains "shows skills" "validate-skills" "$output"
19
+ assert_contains "shows commands" "validate-commands" "$output"
20
+ assert_contains "shows plugin" "validate-plugin" "$output"
21
+ assert_contains "shows hooks" "validate-hooks" "$output"
22
+
23
+ # === Test: Actual toolkit passes strict (no --warn) ===
24
+ output=$(bash "$VALIDATOR" 2>&1 || echo "EXIT:$?")
25
+ assert_contains "actual toolkit strict: shows summary" "validators passed" "$output"
26
+ assert_not_contains "actual toolkit strict: exit 0" "EXIT:" "$output"
27
+
28
+ # === Test: --help exits 0 ===
29
+ output=$(bash "$VALIDATOR" --help 2>&1 || echo "EXIT:$?")
30
+ assert_contains "--help: shows usage" "Usage:" "$output"
31
+ assert_not_contains "--help: exit 0" "EXIT:" "$output"
32
+
33
+ # === Test: No args (empty PASS_ARGS) works under set -u (#23) ===
34
+ # This specifically tests that empty array expansion doesn't error under set -u
35
+ output=$(bash "$VALIDATOR" --warn 2>&1; echo "EXIT:$?")
36
+ assert_contains "empty PASS_ARGS: succeeds" "EXIT:0" "$output"
37
+
38
+ # === Test: Reports FAIL and exits 1 when a validator fails ===
39
+ # Create a controlled fixture with an invalid skill to trigger failure
40
+ TMP_DIR=$(mktemp -d)
41
+ trap 'rm -rf "$TMP_DIR"' EXIT
42
+ # Create a minimal toolkit structure with a broken skill
43
+ mkdir -p "$TMP_DIR/skills/broken-skill"
44
+ # Empty dir = missing SKILL.md → validate-skills fails
45
+ # Create stub validators that pass, except skills points to our broken fixture
46
+ mkdir -p "$TMP_DIR/scripts"
47
+ for v in validate-lessons validate-commands validate-plugin validate-hooks; do
48
+ echo '#!/usr/bin/env bash' > "$TMP_DIR/scripts/${v}.sh"
49
+ echo 'echo "'"$v"': PASS"; exit 0' >> "$TMP_DIR/scripts/${v}.sh"
50
+ done
51
+ # validate-skills uses SKILLS_DIR env — point to broken fixture
52
+ echo '#!/usr/bin/env bash' > "$TMP_DIR/scripts/validate-skills.sh"
53
+ echo 'echo "broken-skill: Missing SKILL.md"; echo "validate-skills: FAIL"; exit 1' >> "$TMP_DIR/scripts/validate-skills.sh"
54
+ # Copy validate-all.sh to the temp dir
55
+ cp "$VALIDATOR" "$TMP_DIR/scripts/validate-all.sh"
56
+ output=$(bash "$TMP_DIR/scripts/validate-all.sh" 2>&1 || echo "EXIT:$?")
57
+ assert_contains "reports failures: exit 1" "EXIT:1" "$output"
58
+ assert_contains "reports failures: shows Failed" "Failed:" "$output"
59
+
60
+ report_results
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env bash
2
+ # Test validate-commands.sh
3
+ set -euo pipefail
4
+
5
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
+ source "$SCRIPT_DIR/test-helpers.sh"
7
+
8
+ VALIDATOR="$SCRIPT_DIR/../validate-commands.sh"
9
+ WORK=$(mktemp -d)
10
+ trap 'rm -rf "$WORK"' EXIT
11
+
12
+ # Helper: create a command file
13
+ create_command() {
14
+ local name="$1" content="$2"
15
+ mkdir -p "$WORK/commands"
16
+ echo "$content" > "$WORK/commands/$name"
17
+ }
18
+
19
+ # Helper: run validator against temp commands dir
20
+ run_validator() {
21
+ local exit_code=0
22
+ COMMANDS_DIR="$WORK/commands" bash "$VALIDATOR" "$@" 2>&1 || exit_code=$?
23
+ echo "EXIT:$exit_code"
24
+ }
25
+
26
+ # === Test: Valid command passes ===
27
+ rm -rf "$WORK/commands"
28
+ create_command "my-command.md" '---
29
+ description: "A test command"
30
+ ---
31
+ Body text here.'
32
+
33
+ output=$(run_validator)
34
+ assert_contains "valid command: PASS" "validate-commands: PASS" "$output"
35
+ assert_contains "valid command: exit 0" "EXIT:0" "$output"
36
+
37
+ # === Test: Missing description field fails ===
38
+ rm -rf "$WORK/commands"
39
+ create_command "no-desc.md" '---
40
+ argument-hint: "<thing>"
41
+ ---
42
+ Body.'
43
+
44
+ output=$(run_validator)
45
+ assert_contains "missing description: reports violation" "Missing required field: description" "$output"
46
+ assert_contains "missing description: exit 1" "EXIT:1" "$output"
47
+
48
+ # === Test: Missing frontmatter start fails ===
49
+ rm -rf "$WORK/commands"
50
+ create_command "no-front.md" 'description: "No frontmatter markers"
51
+ Some body.'
52
+
53
+ output=$(run_validator)
54
+ assert_contains "missing ---: reports violation" "First line must be '---'" "$output"
55
+ assert_contains "missing ---: exit 1" "EXIT:1" "$output"
56
+
57
+ # === Test: Missing closing --- fails ===
58
+ rm -rf "$WORK/commands"
59
+ create_command "no-close.md" '---
60
+ description: "Unclosed frontmatter"
61
+ Body text without closing delimiter.'
62
+
63
+ output=$(run_validator)
64
+ assert_contains "missing close ---: reports violation" "Frontmatter not closed" "$output"
65
+ assert_contains "missing close ---: exit 1" "EXIT:1" "$output"
66
+
67
+ # === Test: --warn exits 0 even with violations ===
68
+ rm -rf "$WORK/commands"
69
+ create_command "warn-test.md" '---
70
+ argument-hint: "<thing>"
71
+ ---
72
+ Body.'
73
+
74
+ output=$(run_validator --warn)
75
+ assert_contains "--warn: still reports violation" "Missing required field: description" "$output"
76
+ assert_contains "--warn: exits 0" "EXIT:0" "$output"
77
+
78
+ # === Test: --help exits 0 ===
79
+ output=$(run_validator --help)
80
+ assert_contains "--help: shows usage" "Usage:" "$output"
81
+ assert_contains "--help: exits 0" "EXIT:0" "$output"
82
+
83
+ # === Test: Missing commands directory fails ===
84
+ rm -rf "$WORK/commands"
85
+ output=$(COMMANDS_DIR="$WORK/nonexistent" bash "$VALIDATOR" 2>&1 || echo "EXIT:$?")
86
+ assert_contains "missing dir: error message" "commands directory not found" "$output"
87
+ assert_contains "missing dir: exit 1" "EXIT:1" "$output"
88
+
89
+ report_results
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env bash
2
+ # Test validate-hooks.sh
3
+ set -euo pipefail
4
+
5
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
+ source "$SCRIPT_DIR/test-helpers.sh"
7
+
8
+ VALIDATOR="$SCRIPT_DIR/../validate-hooks.sh"
9
+ WORK=$(mktemp -d)
10
+ trap 'rm -rf "$WORK"' EXIT
11
+
12
+ # Helper: create hooks dir with hooks.json
13
+ create_hooks() {
14
+ local content="$1"
15
+ mkdir -p "$WORK/hooks"
16
+ echo "$content" > "$WORK/hooks/hooks.json"
17
+ }
18
+
19
+ # Helper: create a script file (optionally executable)
20
+ create_script() {
21
+ local path="$1" executable="${2:-true}"
22
+ mkdir -p "$(dirname "$WORK/$path")"
23
+ echo '#!/usr/bin/env bash' > "$WORK/$path"
24
+ if [[ "$executable" == "true" ]]; then chmod +x "$WORK/$path"; fi
25
+ }
26
+
27
+ # Helper: run validator against temp dir
28
+ run_validator() {
29
+ local exit_code=0
30
+ HOOKS_DIR="$WORK/hooks" TOOLKIT_ROOT="$WORK" bash "$VALIDATOR" "$@" 2>&1 || exit_code=$?
31
+ echo "EXIT:$exit_code"
32
+ }
33
+
34
+ # === Test: Valid hooks.json with existing executable script passes ===
35
+ create_hooks '{
36
+ "hooks": {
37
+ "Stop": [{"hooks": [{"type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/stop-hook.sh"}]}]
38
+ }
39
+ }'
40
+ create_script "hooks/stop-hook.sh"
41
+
42
+ output=$(run_validator)
43
+ assert_contains "valid hooks: PASS" "validate-hooks: PASS" "$output"
44
+ assert_contains "valid hooks: exit 0" "EXIT:0" "$output"
45
+
46
+ # === Test: Nonexistent script fails ===
47
+ create_hooks '{
48
+ "hooks": {
49
+ "Stop": [{"hooks": [{"type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/missing.sh"}]}]
50
+ }
51
+ }'
52
+
53
+ output=$(run_validator)
54
+ assert_contains "missing script: reports violation" "script not found" "$output"
55
+ assert_contains "missing script: exit 1" "EXIT:1" "$output"
56
+
57
+ # === Test: Non-executable script fails ===
58
+ create_hooks '{
59
+ "hooks": {
60
+ "Stop": [{"hooks": [{"type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/not-exec.sh"}]}]
61
+ }
62
+ }'
63
+ create_script "hooks/not-exec.sh" "false"
64
+
65
+ output=$(run_validator)
66
+ assert_contains "not executable: reports violation" "not executable" "$output"
67
+ assert_contains "not executable: exit 1" "EXIT:1" "$output"
68
+
69
+ # === Test: Invalid JSON fails ===
70
+ create_hooks '{invalid json'
71
+
72
+ output=$(run_validator)
73
+ assert_contains "invalid JSON: error" "hooks.json is not valid JSON" "$output"
74
+ assert_contains "invalid JSON: exit 1" "EXIT:1" "$output"
75
+
76
+ # === Test: Missing hooks.json fails ===
77
+ rm -rf "$WORK/hooks"
78
+ output=$(HOOKS_DIR="$WORK/nonexistent" TOOLKIT_ROOT="$WORK" bash "$VALIDATOR" 2>&1 || echo "EXIT:$?")
79
+ assert_contains "missing dir: error message" "hooks directory not found" "$output"
80
+ assert_contains "missing dir: exit 1" "EXIT:1" "$output"
81
+
82
+ # === Test: --warn exits 0 even with violations ===
83
+ create_hooks '{
84
+ "hooks": {
85
+ "Stop": [{"hooks": [{"type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/missing.sh"}]}]
86
+ }
87
+ }'
88
+
89
+ output=$(run_validator --warn)
90
+ assert_contains "--warn: still reports violation" "script not found" "$output"
91
+ assert_contains "--warn: exits 0" "EXIT:0" "$output"
92
+
93
+ # === Test: --help exits 0 ===
94
+ output=$(run_validator --help)
95
+ assert_contains "--help: shows usage" "Usage:" "$output"
96
+ assert_contains "--help: exits 0" "EXIT:0" "$output"
97
+
98
+ report_results
@@ -0,0 +1,150 @@
1
+ #!/usr/bin/env bash
2
+ # Test validate-lessons.sh
3
+ set -euo pipefail
4
+
5
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
+ source "$SCRIPT_DIR/test-helpers.sh"
7
+
8
+ VALIDATOR="$SCRIPT_DIR/../validate-lessons.sh"
9
+ WORK=$(mktemp -d)
10
+ trap 'rm -rf "$WORK"' EXIT
11
+
12
+ # Helper: create a lesson file in the temp lessons dir
13
+ create_lesson() {
14
+ local name="$1" content="$2"
15
+ local dir="$WORK/lessons"
16
+ mkdir -p "$dir"
17
+ echo "$content" > "$dir/$name"
18
+ }
19
+
20
+ # Helper: run validator against temp lessons dir
21
+ run_validator() {
22
+ local exit_code=0
23
+ LESSONS_DIR="$WORK/lessons" bash "$VALIDATOR" "$@" 2>&1 || exit_code=$?
24
+ echo "EXIT:$exit_code"
25
+ }
26
+
27
+ # === Test: Valid lesson passes ===
28
+ rm -rf "$WORK/lessons"
29
+ create_lesson "0001-test.md" '---
30
+ id: 1
31
+ title: "Test lesson"
32
+ severity: blocker
33
+ languages: [python]
34
+ pattern:
35
+ type: syntactic
36
+ regex: "foo"
37
+ ---
38
+ Body text here.'
39
+
40
+ output=$(run_validator)
41
+ assert_contains "valid lesson: PASS" "validate-lessons: PASS" "$output"
42
+ assert_contains "valid lesson: exit 0" "EXIT:0" "$output"
43
+
44
+ # === Test: Missing --- start line fails ===
45
+ rm -rf "$WORK/lessons"
46
+ create_lesson "0002-bad-start.md" 'id: 1
47
+ title: "No frontmatter marker"
48
+ severity: blocker
49
+ languages: [python]
50
+ ---'
51
+
52
+ output=$(run_validator)
53
+ assert_contains "missing ---: reports violation" "First line must be" "$output"
54
+ assert_contains "missing ---: FAIL" "FAIL" "$output"
55
+ assert_contains "missing ---: exit 1" "EXIT:1" "$output"
56
+
57
+ # === Test: Missing required field fails ===
58
+ rm -rf "$WORK/lessons"
59
+ create_lesson "0003-missing-field.md" '---
60
+ id: 3
61
+ severity: blocker
62
+ languages: [python]
63
+ pattern:
64
+ type: semantic
65
+ ---'
66
+
67
+ output=$(run_validator)
68
+ assert_contains "missing title: reports violation" "Missing required field: title" "$output"
69
+ assert_contains "missing title: exit 1" "EXIT:1" "$output"
70
+
71
+ # === Test: Duplicate IDs fail ===
72
+ rm -rf "$WORK/lessons"
73
+ create_lesson "0004-dup-a.md" '---
74
+ id: 42
75
+ title: "First"
76
+ severity: blocker
77
+ languages: [python]
78
+ pattern:
79
+ type: semantic
80
+ ---'
81
+ create_lesson "0005-dup-b.md" '---
82
+ id: 42
83
+ title: "Second"
84
+ severity: blocker
85
+ languages: [python]
86
+ pattern:
87
+ type: semantic
88
+ ---'
89
+
90
+ output=$(run_validator)
91
+ assert_contains "duplicate IDs: reports violation" "Duplicate lesson ID: 42" "$output"
92
+ assert_contains "duplicate IDs: exit 1" "EXIT:1" "$output"
93
+
94
+ # === Test: Invalid severity fails ===
95
+ rm -rf "$WORK/lessons"
96
+ create_lesson "0006-bad-severity.md" '---
97
+ id: 6
98
+ title: "Bad severity"
99
+ severity: critical
100
+ languages: [python]
101
+ pattern:
102
+ type: semantic
103
+ ---'
104
+
105
+ output=$(run_validator)
106
+ assert_contains "invalid severity: reports violation" "Invalid severity" "$output"
107
+ assert_contains "invalid severity: exit 1" "EXIT:1" "$output"
108
+
109
+ # === Test: Syntactic without regex fails ===
110
+ rm -rf "$WORK/lessons"
111
+ create_lesson "0007-no-regex.md" '---
112
+ id: 7
113
+ title: "Missing regex"
114
+ severity: blocker
115
+ languages: [python]
116
+ pattern:
117
+ type: syntactic
118
+ ---'
119
+
120
+ output=$(run_validator)
121
+ assert_contains "syntactic no regex: reports violation" "Syntactic lesson missing regex field" "$output"
122
+ assert_contains "syntactic no regex: exit 1" "EXIT:1" "$output"
123
+
124
+ # === Test: --warn exits 0 even with violations ===
125
+ rm -rf "$WORK/lessons"
126
+ create_lesson "0008-warn-test.md" '---
127
+ id: 8
128
+ severity: blocker
129
+ languages: [python]
130
+ pattern:
131
+ type: semantic
132
+ ---'
133
+
134
+ output=$(run_validator --warn)
135
+ assert_contains "--warn: still reports violation" "Missing required field: title" "$output"
136
+ assert_contains "--warn: exits 0" "EXIT:0" "$output"
137
+
138
+ # === Test: --help exits 0 ===
139
+ output=$(run_validator --help)
140
+ assert_contains "--help: shows usage" "Usage:" "$output"
141
+ assert_contains "--help: exits 0" "EXIT:0" "$output"
142
+
143
+ # === Test: Missing lessons directory fails ===
144
+ SAVE_DIR="$WORK/lessons"
145
+ rm -rf "$WORK/lessons"
146
+ output=$(LESSONS_DIR="$WORK/nonexistent" bash "$VALIDATOR" 2>&1 || echo "EXIT:$?")
147
+ assert_contains "missing dir: error message" "lessons directory not found" "$output"
148
+ assert_contains "missing dir: exit 1" "EXIT:1" "$output"
149
+
150
+ report_results