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,424 @@
1
+ #!/usr/bin/env bash
2
+ # Test run-plan-headless.sh extraction
3
+ set -euo pipefail
4
+
5
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
+ RP="$SCRIPT_DIR/../run-plan.sh"
7
+ RPH="$SCRIPT_DIR/../lib/run-plan-headless.sh"
8
+ RPEB="$SCRIPT_DIR/../lib/run-plan-echo-back.sh"
9
+ RPS="$SCRIPT_DIR/../lib/run-plan-sampling.sh"
10
+
11
+ FAILURES=0
12
+ TESTS=0
13
+
14
+ assert_eq() {
15
+ local desc="$1" expected="$2" actual="$3"
16
+ TESTS=$((TESTS + 1))
17
+ if [[ "$expected" != "$actual" ]]; then
18
+ echo "FAIL: $desc"
19
+ echo " expected: $expected"
20
+ echo " actual: $actual"
21
+ FAILURES=$((FAILURES + 1))
22
+ else
23
+ echo "PASS: $desc"
24
+ fi
25
+ }
26
+
27
+ # === Extracted file exists ===
28
+
29
+ TESTS=$((TESTS + 1))
30
+ if [[ -f "$RPH" ]]; then
31
+ echo "PASS: run-plan-headless.sh exists"
32
+ else
33
+ echo "FAIL: run-plan-headless.sh should exist at scripts/lib/"
34
+ FAILURES=$((FAILURES + 1))
35
+ fi
36
+
37
+ # === Extracted echo-back file exists ===
38
+ TESTS=$((TESTS + 1))
39
+ if [[ -f "$RPEB" ]]; then
40
+ echo "PASS: run-plan-echo-back.sh exists"
41
+ else
42
+ echo "FAIL: run-plan-echo-back.sh should exist at scripts/lib/"
43
+ FAILURES=$((FAILURES + 1))
44
+ fi
45
+
46
+ # === Extracted sampling file exists ===
47
+ TESTS=$((TESTS + 1))
48
+ if [[ -f "$RPS" ]]; then
49
+ echo "PASS: run-plan-sampling.sh exists"
50
+ else
51
+ echo "FAIL: run-plan-sampling.sh should exist at scripts/lib/"
52
+ FAILURES=$((FAILURES + 1))
53
+ fi
54
+
55
+ # === run-plan.sh sources it ===
56
+
57
+ TESTS=$((TESTS + 1))
58
+ if grep -q 'source.*lib/run-plan-headless.sh' "$RP"; then
59
+ echo "PASS: run-plan.sh sources lib/run-plan-headless.sh"
60
+ else
61
+ echo "FAIL: run-plan.sh should source lib/run-plan-headless.sh"
62
+ FAILURES=$((FAILURES + 1))
63
+ fi
64
+
65
+ # === run-plan.sh sources new modules ===
66
+ TESTS=$((TESTS + 1))
67
+ if grep -q 'source.*lib/run-plan-echo-back.sh' "$RP"; then
68
+ echo "PASS: run-plan.sh sources lib/run-plan-echo-back.sh"
69
+ else
70
+ echo "FAIL: run-plan.sh should source lib/run-plan-echo-back.sh"
71
+ FAILURES=$((FAILURES + 1))
72
+ fi
73
+
74
+ TESTS=$((TESTS + 1))
75
+ if grep -q 'source.*lib/run-plan-sampling.sh' "$RP"; then
76
+ echo "PASS: run-plan.sh sources lib/run-plan-sampling.sh"
77
+ else
78
+ echo "FAIL: run-plan.sh should source lib/run-plan-sampling.sh"
79
+ FAILURES=$((FAILURES + 1))
80
+ fi
81
+
82
+ # === run-plan.sh no longer has inline run_mode_headless body ===
83
+ # The function definition should be in the extracted file, not in run-plan.sh
84
+
85
+ TESTS=$((TESTS + 1))
86
+ # Count lines of run_mode_headless in run-plan.sh — should be 0 (no function body)
87
+ if grep -q 'run_mode_headless()' "$RP"; then
88
+ echo "FAIL: run-plan.sh should not define run_mode_headless()"
89
+ FAILURES=$((FAILURES + 1))
90
+ else
91
+ echo "PASS: run_mode_headless() not defined in run-plan.sh"
92
+ fi
93
+
94
+ # === Extracted file defines the function ===
95
+
96
+ TESTS=$((TESTS + 1))
97
+ if grep -q 'run_mode_headless()' "$RPH"; then
98
+ echo "PASS: run-plan-headless.sh defines run_mode_headless()"
99
+ else
100
+ echo "FAIL: run-plan-headless.sh should define run_mode_headless()"
101
+ FAILURES=$((FAILURES + 1))
102
+ fi
103
+
104
+ # === run-plan.sh is under 330 lines ===
105
+
106
+ line_count=$(wc -l < "$RP")
107
+ TESTS=$((TESTS + 1))
108
+ if [[ $line_count -le 330 ]]; then
109
+ echo "PASS: run-plan.sh is $line_count lines (<=330)"
110
+ else
111
+ echo "FAIL: run-plan.sh is $line_count lines (should be <=330)"
112
+ FAILURES=$((FAILURES + 1))
113
+ fi
114
+
115
+ # === Extracted file has the key logic markers ===
116
+
117
+ TESTS=$((TESTS + 1))
118
+ if grep -q 'mkdir -p.*logs' "$RPH"; then
119
+ echo "PASS: headless file creates logs directory"
120
+ else
121
+ echo "FAIL: headless file should create logs directory"
122
+ FAILURES=$((FAILURES + 1))
123
+ fi
124
+
125
+ TESTS=$((TESTS + 1))
126
+ if grep -q 'claude -p' "$RPH"; then
127
+ echo "PASS: headless file calls claude -p"
128
+ else
129
+ echo "FAIL: headless file should call claude -p"
130
+ FAILURES=$((FAILURES + 1))
131
+ fi
132
+
133
+ # === Empty batch detection ===
134
+ # The parser should return empty text for empty batches, and headless mode should skip them.
135
+ # This test verifies the parser side (headless mode integration is tested separately).
136
+
137
+ WORK=$(mktemp -d)
138
+ trap 'rm -rf "$WORK"' EXIT
139
+
140
+ source "$SCRIPT_DIR/../lib/run-plan-parser.sh"
141
+
142
+ # Create a plan with 2 real batches and 1 empty trailing match
143
+ cat > "$WORK/plan-empty.md" << 'PLAN'
144
+ ## Batch 1: Real Batch
145
+ ### Task 1: Do something
146
+ Write some code.
147
+
148
+ ## Batch 2: Also Real
149
+ ### Task 2: Do more
150
+ Write more code.
151
+
152
+ ## Batch 3:
153
+ PLAN
154
+
155
+ # get_batch_text should return empty for batch 3
156
+ val=$(get_batch_text "$WORK/plan-empty.md" 3)
157
+ assert_eq "get_batch_text: empty batch returns empty" "" "$val"
158
+
159
+ # count_batches should count all 3 (parser counts headers)
160
+ val=$(count_batches "$WORK/plan-empty.md")
161
+ assert_eq "count_batches: counts all headers including empty" "3" "$val"
162
+
163
+ # === Bug #4: CLAUDE.md Run-Plan section removal uses awk not sed ===
164
+ # The sed range deletion pattern '/^## Run-Plan:/,/^## [^R]/' has no terminating
165
+ # anchor when Run-Plan is the last section, so it deletes from Run-Plan to EOF.
166
+ # The fix replaces it with awk which handles last-section correctly.
167
+
168
+ TESTS=$((TESTS + 1))
169
+ # awk should be used for section removal; the old sed range pattern should not be present
170
+ if grep -q "awk" "$RPH" && grep -q 'in_section' "$RPH"; then
171
+ echo "PASS: CLAUDE.md section removal uses awk (last-section safe, bug #4)"
172
+ else
173
+ echo "FAIL: CLAUDE.md section removal should use awk to handle last section correctly (bug #4)"
174
+ FAILURES=$((FAILURES + 1))
175
+ fi
176
+
177
+ TESTS=$((TESTS + 1))
178
+ # The old broken sed range pattern must not be present
179
+ if grep -q "sed '/\^## Run-Plan:/,/\^## \[^R\]/" "$RPH" 2>/dev/null || \
180
+ grep -q "sed '/\^\#\# Run-Plan:/,/\^\#\# \[^R\]" "$RPH" 2>/dev/null; then
181
+ echo "FAIL: Old sed range deletion pattern still present (unbounded at last section, bug #4)"
182
+ FAILURES=$((FAILURES + 1))
183
+ else
184
+ echo "PASS: Old unbounded sed range deletion pattern removed"
185
+ fi
186
+
187
+ # === Bug #16/#28: SAMPLE_COUNT resets at top of batch loop using SAMPLE_DEFAULT ===
188
+
189
+ # User's --sample value must be preserved into SAMPLE_DEFAULT before the loop
190
+ TESTS=$((TESTS + 1))
191
+ if grep -q 'SAMPLE_DEFAULT=.*SAMPLE_COUNT' "$RPH"; then
192
+ echo "PASS: SAMPLE_DEFAULT saves user's --sample value before batch loop"
193
+ else
194
+ echo "FAIL: SAMPLE_DEFAULT should save user's --sample value before batch loop (bug #16/#28)"
195
+ FAILURES=$((FAILURES + 1))
196
+ fi
197
+
198
+ # The reset inside the loop must use SAMPLE_DEFAULT, not hardcoded 0
199
+ TESTS=$((TESTS + 1))
200
+ batch_loop_region=$(sed -n '/for ((batch = START_BATCH/,/SAMPLE_ON_RETRY/p' "$RPH")
201
+ if echo "$batch_loop_region" | grep -q 'SAMPLE_COUNT=\$SAMPLE_DEFAULT'; then
202
+ echo "PASS: SAMPLE_COUNT resets to SAMPLE_DEFAULT at start of each batch iteration"
203
+ else
204
+ echo "FAIL: SAMPLE_COUNT should reset to SAMPLE_DEFAULT (not 0) at start of each batch iteration (bug #16/#28)"
205
+ FAILURES=$((FAILURES + 1))
206
+ fi
207
+
208
+ # === Bug #2/#27: Sampling block uses patch files instead of stash ===
209
+ # The fix replaced git stash/pop with git diff > patch + git apply to
210
+ # eliminate LIFO ordering issues. These tests verify the new approach.
211
+
212
+ # Baseline state must be saved as a patch file (not stash)
213
+ TESTS=$((TESTS + 1))
214
+ if grep -q '_baseline_patch' "$RPS"; then
215
+ echo "PASS: run-plan-sampling.sh saves baseline state as a patch file"
216
+ else
217
+ echo "FAIL: run-plan-sampling.sh should save baseline state as a patch file (bug #2/#27)"
218
+ FAILURES=$((FAILURES + 1))
219
+ fi
220
+
221
+ # Winner state must be saved as a patch file (not stash)
222
+ TESTS=$((TESTS + 1))
223
+ if grep -q '_winner_patch\|run-plan-winner' "$RPS"; then
224
+ echo "PASS: run-plan-sampling.sh saves winner state as a patch file"
225
+ else
226
+ echo "FAIL: run-plan-sampling.sh should save winner state as a patch file (bug #2/#27)"
227
+ FAILURES=$((FAILURES + 1))
228
+ fi
229
+
230
+ # No executable git stash usage remaining in sampling module (patch approach replaces it).
231
+ # Filter out comment lines (lines starting with optional whitespace + #).
232
+ TESTS=$((TESTS + 1))
233
+ sampling_block=$(sed -n '/^run_sampling_candidates()/,/^}/p' "$RPS")
234
+ # Strip comment-only lines before counting stash calls
235
+ stash_uses=$(echo "$sampling_block" | grep -v '^\s*#' | grep -c 'git stash' || true)
236
+ if [[ "$stash_uses" -eq 0 ]]; then
237
+ echo "PASS: No git stash calls in run_sampling_candidates (replaced by patch approach)"
238
+ else
239
+ echo "FAIL: Found $stash_uses git stash call(s) in run_sampling_candidates — should use patch files (bug #2/#27)"
240
+ FAILURES=$((FAILURES + 1))
241
+ fi
242
+
243
+ # Restore of winner uses git apply (patch approach)
244
+ TESTS=$((TESTS + 1))
245
+ if echo "$sampling_block" | grep -q 'git apply'; then
246
+ echo "PASS: run_sampling_candidates uses git apply to restore winner state"
247
+ else
248
+ echo "FAIL: run_sampling_candidates should use git apply to restore winner state (bug #2/#27)"
249
+ FAILURES=$((FAILURES + 1))
250
+ fi
251
+
252
+ # === Bug #30: Echo-back gate behavior ===
253
+
254
+ # run-plan.sh must accept --skip-echo-back without error
255
+ TESTS=$((TESTS + 1))
256
+ if grep -q '\-\-skip-echo-back' "$RP"; then
257
+ echo "PASS: run-plan.sh accepts --skip-echo-back flag"
258
+ else
259
+ echo "FAIL: run-plan.sh should define --skip-echo-back flag (bug #30)"
260
+ FAILURES=$((FAILURES + 1))
261
+ fi
262
+
263
+ # run-plan.sh must accept --strict-echo-back without error
264
+ TESTS=$((TESTS + 1))
265
+ if grep -q '\-\-strict-echo-back' "$RP"; then
266
+ echo "PASS: run-plan.sh accepts --strict-echo-back flag"
267
+ else
268
+ echo "FAIL: run-plan.sh should define --strict-echo-back flag (bug #30)"
269
+ FAILURES=$((FAILURES + 1))
270
+ fi
271
+
272
+ # _echo_back_check function must exist in headless file
273
+ TESTS=$((TESTS + 1))
274
+ if grep -q '_echo_back_check()' "$RPEB"; then
275
+ echo "PASS: _echo_back_check() is defined in run-plan-echo-back.sh"
276
+ else
277
+ echo "FAIL: _echo_back_check() should be defined in run-plan-echo-back.sh (bug #30)"
278
+ FAILURES=$((FAILURES + 1))
279
+ fi
280
+
281
+ # Echo-back gate must be non-blocking by default (no early return when STRICT_ECHO_BACK not set)
282
+ TESTS=$((TESTS + 1))
283
+ if grep -q 'STRICT_ECHO_BACK' "$RPEB"; then
284
+ echo "PASS: STRICT_ECHO_BACK controls blocking behavior in echo-back gate"
285
+ else
286
+ echo "FAIL: echo-back gate should check STRICT_ECHO_BACK for blocking mode (bug #30)"
287
+ FAILURES=$((FAILURES + 1))
288
+ fi
289
+
290
+ # echo-back gate is documented as non-blocking by default
291
+ TESTS=$((TESTS + 1))
292
+ if grep -q 'NON-BLOCKING' "$RPEB"; then
293
+ echo "PASS: run-plan-echo-back.sh documents NON-BLOCKING default behavior"
294
+ else
295
+ echo "FAIL: run-plan-echo-back.sh should document NON-BLOCKING default (bug #30)"
296
+ FAILURES=$((FAILURES + 1))
297
+ fi
298
+
299
+ # _echo_back_check: SKIP_ECHO_BACK=true must cause early return without error
300
+ TESTS=$((TESTS + 1))
301
+ (
302
+ source "$RPEB" 2>/dev/null || true
303
+ SKIP_ECHO_BACK=true
304
+ STRICT_ECHO_BACK=false
305
+ _echo_back_check "some batch text here" "/nonexistent/log" 2>/dev/null
306
+ ) && echo "PASS: _echo_back_check returns 0 when SKIP_ECHO_BACK=true" \
307
+ || {
308
+ echo "FAIL: _echo_back_check should return 0 when SKIP_ECHO_BACK=true (bug #30)"
309
+ FAILURES=$((FAILURES + 1))
310
+ }
311
+
312
+ # _echo_back_check: missing log file does not crash
313
+ TESTS=$((TESTS + 1))
314
+ (
315
+ source "$RPEB" 2>/dev/null || true
316
+ SKIP_ECHO_BACK=false
317
+ STRICT_ECHO_BACK=false
318
+ _echo_back_check "some batch text here" "/nonexistent/log" 2>/dev/null
319
+ ) && echo "PASS: _echo_back_check handles missing log file gracefully" \
320
+ || {
321
+ echo "FAIL: _echo_back_check should handle missing log file gracefully (bug #30)"
322
+ FAILURES=$((FAILURES + 1))
323
+ }
324
+
325
+ # _echo_back_check: empty batch text does not crash
326
+ TESTS=$((TESTS + 1))
327
+ tmplog=$(mktemp)
328
+ echo "some agent output here" > "$tmplog"
329
+ (
330
+ source "$RPEB" 2>/dev/null || true
331
+ SKIP_ECHO_BACK=false
332
+ STRICT_ECHO_BACK=false
333
+ _echo_back_check "" "$tmplog" 2>/dev/null
334
+ )
335
+ ec=$?
336
+ rm -f "$tmplog"
337
+ if [[ $ec -eq 0 ]]; then
338
+ echo "PASS: _echo_back_check handles empty batch text gracefully"
339
+ else
340
+ echo "FAIL: _echo_back_check should handle empty batch text without error (bug #30)"
341
+ FAILURES=$((FAILURES + 1))
342
+ fi
343
+
344
+ # === Bug #4 BEHAVIORAL: awk removes Run-Plan even when it's the last section ===
345
+ # The old sed range pattern '/^## Run-Plan:/,/^## [^R]/' had no closing anchor
346
+ # when Run-Plan was the last section, eating the file from Run-Plan to EOF.
347
+ # This behavioral test exercises the actual awk code path with a CLAUDE.md
348
+ # where "## Run-Plan:" is the last section and verifies other sections survive.
349
+
350
+ WORK_AWK=$(mktemp -d)
351
+ # Create a CLAUDE.md where Run-Plan is the LAST section
352
+ cat > "$WORK_AWK/CLAUDE.md" << 'CLAUDE_EOF'
353
+ # Project Config
354
+
355
+ ## Conventions
356
+
357
+ - Use pytest
358
+ - Stage specific files
359
+
360
+ ## Run-Plan: Batch 3
361
+
362
+ ### Recent Commits
363
+ abc1234 fix: something
364
+
365
+ ### Progress Notes
366
+ Batch 2 done.
367
+ CLAUDE_EOF
368
+
369
+ # Run the same awk logic that run-plan-headless.sh uses inline
370
+ awk '
371
+ /^## Run-Plan:/ { in_section=1; next }
372
+ in_section && /^## / { in_section=0 }
373
+ !in_section { print }
374
+ ' "$WORK_AWK/CLAUDE.md" > "$WORK_AWK/CLAUDE.md.tmp"
375
+ mv "$WORK_AWK/CLAUDE.md.tmp" "$WORK_AWK/CLAUDE.md"
376
+
377
+ # Verify: Run-Plan section is gone
378
+ TESTS=$((TESTS + 1))
379
+ if grep -q "## Run-Plan:" "$WORK_AWK/CLAUDE.md"; then
380
+ echo "FAIL: awk last-section: Run-Plan section should be removed"
381
+ FAILURES=$((FAILURES + 1))
382
+ else
383
+ echo "PASS: awk last-section: Run-Plan section removed"
384
+ fi
385
+
386
+ # Verify: Conventions section still exists (not eaten by unbounded deletion)
387
+ TESTS=$((TESTS + 1))
388
+ if grep -q "## Conventions" "$WORK_AWK/CLAUDE.md"; then
389
+ echo "PASS: awk last-section: Conventions section preserved"
390
+ else
391
+ echo "FAIL: awk last-section: Conventions section should survive Run-Plan removal"
392
+ FAILURES=$((FAILURES + 1))
393
+ fi
394
+
395
+ # Verify: content before Run-Plan is preserved
396
+ TESTS=$((TESTS + 1))
397
+ if grep -q "Use pytest" "$WORK_AWK/CLAUDE.md"; then
398
+ echo "PASS: awk last-section: content before Run-Plan preserved"
399
+ else
400
+ echo "FAIL: awk last-section: content before Run-Plan should be preserved"
401
+ FAILURES=$((FAILURES + 1))
402
+ fi
403
+
404
+ rm -rf "$WORK_AWK"
405
+
406
+ # === Bug #38: Empty claude output diagnostic ===
407
+
408
+ # Must check for empty log file after claude invocation
409
+ TESTS=$((TESTS + 1))
410
+ if grep -q 'claude produced no output' "$RPH"; then
411
+ echo "PASS: Empty claude output is diagnosed with a warning message (#38)"
412
+ else
413
+ echo "FAIL: Should diagnose empty claude output (crash/no output case) (bug #38)"
414
+ FAILURES=$((FAILURES + 1))
415
+ fi
416
+
417
+ # === Summary ===
418
+ echo ""
419
+ echo "Results: $((TESTS - FAILURES))/$TESTS passed"
420
+ if [[ $FAILURES -gt 0 ]]; then
421
+ echo "FAILURES: $FAILURES"
422
+ exit 1
423
+ fi
424
+ echo "ALL PASSED"
@@ -0,0 +1,124 @@
1
+ #!/usr/bin/env bash
2
+ # Test notification format functions (no actual Telegram sending)
3
+ set -euo pipefail
4
+
5
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
+ source "$SCRIPT_DIR/../lib/run-plan-notify.sh"
7
+
8
+ FAILURES=0
9
+ TESTS=0
10
+
11
+ assert_contains() {
12
+ local desc="$1" needle="$2" haystack="$3"
13
+ TESTS=$((TESTS + 1))
14
+ if [[ "$haystack" == *"$needle"* ]]; then
15
+ echo "PASS: $desc"
16
+ else
17
+ echo "FAIL: $desc"
18
+ echo " expected to contain: $needle"
19
+ echo " actual: $haystack"
20
+ FAILURES=$((FAILURES + 1))
21
+ fi
22
+ }
23
+
24
+ assert_eq() {
25
+ local desc="$1" expected="$2" actual="$3"
26
+ TESTS=$((TESTS + 1))
27
+ if [[ "$expected" == "$actual" ]]; then
28
+ echo "PASS: $desc"
29
+ else
30
+ echo "FAIL: $desc"
31
+ echo " expected: $expected"
32
+ echo " actual: $actual"
33
+ FAILURES=$((FAILURES + 1))
34
+ fi
35
+ }
36
+
37
+ # --- Test: format_success_message includes plan name ---
38
+ msg=$(format_success_message "my-feature" 3 10 "Context Assembler" 2003 1953 "4m12s" "headless" "")
39
+ assert_contains "success includes plan name" "my-feature" "$msg"
40
+
41
+ # --- Test: format_success_message includes batch X/Y ---
42
+ assert_contains "success includes batch X/Y" "Batch 3/10" "$msg"
43
+
44
+ # --- Test: format_success_message includes batch title ---
45
+ assert_contains "success includes batch title" "Context Assembler" "$msg"
46
+
47
+ # --- Test: format_success_message includes check mark ---
48
+ assert_contains "success includes check mark" "✓" "$msg"
49
+
50
+ # --- Test: format_success_message includes test count ---
51
+ assert_contains "success includes test count" "2003" "$msg"
52
+
53
+ # --- Test: format_success_message includes delta with up arrow ---
54
+ assert_contains "success includes delta" "↑50" "$msg"
55
+
56
+ # --- Test: format_success_message delta calculation: 2003 - 1953 = 50 ---
57
+ TESTS=$((TESTS + 1))
58
+ if [[ "$msg" == *"↑50"* ]] && [[ "$msg" != *"↑500"* ]]; then
59
+ echo "PASS: delta is exactly 50"
60
+ else
61
+ echo "FAIL: delta should be exactly 50 (2003 - 1953)"
62
+ echo " message: $msg"
63
+ FAILURES=$((FAILURES + 1))
64
+ fi
65
+
66
+ # --- Test: format_success_message includes duration ---
67
+ assert_contains "success includes duration" "4m12s" "$msg"
68
+
69
+ # --- Test: format_success_message includes mode ---
70
+ assert_contains "success includes mode" "headless" "$msg"
71
+
72
+ # --- Test: format_success_message with summary ---
73
+ msg=$(format_success_message "my-feature" 1 5 "Quick Fixes" 100 90 "2m30s" "headless" "Added 3 tests, fixed parser")
74
+ assert_contains "success includes summary" "Added 3 tests, fixed parser" "$msg"
75
+
76
+ # --- Test: format_failure_message includes plan name ---
77
+ msg=$(format_failure_message "my-feature" 2 8 "ast-grep Rules" 45 3 "pytest failed" "Fix test_auth.py")
78
+ assert_contains "failure includes plan name" "my-feature" "$msg"
79
+
80
+ # --- Test: format_failure_message includes batch X/Y ---
81
+ assert_contains "failure includes batch X/Y" "Batch 2/8" "$msg"
82
+
83
+ # --- Test: format_failure_message includes batch title ---
84
+ assert_contains "failure includes batch title" "ast-grep Rules" "$msg"
85
+
86
+ # --- Test: format_failure_message includes cross mark ---
87
+ assert_contains "failure includes cross mark" "✗" "$msg"
88
+
89
+ # --- Test: format_failure_message includes test count ---
90
+ assert_contains "failure includes test count" "45" "$msg"
91
+
92
+ # --- Test: format_failure_message includes failing count ---
93
+ assert_contains "failure includes failing count" "3 failing" "$msg"
94
+
95
+ # --- Test: format_failure_message includes error as Issue ---
96
+ assert_contains "failure includes issue text" "pytest failed" "$msg"
97
+
98
+ # --- Test: format_failure_message includes action ---
99
+ assert_contains "failure includes action" "Fix test_auth.py" "$msg"
100
+
101
+ # --- Test: _load_telegram_env warns on missing file ---
102
+ WORK=$(mktemp -d)
103
+ trap 'rm -rf "$WORK"' EXIT
104
+
105
+ msg=$(_load_telegram_env "$WORK/.env-nonexistent" 2>&1 || true)
106
+ assert_contains "warns on missing env file" "warn" "$(echo "$msg" | tr '[:upper:]' '[:lower:]')"
107
+
108
+ # --- Test: _send_telegram warns on missing token ---
109
+ unset TELEGRAM_BOT_TOKEN 2>/dev/null || true
110
+ unset TELEGRAM_CHAT_ID 2>/dev/null || true
111
+ msg=$(_send_telegram "test message" 2>&1 || true)
112
+ assert_contains "send warns on missing credentials" "warn" "$(echo "$msg" | tr '[:upper:]' '[:lower:]')"
113
+
114
+ # --- Test: format_success_message with zero delta ---
115
+ msg=$(format_success_message "zero-delta" 1 1 "Single Batch" 100 100 "1m00s" "team" "")
116
+ assert_contains "zero delta shows ↑0" "↑0" "$msg"
117
+
118
+ echo ""
119
+ echo "Results: $((TESTS - FAILURES))/$TESTS passed"
120
+ if [[ $FAILURES -gt 0 ]]; then
121
+ echo "FAILURES: $FAILURES"
122
+ exit 1
123
+ fi
124
+ echo "ALL PASSED"