ro 4.4.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 (121) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +42 -16
  3. data/MIGRATION.md +320 -0
  4. data/README.md +30 -18
  5. data/a.yml +60 -0
  6. data/bin/ro +10 -0
  7. data/lib/ro/_lib.rb +1 -1
  8. data/lib/ro/asset.rb +46 -5
  9. data/lib/ro/collection.rb +51 -13
  10. data/lib/ro/migrator.rb +285 -0
  11. data/lib/ro/node.rb +53 -13
  12. data/lib/ro/root.rb +75 -1
  13. data/lib/ro/script/migrate.rb +204 -0
  14. data/lib/ro/script/server.rb +1 -1
  15. data/lib/ro.rb +1 -0
  16. data/ro.gemspec +207 -16
  17. data/specs/001-simplify-asset-structure/IMPLEMENTATION_SUMMARY.md +212 -0
  18. data/specs/001-simplify-asset-structure/checklists/requirements.md +36 -0
  19. data/specs/001-simplify-asset-structure/contracts/collection_api.md +407 -0
  20. data/specs/001-simplify-asset-structure/contracts/migrator_api.md +461 -0
  21. data/specs/001-simplify-asset-structure/contracts/node_api.md +294 -0
  22. data/specs/001-simplify-asset-structure/data-model.md +381 -0
  23. data/specs/001-simplify-asset-structure/plan.md +90 -0
  24. data/specs/001-simplify-asset-structure/quickstart.md +575 -0
  25. data/specs/001-simplify-asset-structure/research.md +333 -0
  26. data/specs/001-simplify-asset-structure/spec.md +127 -0
  27. data/specs/001-simplify-asset-structure/tasks.md +349 -0
  28. data/test/fixtures/new_structure/mixed/test-json.json +5 -0
  29. data/test/fixtures/new_structure/mixed/test-yaml.yml +3 -0
  30. data/test/fixtures/new_structure/posts/metadata-only.yml +7 -0
  31. data/test/fixtures/new_structure/posts/nested-test/assets/subdirectory/image.png +2 -0
  32. data/test/fixtures/new_structure/posts/nested-test.yml +7 -0
  33. data/test/fixtures/new_structure/posts/sample-post/assets/body.md +5 -0
  34. data/test/fixtures/new_structure/posts/sample-post/assets/image.jpg +2 -0
  35. data/test/fixtures/new_structure/posts/sample-post.yml +7 -0
  36. data/test/fixtures/old_structure/posts/assets-only/assets/test.txt +1 -0
  37. data/test/fixtures/old_structure/posts/sample-post/assets/body.md +5 -0
  38. data/test/fixtures/old_structure/posts/sample-post/assets/image.jpg +2 -0
  39. data/test/fixtures/old_structure/posts/sample-post/attributes.yml +2 -0
  40. data/test/integration/ro_integration_test.rb +165 -0
  41. data/test/test_helper.rb +149 -0
  42. data/test/tmp/migration_test_1760746513.backup.20251018001513/migration_test_1760746513/posts/sample-post/assets/image.jpg +2 -0
  43. data/test/tmp/migration_test_1760746513.backup.20251018001513/migration_test_1760746513/posts/sample-post/attributes.yml +7 -0
  44. data/test/tmp/migration_test_1760746513.backup.20251018001513/migration_test_1760746513/posts/sample-post/body.md +5 -0
  45. data/test/tmp/migration_test_1760746513.backup.20251018001513/posts/sample-post/assets/image.jpg +2 -0
  46. data/test/tmp/migration_test_1760746513.backup.20251018001513/posts/sample-post/attributes.yml +7 -0
  47. data/test/tmp/migration_test_1760746513.backup.20251018001513/posts/sample-post/body.md +5 -0
  48. data/test/tmp/migration_test_1760746556.backup.20251018001556/migration_test_1760746556/posts/sample-post/assets/image.jpg +2 -0
  49. data/test/tmp/migration_test_1760746556.backup.20251018001556/migration_test_1760746556/posts/sample-post/attributes.yml +7 -0
  50. data/test/tmp/migration_test_1760746556.backup.20251018001556/migration_test_1760746556/posts/sample-post/body.md +5 -0
  51. data/test/tmp/migration_test_1760746556.backup.20251018001556/posts/sample-post/assets/image.jpg +2 -0
  52. data/test/tmp/migration_test_1760746556.backup.20251018001556/posts/sample-post/attributes.yml +7 -0
  53. data/test/tmp/migration_test_1760746556.backup.20251018001556/posts/sample-post/body.md +5 -0
  54. data/test/tmp/migration_test_1760755248.backup.20251018024048/migration_test_1760755248/posts/sample-post/assets/image.jpg +2 -0
  55. data/test/tmp/migration_test_1760755248.backup.20251018024048/migration_test_1760755248/posts/sample-post/attributes.yml +7 -0
  56. data/test/tmp/migration_test_1760755248.backup.20251018024048/migration_test_1760755248/posts/sample-post/body.md +5 -0
  57. data/test/tmp/migration_test_1760755248.backup.20251018024048/posts/sample-post/assets/image.jpg +2 -0
  58. data/test/tmp/migration_test_1760755248.backup.20251018024048/posts/sample-post/attributes.yml +7 -0
  59. data/test/tmp/migration_test_1760755248.backup.20251018024048/posts/sample-post/body.md +5 -0
  60. data/test/tmp/migration_test_1760758803.backup.20251018034003/migration_test_1760758803/posts/sample-post/body.md +5 -0
  61. data/test/tmp/migration_test_1760758803.backup.20251018034003/migration_test_1760758803/posts/sample-post/image.jpg +2 -0
  62. data/test/tmp/migration_test_1760758803.backup.20251018034003/migration_test_1760758803/posts/sample-post.yml +7 -0
  63. data/test/tmp/migration_test_1760758803.backup.20251018034003/posts/sample-post/body.md +5 -0
  64. data/test/tmp/migration_test_1760758803.backup.20251018034003/posts/sample-post/image.jpg +2 -0
  65. data/test/tmp/migration_test_1760758803.backup.20251018034003/posts/sample-post.yml +7 -0
  66. data/test/tmp/migration_test_1760758869.backup.20251018034109/migration_test_1760758869/posts/sample-post/assets/body.md +5 -0
  67. data/test/tmp/migration_test_1760758869.backup.20251018034109/migration_test_1760758869/posts/sample-post/assets/image.jpg +2 -0
  68. data/test/tmp/migration_test_1760758869.backup.20251018034109/migration_test_1760758869/posts/sample-post/attributes.yml +2 -0
  69. data/test/tmp/migration_test_1760758869.backup.20251018034109/posts/sample-post/assets/body.md +5 -0
  70. data/test/tmp/migration_test_1760758869.backup.20251018034109/posts/sample-post/assets/image.jpg +2 -0
  71. data/test/tmp/migration_test_1760758869.backup.20251018034109/posts/sample-post/attributes.yml +2 -0
  72. data/test/tmp/migration_test_1760758920.backup.20251018034200/migration_test_1760758920/posts/sample-post/assets/body.md +5 -0
  73. data/test/tmp/migration_test_1760758920.backup.20251018034200/migration_test_1760758920/posts/sample-post/assets/image.jpg +2 -0
  74. data/test/tmp/migration_test_1760758920.backup.20251018034200/migration_test_1760758920/posts/sample-post/attributes.yml +2 -0
  75. data/test/tmp/migration_test_1760758920.backup.20251018034200/posts/sample-post/assets/body.md +5 -0
  76. data/test/tmp/migration_test_1760758920.backup.20251018034200/posts/sample-post/assets/image.jpg +2 -0
  77. data/test/tmp/migration_test_1760758920.backup.20251018034200/posts/sample-post/attributes.yml +2 -0
  78. data/test/tmp/migration_test_1760824728.backup.20251018215848/migration_test_1760824728/posts/assets-only/assets/test.txt +1 -0
  79. data/test/tmp/migration_test_1760824728.backup.20251018215848/migration_test_1760824728/posts/sample-post/assets/body.md +5 -0
  80. data/test/tmp/migration_test_1760824728.backup.20251018215848/migration_test_1760824728/posts/sample-post/assets/image.jpg +2 -0
  81. data/test/tmp/migration_test_1760824728.backup.20251018215848/migration_test_1760824728/posts/sample-post/attributes.yml +2 -0
  82. data/test/tmp/migration_test_1760824728.backup.20251018215848/posts/assets-only/assets/test.txt +1 -0
  83. data/test/tmp/migration_test_1760824728.backup.20251018215848/posts/sample-post/assets/body.md +5 -0
  84. data/test/tmp/migration_test_1760824728.backup.20251018215848/posts/sample-post/assets/image.jpg +2 -0
  85. data/test/tmp/migration_test_1760824728.backup.20251018215848/posts/sample-post/attributes.yml +2 -0
  86. data/test/tmp/migration_test_1760844153.backup.20251019032233/migration_test_1760844153/posts/assets-only/assets/test.txt +1 -0
  87. data/test/tmp/migration_test_1760844153.backup.20251019032233/migration_test_1760844153/posts/sample-post/assets/body.md +5 -0
  88. data/test/tmp/migration_test_1760844153.backup.20251019032233/migration_test_1760844153/posts/sample-post/assets/image.jpg +2 -0
  89. data/test/tmp/migration_test_1760844153.backup.20251019032233/migration_test_1760844153/posts/sample-post/attributes.yml +2 -0
  90. data/test/tmp/migration_test_1760844153.backup.20251019032233/posts/assets-only/assets/test.txt +1 -0
  91. data/test/tmp/migration_test_1760844153.backup.20251019032233/posts/sample-post/assets/body.md +5 -0
  92. data/test/tmp/migration_test_1760844153.backup.20251019032233/posts/sample-post/assets/image.jpg +2 -0
  93. data/test/tmp/migration_test_1760844153.backup.20251019032233/posts/sample-post/attributes.yml +2 -0
  94. data/test/tmp/new_structure_test_1760746452/mixed/test-json.json +5 -0
  95. data/test/tmp/new_structure_test_1760746452/mixed/test-yaml.yml +3 -0
  96. data/test/tmp/new_structure_test_1760746452/posts/metadata-only.yml +7 -0
  97. data/test/tmp/new_structure_test_1760746452/posts/nested-test/subdirectory/image.png +2 -0
  98. data/test/tmp/new_structure_test_1760746452/posts/nested-test.yml +7 -0
  99. data/test/tmp/new_structure_test_1760746452/posts/sample-post/body.md +5 -0
  100. data/test/tmp/new_structure_test_1760746452/posts/sample-post/image.jpg +2 -0
  101. data/test/tmp/new_structure_test_1760746452/posts/sample-post.yml +7 -0
  102. data/test/unit/asset_test.rb +90 -0
  103. data/test/unit/collection_test.rb +127 -0
  104. data/test/unit/migrator_test.rb +209 -0
  105. data/test/unit/node_test.rb +138 -0
  106. metadata +111 -18
  107. /data/public/ro/nerd/{fastest-possible-embeddings/attributes.yml → fastest-possible-embeddings.yml} +0 -0
  108. /data/public/ro/nerd/{ima/attributes.yml → ima.yml} +0 -0
  109. /data/public/ro/nerd/{index/attributes.yml → index.yml} +0 -0
  110. /data/public/ro/pages/{contact/attributes.yml → contact.yml} +0 -0
  111. /data/public/ro/pages/{cv/attributes.yml → cv.yml} +0 -0
  112. /data/public/ro/pages/{disco/attributes.yml → disco.yml} +0 -0
  113. /data/public/ro/pages/{index/attributes.yml → index.yml} +0 -0
  114. /data/public/ro/pages/{jess/attributes.yml → jess.yml} +0 -0
  115. /data/public/ro/pages/{now/attributes.yml → now.yml} +0 -0
  116. /data/public/ro/posts/{almost-died-in-an-ice-cave/attributes.yml → almost-died-in-an-ice-cave.yml} +0 -0
  117. /data/public/ro/posts/{facebook-and-global-extremism/attributes.yml → facebook-and-global-extremism.yml} +0 -0
  118. /data/public/ro/posts/{lemmings-considered-harmful/attributes.yml → lemmings-considered-harmful.yml} +0 -0
  119. /data/public/ro/posts/{lost-in-the-desert/attributes.yml → lost-in-the-desert.yml} +0 -0
  120. /data/public/ro/posts/{mission/attributes.yml → mission.yml} +0 -0
  121. /data/public/ro/posts/{return-your-laptop/attributes.yml → return-your-laptop.yml} +0 -0
@@ -0,0 +1,149 @@
1
+ # Test Helper for ro gem v5.0 tests
2
+ #
3
+ # Common setup, assertions, and utilities for unit and integration tests
4
+
5
+ require 'pathname'
6
+ require 'fileutils'
7
+ require 'yaml'
8
+ require 'json'
9
+
10
+ # Load the ro gem
11
+ $LOAD_PATH.unshift File.expand_path('../lib', __dir__)
12
+ require 'ro'
13
+
14
+ module TestHelper
15
+ # Get path to test fixtures directory
16
+ def fixtures_path
17
+ Pathname.new(File.expand_path('../fixtures', __FILE__))
18
+ end
19
+
20
+ # Get path to old structure fixtures
21
+ def old_structure_path
22
+ fixtures_path / 'old_structure'
23
+ end
24
+
25
+ # Get path to new structure fixtures
26
+ def new_structure_path
27
+ fixtures_path / 'new_structure'
28
+ end
29
+
30
+ # Create a temporary test directory
31
+ def create_temp_dir(name = 'test')
32
+ dir = Pathname.new(File.expand_path("../tmp/#{name}_#{Time.now.to_i}", __FILE__))
33
+ FileUtils.mkdir_p(dir)
34
+ dir
35
+ end
36
+
37
+ # Clean up temporary directory
38
+ def cleanup_temp_dir(dir)
39
+ FileUtils.rm_rf(dir) if dir && dir.exist?
40
+ end
41
+
42
+ # Assert that a path exists
43
+ def assert_path_exists(path, message = nil)
44
+ assert Pathname.new(path).exist?, message || "Expected path to exist: #{path}"
45
+ end
46
+
47
+ # Assert that a file contains specific content
48
+ def assert_file_contains(path, content, message = nil)
49
+ actual = File.read(path)
50
+ assert actual.include?(content), message || "Expected file #{path} to contain: #{content}"
51
+ end
52
+
53
+ # Assert that a YAML file has a specific key/value
54
+ def assert_yaml_has(path, key, value = nil, message = nil)
55
+ data = YAML.load_file(path)
56
+ assert data.key?(key.to_s) || data.key?(key.to_sym), message || "Expected YAML to have key: #{key}"
57
+ if value
58
+ actual_value = data[key.to_s] || data[key.to_sym]
59
+ assert_equal value, actual_value, message || "Expected #{key} to equal #{value}, got #{actual_value}"
60
+ end
61
+ end
62
+
63
+ # Create a test node in old structure format
64
+ def create_old_structure_node(collection_path, node_id, attributes = {}, assets = [])
65
+ node_dir = collection_path / node_id
66
+ FileUtils.mkdir_p(node_dir)
67
+ FileUtils.mkdir_p(node_dir / 'assets')
68
+
69
+ # Write attributes.yml
70
+ File.write(node_dir / 'attributes.yml', attributes.to_yaml)
71
+
72
+ # Create asset files
73
+ assets.each do |asset_file|
74
+ asset_path = node_dir / 'assets' / asset_file
75
+ FileUtils.mkdir_p(asset_path.dirname)
76
+ File.write(asset_path, "test content for #{asset_file}")
77
+ end
78
+
79
+ node_dir
80
+ end
81
+
82
+ # Create a test node in new structure format
83
+ def create_new_structure_node(collection_path, node_id, attributes = {}, assets = [])
84
+ FileUtils.mkdir_p(collection_path)
85
+
86
+ # Write metadata file (identifier.yml)
87
+ File.write(collection_path / "#{node_id}.yml", attributes.to_yaml)
88
+
89
+ # Create asset directory and files
90
+ if assets.any?
91
+ node_dir = collection_path / node_id
92
+ FileUtils.mkdir_p(node_dir)
93
+
94
+ assets.each do |asset_file|
95
+ asset_path = node_dir / asset_file
96
+ FileUtils.mkdir_p(asset_path.dirname)
97
+ File.write(asset_path, "test content for #{asset_file}")
98
+ end
99
+ end
100
+
101
+ collection_path / "#{node_id}.yml"
102
+ end
103
+ end
104
+
105
+ # Base test class
106
+ class RoTestCase
107
+ include TestHelper
108
+
109
+ def setup
110
+ # Override in subclasses
111
+ end
112
+
113
+ def teardown
114
+ # Override in subclasses
115
+ end
116
+
117
+ # Simple assertion methods (compatible with custom test runner)
118
+ def assert(condition, message = "Assertion failed")
119
+ raise AssertionError, message unless condition
120
+ end
121
+
122
+ def assert_equal(expected, actual, message = nil)
123
+ msg = message || "Expected #{expected.inspect}, got #{actual.inspect}"
124
+ assert expected == actual, msg
125
+ end
126
+
127
+ def assert_nil(actual, message = nil)
128
+ msg = message || "Expected nil, got #{actual.inspect}"
129
+ assert actual.nil?, msg
130
+ end
131
+
132
+ def assert_not_nil(actual, message = nil)
133
+ msg = message || "Expected non-nil value"
134
+ assert !actual.nil?, msg
135
+ end
136
+
137
+ def assert_raises(exception_class, message = nil)
138
+ begin
139
+ yield
140
+ assert false, message || "Expected #{exception_class} to be raised"
141
+ rescue exception_class
142
+ # Expected
143
+ end
144
+ end
145
+
146
+ class AssertionError < StandardError; end
147
+ end
148
+
149
+ puts "Test helper loaded successfully"
@@ -0,0 +1,2 @@
1
+ FAKE_JPEG_DATA_FOR_TESTING_PURPOSES_ONLY
2
+ This is a test file representing an image asset in the old structure.
@@ -0,0 +1,7 @@
1
+ title: "Sample Post (Old Structure)"
2
+ author: "Test Author"
3
+ published_at: "2025-01-01"
4
+ tags:
5
+ - test
6
+ - old-structure
7
+ description: "This is a test fixture in the old structure format"
@@ -0,0 +1,5 @@
1
+ # Sample Post
2
+
3
+ This is the body content of the sample post in the old structure.
4
+
5
+ It has multiple paragraphs and should be treated as content, not an asset.
@@ -0,0 +1,2 @@
1
+ FAKE_JPEG_DATA_FOR_TESTING_PURPOSES_ONLY
2
+ This is a test file representing an image asset in the old structure.
@@ -0,0 +1,7 @@
1
+ title: "Sample Post (Old Structure)"
2
+ author: "Test Author"
3
+ published_at: "2025-01-01"
4
+ tags:
5
+ - test
6
+ - old-structure
7
+ description: "This is a test fixture in the old structure format"
@@ -0,0 +1,5 @@
1
+ # Sample Post
2
+
3
+ This is the body content of the sample post in the old structure.
4
+
5
+ It has multiple paragraphs and should be treated as content, not an asset.
@@ -0,0 +1,2 @@
1
+ FAKE_JPEG_DATA_FOR_TESTING_PURPOSES_ONLY
2
+ This is a test file representing an image asset in the old structure.
@@ -0,0 +1,7 @@
1
+ title: "Sample Post (Old Structure)"
2
+ author: "Test Author"
3
+ published_at: "2025-01-01"
4
+ tags:
5
+ - test
6
+ - old-structure
7
+ description: "This is a test fixture in the old structure format"
@@ -0,0 +1,5 @@
1
+ # Sample Post
2
+
3
+ This is the body content of the sample post in the old structure.
4
+
5
+ It has multiple paragraphs and should be treated as content, not an asset.
@@ -0,0 +1,2 @@
1
+ FAKE_JPEG_DATA_FOR_TESTING_PURPOSES_ONLY
2
+ This is a test file representing an image asset in the old structure.
@@ -0,0 +1,7 @@
1
+ title: "Sample Post (Old Structure)"
2
+ author: "Test Author"
3
+ published_at: "2025-01-01"
4
+ tags:
5
+ - test
6
+ - old-structure
7
+ description: "This is a test fixture in the old structure format"
@@ -0,0 +1,5 @@
1
+ # Sample Post
2
+
3
+ This is the body content of the sample post in the old structure.
4
+
5
+ It has multiple paragraphs and should be treated as content, not an asset.
@@ -0,0 +1,2 @@
1
+ FAKE_JPEG_DATA_FOR_TESTING_PURPOSES_ONLY
2
+ This is a test file representing an image asset in the old structure.
@@ -0,0 +1,7 @@
1
+ title: "Sample Post (Old Structure)"
2
+ author: "Test Author"
3
+ published_at: "2025-01-01"
4
+ tags:
5
+ - test
6
+ - old-structure
7
+ description: "This is a test fixture in the old structure format"
@@ -0,0 +1,5 @@
1
+ # Sample Post
2
+
3
+ This is the body content of the sample post in the old structure.
4
+
5
+ It has multiple paragraphs and should be treated as content, not an asset.
@@ -0,0 +1,2 @@
1
+ FAKE_JPEG_DATA_FOR_TESTING_PURPOSES_ONLY
2
+ This is a test file representing an image asset in the old structure.
@@ -0,0 +1,7 @@
1
+ title: "Sample Post (Old Structure)"
2
+ author: "Test Author"
3
+ published_at: "2025-01-01"
4
+ tags:
5
+ - test
6
+ - old-structure
7
+ description: "This is a test fixture in the old structure format"
@@ -0,0 +1,5 @@
1
+ # Sample Post
2
+
3
+ This is the body content of the sample post in the old structure.
4
+
5
+ It has multiple paragraphs and should be treated as content, not an asset.
@@ -0,0 +1,5 @@
1
+ # Sample Post
2
+
3
+ This is the body content of the sample post in the old structure.
4
+
5
+ It has multiple paragraphs and should be treated as content, not an asset.
@@ -0,0 +1,2 @@
1
+ FAKE_JPEG_DATA_FOR_TESTING_PURPOSES_ONLY
2
+ This is a test file representing an image asset in the old structure.
@@ -0,0 +1,7 @@
1
+ title: "Sample Post (Old Structure)"
2
+ author: "Test Author"
3
+ published_at: "2025-01-01"
4
+ tags:
5
+ - test
6
+ - old-structure
7
+ description: "This is a test fixture in the old structure format"
@@ -0,0 +1,5 @@
1
+ # Sample Post
2
+
3
+ This is the body content of the sample post in the old structure.
4
+
5
+ It has multiple paragraphs and should be treated as content, not an asset.
@@ -0,0 +1,2 @@
1
+ FAKE_JPEG_DATA_FOR_TESTING_PURPOSES_ONLY
2
+ This is a test file representing an image asset in the old structure.
@@ -0,0 +1,7 @@
1
+ title: "Sample Post (Old Structure)"
2
+ author: "Test Author"
3
+ published_at: "2025-01-01"
4
+ tags:
5
+ - test
6
+ - old-structure
7
+ description: "This is a test fixture in the old structure format"
@@ -0,0 +1,5 @@
1
+ # Sample Post
2
+
3
+ This is the body content of the sample post in the old structure.
4
+
5
+ It has multiple paragraphs and should be treated as content, not an asset.
@@ -0,0 +1,2 @@
1
+ FAKE_JPEG_DATA_FOR_TESTING_PURPOSES_ONLY
2
+ This is a test file representing an image asset in the old structure.
@@ -0,0 +1,2 @@
1
+ title: Sample Post (Old Structure)
2
+ author: Test Author
@@ -0,0 +1,5 @@
1
+ # Sample Post
2
+
3
+ This is the body content of the sample post in the old structure.
4
+
5
+ It has multiple paragraphs and should be treated as content, not an asset.
@@ -0,0 +1,2 @@
1
+ FAKE_JPEG_DATA_FOR_TESTING_PURPOSES_ONLY
2
+ This is a test file representing an image asset in the old structure.
@@ -0,0 +1,2 @@
1
+ title: Sample Post (Old Structure)
2
+ author: Test Author
@@ -0,0 +1,5 @@
1
+ # Sample Post
2
+
3
+ This is the body content of the sample post in the old structure.
4
+
5
+ It has multiple paragraphs and should be treated as content, not an asset.
@@ -0,0 +1,2 @@
1
+ FAKE_JPEG_DATA_FOR_TESTING_PURPOSES_ONLY
2
+ This is a test file representing an image asset in the old structure.
@@ -0,0 +1,2 @@
1
+ title: Sample Post (Old Structure)
2
+ author: Test Author
@@ -0,0 +1,5 @@
1
+ # Sample Post
2
+
3
+ This is the body content of the sample post in the old structure.
4
+
5
+ It has multiple paragraphs and should be treated as content, not an asset.
@@ -0,0 +1,2 @@
1
+ FAKE_JPEG_DATA_FOR_TESTING_PURPOSES_ONLY
2
+ This is a test file representing an image asset in the old structure.
@@ -0,0 +1,2 @@
1
+ title: Sample Post (Old Structure)
2
+ author: Test Author
@@ -0,0 +1,5 @@
1
+ # Sample Post
2
+
3
+ This is the body content of the sample post in the old structure.
4
+
5
+ It has multiple paragraphs and should be treated as content, not an asset.
@@ -0,0 +1,2 @@
1
+ FAKE_JPEG_DATA_FOR_TESTING_PURPOSES_ONLY
2
+ This is a test file representing an image asset in the old structure.
@@ -0,0 +1,2 @@
1
+ title: Sample Post (Old Structure)
2
+ author: Test Author
@@ -0,0 +1,5 @@
1
+ # Sample Post
2
+
3
+ This is the body content of the sample post in the old structure.
4
+
5
+ It has multiple paragraphs and should be treated as content, not an asset.
@@ -0,0 +1,2 @@
1
+ FAKE_JPEG_DATA_FOR_TESTING_PURPOSES_ONLY
2
+ This is a test file representing an image asset in the old structure.
@@ -0,0 +1,2 @@
1
+ title: Sample Post (Old Structure)
2
+ author: Test Author
@@ -0,0 +1,5 @@
1
+ # Sample Post
2
+
3
+ This is the body content of the sample post in the old structure.
4
+
5
+ It has multiple paragraphs and should be treated as content, not an asset.
@@ -0,0 +1,2 @@
1
+ FAKE_JPEG_DATA_FOR_TESTING_PURPOSES_ONLY
2
+ This is a test file representing an image asset in the old structure.
@@ -0,0 +1,2 @@
1
+ title: Sample Post (Old Structure)
2
+ author: Test Author
@@ -0,0 +1,5 @@
1
+ # Sample Post
2
+
3
+ This is the body content of the sample post in the old structure.
4
+
5
+ It has multiple paragraphs and should be treated as content, not an asset.
@@ -0,0 +1,2 @@
1
+ FAKE_JPEG_DATA_FOR_TESTING_PURPOSES_ONLY
2
+ This is a test file representing an image asset in the old structure.
@@ -0,0 +1,2 @@
1
+ title: Sample Post (Old Structure)
2
+ author: Test Author
@@ -0,0 +1,5 @@
1
+ {
2
+ "title": "JSON Format Test",
3
+ "format": "json",
4
+ "extension": "json"
5
+ }
@@ -0,0 +1,3 @@
1
+ title: "YAML Format Test"
2
+ format: "yaml"
3
+ extension: "yml"
@@ -0,0 +1,7 @@
1
+ title: "Metadata Only Node"
2
+ author: "Test Author"
3
+ published_at: "2025-01-02"
4
+ tags:
5
+ - test
6
+ - metadata-only
7
+ description: "This node has no asset directory - testing FR-007"
@@ -0,0 +1,2 @@
1
+ FAKE_PNG_DATA_FOR_TESTING_PURPOSES_ONLY
2
+ This is a test file in a nested subdirectory.
@@ -0,0 +1,7 @@
1
+ title: "Nested Assets Test"
2
+ author: "Test Author"
3
+ published_at: "2025-01-03"
4
+ tags:
5
+ - test
6
+ - nested-assets
7
+ description: "This node has nested subdirectories with assets"
@@ -0,0 +1,5 @@
1
+ # Sample Post
2
+
3
+ This is the body content of the sample post in the new structure.
4
+
5
+ It has multiple paragraphs and should be treated as content, not an asset.
@@ -0,0 +1,2 @@
1
+ FAKE_JPEG_DATA_FOR_TESTING_PURPOSES_ONLY
2
+ This is a test file representing an image asset in the new structure (no assets/ subdirectory).
@@ -0,0 +1,7 @@
1
+ title: "Sample Post (New Structure)"
2
+ author: "Test Author"
3
+ published_at: "2025-01-01"
4
+ tags:
5
+ - test
6
+ - new-structure
7
+ description: "This is a test fixture in the new structure format"
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env ruby
2
+ # Unit tests for Ro::Asset with new structure support
3
+
4
+ require_relative '../test_helper'
5
+
6
+ class AssetTest < RoTestCase
7
+ def setup
8
+ @root = Ro::Root.new(new_structure_path)
9
+ @collection = Ro::Collection.new(new_structure_path.join('posts'))
10
+ @metadata_file = new_structure_path.join('posts', 'sample-post.yml')
11
+ @node = Ro::Node.new(@collection, @metadata_file)
12
+ end
13
+
14
+ # T017: Test Asset path resolution with assets/ subdirectory
15
+ def test_asset_path_with_assets_subdirectory
16
+ asset_path = new_structure_path / 'posts' / 'sample-post' / 'assets' / 'image.jpg'
17
+
18
+ # Both old and new structure use assets/ subdirectory
19
+ assert asset_path.to_s.include?('/assets/'), "Asset path should contain /assets/ subdirectory"
20
+ end
21
+
22
+ def test_asset_relative_path_calculation
23
+ asset_path = new_structure_path / 'posts' / 'sample-post' / 'assets' / 'image.jpg'
24
+ asset = Ro::Asset.new(asset_path)
25
+
26
+ # Relative path should be calculated correctly (relative to asset_dir which is node/assets/)
27
+ relative = asset.relative_path
28
+
29
+ assert_not_nil relative, "Relative path should not be nil"
30
+ # Relative path is relative to asset_dir (node/assets/), so just the filename
31
+ assert_equal 'image.jpg', relative.to_s, "Relative path should be image.jpg"
32
+ end
33
+
34
+ def test_nested_asset_path
35
+ # Test with nested subdirectory structure
36
+ asset_path = new_structure_path / 'posts' / 'nested-test' / 'assets' / 'subdirectory' / 'image.png'
37
+ asset = Ro::Asset.new(asset_path)
38
+
39
+ relative = asset.relative_path
40
+
41
+ # Should preserve subdirectory structure (relative to assets/)
42
+ assert_equal 'subdirectory/image.png', relative.to_s, "Nested asset should preserve directory structure"
43
+ end
44
+
45
+ def test_asset_url_generation
46
+ asset_path = new_structure_path / 'posts' / 'sample-post' / 'image.jpg'
47
+ asset = Ro::Asset.new(asset_path)
48
+
49
+ # Asset should be able to generate URL
50
+ # (Exact URL format depends on node.url_for implementation)
51
+ assert_not_nil asset, "Asset should be created successfully"
52
+ end
53
+
54
+ def test_asset_belongs_to_node
55
+ asset_paths = @node.asset_paths
56
+
57
+ assert asset_paths.any?, "Node should have asset paths"
58
+
59
+ # All asset paths should be within node directory
60
+ asset_paths.each do |path|
61
+ assert path.to_s.start_with?(@node.asset_dir.to_s), "Asset path should be within node directory"
62
+ end
63
+ end
64
+ end
65
+
66
+ # Run the tests
67
+ if __FILE__ == $0
68
+ test = AssetTest.new
69
+
70
+ puts "Running Asset unit tests..."
71
+
72
+ tests = [
73
+ :test_asset_path_with_assets_subdirectory,
74
+ :test_asset_relative_path_calculation,
75
+ :test_nested_asset_path,
76
+ :test_asset_url_generation,
77
+ :test_asset_belongs_to_node
78
+ ]
79
+
80
+ tests.each do |test_method|
81
+ begin
82
+ test.setup
83
+ test.send(test_method)
84
+ puts "✓ #{test_method}"
85
+ rescue => e
86
+ puts "✗ #{test_method}: #{e.message}"
87
+ puts " #{e.backtrace.first}"
88
+ end
89
+ end
90
+ end