ro 4.2.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (252) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +57 -10
  3. data/LICENSE +1 -1
  4. data/MIGRATION.md +320 -0
  5. data/README.md +286 -111
  6. data/Rakefile +2 -2
  7. data/a.yml +60 -0
  8. data/bin/ro +10 -0
  9. data/lib/ro/_lib.rb +18 -6
  10. data/lib/ro/asset.rb +67 -16
  11. data/lib/ro/collection.rb +91 -10
  12. data/lib/ro/config.rb +4 -0
  13. data/lib/ro/error.rb +5 -2
  14. data/lib/ro/html.rb +23 -0
  15. data/lib/ro/html_safe.rb +143 -0
  16. data/lib/ro/methods.rb +95 -38
  17. data/lib/ro/migrator.rb +285 -0
  18. data/lib/ro/node.rb +128 -45
  19. data/lib/ro/path.rb +4 -0
  20. data/lib/ro/root.rb +75 -1
  21. data/lib/ro/script/migrate.rb +204 -0
  22. data/lib/ro/script/server.rb +1 -1
  23. data/lib/ro/template.rb +62 -22
  24. data/lib/ro/text.rb +120 -0
  25. data/lib/ro.rb +5 -0
  26. data/public/api/ro/index-1.json +997 -79
  27. data/public/api/ro/index.json +997 -79
  28. data/public/api/ro/nerd/fastest-possible-embeddings/index.json +90 -0
  29. data/public/api/ro/nerd/ima/index.json +49 -0
  30. data/public/api/ro/nerd/index/index.json +74 -0
  31. data/public/api/ro/nerd/index-1.json +204 -0
  32. data/public/api/ro/nerd/index.json +194 -0
  33. data/public/api/ro/pages/about/index.json +60 -0
  34. data/public/api/ro/pages/contact/index.json +50 -0
  35. data/public/api/ro/pages/cv/index.json +49 -0
  36. data/public/api/ro/pages/disco/index.json +117 -0
  37. data/public/api/ro/pages/index/index.json +30 -0
  38. data/public/api/ro/pages/index-1.json +366 -0
  39. data/public/api/ro/pages/index.json +356 -0
  40. data/public/api/ro/pages/jess/index.json +62 -0
  41. data/public/api/ro/pages/now/index.json +43 -0
  42. data/public/api/ro/posts/almost-died-in-an-ice-cave/index.json +265 -0
  43. data/public/api/ro/posts/facebook-and-global-extremism/index.json +90 -0
  44. data/public/api/ro/posts/index-1.json +461 -79
  45. data/public/api/ro/posts/index.json +461 -79
  46. data/public/api/ro/posts/lemmings-considered-harmful/index.json +49 -0
  47. data/public/api/ro/posts/lost-in-the-desert/index.json +49 -0
  48. data/public/api/ro/posts/mission/index.json +49 -0
  49. data/public/api/ro/posts/return-your-laptop/index.json +61 -0
  50. data/public/ro/nerd/fastest-possible-embeddings/assets/giraffe.jpeg +0 -0
  51. data/public/ro/nerd/fastest-possible-embeddings/assets/let-me-in.jpg +0 -0
  52. data/public/ro/nerd/fastest-possible-embeddings/assets/src/fastembed.js +70 -0
  53. data/public/ro/nerd/fastest-possible-embeddings/assets/src/fastembed.rs +68 -0
  54. data/public/ro/nerd/fastest-possible-embeddings/assets/terminal.jpg +0 -0
  55. data/public/ro/nerd/fastest-possible-embeddings/body.md +266 -0
  56. data/public/ro/nerd/fastest-possible-embeddings.yml +7 -0
  57. data/public/ro/nerd/ima/assets/og.jpeg +0 -0
  58. data/public/ro/nerd/ima/body.md +22 -0
  59. data/public/ro/nerd/ima.yml +8 -0
  60. data/public/ro/nerd/index/assets/giraffe.jpeg +0 -0
  61. data/public/ro/nerd/index/assets/let-me-in.jpg +0 -0
  62. data/public/ro/nerd/index/assets/terminal.jpg +0 -0
  63. data/public/ro/nerd/index/body.md +130 -0
  64. data/public/ro/nerd/index.yml +7 -0
  65. data/public/ro/pages/about/assets/og.jpeg +0 -0
  66. data/public/ro/pages/about/assets/speak-english-pulp-fiction.gif +0 -0
  67. data/public/ro/pages/about/body.md +40 -0
  68. data/public/ro/pages/contact/assets/giraffe.jpeg +0 -0
  69. data/public/ro/pages/contact/body.md +9 -0
  70. data/public/ro/pages/contact.yml +7 -0
  71. data/public/ro/pages/cv/assets/ara.jpg +0 -0
  72. data/public/ro/pages/cv/body.md +122 -0
  73. data/public/ro/pages/cv.yml +6 -0
  74. data/public/ro/pages/disco/assets/disco.jpg +0 -0
  75. data/public/ro/pages/disco/assets/disco.png +0 -0
  76. data/public/ro/pages/disco/assets/speak-english-pulp-fiction.gif +0 -0
  77. data/public/ro/pages/disco/assets/src/environment.md +2354 -0
  78. data/public/ro/pages/disco/assets/src/fortune-500.md +2518 -0
  79. data/public/ro/pages/disco/assets/src/greed.md +2703 -0
  80. data/public/ro/pages/disco/assets/src/up-at-night.md +2337 -0
  81. data/public/ro/pages/disco/body.md +99 -0
  82. data/public/ro/pages/disco/samples/environment.md +2354 -0
  83. data/public/ro/pages/disco/samples/fortune-500.md +2518 -0
  84. data/public/ro/pages/disco/samples/greed.md +2703 -0
  85. data/public/ro/pages/disco/samples/up-at-night.md +2337 -0
  86. data/public/ro/pages/disco.yml +9 -0
  87. data/public/ro/pages/index/body.md +15 -0
  88. data/public/ro/pages/index.yml +1 -0
  89. data/public/ro/pages/jess/assets/og.jpg +0 -0
  90. data/public/ro/pages/jess/assets/speak-english-pulp-fiction.gif +0 -0
  91. data/public/ro/pages/jess/body.md +3 -0
  92. data/public/ro/pages/jess.yml +7 -0
  93. data/public/ro/pages/now/assets/speak-english-pulp-fiction.gif +0 -0
  94. data/public/ro/pages/now/body.md +24 -0
  95. data/public/ro/pages/now.yml +1 -0
  96. data/public/ro/posts/almost-died-in-an-ice-cave/assets/image1.png +0 -0
  97. data/public/ro/posts/almost-died-in-an-ice-cave/assets/image10.png +0 -0
  98. data/public/ro/posts/almost-died-in-an-ice-cave/assets/image11.png +0 -0
  99. data/public/ro/posts/almost-died-in-an-ice-cave/assets/image12.png +0 -0
  100. data/public/ro/posts/almost-died-in-an-ice-cave/assets/image13.png +0 -0
  101. data/public/ro/posts/almost-died-in-an-ice-cave/assets/image14.png +0 -0
  102. data/public/ro/posts/almost-died-in-an-ice-cave/assets/image15.png +0 -0
  103. data/public/ro/posts/almost-died-in-an-ice-cave/assets/image2.png +0 -0
  104. data/public/ro/posts/almost-died-in-an-ice-cave/assets/image3.png +0 -0
  105. data/public/ro/posts/almost-died-in-an-ice-cave/assets/image4.png +0 -0
  106. data/public/ro/posts/almost-died-in-an-ice-cave/assets/image5.png +0 -0
  107. data/public/ro/posts/almost-died-in-an-ice-cave/assets/image6.png +0 -0
  108. data/public/ro/posts/almost-died-in-an-ice-cave/assets/image7.png +0 -0
  109. data/public/ro/posts/almost-died-in-an-ice-cave/assets/image8.png +0 -0
  110. data/public/ro/posts/almost-died-in-an-ice-cave/assets/image9.png +0 -0
  111. data/public/ro/posts/almost-died-in-an-ice-cave/assets/josh-pointing.jpg +0 -0
  112. data/public/ro/posts/almost-died-in-an-ice-cave/assets/levi-rawr.png +0 -0
  113. data/public/ro/posts/almost-died-in-an-ice-cave/assets/og.jpg +0 -0
  114. data/public/ro/posts/almost-died-in-an-ice-cave/assets/purple-heart.jpg +0 -0
  115. data/public/ro/posts/almost-died-in-an-ice-cave/body.md +419 -0
  116. data/public/ro/posts/almost-died-in-an-ice-cave.yml +6 -0
  117. data/public/ro/posts/facebook-and-global-extremism/assets/background.html +125 -0
  118. data/public/ro/posts/facebook-and-global-extremism/assets/background.md +95 -0
  119. data/public/ro/posts/facebook-and-global-extremism/assets/og.jpg +0 -0
  120. data/public/ro/posts/facebook-and-global-extremism/assets/prompt.txt +122 -0
  121. data/public/ro/posts/facebook-and-global-extremism/assets/results.md +183 -0
  122. data/public/ro/posts/facebook-and-global-extremism/assets/survey.txt +190 -0
  123. data/public/ro/posts/facebook-and-global-extremism/body.md +393 -0
  124. data/public/ro/posts/facebook-and-global-extremism.yml +7 -0
  125. data/public/ro/posts/lemmings-considered-harmful/assets/lemming.jpeg +0 -0
  126. data/public/ro/posts/lemmings-considered-harmful/body.md +43 -0
  127. data/public/ro/posts/lemmings-considered-harmful.yml +6 -0
  128. data/public/ro/posts/lost-in-the-desert/assets/og.jpg +0 -0
  129. data/public/ro/posts/lost-in-the-desert/body.md +7 -0
  130. data/public/ro/posts/lost-in-the-desert.yml +6 -0
  131. data/public/ro/posts/mission/assets/og.jpg +0 -0
  132. data/public/ro/posts/mission/body.md +4 -0
  133. data/public/ro/posts/mission.yml +6 -0
  134. data/public/ro/posts/return-your-laptop/assets/og.jpg +0 -0
  135. data/public/ro/posts/return-your-laptop/assets/return-your-laptop.png +0 -0
  136. data/public/ro/posts/return-your-laptop/body.md +58 -0
  137. data/public/ro/posts/return-your-laptop.yml +6 -0
  138. data/ro.gemspec +369 -49
  139. data/scripts/speedtest.rb +324 -0
  140. data/specs/001-simplify-asset-structure/IMPLEMENTATION_SUMMARY.md +212 -0
  141. data/specs/001-simplify-asset-structure/checklists/requirements.md +36 -0
  142. data/specs/001-simplify-asset-structure/contracts/collection_api.md +407 -0
  143. data/specs/001-simplify-asset-structure/contracts/migrator_api.md +461 -0
  144. data/specs/001-simplify-asset-structure/contracts/node_api.md +294 -0
  145. data/specs/001-simplify-asset-structure/data-model.md +381 -0
  146. data/specs/001-simplify-asset-structure/plan.md +90 -0
  147. data/specs/001-simplify-asset-structure/quickstart.md +575 -0
  148. data/specs/001-simplify-asset-structure/research.md +333 -0
  149. data/specs/001-simplify-asset-structure/spec.md +127 -0
  150. data/specs/001-simplify-asset-structure/tasks.md +349 -0
  151. data/test/fixtures/new_structure/mixed/test-json.json +5 -0
  152. data/test/fixtures/new_structure/mixed/test-yaml.yml +3 -0
  153. data/test/fixtures/new_structure/posts/metadata-only.yml +7 -0
  154. data/test/fixtures/new_structure/posts/nested-test/assets/subdirectory/image.png +2 -0
  155. data/test/fixtures/new_structure/posts/nested-test.yml +7 -0
  156. data/test/fixtures/new_structure/posts/sample-post/assets/body.md +5 -0
  157. data/test/fixtures/new_structure/posts/sample-post/assets/image.jpg +2 -0
  158. data/test/fixtures/new_structure/posts/sample-post.yml +7 -0
  159. data/test/fixtures/old_structure/posts/assets-only/assets/test.txt +1 -0
  160. data/test/fixtures/old_structure/posts/sample-post/assets/body.md +5 -0
  161. data/test/fixtures/old_structure/posts/sample-post/assets/image.jpg +2 -0
  162. data/test/fixtures/old_structure/posts/sample-post/attributes.yml +2 -0
  163. data/test/integration/ro_integration_test.rb +165 -0
  164. data/test/test_helper.rb +149 -0
  165. data/test/tmp/migration_test_1760746513.backup.20251018001513/migration_test_1760746513/posts/sample-post/assets/image.jpg +2 -0
  166. data/test/tmp/migration_test_1760746513.backup.20251018001513/migration_test_1760746513/posts/sample-post/attributes.yml +7 -0
  167. data/test/tmp/migration_test_1760746513.backup.20251018001513/migration_test_1760746513/posts/sample-post/body.md +5 -0
  168. data/test/tmp/migration_test_1760746513.backup.20251018001513/posts/sample-post/assets/image.jpg +2 -0
  169. data/test/tmp/migration_test_1760746513.backup.20251018001513/posts/sample-post/attributes.yml +7 -0
  170. data/test/tmp/migration_test_1760746513.backup.20251018001513/posts/sample-post/body.md +5 -0
  171. data/test/tmp/migration_test_1760746556.backup.20251018001556/migration_test_1760746556/posts/sample-post/assets/image.jpg +2 -0
  172. data/test/tmp/migration_test_1760746556.backup.20251018001556/migration_test_1760746556/posts/sample-post/attributes.yml +7 -0
  173. data/test/tmp/migration_test_1760746556.backup.20251018001556/migration_test_1760746556/posts/sample-post/body.md +5 -0
  174. data/test/tmp/migration_test_1760746556.backup.20251018001556/posts/sample-post/assets/image.jpg +2 -0
  175. data/test/tmp/migration_test_1760746556.backup.20251018001556/posts/sample-post/attributes.yml +7 -0
  176. data/test/tmp/migration_test_1760746556.backup.20251018001556/posts/sample-post/body.md +5 -0
  177. data/test/tmp/migration_test_1760755248.backup.20251018024048/migration_test_1760755248/posts/sample-post/assets/image.jpg +2 -0
  178. data/test/tmp/migration_test_1760755248.backup.20251018024048/migration_test_1760755248/posts/sample-post/attributes.yml +7 -0
  179. data/test/tmp/migration_test_1760755248.backup.20251018024048/migration_test_1760755248/posts/sample-post/body.md +5 -0
  180. data/test/tmp/migration_test_1760755248.backup.20251018024048/posts/sample-post/assets/image.jpg +2 -0
  181. data/test/tmp/migration_test_1760755248.backup.20251018024048/posts/sample-post/attributes.yml +7 -0
  182. data/test/tmp/migration_test_1760755248.backup.20251018024048/posts/sample-post/body.md +5 -0
  183. data/test/tmp/migration_test_1760758803.backup.20251018034003/migration_test_1760758803/posts/sample-post/body.md +5 -0
  184. data/test/tmp/migration_test_1760758803.backup.20251018034003/migration_test_1760758803/posts/sample-post/image.jpg +2 -0
  185. data/test/tmp/migration_test_1760758803.backup.20251018034003/migration_test_1760758803/posts/sample-post.yml +7 -0
  186. data/test/tmp/migration_test_1760758803.backup.20251018034003/posts/sample-post/body.md +5 -0
  187. data/test/tmp/migration_test_1760758803.backup.20251018034003/posts/sample-post/image.jpg +2 -0
  188. data/test/tmp/migration_test_1760758803.backup.20251018034003/posts/sample-post.yml +7 -0
  189. data/test/tmp/migration_test_1760758869.backup.20251018034109/migration_test_1760758869/posts/sample-post/assets/body.md +5 -0
  190. data/test/tmp/migration_test_1760758869.backup.20251018034109/migration_test_1760758869/posts/sample-post/assets/image.jpg +2 -0
  191. data/test/tmp/migration_test_1760758869.backup.20251018034109/migration_test_1760758869/posts/sample-post/attributes.yml +2 -0
  192. data/test/tmp/migration_test_1760758869.backup.20251018034109/posts/sample-post/assets/body.md +5 -0
  193. data/test/tmp/migration_test_1760758869.backup.20251018034109/posts/sample-post/assets/image.jpg +2 -0
  194. data/test/tmp/migration_test_1760758869.backup.20251018034109/posts/sample-post/attributes.yml +2 -0
  195. data/test/tmp/migration_test_1760758920.backup.20251018034200/migration_test_1760758920/posts/sample-post/assets/body.md +5 -0
  196. data/test/tmp/migration_test_1760758920.backup.20251018034200/migration_test_1760758920/posts/sample-post/assets/image.jpg +2 -0
  197. data/test/tmp/migration_test_1760758920.backup.20251018034200/migration_test_1760758920/posts/sample-post/attributes.yml +2 -0
  198. data/test/tmp/migration_test_1760758920.backup.20251018034200/posts/sample-post/assets/body.md +5 -0
  199. data/test/tmp/migration_test_1760758920.backup.20251018034200/posts/sample-post/assets/image.jpg +2 -0
  200. data/test/tmp/migration_test_1760758920.backup.20251018034200/posts/sample-post/attributes.yml +2 -0
  201. data/test/tmp/migration_test_1760824728.backup.20251018215848/migration_test_1760824728/posts/assets-only/assets/test.txt +1 -0
  202. data/test/tmp/migration_test_1760824728.backup.20251018215848/migration_test_1760824728/posts/sample-post/assets/body.md +5 -0
  203. data/test/tmp/migration_test_1760824728.backup.20251018215848/migration_test_1760824728/posts/sample-post/assets/image.jpg +2 -0
  204. data/test/tmp/migration_test_1760824728.backup.20251018215848/migration_test_1760824728/posts/sample-post/attributes.yml +2 -0
  205. data/test/tmp/migration_test_1760824728.backup.20251018215848/posts/assets-only/assets/test.txt +1 -0
  206. data/test/tmp/migration_test_1760824728.backup.20251018215848/posts/sample-post/assets/body.md +5 -0
  207. data/test/tmp/migration_test_1760824728.backup.20251018215848/posts/sample-post/assets/image.jpg +2 -0
  208. data/test/tmp/migration_test_1760824728.backup.20251018215848/posts/sample-post/attributes.yml +2 -0
  209. data/test/tmp/migration_test_1760844153.backup.20251019032233/migration_test_1760844153/posts/assets-only/assets/test.txt +1 -0
  210. data/test/tmp/migration_test_1760844153.backup.20251019032233/migration_test_1760844153/posts/sample-post/assets/body.md +5 -0
  211. data/test/tmp/migration_test_1760844153.backup.20251019032233/migration_test_1760844153/posts/sample-post/assets/image.jpg +2 -0
  212. data/test/tmp/migration_test_1760844153.backup.20251019032233/migration_test_1760844153/posts/sample-post/attributes.yml +2 -0
  213. data/test/tmp/migration_test_1760844153.backup.20251019032233/posts/assets-only/assets/test.txt +1 -0
  214. data/test/tmp/migration_test_1760844153.backup.20251019032233/posts/sample-post/assets/body.md +5 -0
  215. data/test/tmp/migration_test_1760844153.backup.20251019032233/posts/sample-post/assets/image.jpg +2 -0
  216. data/test/tmp/migration_test_1760844153.backup.20251019032233/posts/sample-post/attributes.yml +2 -0
  217. data/test/tmp/new_structure_test_1760746452/mixed/test-json.json +5 -0
  218. data/test/tmp/new_structure_test_1760746452/mixed/test-yaml.yml +3 -0
  219. data/test/tmp/new_structure_test_1760746452/posts/metadata-only.yml +7 -0
  220. data/test/tmp/new_structure_test_1760746452/posts/nested-test/subdirectory/image.png +2 -0
  221. data/test/tmp/new_structure_test_1760746452/posts/nested-test.yml +7 -0
  222. data/test/tmp/new_structure_test_1760746452/posts/sample-post/body.md +5 -0
  223. data/test/tmp/new_structure_test_1760746452/posts/sample-post/image.jpg +2 -0
  224. data/test/tmp/new_structure_test_1760746452/posts/sample-post.yml +7 -0
  225. data/test/unit/asset_test.rb +90 -0
  226. data/test/unit/collection_test.rb +127 -0
  227. data/test/unit/migrator_test.rb +209 -0
  228. data/test/unit/node_test.rb +138 -0
  229. data/tmp/gem-details.oe +0 -0
  230. metadata +250 -33
  231. data/public/api/ro/posts/first_post/index.json +0 -52
  232. data/public/api/ro/posts/second_post/index.json +0 -51
  233. data/public/api/ro/posts/third_post/index.json +0 -51
  234. data/public/ro/posts/first_post/assets/foo/bar/baz.jpg +0 -0
  235. data/public/ro/posts/first_post/assets/foo.jpg +0 -0
  236. data/public/ro/posts/first_post/assets/src/foo/bar.rb +0 -3
  237. data/public/ro/posts/first_post/attributes.yml +0 -2
  238. data/public/ro/posts/first_post/blurb.erb.md +0 -7
  239. data/public/ro/posts/first_post/body.md +0 -16
  240. data/public/ro/posts/first_post/testing.txt +0 -3
  241. data/public/ro/posts/second_post/assets/foo/bar/baz.jpg +0 -0
  242. data/public/ro/posts/second_post/assets/foo.jpg +0 -0
  243. data/public/ro/posts/second_post/assets/src/foo/bar.rb +0 -3
  244. data/public/ro/posts/second_post/attributes.yml +0 -2
  245. data/public/ro/posts/second_post/blurb.erb.md +0 -5
  246. data/public/ro/posts/second_post/body.md +0 -16
  247. data/public/ro/posts/third_post/assets/foo/bar/baz.jpg +0 -0
  248. data/public/ro/posts/third_post/assets/foo.jpg +0 -0
  249. data/public/ro/posts/third_post/assets/src/foo/bar.rb +0 -3
  250. data/public/ro/posts/third_post/attributes.yml +0 -2
  251. data/public/ro/posts/third_post/blurb.erb.md +0 -5
  252. data/public/ro/posts/third_post/body.md +0 -16
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/env ruby
2
+ # Unit tests for Ro::Collection with new structure support
3
+
4
+ require_relative '../test_helper'
5
+
6
+ class CollectionTest < RoTestCase
7
+ def setup
8
+ @root = Ro::Root.new(new_structure_path)
9
+ @collection = Ro::Collection.new(new_structure_path / 'posts')
10
+ end
11
+
12
+ # T011: Test Collection#metadata_files
13
+ def test_metadata_files_returns_yml_and_json_files
14
+ files = @collection.metadata_files
15
+
16
+ assert_not_nil files, "metadata_files should not be nil"
17
+ assert files.is_a?(Array), "metadata_files should return an Array"
18
+
19
+ # Should find .yml and .json files
20
+ yml_files = files.select { |f| f.to_s.end_with?('.yml') }
21
+ assert yml_files.any?, "Should find at least one .yml file"
22
+
23
+ # Should be Pathname or Ro::Path objects
24
+ assert files.all? { |f| f.is_a?(Pathname) || f.is_a?(Ro::Path) }, "All files should be Pathname or Ro::Path objects"
25
+ end
26
+
27
+ def test_metadata_files_excludes_directories
28
+ files = @collection.metadata_files
29
+
30
+ # Should only return files, not directories
31
+ assert files.all?(&:file?), "metadata_files should only return files, not directories"
32
+ end
33
+
34
+ def test_metadata_files_sorted
35
+ files = @collection.metadata_files
36
+
37
+ # Should be sorted
38
+ assert_equal files.sort.map(&:to_s), files.map(&:to_s), "metadata_files should be sorted"
39
+ end
40
+
41
+ # T012: Test Collection#each with new structure
42
+ def test_each_iterates_nodes_from_metadata_files
43
+ nodes = []
44
+ @collection.each do |node|
45
+ nodes << node
46
+ end
47
+
48
+ assert nodes.any?, "Collection should have at least one node"
49
+ assert nodes.all? { |n| n.is_a?(Ro::Node) }, "All items should be Node instances"
50
+ end
51
+
52
+ def test_each_creates_nodes_from_metadata_files
53
+ node_ids = []
54
+ @collection.each do |node|
55
+ node_ids << node.id
56
+ end
57
+
58
+ # Should find nodes based on metadata files
59
+ assert node_ids.include?('sample-post'), "Should find sample-post node"
60
+ assert node_ids.include?('metadata-only'), "Should find metadata-only node"
61
+ assert node_ids.include?('nested-test'), "Should find nested-test node"
62
+ end
63
+
64
+ def test_each_without_block_returns_enumerator
65
+ enum = @collection.each
66
+
67
+ assert enum.is_a?(Enumerator), "each without block should return Enumerator"
68
+
69
+ nodes = enum.to_a
70
+ assert nodes.any?, "Enumerator should yield nodes"
71
+ end
72
+ end
73
+
74
+ # Run the tests
75
+ if __FILE__ == $0
76
+ test = CollectionTest.new
77
+
78
+ puts "Running Collection unit tests..."
79
+
80
+ begin
81
+ test.setup
82
+ test.test_metadata_files_returns_yml_and_json_files
83
+ puts "✓ test_metadata_files_returns_yml_and_json_files"
84
+ rescue => e
85
+ puts "✗ test_metadata_files_returns_yml_and_json_files: #{e.message}"
86
+ end
87
+
88
+ begin
89
+ test.setup
90
+ test.test_metadata_files_excludes_directories
91
+ puts "✓ test_metadata_files_excludes_directories"
92
+ rescue => e
93
+ puts "✗ test_metadata_files_excludes_directories: #{e.message}"
94
+ end
95
+
96
+ begin
97
+ test.setup
98
+ test.test_metadata_files_sorted
99
+ puts "✓ test_metadata_files_sorted"
100
+ rescue => e
101
+ puts "✗ test_metadata_files_sorted: #{e.message}"
102
+ end
103
+
104
+ begin
105
+ test.setup
106
+ test.test_each_iterates_nodes_from_metadata_files
107
+ puts "✓ test_each_iterates_nodes_from_metadata_files"
108
+ rescue => e
109
+ puts "✗ test_each_iterates_nodes_from_metadata_files: #{e.message}"
110
+ end
111
+
112
+ begin
113
+ test.setup
114
+ test.test_each_creates_nodes_from_metadata_files
115
+ puts "✓ test_each_creates_nodes_from_metadata_files"
116
+ rescue => e
117
+ puts "✗ test_each_creates_nodes_from_metadata_files: #{e.message}"
118
+ end
119
+
120
+ begin
121
+ test.setup
122
+ test.test_each_without_block_returns_enumerator
123
+ puts "✓ test_each_without_block_returns_enumerator"
124
+ rescue => e
125
+ puts "✗ test_each_without_block_returns_enumerator: #{e.message}"
126
+ end
127
+ end
@@ -0,0 +1,209 @@
1
+ #!/usr/bin/env ruby
2
+ # Unit tests for Ro::Migrator with asset structure migration
3
+
4
+ require_relative '../test_helper'
5
+
6
+ class MigratorTest < RoTestCase
7
+ def setup
8
+ @old_root = Ro::Root.new(old_structure_path)
9
+ @temp_dir = create_temp_dir('migration_test')
10
+
11
+ # Copy old structure fixtures to temp for migration testing
12
+ FileUtils.cp_r(old_structure_path.to_s + '/.', @temp_dir.to_s)
13
+ end
14
+
15
+ def teardown
16
+ FileUtils.rm_rf(@temp_dir) if @temp_dir && @temp_dir.exist?
17
+ end
18
+
19
+ # T042: Test Migrator#initialize
20
+ def test_initialize_with_root_path
21
+ migrator = Ro::Migrator.new(@temp_dir)
22
+
23
+ assert_not_nil migrator, "Migrator should be created"
24
+ assert_equal @temp_dir.to_s, migrator.root_path.to_s, "Migrator should store root path"
25
+ end
26
+
27
+ def test_initialize_with_options
28
+ migrator = Ro::Migrator.new(@temp_dir, dry_run: true, backup: true, verbose: true)
29
+
30
+ assert migrator.dry_run?, "Migrator should be in dry-run mode"
31
+ assert migrator.backup?, "Migrator should have backup enabled"
32
+ assert migrator.verbose?, "Migrator should be verbose"
33
+ end
34
+
35
+ # T043: Test Migrator#validate detecting old structure
36
+ def test_validate_detects_old_structure
37
+ migrator = Ro::Migrator.new(@temp_dir)
38
+ result = migrator.validate
39
+
40
+ assert result[:has_old_structure], "Should detect old structure"
41
+ assert result[:old_nodes].any?, "Should find old structure nodes"
42
+ end
43
+
44
+ # T044: Test Migrator#validate detecting new structure
45
+ def test_validate_detects_new_structure
46
+ new_dir = create_temp_dir('new_structure_test')
47
+ FileUtils.cp_r(new_structure_path.to_s + '/.', new_dir.to_s)
48
+
49
+ migrator = Ro::Migrator.new(new_dir)
50
+ result = migrator.validate
51
+
52
+ assert result[:has_new_structure], "Should detect new structure"
53
+ assert result[:new_nodes].any?, "Should find new structure nodes"
54
+
55
+ FileUtils.rm_rf(new_dir)
56
+ end
57
+
58
+ # T045: Test Migrator#preview generating migration plan
59
+ def test_preview_generates_migration_plan
60
+ migrator = Ro::Migrator.new(@temp_dir)
61
+ plan = migrator.preview
62
+
63
+ assert plan.is_a?(Array), "Preview should return array of migration steps"
64
+ assert plan.any?, "Preview should have migration steps"
65
+
66
+ # Check that plan includes details about what will be migrated
67
+ first_step = plan.first
68
+ assert first_step.key?(:node_id), "Step should include node_id"
69
+ assert first_step.key?(:old_path), "Step should include old_path"
70
+ assert first_step.key?(:new_metadata_file), "Step should include new_metadata_file"
71
+ assert first_step.key?(:new_asset_dir), "Step should include new_asset_dir"
72
+ end
73
+
74
+ # T046: Test Migrator#migrate_node for single node
75
+ def test_migrate_node_single_node
76
+ migrator = Ro::Migrator.new(@temp_dir)
77
+ posts_dir = @temp_dir / 'posts'
78
+ old_node_dir = posts_dir / 'sample-post'
79
+
80
+ result = migrator.migrate_node('posts', 'sample-post')
81
+
82
+ assert result[:success], "Migration should succeed"
83
+
84
+ # Check new structure was created
85
+ metadata_file = posts_dir / 'sample-post.yml'
86
+ asset_dir = posts_dir / 'sample-post'
87
+
88
+ assert metadata_file.exist?, "Metadata file should exist at collection level"
89
+ assert asset_dir.directory?, "Asset directory should exist"
90
+
91
+ # Check old attributes file was removed, but assets/ directory remains
92
+ old_attributes_file = asset_dir / 'attributes.yml'
93
+ assets_dir = asset_dir / 'assets'
94
+
95
+ assert !old_attributes_file.exist?, "Old attributes.yml should be removed"
96
+ assert assets_dir.exist?, "Assets/ directory should still exist (not moved)"
97
+ end
98
+
99
+ # T047: Test Migrator#migrate_collection for entire collection
100
+ def test_migrate_collection
101
+ migrator = Ro::Migrator.new(@temp_dir)
102
+
103
+ result = migrator.migrate_collection('posts')
104
+
105
+ assert result[:success], "Collection migration should succeed"
106
+ assert result[:migrated_count] > 0, "Should migrate at least one node"
107
+ end
108
+
109
+ # T048: Test Migrator#migrate for full migration
110
+ def test_migrate_full_root
111
+ migrator = Ro::Migrator.new(@temp_dir)
112
+
113
+ result = migrator.migrate
114
+
115
+ assert result[:success], "Full migration should succeed"
116
+ assert result[:collections_migrated] > 0, "Should migrate at least one collection"
117
+ assert result[:nodes_migrated] > 0, "Should migrate at least one node"
118
+ end
119
+
120
+ # T049: Test Migrator#backup creating backup
121
+ def test_backup_creates_backup
122
+ migrator = Ro::Migrator.new(@temp_dir, backup: true)
123
+
124
+ backup_path = migrator.backup
125
+
126
+ assert backup_path.exist?, "Backup should be created"
127
+ assert backup_path.to_s.include?('.backup.'), "Backup should have .backup. in name"
128
+ end
129
+
130
+ # T050: Test Migrator#rollback restoring from backup
131
+ def test_rollback_restores_from_backup
132
+ migrator = Ro::Migrator.new(@temp_dir, backup: true)
133
+
134
+ # Create backup, migrate, then rollback
135
+ backup_path = migrator.backup
136
+ migrator.migrate
137
+ result = migrator.rollback
138
+
139
+ assert result[:success], "Rollback should succeed"
140
+ assert result[:restored_from].to_s == backup_path.to_s, "Should restore from backup"
141
+ end
142
+
143
+ # T051: Test Migrator finds nodes without attributes.yml
144
+ def test_validate_finds_nodes_without_attributes
145
+ migrator = Ro::Migrator.new(@temp_dir)
146
+ validation = migrator.validate
147
+
148
+ # Should find both sample-post (with attributes) and assets-only (without)
149
+ assert validation[:old_nodes].size >= 2, "Should find at least 2 nodes"
150
+
151
+ assets_only_node = validation[:old_nodes].find { |n| n[:node_id] == 'assets-only' }
152
+ assert_not_nil assets_only_node, "Should find assets-only node"
153
+ assert !assets_only_node[:has_attributes], "assets-only should not have attributes"
154
+ end
155
+
156
+ # T052: Test migration creates empty metadata for nodes without attributes
157
+ def test_migrate_node_without_attributes
158
+ migrator = Ro::Migrator.new(@temp_dir)
159
+ posts_dir = @temp_dir / 'posts'
160
+
161
+ result = migrator.migrate_node('posts', 'assets-only')
162
+
163
+ assert result[:success], "Migration should succeed"
164
+ assert !result[:had_attributes], "Node should not have had attributes"
165
+
166
+ # Check metadata file was created
167
+ metadata_file = posts_dir / 'assets-only.yml'
168
+ assert metadata_file.exist?, "Empty metadata file should be created"
169
+
170
+ # Check it contains empty hash
171
+ content = YAML.load_file(metadata_file.to_s)
172
+ assert content.is_a?(Hash), "Metadata should be a hash"
173
+ end
174
+ end
175
+
176
+ # Run the tests
177
+ if __FILE__ == $0
178
+ test = MigratorTest.new
179
+
180
+ puts "Running Migrator unit tests..."
181
+
182
+ tests = [
183
+ :test_initialize_with_root_path,
184
+ :test_initialize_with_options,
185
+ :test_validate_detects_old_structure,
186
+ :test_validate_detects_new_structure,
187
+ :test_preview_generates_migration_plan,
188
+ :test_migrate_node_single_node,
189
+ :test_migrate_collection,
190
+ :test_migrate_full_root,
191
+ :test_backup_creates_backup,
192
+ :test_rollback_restores_from_backup,
193
+ :test_validate_finds_nodes_without_attributes,
194
+ :test_migrate_node_without_attributes
195
+ ]
196
+
197
+ tests.each do |test_method|
198
+ begin
199
+ test.setup
200
+ test.send(test_method)
201
+ test.teardown
202
+ puts "✓ #{test_method}"
203
+ rescue => e
204
+ puts "✗ #{test_method}: #{e.message}"
205
+ puts " #{e.backtrace.first}"
206
+ test.teardown rescue nil
207
+ end
208
+ end
209
+ end
@@ -0,0 +1,138 @@
1
+ #!/usr/bin/env ruby
2
+ # Unit tests for Ro::Node with new structure support
3
+
4
+ require_relative '../test_helper'
5
+
6
+ class NodeTest < RoTestCase
7
+ def setup
8
+ @root = Ro::Root.new(new_structure_path)
9
+ @collection = Ro::Collection.new(new_structure_path / 'posts')
10
+ @metadata_file = new_structure_path / 'posts' / 'sample-post.yml'
11
+ end
12
+
13
+ # T013: Test Node initialization with metadata_file parameter
14
+ def test_initialize_with_metadata_file
15
+ node = Ro::Node.new(@collection, @metadata_file)
16
+
17
+ assert_not_nil node, "Node should be created"
18
+ assert_equal @collection, node.collection, "Node should store collection reference"
19
+ end
20
+
21
+ def test_initialize_stores_metadata_file
22
+ node = Ro::Node.new(@collection, @metadata_file)
23
+
24
+ assert_not_nil node.metadata_file, "Node should store metadata_file"
25
+ assert_equal @metadata_file.to_s, node.metadata_file.to_s, "metadata_file path should match constructor parameter"
26
+ end
27
+
28
+ def test_initialize_raises_error_for_missing_file
29
+ missing_file = new_structure_path / 'posts' / 'nonexistent.yml'
30
+
31
+ assert_raises(Errno::ENOENT) do
32
+ Ro::Node.new(@collection, missing_file)
33
+ end
34
+ end
35
+
36
+ # T014: Test Node#id derived from metadata filename
37
+ def test_id_derived_from_metadata_filename
38
+ node = Ro::Node.new(@collection, @metadata_file)
39
+
40
+ assert_equal 'sample-post', node.id, "Node ID should be derived from metadata filename (without extension)"
41
+ end
42
+
43
+ def test_id_strips_yml_extension
44
+ node = Ro::Node.new(@collection, @metadata_file)
45
+
46
+ assert !node.id.end_with?('.yml'), "Node ID should not include .yml extension"
47
+ end
48
+
49
+ def test_id_handles_json_extension
50
+ json_file = new_structure_path / 'mixed' / 'test-json.json'
51
+ collection = Ro::Collection.new(new_structure_path / 'mixed')
52
+ node = Ro::Node.new(collection, json_file)
53
+
54
+ assert_equal 'test-json', node.id, "Node ID should be derived from .json filename"
55
+ assert !node.id.end_with?('.json'), "Node ID should not include .json extension"
56
+ end
57
+
58
+ # T015: Test Node#asset_dir returns assets/ subdirectory
59
+ def test_asset_dir_returns_assets_subdirectory
60
+ node = Ro::Node.new(@collection, @metadata_file)
61
+ expected_dir = new_structure_path / 'posts' / 'sample-post' / 'assets'
62
+
63
+ assert_equal expected_dir.to_s, node.asset_dir.to_s, "asset_dir should return assets/ subdirectory"
64
+ end
65
+
66
+ def test_asset_dir_ends_with_assets
67
+ node = Ro::Node.new(@collection, @metadata_file)
68
+
69
+ assert node.asset_dir.to_s.end_with?('/assets'), "asset_dir should end with /assets"
70
+ end
71
+
72
+ def test_asset_dir_for_metadata_only_node
73
+ metadata_only_file = new_structure_path / 'posts' / 'metadata-only.yml'
74
+ node = Ro::Node.new(@collection, metadata_only_file)
75
+ expected_dir = new_structure_path / 'posts' / 'metadata-only' / 'assets'
76
+
77
+ # asset_dir should still return the expected path even if directory doesn't exist
78
+ assert_equal expected_dir.to_s, node.asset_dir.to_s
79
+ end
80
+
81
+ # T016: Test Node#_load_base_attributes loading from external metadata file
82
+ def test_load_base_attributes_from_metadata_file
83
+ node = Ro::Node.new(@collection, @metadata_file)
84
+
85
+ # Trigger attribute loading (happens in initialize)
86
+ attrs = node.attributes
87
+
88
+ assert_not_nil attrs, "Attributes should be loaded"
89
+ assert attrs.is_a?(Hash), "Attributes should be a Hash"
90
+ end
91
+
92
+ def test_attributes_loaded_from_correct_file
93
+ node = Ro::Node.new(@collection, @metadata_file)
94
+
95
+ assert_equal 'Sample Post (New Structure)', node[:title], "Should load title from metadata file"
96
+ assert_equal 'Test Author', node[:author], "Should load author from metadata file"
97
+ end
98
+
99
+ def test_attributes_support_symbol_and_string_keys
100
+ node = Ro::Node.new(@collection, @metadata_file)
101
+
102
+ # Should work with both symbol and string keys
103
+ assert_equal node[:title], node['title'], "Attributes should work with both symbol and string keys"
104
+ end
105
+ end
106
+
107
+ # Run the tests
108
+ if __FILE__ == $0
109
+ test = NodeTest.new
110
+
111
+ puts "Running Node unit tests..."
112
+
113
+ tests = [
114
+ :test_initialize_with_metadata_file,
115
+ :test_initialize_stores_metadata_file,
116
+ :test_initialize_raises_error_for_missing_file,
117
+ :test_id_derived_from_metadata_filename,
118
+ :test_id_strips_yml_extension,
119
+ :test_id_handles_json_extension,
120
+ :test_asset_dir_returns_assets_subdirectory,
121
+ :test_asset_dir_ends_with_assets,
122
+ :test_asset_dir_for_metadata_only_node,
123
+ :test_load_base_attributes_from_metadata_file,
124
+ :test_attributes_loaded_from_correct_file,
125
+ :test_attributes_support_symbol_and_string_keys
126
+ ]
127
+
128
+ tests.each do |test_method|
129
+ begin
130
+ test.setup
131
+ test.send(test_method)
132
+ puts "✓ #{test_method}"
133
+ rescue => e
134
+ puts "✗ #{test_method}: #{e.message}"
135
+ puts " #{e.backtrace.first}"
136
+ end
137
+ end
138
+ end
File without changes