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,219 @@
1
+ # Test file for asset management functionality
2
+ # Tests the $$asset and $$image Livetext functions
3
+
4
+ require 'minitest/autorun'
5
+ require_relative '../../lib/scriptorium'
6
+ require_relative '../test_helpers'
7
+ require 'fileutils'
8
+
9
+ class TestAssetManagement < Minitest::Test
10
+ include TestHelpers
11
+
12
+ def setup
13
+ @repo_name = "test/scriptorium-TEST"
14
+ @view_name = "asset_test_view"
15
+
16
+ # Create test repo and API
17
+ @api = Scriptorium::API.new
18
+ @api.create_repo(@repo_name)
19
+ @api.open_repo(@repo_name)
20
+
21
+ # Create a view for testing
22
+ @api.create_view(@view_name, "Asset Test View")
23
+
24
+ # Create test posts
25
+ @api.create_post("Test Post 1", "This is the first test post for asset testing.")
26
+ @api.create_post("Test Post 2", "This is the second test post for asset testing.")
27
+
28
+ # Generate the view to create output directory structure
29
+ @api.generate_view(@view_name)
30
+ end
31
+
32
+ def teardown
33
+ FileUtils.rm_rf("test/scriptorium-TEST") # Commented out for debugging
34
+ end
35
+
36
+ def test_001_asset_function_basic_functionality
37
+ # Test that asset() function returns correct paths for existing assets
38
+ # Create a global asset
39
+ global_file = Tempfile.new(['global-image', '.jpg'])
40
+ global_file.write("global image content")
41
+ global_file.close
42
+ @api.upload_asset(global_file.path, filename: "global-image.jpg")
43
+
44
+ # Update existing post 0001 with asset reference
45
+ source_file = "test/scriptorium-TEST/posts/0001/source.lt3"
46
+ File.write(source_file, "Global asset: $$asset[global-image.jpg]")
47
+
48
+ # Generate the view
49
+ @api.generate_view(@view_name)
50
+
51
+ # Check that the asset path is correct in generated HTML
52
+ html_file = "test/scriptorium-TEST/views/#{@view_name}/output/posts/0001-test-post-1.html"
53
+ html_content = File.read(html_file)
54
+
55
+ assert_includes_concise_string html_content, "Global asset: ../assets/global-image.jpg", "Asset function should return correct path"
56
+ end
57
+
58
+ def test_002_asset_function_missing_asset
59
+ # Test that asset() function returns fallback for missing assets
60
+ # Update existing post 0002 with missing asset reference
61
+ source_file = "test/scriptorium-TEST/posts/0002/source.lt3"
62
+ File.write(source_file, "Missing asset: $$asset[missing-image.jpg]")
63
+
64
+ # Generate the view
65
+ @api.generate_view(@view_name)
66
+
67
+ # Check that the fallback path is used
68
+ html_file = "test/scriptorium-TEST/views/#{@view_name}/output/posts/0002-test-post-2.html"
69
+ html_content = File.read(html_file)
70
+
71
+ assert_includes_concise_string html_content, "Missing asset: ../assets/imagenotfound.jpg", "Asset function should return fallback for missing assets"
72
+ end
73
+
74
+ def test_003_image_function_basic_functionality
75
+ # Test that image() function generates correct HTML
76
+ # Create a global asset
77
+ global_file = Tempfile.new(['global-image', '.jpg'])
78
+ global_file.write("global image content")
79
+ global_file.close
80
+ @api.upload_asset(global_file.path, filename: "global-image.jpg")
81
+
82
+ # Update existing post 0001 with image reference
83
+ source_file = "test/scriptorium-TEST/posts/0001/source.lt3"
84
+ File.write(source_file, "Image:\n.image global-image.jpg")
85
+
86
+ # Generate the view
87
+ @api.generate_view(@view_name)
88
+
89
+ # Check that the image tag is correct
90
+ html_file = "test/scriptorium-TEST/views/#{@view_name}/output/posts/0001-test-post-1.html"
91
+ html_content = File.read(html_file)
92
+
93
+ assert_includes_concise_string html_content, "<img src=../assets/global-image.jpg alt='No alt text'></img>", "Image function should generate correct HTML"
94
+ end
95
+
96
+ def test_004_image_function_with_real_asset
97
+ # Test that image() function works with actual test assets
98
+ # Upload a real test asset
99
+ @api.upload_asset("test/assets/testbanner.jpg", filename: "testbanner.jpg")
100
+
101
+ # Update existing post 0002 with image reference
102
+ source_file = "test/scriptorium-TEST/posts/0002/source.lt3"
103
+ File.write(source_file, "Real asset:\n.image testbanner.jpg")
104
+
105
+ # Generate the view
106
+ @api.generate_view(@view_name)
107
+
108
+ # Check that the image tag is correct
109
+ html_file = "test/scriptorium-TEST/views/#{@view_name}/output/posts/0002-test-post-2.html"
110
+ html_content = File.read(html_file)
111
+
112
+ assert_includes_concise_string html_content, "<img src=../assets/testbanner.jpg alt='No alt text'></img>", "Image function should work with real assets"
113
+ end
114
+
115
+ def test_005_global_asset_copied_to_view
116
+ # Test that global assets are automatically copied to view assets during generation
117
+ # First, create a global asset file directly in the repo
118
+ global_asset_path = "test/scriptorium-TEST/assets/global-copy-test.jpg"
119
+ FileUtils.cp("test/assets/testbanner.jpg", global_asset_path)
120
+
121
+ # Update existing post 0001 to reference the global asset
122
+ source_file = "test/scriptorium-TEST/posts/0001/source.lt3"
123
+ File.write(source_file, "Global asset copy test:\n.image global-copy-test.jpg")
124
+
125
+ # Assert initial state before generation
126
+ view_assets_dir = "test/scriptorium-TEST/views/#{@view_name}/assets"
127
+
128
+ # The global asset we just created should NOT be in view assets yet
129
+ view_asset_path = "#{view_assets_dir}/global-copy-test.jpg"
130
+ refute File.exist?(view_asset_path), "Global asset should not be in view assets before generation"
131
+
132
+ # Generate the view (this should copy the global asset to the view)
133
+ @api.generate_view(@view_name)
134
+
135
+ # Check that the global asset was copied to the view assets directory
136
+ view_asset_path = "test/scriptorium-TEST/views/#{@view_name}/assets/global-copy-test.jpg"
137
+ assert File.exist?(view_asset_path), "Global asset should be copied to view assets directory"
138
+
139
+ # Check that the image tag references the correct path
140
+ html_file = "test/scriptorium-TEST/views/#{@view_name}/output/posts/0001-test-post-1.html"
141
+ html_content = File.read(html_file)
142
+ assert_includes_concise_string html_content, "<img src=../assets/global-copy-test.jpg alt='No alt text'></img>", "Image should reference the copied asset"
143
+ end
144
+
145
+ def test_006_asset_priority_hierarchy
146
+ # Test that post assets take precedence over view assets with same filename
147
+ # 1. Create a file with specific contents and upload to view assets
148
+ view_file = Tempfile.new(['priority-test', '.txt'])
149
+ view_file.write("File 1, view asset")
150
+ view_file.close
151
+ @api.upload_asset(view_file.path, 'view', @view_name, filename: "priority-test.txt")
152
+
153
+ # 2. Create another file with SAME NAME but different contents and upload to post 2 assets
154
+ post_file = Tempfile.new(['priority-test', '.txt'])
155
+ post_file.write("File 2, post asset")
156
+ post_file.close
157
+ @api.upload_asset(post_file.path, 'post', 2, filename: "priority-test.txt")
158
+
159
+ # 3. Update both posts to reference the asset
160
+ source_file_1 = "test/scriptorium-TEST/posts/0001/source.lt3"
161
+ File.write(source_file_1, "Asset priority test:\n$$asset[priority-test.txt]")
162
+
163
+ source_file_2 = "test/scriptorium-TEST/posts/0002/source.lt3"
164
+ File.write(source_file_2, "Asset priority test:\n$$asset[priority-test.txt]")
165
+
166
+ # 4. Generate the view
167
+ @api.generate_view(@view_name)
168
+
169
+ # 5. Check that post 1 refers to the view asset (since it has no post asset)
170
+ html_file_1 = "test/scriptorium-TEST/views/#{@view_name}/output/posts/0001-test-post-1.html"
171
+ html_content_1 = File.read(html_file_1)
172
+ assert_includes_concise_string html_content_1, "../assets/priority-test.txt", "Post 1 should reference the view asset"
173
+
174
+ # 6. Check that post 2 refers to its own post asset (post assets take precedence)
175
+ html_file_2 = "test/scriptorium-TEST/views/#{@view_name}/output/posts/0002-test-post-2.html"
176
+ html_content_2 = File.read(html_file_2)
177
+ assert_includes_concise_string html_content_2, "../assets/posts/0002/priority-test.txt", "Post 2 should reference its post asset"
178
+
179
+ # 7. Verify the actual files exist in the correct locations
180
+ view_asset_path = "test/scriptorium-TEST/views/#{@view_name}/assets/priority-test.txt"
181
+ post_asset_path = "test/scriptorium-TEST/posts/0002/assets/priority-test.txt"
182
+
183
+ assert File.exist?(view_asset_path), "View asset should exist"
184
+ assert File.exist?(post_asset_path), "Post asset should exist"
185
+
186
+ # 8. Check that the files have the correct contents
187
+ view_content = File.read(view_asset_path)
188
+ post_content = File.read(post_asset_path)
189
+
190
+ assert_includes view_content, "File 1, view asset", "View asset should have correct content"
191
+ assert_includes post_content, "File 2, post asset", "Post asset should have correct content"
192
+ end
193
+
194
+ def test_007_asset_paths_in_post_context
195
+ # Test that asset paths work correctly in post page context (subdirectory)
196
+
197
+ # 1. Create a test asset
198
+ test_file = Tempfile.new(['path-test', '.txt'])
199
+ test_file.write("Path test asset")
200
+ test_file.close
201
+ @api.upload_asset(test_file.path, 'post', 1, filename: "path-test.txt")
202
+
203
+ # 2. Update post 1 to reference the asset
204
+ source_file_1 = "test/scriptorium-TEST/posts/0001/source.lt3"
205
+ File.write(source_file_1, "Asset path test:\n$$asset[path-test.txt]")
206
+
207
+ # 3. Generate the view
208
+ @api.generate_view(@view_name)
209
+
210
+ # 4. Check post page - should have ../assets/posts/0001/path-test.txt
211
+ post_file = "test/scriptorium-TEST/views/#{@view_name}/output/posts/0001-test-post-1.html"
212
+ post_content = File.read(post_file)
213
+ assert_includes_concise_string post_content, "../assets/posts/0001/path-test.txt", "Post page should reference assets with ../ prefix"
214
+
215
+ # 5. Verify the asset file exists in the output directory
216
+ asset_file = "test/scriptorium-TEST/views/#{@view_name}/output/assets/posts/0001/path-test.txt"
217
+ assert File.exist?(asset_file), "Asset should be copied to output directory"
218
+ end
219
+ end
@@ -0,0 +1,451 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'minitest/autorun'
4
+ require 'fileutils'
5
+ require 'json'
6
+ require_relative '../../lib/scriptorium'
7
+ require_relative '../test_helpers'
8
+
9
+ class BackupTestFixed < Minitest::Test
10
+ include Scriptorium::Exceptions
11
+ include Scriptorium::Helpers
12
+
13
+ def setup
14
+ @test_dir = "test/scriptorium-TEST"
15
+ # Clean up any existing test directory first
16
+ FileUtils.rm_rf(@test_dir) if Dir.exist?(@test_dir)
17
+ @api = Scriptorium::API.new(testmode: true)
18
+ @api.create_repo(@test_dir)
19
+ end
20
+
21
+ def teardown
22
+ FileUtils.rm_rf(@test_dir) if Dir.exist?(@test_dir)
23
+ # Clean up backup directory too
24
+ if @api
25
+ backup_dir = @api.get_backup_directory
26
+ FileUtils.rm_rf(backup_dir) if Dir.exist?(backup_dir)
27
+ end
28
+ @api = nil
29
+ end
30
+
31
+ private def count_posts_in_compressed_backup(backup_dir)
32
+ tar_gz_path = backup_dir/"data.tar.gz"
33
+ return 0 unless File.exist?(tar_gz_path)
34
+
35
+ # Use tar -tf to list files and count posts directories
36
+ output = `tar -tf '#{tar_gz_path}' 2>/dev/null`
37
+ return 0 unless $?.success?
38
+
39
+ # Count unique post directories (tar output has ./ prefix)
40
+ post_dirs = output.lines.select { |line| line.strip.match(/^\.\/posts\/\d+\//) }
41
+ post_dirs.map { |line| line.strip.split('/')[2] }.uniq.length
42
+ end
43
+
44
+ def test_001_create_full_backup
45
+ # Create some test content
46
+ @api.create_view("test-view", "Test View", "A test view")
47
+ @api.create_post("Test Post", "This is a test post", views: "test-view")
48
+
49
+ # Create a full backup
50
+ backup_name = @api.create_backup(type: :full, label: "test-backup")
51
+
52
+ # Verify backup was created
53
+ assert_match(/^\d{8}-\d{6}-full$/, backup_name)
54
+
55
+ backup_dir = @api.get_backup_directory/"data"/backup_name
56
+ assert Dir.exist?(backup_dir), "Backup directory should exist"
57
+
58
+ # Verify compressed backup structure
59
+ assert File.exist?(backup_dir/"data.tar.gz"), "Compressed backup data should exist"
60
+
61
+ # Verify backup info file exists (uncompressed)
62
+ assert File.exist?(backup_dir/"backup-info.txt"), "Backup info file should exist"
63
+
64
+ # Verify manifest was created
65
+ manifest_file = @api.get_backup_directory/"manifest.txt"
66
+ assert File.exist?(manifest_file), "Backup manifest should exist"
67
+
68
+ manifest_content = File.read(manifest_file)
69
+ assert_includes manifest_content, backup_name, "Backup should be in manifest"
70
+ assert_includes manifest_content, "test-backup", "Label should be in manifest"
71
+
72
+ # Verify backup info file was created
73
+ backup_info_file = backup_dir/"backup-info.txt"
74
+ assert File.exist?(backup_info_file), "Backup info file should exist"
75
+
76
+ info_content = File.read(backup_info_file)
77
+ assert_includes info_content, "scriptorium_version:", "Should contain scriptorium version"
78
+ assert_includes info_content, "livetext_version:", "Should contain livetext version"
79
+ assert_includes info_content, "ruby_version:", "Should contain ruby version"
80
+ assert_includes info_content, "backup_type: full", "Should contain backup type"
81
+ end
82
+
83
+ def test_002_create_incremental_backup
84
+ # Create some test content
85
+ @api.create_view("test-view", "Test View", "A test view")
86
+ @api.create_post("Test Post", "This is a test post", views: "test-view")
87
+
88
+ # Create an incremental backup
89
+ backup_name = @api.create_backup(type: :incremental, label: "incremental-test")
90
+
91
+ # Verify backup was created
92
+ assert_match(/^\d{8}-\d{6}-incr$/, backup_name)
93
+
94
+ backup_dir = @api.get_backup_directory/"data"/backup_name
95
+ assert Dir.exist?(backup_dir), "Backup directory should exist"
96
+
97
+ # Verify compressed backup structure
98
+ assert File.exist?(backup_dir/"data.tar.gz"), "Compressed backup data should exist"
99
+
100
+ # Verify manifest was created
101
+ manifest_file = @api.get_backup_directory/"manifest.txt"
102
+ assert File.exist?(manifest_file), "Backup manifest should exist"
103
+
104
+ manifest_content = File.read(manifest_file)
105
+ assert_includes manifest_content, backup_name, "Backup should be in manifest"
106
+ assert_includes manifest_content, "incremental-test", "Label should be in manifest"
107
+
108
+ # Verify backup info file was created
109
+ backup_info_file = backup_dir/"backup-info.txt"
110
+ assert File.exist?(backup_info_file), "Backup info file should exist"
111
+
112
+ info_content = File.read(backup_info_file)
113
+ assert_includes info_content, "scriptorium_version:", "Should contain scriptorium version"
114
+ assert_includes info_content, "livetext_version:", "Should contain livetext version"
115
+ assert_includes info_content, "ruby_version:", "Should contain ruby version"
116
+ assert_includes info_content, "backup_type: incremental", "Should contain backup type"
117
+ end
118
+
119
+ def test_003_list_backups
120
+ # Create multiple backups
121
+ backup1 = @api.create_backup(type: :full, label: "first")
122
+ sleep(1) # Ensure different timestamps
123
+ backup2 = @api.create_backup(type: :incremental, label: "second")
124
+
125
+ # List backups
126
+ backups = @api.list_backups
127
+
128
+ assert_equal 2, backups.length, "Should have 2 backups"
129
+
130
+ # Should be sorted by creation time, newest first
131
+ assert_equal backup2, backups[0][:name], "Newest backup should be first"
132
+ assert_equal backup1, backups[1][:name], "Older backup should be second"
133
+
134
+ # Verify backup info
135
+ backup_info = backups[0]
136
+ assert_equal backup2, backup_info[:name]
137
+ assert_equal :incremental, backup_info[:type]
138
+ assert_equal "second", backup_info[:description]
139
+ assert backup_info[:timestamp].is_a?(Time)
140
+ assert backup_info[:size].is_a?(Integer)
141
+ assert backup_info[:file_count].is_a?(Integer)
142
+ end
143
+
144
+ def test_004_restore_backup_safe_strategy
145
+ # Create initial content
146
+ @api.create_view("test-view", "Test View", "A test view")
147
+ @api.create_post("Original Post", "Original content", views: "test-view")
148
+
149
+ # Create a backup
150
+ backup_name = @api.create_backup(type: :full, label: "before-changes")
151
+
152
+ # Modify content
153
+ @api.create_post("New Post", "New content", views: "test-view")
154
+
155
+ # Restore from backup using safe strategy (default)
156
+ result = @api.restore_backup(backup_name, strategy: :safe)
157
+ assert result.is_a?(Hash), "Restore should return a hash"
158
+ assert_equal backup_name, result[:restored], "Should return the restored backup name"
159
+ assert result[:pre_restore], "Should have created a pre-restore backup"
160
+
161
+ # Verify content was restored
162
+ posts = @api.posts("test-view")
163
+ assert_equal 1, posts.length, "Should have only 1 post after restore"
164
+ assert_equal "Original Post", posts[0].title, "Should have original post"
165
+
166
+ # Verify pre-restore backup was created
167
+ backups = @api.list_backups
168
+ pre_restore = backups.find { |b| b[:description]&.include?("pre-restore") }
169
+ assert pre_restore, "Should have created a pre-restore backup"
170
+ end
171
+
172
+ def test_004b_restore_backup_destroy_strategy
173
+ # Create initial content
174
+ @api.create_view("test-view", "Test View", "A test view")
175
+ @api.create_post("Original Post", "Original content", views: "test-view")
176
+
177
+ # Create a backup
178
+ backup_name = @api.create_backup(type: :full, label: "before-changes")
179
+
180
+ # Modify content
181
+ @api.create_post("New Post", "New content", views: "test-view")
182
+
183
+ # Restore from backup using destroy strategy
184
+ result = @api.restore_backup(backup_name, strategy: :destroy)
185
+ assert result.is_a?(Hash), "Restore should return a hash"
186
+ assert_equal backup_name, result[:restored], "Should return the restored backup name"
187
+ assert_equal :destroy, result[:strategy], "Should indicate destroy strategy"
188
+
189
+ # Verify content was restored
190
+ posts = @api.posts("test-view")
191
+ assert_equal 1, posts.length, "Should have only 1 post after restore"
192
+ assert_equal "Original Post", posts[0].title, "Should have original post"
193
+ end
194
+
195
+ def test_004c_restore_backup_merge_strategy
196
+ # Create initial content
197
+ @api.create_view("test-view", "Test View", "A test view")
198
+ @api.create_post("Original Post", "Original content", views: "test-view")
199
+
200
+ # Create a backup
201
+ backup_name = @api.create_backup(type: :full, label: "before-changes")
202
+
203
+ # Modify content
204
+ @api.create_post("New Post", "New content", views: "test-view")
205
+
206
+ # Restore from backup using merge strategy
207
+ result = @api.restore_backup(backup_name, strategy: :merge)
208
+ assert result.is_a?(Hash), "Restore should return a hash"
209
+ assert_equal backup_name, result[:restored], "Should return the restored backup name"
210
+ assert_equal :merge, result[:strategy], "Should indicate merge strategy"
211
+
212
+ # Verify content was restored (merge should keep existing files)
213
+ posts = @api.posts("test-view")
214
+ assert_equal 2, posts.length, "Should have 2 posts after merge restore"
215
+ post_titles = posts.map(&:title).sort
216
+ assert_equal ["New Post", "Original Post"], post_titles, "Should have both posts"
217
+ end
218
+
219
+ def test_005_delete_backup
220
+ # Create a backup
221
+ backup_name = @api.create_backup(type: :full, label: "to-delete")
222
+
223
+ # Verify it exists
224
+ backups = @api.list_backups
225
+ assert_equal 1, backups.length, "Should have 1 backup"
226
+
227
+ # Delete the backup
228
+ result = @api.delete_backup(backup_name)
229
+ assert result, "Delete should succeed"
230
+
231
+ # Verify it's gone
232
+ backups = @api.list_backups
233
+ assert_equal 0, backups.length, "Should have 0 backups after delete"
234
+
235
+ # Verify directory is gone
236
+ backup_dir = @api.get_backup_directory/"data"/backup_name
237
+ assert !Dir.exist?(backup_dir), "Backup directory should be deleted"
238
+ end
239
+
240
+ def test_006_incremental_backup_tracks_changes
241
+ # Create initial content and full backup first
242
+ @api.create_view("test-view", "Test View", "A test view")
243
+ @api.create_post("Post 1", "Content 1", views: "test-view")
244
+
245
+
246
+ backup1 = @api.create_backup(type: :full, label: "initial")
247
+
248
+ # Now add new content and create incremental backup
249
+ @api.create_post("Post 2", "Content 2", views: "test-view")
250
+ backup2 = @api.create_backup(type: :incremental, label: "after-changes")
251
+
252
+ # Verify backups contain expected content
253
+ backup1_dir = @api.get_backup_directory/"data"/backup1
254
+ backup2_dir = @api.get_backup_directory/"data"/backup2
255
+
256
+ # Full backup should have 1 post (check compressed content)
257
+ backup1_posts = count_posts_in_compressed_backup(backup1_dir)
258
+ assert_equal 1, backup1_posts, "Full backup should have 1 post"
259
+
260
+ # Incremental backup should have 2 posts (both posts, since post creation modifies existing files)
261
+ backup2_posts = count_posts_in_compressed_backup(backup2_dir)
262
+ assert_equal 2, backup2_posts, "Incremental backup should have 2 posts (both posts, since post creation modifies existing files)"
263
+
264
+ # Verify the incremental backup contains the new post
265
+ # Extract and check compressed content
266
+ temp_extract_dir = backup2_dir/"temp_extract"
267
+ FileUtils.mkdir_p(temp_extract_dir)
268
+
269
+ begin
270
+ # Extract tar.gz to temporary directory
271
+ system("tar -xzf '#{backup2_dir}/data.tar.gz' -C '#{temp_extract_dir}'")
272
+ assert $?.success?, "Should successfully extract compressed backup"
273
+
274
+ # Check for post directories
275
+ backup2_post_dirs = Dir.glob("#{temp_extract_dir}/posts/*")
276
+ assert_equal 2, backup2_post_dirs.length, "Should have exactly two post directories"
277
+
278
+ # The incremental backup should contain both posts
279
+ # Check that both post directories exist and contain the expected content
280
+ post_dirs = backup2_post_dirs.sort
281
+
282
+ # Check first post (0001)
283
+ source_file_1 = "#{post_dirs[0]}/source.lt3"
284
+ assert File.exist?(source_file_1), "Source file should exist for post 1"
285
+ content_1 = File.read(source_file_1)
286
+ assert_includes content_1, "Post 1", "Should contain Post 1"
287
+
288
+ # Check second post (0002)
289
+ source_file_2 = "#{post_dirs[1]}/source.lt3"
290
+ assert File.exist?(source_file_2), "Source file should exist for post 2"
291
+ content_2 = File.read(source_file_2)
292
+ assert_includes content_2, "Post 2", "Should contain Post 2"
293
+ ensure
294
+ # Clean up temporary directory
295
+ FileUtils.rm_rf(temp_extract_dir) if Dir.exist?(temp_extract_dir)
296
+ end
297
+ end
298
+
299
+ def test_007_backup_validation
300
+ # Temporarily enable contracts for this test
301
+ original_dbc = ENV['DBC_DISABLED']
302
+ ENV['DBC_DISABLED'] = nil
303
+
304
+ begin
305
+ # Test invalid backup type - this should fail the assume check
306
+ assert_raises(RuntimeError) do
307
+ @api.create_backup(type: :invalid)
308
+ end
309
+
310
+ # Test with nil repo - this should fail the assume check
311
+ api_no_repo = Scriptorium::API.new(testmode: true)
312
+ assert_raises(RuntimeError) do
313
+ api_no_repo.create_backup(type: :full)
314
+ end
315
+ ensure
316
+ # Restore original contract setting
317
+ ENV['DBC_DISABLED'] = original_dbc
318
+ end
319
+ end
320
+
321
+ def test_008_restore_nonexistent_backup
322
+ # Try to restore a backup that doesn't exist
323
+ assert_raises(BackupNotFound) do
324
+ @api.restore_backup("nonexistent-backup")
325
+ end
326
+ end
327
+
328
+ def test_009_delete_nonexistent_backup
329
+ # Try to delete a backup that doesn't exist
330
+ assert_raises(BackupNotFound) do
331
+ @api.delete_backup("nonexistent-backup")
332
+ end
333
+ end
334
+
335
+ def test_010_restore_incremental_backup_with_dependencies
336
+ # Create initial content
337
+ @api.create_view("test-view", "Test View", "A test view")
338
+ @api.create_post("Post 1", "Content 1", views: "test-view")
339
+
340
+ # Create full backup
341
+ full_backup = @api.create_backup(type: :full, label: "initial")
342
+ sleep(1) # Ensure different timestamps
343
+
344
+ # Add more content
345
+ @api.create_post("Post 2", "Content 2", views: "test-view")
346
+
347
+ # Create incremental backup
348
+ incr_backup = @api.create_backup(type: :incremental, label: "after-post-2")
349
+ sleep(1) # Ensure different timestamps
350
+
351
+ # Add more content
352
+ @api.create_post("Post 3", "Content 3", views: "test-view")
353
+
354
+ # Restore from incremental backup using safe strategy
355
+ result = @api.restore_backup(incr_backup, strategy: :safe)
356
+ assert result.is_a?(Hash), "Restore should return a hash"
357
+ assert_equal incr_backup, result[:restored], "Should return the restored backup name"
358
+ assert result[:pre_restore], "Should have created a pre-restore backup"
359
+
360
+ # Verify content was restored correctly (should have Post 1 and Post 2)
361
+ posts = @api.posts("test-view")
362
+ assert_equal 2, posts.length, "Should have 2 posts after restore"
363
+ post_titles = posts.map(&:title).sort
364
+ assert_equal ["Post 1", "Post 2"], post_titles, "Should have correct posts"
365
+ end
366
+
367
+ def test_011_restore_invalid_strategy
368
+ # Create a backup first
369
+ @api.create_view("test-view", "Test View", "A test view")
370
+ @api.create_post("Test Post", "Test content", views: "test-view")
371
+ backup_name = @api.create_backup(type: :full, label: "test")
372
+
373
+ # Try to restore with invalid strategy
374
+ assert_raises(ArgumentError) do
375
+ @api.restore_backup(backup_name, strategy: :invalid)
376
+ end
377
+ end
378
+
379
+ def test_012_restore_default_strategy_is_safe
380
+ # Create initial content
381
+ @api.create_view("test-view", "Test View", "A test view")
382
+ @api.create_post("Original Post", "Original content", views: "test-view")
383
+
384
+ # Create a backup
385
+ backup_name = @api.create_backup(type: :full, label: "before-changes")
386
+
387
+ # Modify content
388
+ @api.create_post("New Post", "New content", views: "test-view")
389
+
390
+ # Restore from backup without specifying strategy (should default to :safe)
391
+ result = @api.restore_backup(backup_name)
392
+ assert result.is_a?(Hash), "Restore should return a hash"
393
+ assert_equal backup_name, result[:restored], "Should return the restored backup name"
394
+ assert result[:pre_restore], "Should have created a pre-restore backup (default is safe)"
395
+
396
+ # Verify content was restored
397
+ posts = @api.posts("test-view")
398
+ assert_equal 1, posts.length, "Should have only 1 post after restore"
399
+ assert_equal "Original Post", posts[0].title, "Should have original post"
400
+ end
401
+
402
+ def test_013_restore_with_pre_restore_backup_timestamp_handling
403
+ # Create initial content
404
+ @api.create_view("test-view", "Test View", "A test view")
405
+ @api.create_post("Original Post", "Original content", views: "test-view")
406
+
407
+ # Create a backup
408
+ backup_name = @api.create_backup(type: :full, label: "before-changes")
409
+
410
+ # Modify content
411
+ @api.create_post("New Post", "New content", views: "test-view")
412
+
413
+ # Restore from backup using safe strategy
414
+ result = @api.restore_backup(backup_name, strategy: :safe)
415
+
416
+ # Verify that the pre-restore backup was created and is not used as the base for restore
417
+ backups = @api.list_backups
418
+ pre_restore = backups.find { |b| b[:description]&.include?("pre-restore") }
419
+ assert pre_restore, "Should have created a pre-restore backup"
420
+
421
+ # The pre-restore backup should not be the same as the original backup
422
+ refute_equal backup_name, pre_restore[:name], "Pre-restore backup should be different from original"
423
+
424
+ # Verify content was restored correctly
425
+ posts = @api.posts("test-view")
426
+ assert_equal 1, posts.length, "Should have only 1 post after restore"
427
+ assert_equal "Original Post", posts[0].title, "Should have original post"
428
+ end
429
+
430
+ def test_014_merge_strategy_preserves_existing_files
431
+ # Create initial content
432
+ @api.create_view("test-view", "Test View", "A test view")
433
+ @api.create_post("Original Post", "Original content", views: "test-view")
434
+
435
+ # Create a backup
436
+ backup_name = @api.create_backup(type: :full, label: "before-changes")
437
+
438
+ # Add content that should be preserved
439
+ @api.create_post("Keep This Post", "This should be kept", views: "test-view")
440
+
441
+ # Restore from backup using merge strategy
442
+ result = @api.restore_backup(backup_name, strategy: :merge)
443
+
444
+ # Verify both posts exist (merge should preserve existing files)
445
+ posts = @api.posts("test-view")
446
+ assert_equal 2, posts.length, "Should have 2 posts after merge restore"
447
+ post_titles = posts.map(&:title).sort
448
+ assert_equal ["Keep This Post", "Original Post"], post_titles, "Should have both posts"
449
+ end
450
+
451
+ end