scriptorium 0.0.3 → 0.7.2

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 (353) hide show
  1. checksums.yaml +4 -4
  2. data/README.lt3 +324 -0
  3. data/README.md +3155 -1
  4. data/assets/.DS_Store +0 -0
  5. data/assets/README.md +44 -0
  6. data/assets/icons/social/reddit.png +0 -0
  7. data/assets/icons/social/x-logo.png +0 -0
  8. data/assets/icons/ui/.DS_Store +0 -0
  9. data/assets/icons/ui/back.png +0 -0
  10. data/assets/icons/ui/copy.png +0 -0
  11. data/assets/icons/ui/down.png +0 -0
  12. data/assets/icons/ui/end.png +0 -0
  13. data/assets/icons/ui/exit.png +0 -0
  14. data/assets/icons/ui/foo +10 -0
  15. data/assets/icons/ui/home.png +0 -0
  16. data/assets/icons/ui/left.png +0 -0
  17. data/assets/icons/ui/next.png +0 -0
  18. data/assets/icons/ui/right.png +0 -0
  19. data/assets/icons/ui/start.png +0 -0
  20. data/assets/icons/ui/up.png +0 -0
  21. data/assets/imagenotfound.jpg +0 -0
  22. data/assets/samples/placeholder.svg +9 -0
  23. data/assets/themes/standard/favicon.svg +6 -0
  24. data/bin/sblog +84 -5
  25. data/bin/scriptorium +1 -0
  26. data/doc/README.txt +6 -0
  27. data/doc/anti-amnesia/20250727-054000-scriptorium-overview.md +94 -0
  28. data/doc/anti-amnesia/20250727-123000-anti-amnesia-conventions.md +2 -0
  29. data/doc/anti-amnesia/20250727-172600-cursor-rbenv-ruby-version-mystery.md +45 -0
  30. data/doc/anti-amnesia/20250727-172900-ai-cognitive-assessment-capabilities.md +40 -0
  31. data/doc/anti-amnesia/20250728-124243-aaa-syntax-clarification.md +46 -0
  32. data/doc/anti-amnesia/20250729-210000-reddit-autopost-integration-complete.md +158 -0
  33. data/doc/anti-amnesia/20250804-190500-cognitive-loop-bug.md +35 -0
  34. data/doc/anti-amnesia/20250804-190700-anti-amnesia-timestamping-fix.md +27 -0
  35. data/doc/anti-amnesia/20250807-213025.md +116 -0
  36. data/doc/anti-amnesia/20250901-211714-codemirror-integration-and-web-tests.md +172 -0
  37. data/doc/anti-amnesia/20250902-002402-backup-restore-system.md +126 -0
  38. data/doc/anti-amnesia/20250907-203339-backup-metadata-implementation.md +66 -0
  39. data/doc/banner_svg_config.md +114 -0
  40. data/doc/contrib.lt3 +8 -0
  41. data/doc/dependencies.md +281 -0
  42. data/doc/hacker.lt3 +5 -0
  43. data/doc/imported/0001-elixir-conf-2014/metadata.txt +7 -0
  44. data/doc/imported/0001-elixir-conf-2014/post.html +37 -0
  45. data/doc/imported/0001-elixir-conf-2014/source.lt3 +22 -0
  46. data/doc/imported/0002-programmers-and-word-processing/metadata.txt +7 -0
  47. data/doc/imported/0002-programmers-and-word-processing/post.html +192 -0
  48. data/doc/imported/0002-programmers-and-word-processing/source.lt3 +146 -0
  49. data/doc/imported/0003-how-to-turn-your-brain-sideways/metadata.txt +7 -0
  50. data/doc/imported/0003-how-to-turn-your-brain-sideways/post.html +60 -0
  51. data/doc/imported/0003-how-to-turn-your-brain-sideways/source.lt3 +40 -0
  52. data/doc/imported/0004-upcoming-lone-star-ruby-conference/metadata.txt +7 -0
  53. data/doc/imported/0004-upcoming-lone-star-ruby-conference/post.html +42 -0
  54. data/doc/imported/0004-upcoming-lone-star-ruby-conference/source.lt3 +24 -0
  55. data/doc/imported/0005-elixir-conf-2015-announced/metadata.txt +7 -0
  56. data/doc/imported/0005-elixir-conf-2015-announced/post.html +30 -0
  57. data/doc/imported/0005-elixir-conf-2015-announced/source.lt3 +16 -0
  58. data/doc/imported/0006-ruby-for-dinosaurs/metadata.txt +7 -0
  59. data/doc/imported/0006-ruby-for-dinosaurs/post.html +43 -0
  60. data/doc/imported/0006-ruby-for-dinosaurs/source.lt3 +27 -0
  61. data/doc/imported/0007-phoenix-isnt-rails/metadata.txt +7 -0
  62. data/doc/imported/0007-phoenix-isnt-rails/post.html +116 -0
  63. data/doc/imported/0007-phoenix-isnt-rails/source.lt3 +87 -0
  64. data/doc/imported/0008-concerning-the-term-monkeypatching/metadata.txt +7 -0
  65. data/doc/imported/0008-concerning-the-term-monkeypatching/post.html +129 -0
  66. data/doc/imported/0008-concerning-the-term-monkeypatching/source.lt3 +92 -0
  67. data/doc/imported/0009-announcement-coming-soon/metadata.txt +7 -0
  68. data/doc/imported/0009-announcement-coming-soon/post.html +33 -0
  69. data/doc/imported/0009-announcement-coming-soon/source.lt3 +19 -0
  70. data/doc/imported/0010-immutable-data-ditching-the-wax-tablet/metadata.txt +7 -0
  71. data/doc/imported/0010-immutable-data-ditching-the-wax-tablet/post.html +175 -0
  72. data/doc/imported/0010-immutable-data-ditching-the-wax-tablet/source.lt3 +139 -0
  73. data/doc/imported/0011-computer-science-as-a-lost-art/metadata.txt +7 -0
  74. data/doc/imported/0011-computer-science-as-a-lost-art/post.html +139 -0
  75. data/doc/imported/0011-computer-science-as-a-lost-art/source.lt3 +104 -0
  76. data/doc/imported/0012-ruby-day-in-turin-italy/metadata.txt +7 -0
  77. data/doc/imported/0012-ruby-day-in-turin-italy/post.html +42 -0
  78. data/doc/imported/0012-ruby-day-in-turin-italy/source.lt3 +24 -0
  79. data/doc/imported/0013-rubyday-was-a-success/metadata.txt +7 -0
  80. data/doc/imported/0013-rubyday-was-a-success/post.html +44 -0
  81. data/doc/imported/0013-rubyday-was-a-success/source.lt3 +27 -0
  82. data/doc/imported/0014-working-on-the-blogging-software/metadata.txt +7 -0
  83. data/doc/imported/0014-working-on-the-blogging-software/post.html +63 -0
  84. data/doc/imported/0014-working-on-the-blogging-software/source.lt3 +41 -0
  85. data/doc/imported/0015-ok-its-not-really-a-lost-art/metadata.txt +7 -0
  86. data/doc/imported/0015-ok-its-not-really-a-lost-art/post.html +172 -0
  87. data/doc/imported/0015-ok-its-not-really-a-lost-art/source.lt3 +134 -0
  88. data/doc/imported/0016-an-in-operator-for-ruby/metadata.txt +7 -0
  89. data/doc/imported/0016-an-in-operator-for-ruby/post.html +155 -0
  90. data/doc/imported/0016-an-in-operator-for-ruby/source.lt3 +106 -0
  91. data/doc/imported/0017-the-forgotten-mathematician/metadata.txt +7 -0
  92. data/doc/imported/0017-the-forgotten-mathematician/post.html +161 -0
  93. data/doc/imported/0017-the-forgotten-mathematician/source.lt3 +119 -0
  94. data/doc/imported/0018-ruby-puns/metadata.txt +7 -0
  95. data/doc/imported/0018-ruby-puns/post.html +46 -0
  96. data/doc/imported/0018-ruby-puns/source.lt3 +28 -0
  97. data/doc/imported/0019-custom-exceptions-via-metaprogramming/metadata.txt +7 -0
  98. data/doc/imported/0019-custom-exceptions-via-metaprogramming/post.html +138 -0
  99. data/doc/imported/0019-custom-exceptions-via-metaprogramming/source.lt3 +101 -0
  100. data/doc/imported/0020-fffff/metadata.txt +7 -0
  101. data/doc/imported/0020-fffff/post.html +24 -0
  102. data/doc/imported/0020-fffff/source.lt3 +12 -0
  103. data/doc/imported/0021-trying-ror-yet-again/metadata.txt +7 -0
  104. data/doc/imported/0021-trying-ror-yet-again/post.html +26 -0
  105. data/doc/imported/0021-trying-ror-yet-again/source.lt3 +12 -0
  106. data/doc/imported/0023-doctor-sleep/metadata.txt +7 -0
  107. data/doc/imported/0023-doctor-sleep/post.html +63 -0
  108. data/doc/imported/0023-doctor-sleep/source.lt3 +44 -0
  109. data/doc/imported/0024-just-a-test/metadata.txt +7 -0
  110. data/doc/imported/0024-just-a-test/post.html +24 -0
  111. data/doc/imported/0024-just-a-test/source.lt3 +12 -0
  112. data/doc/imported/import_summary.txt +98 -0
  113. data/doc/livetext-informal-spec.txt +65 -0
  114. data/doc/myuserdoc/ch-0.lt3 +31 -0
  115. data/doc/myuserdoc/ch-1.lt3 +37 -0
  116. data/doc/myuserdoc/ch-10.lt3 +22 -0
  117. data/doc/myuserdoc/ch-2.lt3 +37 -0
  118. data/doc/myuserdoc/ch-3.lt3 +19 -0
  119. data/doc/myuserdoc/ch-4.lt3 +43 -0
  120. data/doc/myuserdoc/ch-5.lt3 +22 -0
  121. data/doc/myuserdoc/ch-6.lt3 +19 -0
  122. data/doc/myuserdoc/ch-7.lt3 +16 -0
  123. data/doc/myuserdoc/ch-8.lt3 +13 -0
  124. data/doc/myuserdoc/ch-9.lt3 +19 -0
  125. data/doc/myuserdoc/tweak.rb +18 -0
  126. data/doc/myuserdoc/userdoc-toc.txt +88 -0
  127. data/doc/old-posts/0001-elixir-conf-2014.lt3 +24 -0
  128. data/doc/old-posts/0002-programmers-and-word-processing.lt3 +150 -0
  129. data/doc/old-posts/0003-how-to-turn-your-brain-sideways.lt3 +43 -0
  130. data/doc/old-posts/0004-upcoming-lone-star-ruby-conference.lt3 +26 -0
  131. data/doc/old-posts/0005-elixir-conf-2015-announced.lt3 +17 -0
  132. data/doc/old-posts/0006-ruby-for-dinosaurs.lt3 +30 -0
  133. data/doc/old-posts/0007-phoenix-isnt-rails.lt3 +90 -0
  134. data/doc/old-posts/0008-concerning-the-term-monkeypatching.lt3 +105 -0
  135. data/doc/old-posts/0009-announcement-coming-soon.lt3 +20 -0
  136. data/doc/old-posts/0010-immutable-data-ditching-the-wax-tablet.lt3 +142 -0
  137. data/doc/old-posts/0011-computer-science-as-a-lost-art.lt3 +117 -0
  138. data/doc/old-posts/0012-ruby-day-in-turin-italy.lt3 +26 -0
  139. data/doc/old-posts/0013-rubyday-was-a-success.lt3 +28 -0
  140. data/doc/old-posts/0014-working-on-the-blogging-software.lt3 +42 -0
  141. data/doc/old-posts/0015-ok-its-not-really-a-lost-art.lt3 +137 -0
  142. data/doc/old-posts/0016-an-in-operator-for-ruby.lt3 +142 -0
  143. data/doc/old-posts/0017-the-forgotten-mathematician.lt3 +129 -0
  144. data/doc/old-posts/0018-ruby-puns.lt3 +31 -0
  145. data/doc/old-posts/0019-custom-exceptions-via-metaprogramming.lt3 +116 -0
  146. data/doc/old-posts/0021-trying-ror-yet-again.lt3 +35 -0
  147. data/doc/old-posts/0023-doctor-sleep.lt3 +43 -0
  148. data/doc/old-posts/0024-just-a-test.lt3 +12 -0
  149. data/doc/old-posts/0025-trying-another-post.lt3 +12 -0
  150. data/doc/old-repo +1 -0
  151. data/doc/reddit_credentials_template.json +8 -0
  152. data/doc/reddit_integration.md +207 -0
  153. data/doc/user.lt3 +35 -0
  154. data/doc/user_guide_section_1.md +137 -0
  155. data/doc/user_guide_section_10.md +515 -0
  156. data/doc/user_guide_section_11.md +708 -0
  157. data/doc/user_guide_section_2.md +233 -0
  158. data/doc/user_guide_section_3.md +5 -0
  159. data/doc/user_guide_section_4.md +221 -0
  160. data/doc/user_guide_section_5.md +243 -0
  161. data/doc/user_guide_section_6.md +147 -0
  162. data/doc/user_guide_section_7.md +311 -0
  163. data/doc/user_guide_section_8.md +224 -0
  164. data/doc/user_guide_section_9.md +375 -0
  165. data/lib/rouge/lexers/livetext.rb +74 -0
  166. data/lib/scriptorium/api.rb +2373 -0
  167. data/lib/scriptorium/banner_svg.rb +729 -0
  168. data/lib/scriptorium/contract.rb +34 -0
  169. data/lib/scriptorium/exceptions.rb +201 -1
  170. data/lib/scriptorium/helpers.rb +675 -0
  171. data/lib/scriptorium/post.rb +259 -0
  172. data/lib/scriptorium/reddit.rb +83 -0
  173. data/lib/scriptorium/repo.rb +938 -0
  174. data/lib/scriptorium/standard_files.rb +149 -0
  175. data/lib/scriptorium/support/bootstrap/css.txt +5 -0
  176. data/lib/scriptorium/support/bootstrap/js.txt +4 -0
  177. data/lib/scriptorium/support/common_js/clipboard.js +35 -0
  178. data/lib/scriptorium/support/common_js/content-loader.js +187 -0
  179. data/lib/scriptorium/support/common_js/navigation.js +52 -0
  180. data/lib/scriptorium/support/common_js/syntax-highlighting.js +27 -0
  181. data/lib/scriptorium/support/config/reddit.txt +10 -0
  182. data/lib/scriptorium/support/config/reddit_template.txt +17 -0
  183. data/lib/scriptorium/support/config/social.txt +8 -0
  184. data/lib/scriptorium/support/highlight/css.txt +2 -0
  185. data/lib/scriptorium/support/highlight/custom.css +119 -0
  186. data/lib/scriptorium/support/highlight/js.txt +1 -0
  187. data/lib/scriptorium/support/post_index/config.txt +15 -0
  188. data/lib/scriptorium/support/post_index/style.css +55 -0
  189. data/lib/scriptorium/support/templates/index_entry.lt3 +16 -0
  190. data/lib/scriptorium/support/templates/initial_post.lt3 +12 -0
  191. data/lib/scriptorium/support/templates/layout.txt +5 -0
  192. data/lib/scriptorium/support/templates/post.lt3 +104 -0
  193. data/lib/scriptorium/support/theme/footer.lt3 +2 -0
  194. data/lib/scriptorium/support/theme/header.lt3 +4 -0
  195. data/lib/scriptorium/support/theme/left.lt3 +3 -0
  196. data/lib/scriptorium/support/theme/main.lt3 +5 -0
  197. data/lib/scriptorium/support/theme/right.lt3 +3 -0
  198. data/lib/scriptorium/theme.rb +192 -0
  199. data/lib/scriptorium/version.rb +1 -1
  200. data/lib/scriptorium/view.rb +1021 -0
  201. data/lib/scriptorium/widgets/featured_posts.rb +149 -0
  202. data/lib/scriptorium/widgets/links.rb +112 -0
  203. data/lib/scriptorium/widgets/pages.rb +133 -0
  204. data/lib/scriptorium/widgets/widget.rb +133 -0
  205. data/lib/scriptorium.rb +38 -34
  206. data/lib/skeleton.rb +10 -1
  207. data/scriptorium.gemspec +17 -5
  208. data/test/README.md +69 -0
  209. data/test/WEB_INTEGRATION_README.md +196 -0
  210. data/test/all +83 -0
  211. data/test/api_demo.rb +99 -0
  212. data/test/assets/imagenotfound.jpg +0 -0
  213. data/test/assets/images/.DS_Store +0 -0
  214. data/test/assets/images/README.md +27 -0
  215. data/test/assets/images/odd_aspect.png +0 -0
  216. data/test/assets/images/perfect.png +0 -0
  217. data/test/assets/images/small.png +0 -0
  218. data/test/assets/images/tall.png +0 -0
  219. data/test/assets/images/very_tall.png +0 -0
  220. data/test/assets/images/very_wide.png +0 -0
  221. data/test/assets/images/wide.png +0 -0
  222. data/test/assets/testbanner.jpg +0 -0
  223. data/test/banner_svg/simple_helpers.rb +13 -0
  224. data/test/banner_svg/unit.rb +1000 -0
  225. data/test/config/deployment.txt +5 -0
  226. data/test/ed_test.rb +204 -0
  227. data/test/integration/cursor_banner_combinations.rb +193 -0
  228. data/test/integration/cursor_banner_features.rb +374 -0
  229. data/test/integration/integration_test.rb +326 -0
  230. data/test/integration/preview_flow_test.rb +94 -0
  231. data/test/livetext_plugin_test.rb +500 -0
  232. data/test/manual/asset_mgmt.rb +67 -0
  233. data/test/manual/banner-tests/index.html +45 -0
  234. data/test/manual/banner-tests/svg.txt +3 -0
  235. data/test/manual/banner-tests/test01.html +122 -0
  236. data/test/manual/banner-tests/test02.html +122 -0
  237. data/test/manual/banner-tests/test03.html +122 -0
  238. data/test/manual/banner-tests/test04.html +129 -0
  239. data/test/manual/banner-tests/test05.html +129 -0
  240. data/test/manual/banner-tests/test06.html +129 -0
  241. data/test/manual/banner-tests/test07.html +129 -0
  242. data/test/manual/banner-tests/test08.html +123 -0
  243. data/test/manual/banner-tests/test09.html +123 -0
  244. data/test/manual/banner-tests/test10.html +123 -0
  245. data/test/manual/banner-tests/test11.html +123 -0
  246. data/test/manual/banner-tests/test12.html +123 -0
  247. data/test/manual/banner-tests/test13.html +123 -0
  248. data/test/manual/banner-tests/test14.html +123 -0
  249. data/test/manual/banner-tests/test15.html +122 -0
  250. data/test/manual/banner-tests/test16.html +122 -0
  251. data/test/manual/banner-tests/test17.html +122 -0
  252. data/test/manual/banner-tests/test18.html +132 -0
  253. data/test/manual/banner-tests/test19.html +132 -0
  254. data/test/manual/banner-tests/test20.html +132 -0
  255. data/test/manual/banner-tests/test21.html +132 -0
  256. data/test/manual/banner-tests/test22.html +132 -0
  257. data/test/manual/banner-tests/test23.html +132 -0
  258. data/test/manual/banner-tests/test24.html +132 -0
  259. data/test/manual/banner-tests/test25.html +131 -0
  260. data/test/manual/banner_environment.rb +205 -0
  261. data/test/manual/codemirror_demo.html +773 -0
  262. data/test/manual/create_posts_for_web.rb +114 -0
  263. data/test/manual/environment.rb +67 -0
  264. data/test/manual/make_banner.rb +153 -0
  265. data/test/manual/preview_manual_test.rb +129 -0
  266. data/test/manual/sample_banner_config.txt +12 -0
  267. data/test/manual/test_advanced_widgets.rb +73 -0
  268. data/test/manual/test_banner_combinations.rb +120 -0
  269. data/test/manual/test_banner_features.rb +306 -0
  270. data/test/manual/test_banner_integration.rb +115 -0
  271. data/test/manual/test_banner_radial.rb +87 -0
  272. data/test/manual/test_basic_posts.rb +47 -0
  273. data/test/manual/test_layout_widgets.rb +40 -0
  274. data/test/manual/test_pagination.rb +24 -0
  275. data/test/manual/test_random_posts.rb +38 -0
  276. data/test/manual/test_syntax_highlighting.rb +167 -0
  277. data/test/rubytext/rubytext_comprehensive_test.rb +307 -0
  278. data/test/rubytext/rubytext_demo_test.rb +42 -0
  279. data/test/rubytext/rubytext_testing_guide.md +277 -0
  280. data/test/run_automated_tests.rb +45 -0
  281. data/test/staging/.DS_Store +0 -0
  282. data/test/support/preview_utils.rb +88 -0
  283. data/test/syntax_highlighting_test.lt3 +124 -0
  284. data/test/test_gem_assets.rb +48 -0
  285. data/test/test_helpers.rb +240 -0
  286. data/test/tui_editor_integration_test.rb +296 -0
  287. data/test/tui_integration_test.rb +883 -0
  288. data/test/unit/api.rb +1776 -0
  289. data/test/unit/asset_management.rb +219 -0
  290. data/test/unit/backup_test.rb +451 -0
  291. data/test/unit/clipboard_test.rb +60 -0
  292. data/test/unit/contract_test.rb +69 -0
  293. data/test/unit/core.rb +1211 -0
  294. data/test/unit/deploy_config_test.rb +248 -0
  295. data/test/unit/deploy_test.rb +478 -0
  296. data/test/unit/edit_post_test.rb +168 -0
  297. data/test/unit/gem_asset_management.rb +183 -0
  298. data/test/unit/livetext_basic.rb +57 -0
  299. data/test/unit/livetext_compatibility.rb +82 -0
  300. data/test/unit/parse_cmd_test.rb +260 -0
  301. data/test/unit/permalink_copy_test.rb +211 -0
  302. data/test/unit/post.rb +309 -0
  303. data/test/unit/post_index_config_test.rb +258 -0
  304. data/test/unit/post_state_helpers_test.rb +137 -0
  305. data/test/unit/read_commented_file_test.rb +278 -0
  306. data/test/unit/reddit_test.rb +235 -0
  307. data/test/unit/repo.rb +569 -0
  308. data/test/unit/social_test.rb +366 -0
  309. data/test/unit/syntax_highlighting.rb +70 -0
  310. data/test/unit/theme_management_test.rb +91 -0
  311. data/test/unit/view.rb +498 -0
  312. data/test/unit/widgets.rb +669 -0
  313. data/test/web_integration_test.rb +231 -0
  314. data/test/web_test_helper.rb +218 -0
  315. data/test/web_workflow_test.rb +527 -0
  316. data/test/wizard_test.rb +123 -0
  317. data/ui/README.md +67 -0
  318. data/ui/common/lib/ui_common.rb +8 -0
  319. data/ui/rubytext/README.md +191 -0
  320. data/ui/rubytext/bin/scriptorium-rubytext +402 -0
  321. data/ui/rubytext/lib/rubytext_ui.rb +300 -0
  322. data/ui/tui/bin/scriptorium +1890 -0
  323. data/ui/tui/test/tui_test.rb +23 -0
  324. data/ui/web/app/app.rb +2600 -0
  325. data/ui/web/app/assets/livetext_mode.js +244 -0
  326. data/ui/web/app/error_helpers.rb +150 -0
  327. data/ui/web/app/views/advanced_config.erb +196 -0
  328. data/ui/web/app/views/asset_management.erb +645 -0
  329. data/ui/web/app/views/backup_management.erb +238 -0
  330. data/ui/web/app/views/banner_config.erb +200 -0
  331. data/ui/web/app/views/config_widget.erb +232 -0
  332. data/ui/web/app/views/configure_view.erb +401 -0
  333. data/ui/web/app/views/dashboard.erb +154 -0
  334. data/ui/web/app/views/deploy_config.erb +149 -0
  335. data/ui/web/app/views/edit_pages.erb +363 -0
  336. data/ui/web/app/views/edit_post.erb +175 -0
  337. data/ui/web/app/views/edit_theme.erb +73 -0
  338. data/ui/web/app/views/edit_theme_file.erb +74 -0
  339. data/ui/web/app/views/error_page.erb +29 -0
  340. data/ui/web/app/views/header_config.erb +155 -0
  341. data/ui/web/app/views/layout_config.erb +147 -0
  342. data/ui/web/app/views/navbar_config.erb +411 -0
  343. data/ui/web/app/views/theme_management.erb +130 -0
  344. data/ui/web/app/views/view_dashboard.erb +779 -0
  345. data/ui/web/app/views/widgets.erb +249 -0
  346. data/ui/web/bin/scriptorium-web +164 -0
  347. data/ui/web/test/web_basic_test.rb +38 -0
  348. data/ui/web/test_navbar.txt +7 -0
  349. data/ui/web/tmp/timing.log +17 -0
  350. data/ui/web/tmp/web_server.log +0 -0
  351. metadata +434 -8
  352. data/lib/scriptorium/engine.rb +0 -22
  353. data/test/engine/unit.rb +0 -44
@@ -0,0 +1,527 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'minitest/autorun'
4
+ require_relative '../lib/scriptorium'
5
+ require_relative 'web_test_helper'
6
+
7
+ class WebWorkflowTest < Minitest::Test
8
+ include Scriptorium::Helpers
9
+ include WebTestHelper
10
+
11
+ # Test repository path and web server config are now in WebTestHelper
12
+
13
+ def setup
14
+ cleanup_test_repo
15
+ # Disable DBC contracts in tests
16
+ ENV['DBC_DISABLED'] = 'true'
17
+
18
+ # rbenv hack to ensure correct Ruby version
19
+ ENV['PATH'] = "#{ENV['HOME']}/.rbenv/shims:#{ENV['PATH']}"
20
+ ENV['RBENV_VERSION'] = '3.2.3'
21
+ end
22
+
23
+ def teardown
24
+ cleanup_test_repo
25
+ stop_web_server
26
+ end
27
+
28
+ # Test complete repository setup workflow via web
29
+ def test_001_complete_repository_setup
30
+ start_web_server
31
+
32
+ # Step 1: Check initial state (no repository)
33
+ response = get("/")
34
+ assert_response_success(response, "Dashboard should load initially")
35
+ assert_includes_concise response, "No repository loaded", "Should show no repository message"
36
+
37
+ # Step 2: Create repository via web form
38
+ response = post("/create_repo", {})
39
+ assert_response_redirect(response, "Repository creation should redirect after success")
40
+
41
+ # Step 3: Verify repository was created
42
+ response = get("/")
43
+ assert_response_success(response, "Dashboard should load after repo creation")
44
+ assert_includes_concise response, "scriptorium-TEST", "Should show repository name"
45
+
46
+ # Complete repository setup workflow working
47
+ end
48
+
49
+ # Test view creation workflow via web
50
+ def test_002_view_creation_workflow
51
+ start_web_server
52
+
53
+ # Step 1: Create repository first
54
+ post("/create_repo", {})
55
+
56
+ # Step 2: Create view via web form
57
+ response = post("/create_view", {
58
+ name: "test-view",
59
+ title: "Test View",
60
+ subtitle: "A test view for web testing"
61
+ })
62
+ assert_response_redirect(response, "View creation should redirect after success")
63
+
64
+ # Step 3: Verify view was created
65
+ response = get("/")
66
+ assert_response_success(response, "Dashboard should load after view creation")
67
+ assert_includes_concise response, "test-view", "Should show created view"
68
+
69
+ # View creation workflow working
70
+ end
71
+
72
+ # Test post creation workflow via web
73
+ def test_003_post_creation_workflow
74
+ start_web_server
75
+
76
+ # Step 1: Setup repository and view
77
+ post("/create_repo", {})
78
+ post("/create_view", {
79
+ name: "test-view",
80
+ title: "Test View",
81
+ subtitle: "A test view for web testing"
82
+ })
83
+
84
+ # Step 2: Create post via web form
85
+ response = post("/create_post", {
86
+ title: "Test Post via Web",
87
+ content: "This is a test post created via the web interface."
88
+ })
89
+ assert_response_redirect(response, "Post creation should redirect after success")
90
+
91
+ # Step 3: Verify post was created
92
+ response = get("/view/test-view")
93
+ assert_response_success(response, "View dashboard should load")
94
+ assert_includes_concise response, "Test Post via Web", "Should show created post"
95
+
96
+ # Post creation workflow working
97
+ end
98
+
99
+ # Test post editing workflow via web
100
+ def test_004_post_editing_workflow
101
+ start_web_server
102
+
103
+ # Step 1: Create a fresh test repository
104
+ response = post("/create_repo", {})
105
+ assert_response_redirect(response, "Repository creation should redirect after success")
106
+
107
+ # Step 2: Create a test view
108
+ response = post("/create_view", {
109
+ name: "test-view",
110
+ title: "Test View",
111
+ subtitle: "A test view for web testing"
112
+ })
113
+ assert_response_redirect(response, "View creation should redirect after success")
114
+
115
+ # Step 3: Create a post via the web interface
116
+ response = post("/create_post", {
117
+ title: "Test Post for Editing",
118
+ content: "Original content of the test post."
119
+ })
120
+ assert_response_redirect(response, "Post creation should redirect after success")
121
+
122
+ # Step 4: Get the post ID from the redirect
123
+ # The redirect should be to /view/test-view?edit_post=#{post_num}
124
+ redirect_location = response['Location']
125
+ assert_includes redirect_location, "edit_post=", "Should redirect with edit_post parameter"
126
+
127
+ # Extract post ID from redirect URL
128
+ post_id = redirect_location.match(/edit_post=(\d+)/)[1]
129
+ assert_match(/^\d+$/, post_id, "Post ID should be numeric")
130
+
131
+ # Step 5: Edit the post content
132
+ edited_content = "Updated content with LiveText syntax.\n\n.h1 Updated Title\n\nThis is *bold text and _italic text.\n\n.raw\nRaw content here\n__RAW__\n\n.def test_function\n puts 'Hello World'\n.end"
133
+
134
+ response = post("/save_post/#{post_id}", {
135
+ content: edited_content
136
+ })
137
+ assert_response_redirect(response, "Post saving should redirect after success")
138
+
139
+ # Step 6: Verify the post was saved by checking the redirect message
140
+ assert_includes response['Location'], "message=Post saved successfully", "Should show success message"
141
+
142
+ # Step 7: Verify the content was actually saved by reading the post file
143
+ # This tests the actual file writing functionality
144
+ # The file path uses the 4-digit post number format (e.g., 0001, 0002)
145
+ post_number = sprintf("%04d", post_id.to_i)
146
+ post_file_path = "#{WebTestHelper::TEST_REPO_PATH}/posts/#{post_number}/source.lt3"
147
+ assert File.exist?(post_file_path), "Post source file should exist at #{post_file_path}"
148
+
149
+ saved_content = File.read(post_file_path)
150
+
151
+ # The write_file helper adds a trailing newline, so we need to account for that
152
+ expected_content = edited_content + "\n"
153
+ assert_equal expected_content, saved_content, "Saved content should match edited content (with trailing newline)"
154
+
155
+ # Post editing workflow working
156
+ end
157
+
158
+ # Test asset management workflow via web
159
+ def test_005_asset_management_workflow
160
+ start_web_server
161
+
162
+ # Step 1: Setup repository and view
163
+ post("/create_repo", {})
164
+ post("/create_view", {
165
+ name: "test-view",
166
+ title: "Test View",
167
+ subtitle: "A test view for web testing"
168
+ })
169
+
170
+ # Step 2: Access asset management
171
+ response = get("/asset_management")
172
+ assert_response_success(response, "Asset management page should load")
173
+ assert_includes_concise response, "Asset Management", "Should show asset management interface"
174
+
175
+ # Step 3: Test asset upload (simulated)
176
+ # Note: This would require file upload handling in a real test
177
+ assert_includes_concise response, "upload", "Should have upload functionality"
178
+
179
+ # Asset management workflow accessible
180
+ end
181
+
182
+ # Test view configuration workflow via web
183
+ def test_006_view_configuration_workflow
184
+ start_web_server
185
+
186
+ # Step 1: Setup repository and view
187
+ post("/create_repo", {})
188
+ post("/create_view", {
189
+ name: "test-view",
190
+ title: "Test View",
191
+ subtitle: "A test view for web testing"
192
+ })
193
+
194
+ # Step 2: Access view configuration
195
+ response = get("/configure_view/test-view")
196
+ assert_response_success(response, "View configuration page should load")
197
+ assert_includes_concise response, "Configure View", "Should show configuration interface"
198
+
199
+ # Step 3: Test configuration saving
200
+ response = post("/save_view_config/test-view", {
201
+ view_title: "Updated Test View",
202
+ view_subtitle: "Updated subtitle",
203
+ view_theme: "standard"
204
+ })
205
+ assert_response_redirect(response, "Configuration save should redirect after success")
206
+
207
+ # View configuration workflow working
208
+ end
209
+
210
+ # Test deployment workflow via web
211
+ def test_007_deployment_workflow
212
+ start_web_server
213
+
214
+ # Step 1: Setup repository and view
215
+ post("/create_repo", {})
216
+ post("/create_view", {
217
+ name: "test-view",
218
+ title: "Test View",
219
+ subtitle: "A test view for web testing"
220
+ })
221
+
222
+ # Step 2: Access deployment configuration
223
+ response = get("/deploy_config")
224
+ assert_response_success(response, "Deployment config page should load")
225
+ assert_includes_concise response, "Deployment Configuration", "Should show deployment interface"
226
+
227
+ # Step 3: Test deployment configuration saving
228
+ response = post("/deploy_config", {
229
+ deploy_config: "proto http\nserver localhost\npath /test",
230
+ from_deploy: "0"
231
+ })
232
+ assert_response_redirect(response, "Deployment config save should redirect after success")
233
+
234
+ # Deployment workflow accessible
235
+ end
236
+
237
+ # Test publish/unpublish workflow via web
238
+ def test_008_publish_unpublish_workflow
239
+ start_web_server
240
+ setup_test_environment
241
+
242
+ # Create a post
243
+ response = post("/create_post", { title: "Test Publish Post", content: "Content for publish test" })
244
+ assert_response_redirect(response, "Should create post successfully")
245
+
246
+ # Extract post ID from redirect
247
+ redirect_location = response['Location']
248
+ post_id = redirect_location.match(/edit_post=(\d+)/)[1]
249
+
250
+ # Initially post should be unpublished
251
+ post_file = "#{WebTestHelper::TEST_REPO_PATH}/posts/#{sprintf("%04d", post_id.to_i)}/source.lt3"
252
+ assert File.exist?(post_file), "Post file should exist"
253
+
254
+ # Publish the post via web interface
255
+ response = post("/toggle_post_status/#{post_id}", {})
256
+ assert_equal 200, response.code.to_i, "Should return 200 for publish"
257
+
258
+ # Parse JSON response
259
+ json_response = JSON.parse(response.body)
260
+ assert json_response['success'], "Publish should succeed"
261
+ assert json_response['published'], "Post should be marked as published"
262
+
263
+ # Verify the HTML shows the post as published
264
+ response = get("/view/test-view")
265
+ assert_response_success(response, "Should load view dashboard")
266
+ assert_includes_concise response, "Published", "HTML should show 'Published' status"
267
+
268
+ # Unpublish the post via web interface
269
+ response = post("/toggle_post_status/#{post_id}", {})
270
+ assert_equal 200, response.code.to_i, "Should return 200 for unpublish"
271
+
272
+ # Parse JSON response
273
+ json_response = JSON.parse(response.body)
274
+ assert json_response['success'], "Unpublish should succeed"
275
+ refute json_response['published'], "Post should be marked as unpublished"
276
+
277
+ # Verify the HTML shows the post as unpublished
278
+ response = get("/view/test-view")
279
+ assert_response_success(response, "Should load view dashboard")
280
+ assert_includes_concise response, "unpublished", "HTML should show 'unpublished' status"
281
+ end
282
+
283
+ # Test delete/restore workflow via web
284
+ def test_009_delete_restore_workflow
285
+ start_web_server
286
+ setup_test_environment
287
+
288
+ # Create a post
289
+ response = post("/create_post", { title: "Test Delete Post", content: "Content for delete test" })
290
+ assert_response_redirect(response, "Should create post successfully")
291
+
292
+ # Extract post ID from redirect
293
+ redirect_location = response['Location']
294
+ post_id = redirect_location.match(/edit_post=(\d+)/)[1]
295
+
296
+ # Initially post should be visible
297
+ response = get("/view/test-view")
298
+ assert_response_success(response, "Should load view dashboard")
299
+ assert_includes_concise response, "Test Delete Post", "HTML should show post title"
300
+ refute_includes response.body, "text-decoration: line-through", "Post should not be strikethrough"
301
+
302
+ # Delete the post via web interface
303
+ response = post("/delete_post/#{post_id}", {})
304
+ assert_response_redirect(response, "Should redirect after delete")
305
+
306
+ # Verify the HTML shows the post as deleted (strikethrough)
307
+ response = get("/view/test-view")
308
+ assert_response_success(response, "Should load view dashboard")
309
+ assert_includes_concise response, "Test Delete Post", "HTML should still show post title"
310
+ assert_includes_concise response, "text-decoration: line-through", "Post should be strikethrough when deleted"
311
+
312
+ # Restore the post via web interface
313
+ response = post("/restore_post/#{post_id}", {})
314
+ assert_response_redirect(response, "Should redirect after restore")
315
+
316
+ # Verify the HTML shows the post as restored (no strikethrough)
317
+ response = get("/view/test-view")
318
+ assert_response_success(response, "Should load view dashboard")
319
+ assert_includes_concise response, "Test Delete Post", "HTML should show post title"
320
+ refute_includes response.body, "text-decoration: line-through", "Post should not be strikethrough when restored"
321
+ end
322
+
323
+ # Test error handling and edge cases
324
+ def test_010_error_handling
325
+ start_web_server
326
+
327
+ # Test 1: Access non-existent view
328
+ response = get("/view/non-existent-view")
329
+ assert_equal "302", response.code, "Should redirect for non-existent view"
330
+
331
+ # Test 2: Access pages without repository
332
+ response = get("/asset_management")
333
+ assert_equal "302", response.code, "Should redirect without repository"
334
+
335
+ # Test 3: Access pages without view
336
+ response = get("/configure_view/test-view")
337
+ assert_equal "302", response.code, "Should redirect without view"
338
+
339
+ # Error handling working correctly
340
+ end
341
+
342
+ # Test backup management page access
343
+ def test_005_backup_management_page_access
344
+ start_web_server
345
+ setup_test_environment
346
+
347
+ # Test accessing backup management page
348
+ response = get("/backup_management")
349
+ assert_response_success(response, "Backup management page should load")
350
+ assert_includes_concise response, "Backup Management", "Should show backup management title"
351
+ assert_includes_concise response, "Create New Backup", "Should show backup creation form"
352
+ assert_includes_concise response, "Available Backups", "Should show backups list section"
353
+ end
354
+
355
+ # Test backup creation via web form
356
+ def test_006_backup_creation_workflow
357
+ start_web_server
358
+ setup_test_environment
359
+
360
+ # Test creating a full backup
361
+ response = post("/backup_management/create", {
362
+ "type" => "full",
363
+ "description" => "Test full backup from web"
364
+ })
365
+ assert_response_redirect(response, "Backup creation should redirect after success")
366
+
367
+ # Verify redirect includes success message
368
+ assert_includes response['Location'], "message=Backup created successfully", "Should redirect with success message"
369
+
370
+ # Test accessing backup management page to verify backup was created
371
+ response = get("/backup_management")
372
+ assert_response_success(response, "Backup management page should load after backup creation")
373
+ assert_includes_concise response, "Test full backup from web", "Should show created backup description"
374
+ assert_includes_concise response, "full", "Should show backup type"
375
+ assert_includes_concise response, "ago", "Should show human-readable age"
376
+ end
377
+
378
+ # Test incremental backup creation
379
+ def test_007_incremental_backup_creation
380
+ start_web_server
381
+ setup_test_environment
382
+
383
+ # Create a full backup first
384
+ response = post("/backup_management/create", {
385
+ "type" => "full",
386
+ "description" => "Initial full backup"
387
+ })
388
+ assert_response_redirect(response, "Full backup creation should redirect")
389
+
390
+ # Create an incremental backup
391
+ response = post("/backup_management/create", {
392
+ "type" => "incr",
393
+ "description" => "Incremental backup after changes"
394
+ })
395
+ assert_response_redirect(response, "Incremental backup creation should redirect")
396
+
397
+ # Verify both backups are listed
398
+ response = get("/backup_management")
399
+ assert_response_success(response, "Backup management page should load")
400
+ assert_includes_concise response, "Initial full backup", "Should show full backup"
401
+ assert_includes_concise response, "Incremental backup after changes", "Should show incremental backup"
402
+ assert_includes_concise response, "incr", "Should show incremental type"
403
+ end
404
+
405
+ # Test backup creation with empty description
406
+ def test_008_backup_creation_empty_description
407
+ start_web_server
408
+ setup_test_environment
409
+
410
+ # Test creating backup with empty description
411
+ response = post("/backup_management/create", {
412
+ "type" => "full",
413
+ "description" => ""
414
+ })
415
+ assert_response_redirect(response, "Backup creation should redirect even with empty description")
416
+
417
+ # Verify backup was created
418
+ response = get("/backup_management")
419
+ assert_response_success(response, "Backup management page should load")
420
+ assert_includes_concise response, "No description", "Should show no description message"
421
+ end
422
+
423
+ # Test backup creation with invalid type
424
+ def test_009_backup_creation_invalid_type
425
+ start_web_server
426
+ setup_test_environment
427
+
428
+ # Test creating backup with invalid type
429
+ response = post("/backup_management/create", {
430
+ "type" => "invalid",
431
+ "description" => "Invalid backup type"
432
+ })
433
+ assert_response_redirect(response, "Invalid backup type should redirect with error")
434
+
435
+ # Verify error message in redirect
436
+ assert_includes response['Location'], "error=Invalid backup type", "Should redirect with error message"
437
+ end
438
+
439
+ # Test backup management page with no backups
440
+ def test_010_backup_management_empty_state
441
+ start_web_server
442
+ setup_test_environment
443
+
444
+ # Access backup management page with no backups
445
+ response = get("/backup_management")
446
+ assert_response_success(response, "Backup management page should load")
447
+ assert_includes_concise response, "No backups available yet", "Should show empty state message"
448
+ assert_includes_concise response, "Create your first backup using the form above", "Should show empty state instructions"
449
+ end
450
+
451
+ # Test backup management page styling and UI elements
452
+ def test_011_backup_management_ui_elements
453
+ start_web_server
454
+ setup_test_environment
455
+
456
+ # Create a backup first
457
+ post("/backup_management/create", {
458
+ "type" => "full",
459
+ "description" => "UI test backup"
460
+ })
461
+
462
+ # Test UI elements
463
+ response = get("/backup_management")
464
+ assert_response_success(response, "Backup management page should load")
465
+
466
+ # Check for key UI elements
467
+ assert_includes_concise response, "backup-form", "Should have backup form styling"
468
+ assert_includes_concise response, "backups-table", "Should have backups table styling"
469
+ assert_includes_concise response, "backup-type", "Should have backup type styling"
470
+ assert_includes_concise response, "backup-age", "Should have backup age styling"
471
+ assert_includes_concise response, "Back to Dashboard", "Should have back navigation link"
472
+ end
473
+
474
+ # Test backup management page with multiple backups
475
+ def test_012_backup_management_multiple_backups
476
+ start_web_server
477
+ setup_test_environment
478
+
479
+ # Create multiple backups
480
+ post("/backup_management/create", {
481
+ "type" => "full",
482
+ "description" => "First backup"
483
+ })
484
+
485
+ sleep(1) # Ensure different timestamps
486
+
487
+ post("/backup_management/create", {
488
+ "type" => "incr",
489
+ "description" => "Second backup"
490
+ })
491
+
492
+ sleep(1) # Ensure different timestamps
493
+
494
+ post("/backup_management/create", {
495
+ "type" => "full",
496
+ "description" => "Third backup"
497
+ })
498
+
499
+ # Verify all backups are listed
500
+ response = get("/backup_management")
501
+ assert_response_success(response, "Backup management page should load")
502
+
503
+ # Check that all backups are present
504
+ assert_includes_concise response, "First backup", "Should show first backup"
505
+ assert_includes_concise response, "Second backup", "Should show second backup"
506
+ assert_includes_concise response, "Third backup", "Should show third backup"
507
+
508
+ # Check that backups are sorted by timestamp (newest first)
509
+ # This is harder to test without parsing the HTML, but we can verify the structure
510
+ assert_includes_concise response, "backups-table", "Should have backups table"
511
+ end
512
+
513
+ # Test backup management page error handling
514
+ def test_013_backup_management_error_handling
515
+ start_web_server
516
+ setup_test_environment
517
+
518
+ # Test accessing backup management without a view (should redirect)
519
+ # This is harder to test since we need to simulate no current view
520
+ # For now, just verify the page loads normally with a view
521
+ response = get("/backup_management")
522
+ assert_response_success(response, "Backup management page should load with valid view")
523
+ end
524
+
525
+ private
526
+ # All helper methods are now in WebTestHelper
527
+ end
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'open3'
4
+ require 'timeout'
5
+ require 'minitest/autorun'
6
+ require_relative '../lib/scriptorium'
7
+
8
+ class WizardTest < Minitest::Test
9
+ def setup
10
+ @test_repo_path = "scriptorium-TEST"
11
+ cleanup_test_repo
12
+ end
13
+
14
+ def teardown
15
+ cleanup_test_repo
16
+ end
17
+
18
+ def test_001_wizard_first_view_flow
19
+ # Test the wizard flow for creating the first view
20
+ # This should create a new repository and set up the first view
21
+
22
+ # Ensure clean start
23
+ cleanup_test_repo
24
+
25
+ # Run the wizard
26
+ result = system("echo -e 'y\n4\nn' | ruby bin/scriptorium")
27
+ assert result, "Wizard should complete successfully"
28
+
29
+ # Verify repository was created
30
+ assert Dir.exist?(TEST_REPO_PATH), "Repository should be created"
31
+
32
+ # Verify sample view was created
33
+ sample_view_path = File.join(TEST_REPO_PATH, "views", "sample")
34
+ assert Dir.exist?(sample_view_path), "Sample view should be created"
35
+
36
+ # Verify config files were created
37
+ config_path = File.join(TEST_REPO_PATH, "config")
38
+ assert Dir.exist?(config_path), "Config directory should be created"
39
+
40
+ # Verify current view is set
41
+ current_view_file = File.join(config_path, "currentview.txt")
42
+ assert File.exist?(current_view_file), "Current view file should be created"
43
+ assert_equal "sample", File.read(current_view_file).strip, "Current view should be 'sample'"
44
+ end
45
+
46
+ def test_002_wizard_method_exists
47
+ # Test that the wizard method exists and can be called
48
+ # This is a basic smoke test to ensure the wizard is available
49
+
50
+ # Load the scriptorium binary to access the wizard method
51
+ load 'bin/scriptorium'
52
+
53
+ # The wizard method should be available in the TUI class
54
+ assert defined?(TUI), "TUI class should be defined"
55
+ assert TUI.instance_methods.include?(:wizard), "TUI should have wizard method"
56
+ end
57
+
58
+ def test_003_wizard_with_new_repo
59
+ # Test wizard behavior when creating a completely new repository
60
+ # This tests the full wizard flow including repository creation
61
+
62
+ # Ensure clean start
63
+ cleanup_test_repo
64
+
65
+ # Create a new API instance to simulate fresh start
66
+ api = Scriptorium::API.new(testmode: true)
67
+
68
+ # Verify no repository exists initially
69
+ refute Dir.exist?(TEST_REPO_PATH), "Repository should not exist initially"
70
+
71
+ # Run wizard with new repo creation
72
+ result = system("echo -e 'y\n4\ny\ntestview\nTest View\nTest Subtitle\nn\nn\nn\nn\nn' | ruby bin/scriptorium")
73
+ assert result, "Wizard should complete successfully with new repo"
74
+
75
+ # Verify repository was created
76
+ assert Dir.exist?(TEST_REPO_PATH), "Repository should be created"
77
+
78
+ # Verify test view was created
79
+ test_view_path = File.join(TEST_REPO_PATH, "views", "testview")
80
+ assert Dir.exist?(test_view_path), "Test view should be created"
81
+
82
+ # Verify view config
83
+ view_config = File.join(test_view_path, "config.txt")
84
+ assert File.exist?(view_config), "View config should be created"
85
+ config_content = File.read(view_config)
86
+ assert_includes config_content, "title Test View", "View title should be set"
87
+ assert_includes config_content, "subtitle Test Subtitle", "View subtitle should be set"
88
+ end
89
+
90
+ def await_output(stdout, expected_str, timeout_secs = 10)
91
+ buffer = ""
92
+ start_time = Time.now
93
+
94
+ loop do
95
+ if Time.now - start_time > timeout_secs
96
+ raise "Timeout waiting for '#{expected_str}'. Buffer: #{buffer.inspect}"
97
+ end
98
+
99
+ begin
100
+ chunk = stdout.read_nonblock(1000)
101
+ buffer += chunk
102
+ puts "Buffer: #{buffer.inspect}"
103
+
104
+ if buffer.include?(expected_str)
105
+ puts "Found expected string: #{expected_str.inspect}"
106
+ return buffer
107
+ end
108
+ rescue IO::EAGAINWaitReadable, IO::EWOULDBLOCKWaitReadable
109
+ sleep 0.1
110
+ rescue EOFError
111
+ raise "EOF reached while waiting for '#{expected_str}'. Buffer: #{buffer.inspect}"
112
+ end
113
+ end
114
+ end
115
+
116
+ private
117
+
118
+ def cleanup_test_repo
119
+ if Dir.exist?(@test_repo_path)
120
+ FileUtils.rm_rf(@test_repo_path)
121
+ end
122
+ end
123
+ end