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,478 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'minitest/autorun'
4
+
5
+ require_relative '../../lib/scriptorium'
6
+ require_relative '../test_helpers'
7
+
8
+ class TestDeploy < Minitest::Test
9
+ include Scriptorium::Exceptions
10
+ include Scriptorium::Helpers
11
+ include TestHelpers
12
+
13
+ def setup
14
+ # Clean up any existing test directory first
15
+ test_dir = "test/scriptorium-TEST"
16
+ FileUtils.rm_rf(test_dir) if Dir.exist?(test_dir)
17
+
18
+ @repo = Scriptorium::Repo.create(test_dir, testmode: true)
19
+ @view = @repo.create_view("testview", "Test View", "Test Subtitle")
20
+ @api = Scriptorium::API.new(testmode: true)
21
+ @api.open_repo(@repo.root)
22
+ end
23
+
24
+ def teardown
25
+ # Clean up any test directories that were created
26
+ Dir.glob("test/scriptorium-TEST*").each do |dir|
27
+ system("rm -rf #{dir}")
28
+ end
29
+ # Clean up any root directory created during deployment testing
30
+ # FileUtils.rm_rf("root") if Dir.exist?("root")
31
+ end
32
+
33
+ def test_001_deploy_config_file_creation
34
+ # Test that deploy.txt can be created and read
35
+ deploy_file = @view.dir/:config/"deploy.txt"
36
+ write_file(deploy_file, "user@example.com:/var/www/html/")
37
+
38
+ assert File.exist?(deploy_file), "Deploy config file should exist"
39
+ content = read_file(deploy_file)
40
+ assert_equal "user@example.com:/var/www/html/", content.strip
41
+ end
42
+
43
+ def test_002_deploy_config_validation
44
+ # Test various deploy config formats
45
+ deploy_file = @view.dir/:config/"deploy.txt"
46
+
47
+ # Valid formats
48
+ valid_configs = [
49
+ "user@server:/path/",
50
+ "user@server.com:/var/www/html/",
51
+ "deploy@mysite.com:/home/deploy/public_html/"
52
+ ]
53
+
54
+ valid_configs.each do |config|
55
+ write_file(deploy_file, config)
56
+ content = read_file(deploy_file).strip
57
+ assert_equal config, content, "Should read config: #{config}"
58
+ end
59
+ end
60
+
61
+ def test_003_deploy_requires_output_directory
62
+ # Test that deployment requires output directory to exist
63
+ deploy_file = @view.dir/:config/"deploy.txt"
64
+ write_file(deploy_file, "user@server:/path/")
65
+
66
+ # Output directory should exist after view creation (created by make_tree)
67
+ output_dir = @view.dir/:output
68
+ assert Dir.exist?(output_dir), "Output directory should exist after view creation"
69
+ end
70
+
71
+ def test_004_deploy_rsync_command_format
72
+ # Test that the rsync command format is correct
73
+ deploy_config = "user@server:/var/www/html/"
74
+ output_dir = @view.dir/:output
75
+ make_dir(output_dir)
76
+
77
+ # Expected rsync command format
78
+ expected_cmd = "rsync -r -z #{output_dir}/ #{deploy_config}/"
79
+
80
+ # This is the format used in the TUI
81
+ actual_cmd = "rsync -r -z #{output_dir}/ #{deploy_config}/"
82
+
83
+ assert_equal expected_cmd, actual_cmd, "Rsync command format should match"
84
+ end
85
+
86
+ def test_005_deploy_with_sample_content
87
+ # Test deployment with actual content
88
+ deploy_file = @view.dir/:config/"deploy.txt"
89
+ write_file(deploy_file, "user@server:/path/")
90
+
91
+ # Create output directory and some content
92
+ output_dir = @view.dir/:output
93
+ make_dir(output_dir)
94
+ make_dir(output_dir/:posts)
95
+ make_dir(output_dir/:permalink)
96
+
97
+ # Create a sample file
98
+ sample_file = output_dir/"index.html"
99
+ write_file(sample_file, "<html><body>Test content</body></html>")
100
+
101
+ assert File.exist?(sample_file), "Sample file should exist"
102
+ assert Dir.exist?(output_dir/:posts), "Posts directory should exist"
103
+ assert Dir.exist?(output_dir/:permalink), "Permalink directory should exist"
104
+ end
105
+
106
+ def test_006_deploy_marker_file_creation
107
+ # Test that deployment creates a marker file
108
+ output_dir = @view.dir/:output
109
+
110
+ # Simulate deployment marker creation
111
+ marker_content = "Deployed: #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}"
112
+ marker_file = output_dir/"last-deployed.txt"
113
+ write_file(marker_file, marker_content)
114
+
115
+ assert File.exist?(marker_file), "Deployment marker file should exist"
116
+ content = read_file(marker_file)
117
+ assert content.start_with?("Deployed:"), "Marker file should start with 'Deployed:'"
118
+ end
119
+
120
+ def test_007_deploy_with_symlinks
121
+ # Test that deployment handles symlinks correctly
122
+ output_dir = @view.dir/:output
123
+ make_dir(output_dir/:permalink)
124
+
125
+ # Create a test symlink
126
+ target_file = output_dir/:permalink/"0001-test-post.html"
127
+ symlink_file = output_dir/:permalink/"test-post.html"
128
+
129
+ # Create the target file
130
+ write_file(target_file, "<html><body>Test post</body></html>")
131
+
132
+ # Create the copy
133
+ FileUtils.cp(target_file, symlink_file)
134
+
135
+ # Verify copy exists
136
+ assert File.exist?(symlink_file), "Copy should exist"
137
+ assert !File.symlink?(symlink_file), "Should not be a symlink"
138
+
139
+ # Test that rsync command works with regular files (no symlink preservation needed)
140
+ expected_cmd = "rsync -r -z #{output_dir}/ user@server:/path/"
141
+ actual_cmd = "rsync -r -z #{output_dir}/ user@server:/path/"
142
+
143
+ assert_equal expected_cmd, actual_cmd, "Rsync command should work with regular files"
144
+ end
145
+
146
+ def test_008_deploy_symlink_target_verification
147
+ # Test that symlink targets are accessible after deployment
148
+ output_dir = @view.dir/:output
149
+ make_dir(output_dir/:permalink)
150
+
151
+ # Create a test symlink with target
152
+ target_file = output_dir/:permalink/"0001-another-post.html"
153
+ symlink_file = output_dir/:permalink/"another-post.html"
154
+
155
+ # Create the target file
156
+ write_file(target_file, "<html><body>Another test post</body></html>")
157
+
158
+ # Create the copy
159
+ FileUtils.cp(target_file, symlink_file)
160
+
161
+ # Verify copy exists and has same content
162
+ assert File.exist?(symlink_file), "Copy should exist"
163
+ assert !File.symlink?(symlink_file), "Should not be a symlink"
164
+
165
+ # Verify copy has same content as target
166
+ copy_content = File.read(symlink_file)
167
+ target_content = File.read(target_file)
168
+ assert_equal target_content, copy_content, "Copy should have same content as target"
169
+ end
170
+
171
+ def test_009_domain_extraction_from_deploy_config
172
+ # Test domain extraction from various deploy config formats
173
+ test_cases = [
174
+ ["user@example.com:/var/www/html/", "example.com"],
175
+ ["deploy@mysite.com:/home/deploy/public_html/", "mysite.com"],
176
+ ["admin@blog.example.org:/srv/www/", "blog.example.org"]
177
+ ]
178
+
179
+ test_cases.each do |config, expected_domain|
180
+ # Simulate the domain extraction logic
181
+ domain = nil
182
+ if config =~ /@([^:]+):/
183
+ domain = $1
184
+ end
185
+
186
+ assert_equal expected_domain, domain, "Should extract domain from: #{config}"
187
+ end
188
+ end
189
+
190
+ def test_010_deploy_verification_url_format
191
+ # Test that verification URL is correctly formatted
192
+ domain = "example.com"
193
+ expected_url = "https://#{domain}/last-deployed.txt"
194
+
195
+ assert_equal "https://example.com/last-deployed.txt", expected_url, "Verification URL should be correctly formatted"
196
+ end
197
+
198
+ # New API deployment method tests
199
+
200
+ def test_011_can_deploy_status_check
201
+ # Test can_deploy? with various status configurations
202
+
203
+ # Test with deploy status 'n' (should fail)
204
+ status_file = @repo.root/"views"/"testview"/"config"/"status.txt"
205
+ write_file(status_file, "deploy n")
206
+
207
+ refute @api.can_deploy?("testview"), "Should not deploy when status is 'n'"
208
+
209
+ # Test with deploy status 'y' (should pass status check)
210
+ write_file(status_file, "deploy y")
211
+
212
+ # This will still fail because deploy.txt doesn't exist, but status check passes
213
+ refute @api.can_deploy?("testview"), "Should fail when deploy.txt is missing"
214
+ end
215
+
216
+ def test_012_can_deploy_config_validation
217
+ # Test can_deploy? with various deploy.txt configurations
218
+
219
+ # Set deploy status to 'y'
220
+ status_file = @repo.root/"views"/"testview"/"config"/"status.txt"
221
+ write_file(status_file, "deploy y")
222
+
223
+ # Test with missing deploy.txt
224
+ refute @api.can_deploy?("testview"), "Should fail when deploy.txt is missing"
225
+
226
+ # Test with incomplete deploy.txt
227
+ deploy_file = @repo.root/"views"/"testview"/"config"/"deploy.txt"
228
+ write_file(deploy_file, "user root\nserver example.com")
229
+
230
+ refute @api.can_deploy?("testview"), "Should fail when required fields are missing"
231
+
232
+ # Test with complete deploy.txt
233
+ write_file(deploy_file, "user root\nserver.example.com\ndocroot /var/www/html\npath testview")
234
+
235
+ # This will fail SSH test, but we'll skip that for now
236
+ # The method should at least pass the config validation
237
+ begin
238
+ result = @api.can_deploy?("testview")
239
+ # If SSH test passes, result should be true
240
+ # If SSH test fails, result should be false
241
+ assert result == false || result == true, "Should return boolean result"
242
+ rescue => e
243
+ # SSH test might fail, which is expected in test environment
244
+ skip "SSH test failed (expected in test environment): #{e.message}"
245
+ end
246
+ end
247
+
248
+ def test_013_deploy_dry_run_mode
249
+ # Test deploy method in dry-run mode
250
+
251
+ # Set up deployment configuration
252
+ status_file = @repo.root/"views"/"testview"/"config"/"status.txt"
253
+ write_file(status_file, "deploy y")
254
+
255
+ deploy_file = @repo.root/"views"/"testview"/"config"/"deploy.txt"
256
+ write_file(deploy_file, "user root\nserver.example.com\ndocroot /var/www/html\npath testview")
257
+
258
+ # Create some output content
259
+ output_dir = @repo.root/"views"/"testview"/"output"
260
+ make_dir(output_dir)
261
+ write_file(output_dir/"index.html", "<html><body>Test content</body></html>")
262
+
263
+ # Test that the deploy method can be called (deployment test will fail, but that's expected)
264
+ begin
265
+ result = @api.deploy("testview", dry_run: true)
266
+ assert result, "Dry-run should succeed"
267
+ rescue DeploymentNotReady => e
268
+ # Deployment not ready, which is expected in test environment
269
+ skip "Deployment not ready (expected in test environment): #{e.message}"
270
+ rescue => e
271
+ raise e
272
+ end
273
+ end
274
+
275
+ def test_014_deploy_missing_configuration
276
+ # Test deploy method with missing configuration
277
+
278
+ # Test with no status file
279
+ assert_raises(DeploymentNotReady, "Should fail when status is not ready") do
280
+ @api.deploy("testview")
281
+ end
282
+
283
+ # Test with deploy status 'n'
284
+ status_file = @repo.root/"views"/"testview"/"config"/"status.txt"
285
+ write_file(status_file, "deploy n")
286
+
287
+ assert_raises(DeploymentNotReady, "Should fail when status is not ready") do
288
+ @api.deploy("testview")
289
+ end
290
+ end
291
+
292
+ def test_015_deploy_invalid_configuration
293
+ # Test deploy method with invalid configuration
294
+
295
+ # Set deploy status to 'y'
296
+ status_file = @repo.root/"views"/"testview"/"config"/"status.txt"
297
+ write_file(status_file, "deploy y")
298
+
299
+ # Test with missing deploy.txt
300
+ assert_raises(DeploymentNotReady, "Should fail when deploy.txt is missing") do
301
+ @api.deploy("testview")
302
+ end
303
+
304
+ # Test with incomplete deploy.txt
305
+ deploy_file = @repo.root/"views"/"testview"/"config"/"deploy.txt"
306
+ write_file(deploy_file, "user root\nserver example.com")
307
+
308
+ assert_raises(DeploymentNotReady, "Should fail when required fields are missing") do
309
+ @api.deploy("testview")
310
+ end
311
+ end
312
+
313
+ def test_016_deploy_rsync_command_construction
314
+ # Test that deploy method constructs correct rsync command
315
+
316
+ # Set up deployment configuration
317
+ status_file = @repo.root/"views"/"testview"/"config"/"status.txt"
318
+ write_file(status_file, "deploy y")
319
+
320
+ deploy_file = @repo.root/"views"/"testview"/"config"/"deploy.txt"
321
+ write_file(deploy_file, "user root\nserver example.com\ndocroot /var/www/html\npath testview")
322
+
323
+ # Create output content
324
+ output_dir = @repo.root/"views"/"testview"/"output"
325
+ make_dir(output_dir)
326
+ write_file(output_dir/"index.html", "<html><body>Test content</body></html>")
327
+
328
+ # Test dry-run to see the command that would be executed
329
+ begin
330
+ result = @api.deploy("testview", dry_run: true)
331
+ assert result, "Dry-run should succeed"
332
+ rescue DeploymentNotReady => e
333
+ # Deployment not ready, which is expected in test environment
334
+ skip "Deployment not ready (expected in test environment): #{e.message}"
335
+ rescue => e
336
+ raise e
337
+ end
338
+
339
+ # The dry-run should output the rsync command to stdout
340
+ # We can't easily capture this in the test, but we can verify the method runs
341
+ # The actual command format is: "rsync -r -z -l #{output_dir}/ #{remote_path}/"
342
+ end
343
+
344
+
345
+
346
+ def test_018_deploy_ssh_keys_test
347
+ # Test SSH key validation (will likely be skipped in test environment)
348
+
349
+ # Set up deployment configuration
350
+ status_file = @repo.root/"views"/"testview"/"config"/"status.txt"
351
+ write_file(status_file, "deploy y")
352
+
353
+ deploy_file = @repo.root/"views"/"testview"/"config"/"deploy.txt"
354
+ write_file(deploy_file, "user root\nserver example.com\ndocroot /var/www/html\npath testview")
355
+
356
+ # Test SSH key validation
357
+ begin
358
+ result = @api.can_deploy?("testview")
359
+ # This might pass or fail depending on SSH configuration
360
+ assert result == false || result == true, "Should return boolean result"
361
+ rescue => e
362
+ # SSH test might fail, which is expected in test environment
363
+ skip "SSH test failed (expected in test environment): #{e.message}"
364
+ end
365
+ end
366
+
367
+ def test_020_deploy_config_parsing
368
+ # Test various deployment config formats
369
+ test_cases = [
370
+ # Valid space-separated format
371
+ {
372
+ input: "user root\nserver example.com\npath sample",
373
+ expected: {"user" => "root", "server" => "example.com", "path" => "sample"},
374
+ description: "valid space-separated format"
375
+ },
376
+ # Config with comments
377
+ {
378
+ input: "# This is a comment\nuser root\nserver example.com\npath sample",
379
+ expected: {"user" => "root", "server" => "example.com", "path" => "sample"},
380
+ description: "config with comments"
381
+ },
382
+ # Missing required fields
383
+ {
384
+ input: "user root\nserver example.com",
385
+ expected: {"user" => "root", "server" => "example.com"},
386
+ description: "missing path field"
387
+ },
388
+ # Empty config
389
+ {
390
+ input: "",
391
+ expected: {},
392
+ description: "empty config"
393
+ },
394
+ # Config with junk lines
395
+ {
396
+ input: "user root\njunk line here\nserver example.com\npath sample",
397
+ expected: {"user" => "root", "junk" => "line here", "server" => "example.com", "path" => "sample"},
398
+ description: "config with junk lines"
399
+ },
400
+ # Config with extra whitespace
401
+ {
402
+ input: " user root \n server example.com \n path sample ",
403
+ expected: {"user" => "root", "server" => "example.com", "path" => "sample"},
404
+ description: "config with extra whitespace"
405
+ }
406
+ ]
407
+
408
+ test_cases.each do |test_case|
409
+ result = @api.parse_deploy_config(test_case[:input])
410
+ assert_equal test_case[:expected], result,
411
+ "Failed for #{test_case[:description]}: expected '#{test_case[:expected]}', got '#{result}'"
412
+ end
413
+ end
414
+
415
+ def test_020b_build_rsync_destination
416
+ # Test building rsync destinations from config hashes
417
+ test_cases = [
418
+ {
419
+ config: {"user" => "root", "server" => "example.com", "path" => "sample"},
420
+ expected: "root@example.com:sample",
421
+ description: "complete config"
422
+ },
423
+ {
424
+ config: {"user" => "root", "server" => "example.com"},
425
+ expected: nil,
426
+ description: "missing path"
427
+ },
428
+ {
429
+ config: {},
430
+ expected: nil,
431
+ description: "empty config"
432
+ }
433
+ ]
434
+
435
+ test_cases.each do |test_case|
436
+ result = @api.build_rsync_destination(test_case[:config])
437
+ if test_case[:expected].nil?
438
+ assert_nil result, "Failed for #{test_case[:description]}: expected nil, got '#{result}'"
439
+ else
440
+ assert_equal test_case[:expected], result,
441
+ "Failed for #{test_case[:description]}: expected '#{test_case[:expected]}', got '#{result}'"
442
+ end
443
+ end
444
+ end
445
+
446
+
447
+
448
+ def test_022_deploy_config_edge_cases
449
+ # Test edge cases that could cause issues
450
+ edge_cases = [
451
+ # Config with only junk
452
+ {
453
+ input: "this is not a config\nneither is this\nor this",
454
+ expected: {"this" => "is not a config", "neither" => "is this", "or" => "this"},
455
+ description: "only junk lines"
456
+ },
457
+ # Config with empty lines
458
+ {
459
+ input: "user root\n\nserver example.com\n\npath sample\n",
460
+ expected: {"user" => "root", "server" => "example.com", "path" => "sample"},
461
+ description: "config with empty lines"
462
+ },
463
+ # Config with special characters
464
+ {
465
+ input: "user root\nserver example.com\npath /var/www/html",
466
+ expected: {"user" => "root", "server" => "example.com", "path" => "/var/www/html"},
467
+ description: "path with special characters"
468
+ }
469
+ ]
470
+
471
+ edge_cases.each do |test_case|
472
+ result = @api.parse_deploy_config(test_case[:input])
473
+ assert_equal test_case[:expected], result,
474
+ "Failed for #{test_case[:description]}: expected '#{test_case[:expected]}', got '#{result}'"
475
+ end
476
+ end
477
+
478
+ end
@@ -0,0 +1,168 @@
1
+ require 'minitest/autorun'
2
+ require_relative '../../lib/scriptorium'
3
+ require_relative '../test_helpers'
4
+
5
+ class TestEditPost < Minitest::Test
6
+ include TestHelpers
7
+
8
+ def setup
9
+ @test_dir = "test/scriptorium-TEST"
10
+ FileUtils.rm_rf(@test_dir) if Dir.exist?(@test_dir)
11
+
12
+ @api = Scriptorium::API.new(testmode: true)
13
+ @api.create_repo(@test_dir)
14
+ @api.open_repo(@test_dir)
15
+ end
16
+
17
+ def teardown
18
+ FileUtils.rm_rf(@test_dir) if Dir.exist?(@test_dir)
19
+ end
20
+
21
+ def test_001_edit_post_no_changes
22
+ @api.create_view("test_view", "Test View")
23
+ post = @api.create_post("Test Post", "Test body")
24
+
25
+ # Initially unpublished and undeployed
26
+ refute @api.post_published?(post.id)
27
+ refute @api.post_deployed?(post.id)
28
+
29
+ # Mock the editor by not changing the file content
30
+ # (file content remains the same, so checksum will be identical)
31
+
32
+ # Call edit_post with mock (should detect no changes)
33
+ @api.edit_post(post.id, mock: true)
34
+
35
+ # State should remain unchanged
36
+ refute @api.post_published?(post.id)
37
+ refute @api.post_deployed?(post.id)
38
+ end
39
+
40
+ def test_002_edit_post_with_changes
41
+ @api.create_view("test_view", "Test View")
42
+ post = @api.create_post("Test Post", "Test body")
43
+
44
+ # Initially unpublished and undeployed
45
+ refute @api.post_published?(post.id)
46
+ refute @api.post_deployed?(post.id)
47
+
48
+ # Mock the editor by simulating a different checksum
49
+ @api.edit_post(post.id, mock: [:checksum, "different_checksum_for_testing"])
50
+
51
+ # State should remain unchanged since it was already unpublished/undeployed
52
+ refute @api.post_published?(post.id)
53
+ refute @api.post_deployed?(post.id)
54
+ end
55
+
56
+ def test_003_edit_post_published_post
57
+ @api.create_view("test_view", "Test View")
58
+ @api.repo.instance_variable_set(:@current_view, @api.repo.lookup_view("test_view"))
59
+
60
+ post = @api.create_post("Test Post", "Test body")
61
+
62
+ # Publish the post
63
+ @api.publish_post(post.id)
64
+ assert @api.post_published?(post.id)
65
+
66
+ # Mock the editor by simulating a different checksum
67
+ @api.edit_post(post.id, mock: [:checksum, "different_checksum_for_testing"])
68
+
69
+ # Post should now be unpublished
70
+ refute @api.post_published?(post.id)
71
+ end
72
+
73
+ def test_004_edit_post_deployed_post
74
+ @api.create_view("test_view", "Test View")
75
+ @api.repo.instance_variable_set(:@current_view, @api.repo.lookup_view("test_view"))
76
+
77
+ post = @api.create_post("Test Post", "Test body")
78
+
79
+ # Publish and deploy the post
80
+ @api.publish_post(post.id)
81
+ @api.mark_post_deployed(post.id)
82
+ assert @api.post_published?(post.id)
83
+ assert @api.post_deployed?(post.id)
84
+
85
+ # Mock the editor by simulating a different checksum
86
+ @api.edit_post(post.id, mock: [:checksum, "different_checksum_for_testing"])
87
+
88
+ # Post should now be unpublished and undeployed
89
+ refute @api.post_published?(post.id)
90
+ refute @api.post_deployed?(post.id)
91
+ end
92
+
93
+ def test_005_edit_post_multiple_views
94
+ @api.create_view("test_view", "Test View")
95
+ @api.create_view("other_view", "Other View")
96
+
97
+ post = @api.create_post("Test Post", "Test body", views: "test_view other_view")
98
+
99
+ # Publish in both views
100
+ @api.publish_post(post.id, "test_view")
101
+ @api.publish_post(post.id, "other_view")
102
+ @api.mark_post_deployed(post.id, "test_view")
103
+ @api.mark_post_deployed(post.id, "other_view")
104
+
105
+ assert @api.post_published?(post.id, "test_view")
106
+ assert @api.post_published?(post.id, "other_view")
107
+ assert @api.post_deployed?(post.id, "test_view")
108
+ assert @api.post_deployed?(post.id, "other_view")
109
+
110
+ # Mock the editor by simulating a different checksum
111
+ @api.edit_post(post.id, mock: [:checksum, "different_checksum_for_testing"])
112
+
113
+ # Post should now be unpublished and undeployed in both views
114
+
115
+ refute @api.post_published?(post.id, "test_view")
116
+ refute @api.post_published?(post.id, "other_view")
117
+ refute @api.post_deployed?(post.id, "test_view")
118
+ refute @api.post_deployed?(post.id, "other_view")
119
+ end
120
+
121
+ def test_006_edit_post_timestamp_change_only
122
+ @api.create_view("test_view", "Test View")
123
+ post = @api.create_post("Test Post", "Test body")
124
+
125
+ # Initially unpublished and undeployed
126
+ refute @api.post_published?(post.id)
127
+ refute @api.post_deployed?(post.id)
128
+
129
+ # Mock the editor by simulating no content changes (same checksum)
130
+ @api.edit_post(post.id, mock: [:checksum, Digest::MD5.file("#{@test_dir}/posts/#{post.num}/source.lt3").hexdigest])
131
+
132
+ # State should remain unchanged since content didn't change
133
+ refute @api.post_published?(post.id)
134
+ refute @api.post_deployed?(post.id)
135
+ end
136
+
137
+ def test_007_edit_post_missing_source
138
+ @api.create_view("test_view", "Test View")
139
+ post = @api.create_post("Test Post", "Test body")
140
+
141
+ # Remove source.lt3 to test error handling
142
+ source_path = "#{@test_dir}/posts/#{post.num}/source.lt3"
143
+ File.delete(source_path) if File.exist?(source_path)
144
+
145
+ # Ensure body.html exists
146
+ body_path = "#{@test_dir}/posts/#{post.num}/body.html"
147
+ assert File.exist?(body_path)
148
+
149
+ # Call edit_post should raise error since source.lt3 is missing
150
+ assert_raises(RuntimeError) do
151
+ @api.edit_post(post.id, mock: true)
152
+ end
153
+ end
154
+
155
+ def test_008_edit_post_deleted_post
156
+ @api.create_view("test_view", "Test View")
157
+ post = @api.create_post("Test Post", "Test body")
158
+
159
+ # Delete the post
160
+ @api.delete_post(post.id)
161
+ assert @api.post_deleted?(post.id)
162
+
163
+ # Try to edit deleted post
164
+ assert_raises(PostDeleted) do
165
+ @api.edit_post(post.id, mock: true)
166
+ end
167
+ end
168
+ end