familia 2.0.0.pre5 → 2.0.0.pre7

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 (151) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/claude-code-review.yml +57 -0
  3. data/.github/workflows/claude.yml +71 -0
  4. data/.gitignore +5 -1
  5. data/.rubocop.yml +3 -0
  6. data/CLAUDE.md +32 -10
  7. data/Gemfile +2 -2
  8. data/Gemfile.lock +4 -3
  9. data/docs/wiki/API-Reference.md +95 -18
  10. data/docs/wiki/Connection-Pooling-Guide.md +437 -0
  11. data/docs/wiki/Encrypted-Fields-Overview.md +40 -3
  12. data/docs/wiki/Expiration-Feature-Guide.md +596 -0
  13. data/docs/wiki/Feature-System-Guide.md +631 -0
  14. data/docs/wiki/Features-System-Developer-Guide.md +892 -0
  15. data/docs/wiki/Field-System-Guide.md +784 -0
  16. data/docs/wiki/Home.md +82 -15
  17. data/docs/wiki/Implementation-Guide.md +126 -33
  18. data/docs/wiki/Quantization-Feature-Guide.md +721 -0
  19. data/docs/wiki/Relationships-Guide.md +684 -0
  20. data/docs/wiki/Security-Model.md +65 -25
  21. data/docs/wiki/Transient-Fields-Guide.md +280 -0
  22. data/examples/bit_encoding_integration.rb +237 -0
  23. data/examples/redis_command_validation_example.rb +231 -0
  24. data/examples/relationships_basic.rb +273 -0
  25. data/lib/familia/base.rb +1 -1
  26. data/lib/familia/connection.rb +3 -3
  27. data/lib/familia/data_type/types/counter.rb +38 -0
  28. data/lib/familia/data_type/types/hashkey.rb +18 -0
  29. data/lib/familia/data_type/types/lock.rb +43 -0
  30. data/lib/familia/data_type/types/string.rb +9 -2
  31. data/lib/familia/data_type.rb +9 -6
  32. data/lib/familia/encryption/encrypted_data.rb +137 -0
  33. data/lib/familia/encryption/manager.rb +21 -4
  34. data/lib/familia/encryption/providers/aes_gcm_provider.rb +20 -0
  35. data/lib/familia/encryption/providers/xchacha20_poly1305_provider.rb +20 -0
  36. data/lib/familia/encryption.rb +1 -1
  37. data/lib/familia/errors.rb +17 -3
  38. data/lib/familia/features/encrypted_fields/concealed_string.rb +293 -0
  39. data/lib/familia/features/encrypted_fields/encrypted_field_type.rb +94 -26
  40. data/lib/familia/features/encrypted_fields.rb +413 -4
  41. data/lib/familia/features/expiration.rb +319 -33
  42. data/lib/familia/features/quantization.rb +385 -44
  43. data/lib/familia/features/relationships/cascading.rb +438 -0
  44. data/lib/familia/features/relationships/indexing.rb +370 -0
  45. data/lib/familia/features/relationships/membership.rb +503 -0
  46. data/lib/familia/features/relationships/permission_management.rb +264 -0
  47. data/lib/familia/features/relationships/querying.rb +620 -0
  48. data/lib/familia/features/relationships/redis_operations.rb +274 -0
  49. data/lib/familia/features/relationships/score_encoding.rb +442 -0
  50. data/lib/familia/features/relationships/tracking.rb +379 -0
  51. data/lib/familia/features/relationships.rb +466 -0
  52. data/lib/familia/features/safe_dump.rb +1 -1
  53. data/lib/familia/features/transient_fields/redacted_string.rb +1 -1
  54. data/lib/familia/features/transient_fields.rb +192 -10
  55. data/lib/familia/features.rb +2 -1
  56. data/lib/familia/field_type.rb +5 -2
  57. data/lib/familia/horreum/{connection.rb → core/connection.rb} +2 -8
  58. data/lib/familia/horreum/{database_commands.rb → core/database_commands.rb} +14 -3
  59. data/lib/familia/horreum/core/serialization.rb +535 -0
  60. data/lib/familia/horreum/{utils.rb → core/utils.rb} +0 -2
  61. data/lib/familia/horreum/core.rb +21 -0
  62. data/lib/familia/horreum/{settings.rb → shared/settings.rb} +0 -2
  63. data/lib/familia/horreum/{definition_methods.rb → subclass/definition.rb} +45 -29
  64. data/lib/familia/horreum/{management_methods.rb → subclass/management.rb} +9 -8
  65. data/lib/familia/horreum/{related_fields_management.rb → subclass/related_fields_management.rb} +15 -10
  66. data/lib/familia/horreum.rb +17 -17
  67. data/lib/familia/validation/command_recorder.rb +336 -0
  68. data/lib/familia/validation/expectations.rb +519 -0
  69. data/lib/familia/validation/test_helpers.rb +443 -0
  70. data/lib/familia/validation/validator.rb +412 -0
  71. data/lib/familia/validation.rb +140 -0
  72. data/lib/familia/version.rb +1 -1
  73. data/lib/familia.rb +1 -1
  74. data/try/core/create_method_try.rb +240 -0
  75. data/try/core/database_consistency_try.rb +299 -0
  76. data/try/core/errors_try.rb +25 -4
  77. data/try/core/familia_try.rb +1 -1
  78. data/try/core/persistence_operations_try.rb +297 -0
  79. data/try/data_types/counter_try.rb +93 -0
  80. data/try/data_types/lock_try.rb +133 -0
  81. data/try/debugging/debug_aad_process.rb +82 -0
  82. data/try/debugging/debug_concealed_internal.rb +59 -0
  83. data/try/debugging/debug_concealed_reveal.rb +61 -0
  84. data/try/debugging/debug_context_aad.rb +68 -0
  85. data/try/debugging/debug_context_simple.rb +80 -0
  86. data/try/debugging/debug_cross_context.rb +62 -0
  87. data/try/debugging/debug_database_load.rb +64 -0
  88. data/try/debugging/debug_encrypted_json_check.rb +53 -0
  89. data/try/debugging/debug_encrypted_json_step_by_step.rb +62 -0
  90. data/try/debugging/debug_exists_lifecycle.rb +54 -0
  91. data/try/debugging/debug_field_decrypt.rb +74 -0
  92. data/try/debugging/debug_fresh_cross_context.rb +73 -0
  93. data/try/debugging/debug_load_path.rb +66 -0
  94. data/try/debugging/debug_method_definition.rb +46 -0
  95. data/try/debugging/debug_method_resolution.rb +41 -0
  96. data/try/debugging/debug_minimal.rb +24 -0
  97. data/try/debugging/debug_provider.rb +68 -0
  98. data/try/debugging/debug_secure_behavior.rb +73 -0
  99. data/try/debugging/debug_string_class.rb +46 -0
  100. data/try/debugging/debug_test.rb +46 -0
  101. data/try/debugging/debug_test_design.rb +80 -0
  102. data/try/edge_cases/hash_symbolization_try.rb +1 -0
  103. data/try/edge_cases/reserved_keywords_try.rb +1 -0
  104. data/try/edge_cases/string_coercion_try.rb +2 -0
  105. data/try/encryption/encryption_core_try.rb +6 -4
  106. data/try/features/categorical_permissions_try.rb +515 -0
  107. data/try/features/encrypted_fields_core_try.rb +19 -11
  108. data/try/features/encrypted_fields_integration_try.rb +66 -70
  109. data/try/features/encrypted_fields_no_cache_security_try.rb +22 -8
  110. data/try/features/encrypted_fields_security_try.rb +151 -144
  111. data/try/features/encryption_fields/aad_protection_try.rb +108 -23
  112. data/try/features/encryption_fields/concealed_string_core_try.rb +253 -0
  113. data/try/features/encryption_fields/context_isolation_try.rb +30 -8
  114. data/try/features/encryption_fields/error_conditions_try.rb +6 -6
  115. data/try/features/encryption_fields/fresh_key_derivation_try.rb +20 -14
  116. data/try/features/encryption_fields/fresh_key_try.rb +27 -22
  117. data/try/features/encryption_fields/key_rotation_try.rb +16 -10
  118. data/try/features/encryption_fields/nonce_uniqueness_try.rb +15 -13
  119. data/try/features/encryption_fields/secure_by_default_behavior_try.rb +310 -0
  120. data/try/features/encryption_fields/thread_safety_try.rb +6 -6
  121. data/try/features/encryption_fields/universal_serialization_safety_try.rb +174 -0
  122. data/try/features/feature_dependencies_try.rb +3 -3
  123. data/try/features/relationships_edge_cases_try.rb +145 -0
  124. data/try/features/relationships_performance_minimal_try.rb +132 -0
  125. data/try/features/relationships_performance_simple_try.rb +155 -0
  126. data/try/features/relationships_performance_try.rb +420 -0
  127. data/try/features/relationships_performance_working_try.rb +144 -0
  128. data/try/features/relationships_try.rb +237 -0
  129. data/try/features/safe_dump_try.rb +3 -0
  130. data/try/features/transient_fields/redacted_string_try.rb +2 -0
  131. data/try/features/transient_fields/single_use_redacted_string_try.rb +2 -0
  132. data/try/features/transient_fields_core_try.rb +1 -1
  133. data/try/features/transient_fields_integration_try.rb +1 -1
  134. data/try/helpers/test_helpers.rb +26 -1
  135. data/try/horreum/base_try.rb +14 -8
  136. data/try/horreum/enhanced_conflict_handling_try.rb +3 -1
  137. data/try/horreum/initialization_try.rb +1 -1
  138. data/try/horreum/relations_try.rb +2 -2
  139. data/try/horreum/serialization_persistent_fields_try.rb +8 -8
  140. data/try/horreum/serialization_try.rb +39 -4
  141. data/try/models/customer_safe_dump_try.rb +1 -1
  142. data/try/models/customer_try.rb +1 -1
  143. data/try/validation/atomic_operations_try.rb.disabled +320 -0
  144. data/try/validation/command_validation_try.rb.disabled +207 -0
  145. data/try/validation/performance_validation_try.rb.disabled +324 -0
  146. data/try/validation/real_world_scenarios_try.rb.disabled +390 -0
  147. metadata +81 -12
  148. data/TEST_COVERAGE.md +0 -40
  149. data/lib/familia/features/relatable_objects.rb +0 -125
  150. data/lib/familia/horreum/serialization.rb +0 -473
  151. data/try/features/relatable_objects_try.rb +0 -220
@@ -43,7 +43,7 @@ Customer.find_by_id(ident).planid
43
43
  @customer.secrets_created.delete!
44
44
  @customer.secrets_created.increment
45
45
  @customer.secrets_created.value
46
- #=> '1'
46
+ #=> 1
47
47
 
48
48
  ## Customer can add custom domain via add method
49
49
  @customer.custom_domains.add(@now, 'example.org')
@@ -0,0 +1,320 @@
1
+ # Validation testing for atomic operations and transaction boundaries
2
+
3
+ require_relative '../helpers/test_helpers'
4
+ require_relative '../../lib/familia/validation'
5
+
6
+ extend Familia::Validation::TestHelpers
7
+
8
+ # Test models for atomic validation
9
+ class AtomicTestAccount < Familia::Horreum
10
+ identifier_field :account_id
11
+ field :account_id
12
+ field :balance
13
+ field :status
14
+
15
+ def transfer_to(target_account, amount)
16
+ # This should be atomic - but let's test both atomic and non-atomic versions
17
+ new_balance = balance.to_i - amount.to_i
18
+ target_new_balance = target_account.balance.to_i + amount.to_i
19
+
20
+ # Non-atomic version (should fail validation)
21
+ self.balance = new_balance.to_s
22
+ target_account.balance = target_new_balance.to_s
23
+ save
24
+ target_account.save
25
+ end
26
+
27
+ def atomic_transfer_to(target_account, amount)
28
+ # Proper atomic version using transaction
29
+ new_balance = balance.to_i - amount.to_i
30
+ target_new_balance = target_account.balance.to_i + amount.to_i
31
+
32
+ transaction do |conn|
33
+ conn.hset(dbkey, 'balance', new_balance.to_s)
34
+ conn.hset(target_account.dbkey, 'balance', target_new_balance.to_s)
35
+ end
36
+
37
+ # Update local state
38
+ self.balance = new_balance.to_s
39
+ target_account.balance = target_new_balance.to_s
40
+ end
41
+ end
42
+
43
+ class AtomicTestCounter < Familia::Horreum
44
+ identifier_field :name
45
+ field :name
46
+ field :value
47
+
48
+ def increment_by(amount)
49
+ transaction do |conn|
50
+ conn.hincrby(dbkey, 'value', amount)
51
+ end
52
+ self.value = (value.to_i + amount).to_s
53
+ end
54
+ end
55
+
56
+ extend Familia::Validation::TestHelpers
57
+ setup_validation_test
58
+
59
+ ## Clean up any existing test data
60
+ cleanup_keys = Familia.dbclient.keys("atomictestaccount:*")
61
+ cleanup_keys.concat(Familia.dbclient.keys("atomictestcounter:*"))
62
+ Familia.dbclient.del(*cleanup_keys) if cleanup_keys.any?
63
+ true
64
+ #=> true
65
+
66
+ ## Transaction expectations validate MULTI/EXEC boundaries
67
+ validator = Familia::Validation::Validator.new
68
+ result = validator.validate do |expect|
69
+ expect.transaction do |tx|
70
+ tx.hset("atomictestaccount:acc1:object", "balance", "900")
71
+ .hset("atomictestaccount:acc2:object", "balance", "1100")
72
+ end
73
+
74
+ # Execute atomic transfer
75
+ acc1 = AtomicTestAccount.new(account_id: "acc1", balance: "1000")
76
+ acc2 = AtomicTestAccount.new(account_id: "acc2", balance: "1000")
77
+ acc1.save
78
+ acc2.save
79
+
80
+ acc1.atomic_transfer_to(acc2, 100)
81
+ end
82
+ result.valid?
83
+ #=> true
84
+
85
+ ## Non-atomic operations should fail strict atomicity validation
86
+ validator = Familia::Validation::Validator.new(strict_atomicity: true)
87
+ result = validator.validate do |expect|
88
+ expect.transaction do |tx|
89
+ tx.hset("atomictestaccount:acc3:object", "balance", "800")
90
+ .hset("atomictestaccount:acc4:object", "balance", "1200")
91
+ end
92
+
93
+ # This will NOT use transaction, so should fail
94
+ acc3 = AtomicTestAccount.new(account_id: "acc3", balance: "1000")
95
+ acc4 = AtomicTestAccount.new(account_id: "acc4", balance: "1000")
96
+ acc3.save
97
+ acc4.save
98
+
99
+ acc3.transfer_to(acc4, 200) # Non-atomic version
100
+ end
101
+ result.valid?
102
+ #=> false
103
+
104
+ ## assert_atomic_operation helper works correctly
105
+ account_a = AtomicTestAccount.new(account_id: "acc5", balance: "2000")
106
+ account_b = AtomicTestAccount.new(account_id: "acc6", balance: "500")
107
+ account_a.save
108
+ account_b.save
109
+
110
+ assert_atomic_operation do |expect|
111
+ expect.transaction do |tx|
112
+ tx.hset("atomictestaccount:acc5:object", "balance", "1500")
113
+ .hset("atomictestaccount:acc6:object", "balance", "1000")
114
+ end
115
+
116
+ account_a.atomic_transfer_to(account_b, 500)
117
+ end
118
+ #=> true
119
+
120
+ ## assert_transaction_used helper works
121
+ result = assert_transaction_used do
122
+ counter = AtomicTestCounter.new(name: "test_counter", value: "0")
123
+ counter.save
124
+ counter.increment_by(5)
125
+ end
126
+ result
127
+ #=> true
128
+
129
+ ## assert_no_transaction_used helper works
130
+ result = assert_no_transaction_used do
131
+ simple_account = AtomicTestAccount.new(account_id: "simple", balance: "100")
132
+ simple_account.save # Just a save, no transaction needed
133
+ end
134
+ result
135
+ #=> true
136
+
137
+ ## Transaction validation detects missing MULTI command
138
+ commands = []
139
+ commands << Familia::Validation::CommandRecorder::RecordedCommand.new(
140
+ command: "HSET", args: ["key", "field", "value"],
141
+ result: "OK", timestamp: Time.now, duration_us: 100
142
+ )
143
+ commands << Familia::Validation::CommandRecorder::RecordedCommand.new(
144
+ command: "EXEC", args: [],
145
+ result: ["OK"], timestamp: Time.now, duration_us: 50
146
+ )
147
+
148
+ sequence = Familia::Validation::CommandRecorder::CommandSequence.new
149
+ commands.each { |cmd| sequence.add_command(cmd) }
150
+
151
+ atomicity_validator = Familia::Validation::AtomicityValidator.new(sequence)
152
+ result = atomicity_validator.validate
153
+ result.valid?
154
+ #=> false
155
+
156
+ ## Transaction validation detects missing EXEC command
157
+ commands = []
158
+ commands << Familia::Validation::CommandRecorder::RecordedCommand.new(
159
+ command: "MULTI", args: [],
160
+ result: "OK", timestamp: Time.now, duration_us: 100
161
+ )
162
+ commands << Familia::Validation::CommandRecorder::RecordedCommand.new(
163
+ command: "HSET", args: ["key", "field", "value"],
164
+ result: "QUEUED", timestamp: Time.now, duration_us: 50
165
+ )
166
+
167
+ sequence = Familia::Validation::CommandRecorder::CommandSequence.new
168
+ commands.each { |cmd| sequence.add_command(cmd) }
169
+
170
+ atomicity_validator = Familia::Validation::AtomicityValidator.new(sequence)
171
+ result = atomicity_validator.validate
172
+ result.valid?
173
+ #=> false
174
+
175
+ ## Proper transaction structure passes validation
176
+ commands = []
177
+ commands << Familia::Validation::CommandRecorder::RecordedCommand.new(
178
+ command: "MULTI", args: [],
179
+ result: "OK", timestamp: Time.now, duration_us: 100,
180
+ context: { transaction: false }
181
+ )
182
+ commands << Familia::Validation::CommandRecorder::RecordedCommand.new(
183
+ command: "HSET", args: ["key", "field", "value"],
184
+ result: "QUEUED", timestamp: Time.now, duration_us: 50,
185
+ context: { transaction: true }
186
+ )
187
+ commands << Familia::Validation::CommandRecorder::RecordedCommand.new(
188
+ command: "INCR", args: ["counter"],
189
+ result: "QUEUED", timestamp: Time.now, duration_us: 30,
190
+ context: { transaction: true }
191
+ )
192
+ commands << Familia::Validation::CommandRecorder::RecordedCommand.new(
193
+ command: "EXEC", args: [],
194
+ result: [1, 1], timestamp: Time.now, duration_us: 200,
195
+ context: { transaction: false }
196
+ )
197
+
198
+ sequence = Familia::Validation::CommandRecorder::CommandSequence.new
199
+ sequence.start_transaction
200
+ commands.each { |cmd| sequence.add_command(cmd) }
201
+ sequence.end_transaction
202
+
203
+ atomicity_validator = Familia::Validation::AtomicityValidator.new(sequence)
204
+ result = atomicity_validator.validate
205
+ result.valid?
206
+ #=> true
207
+
208
+ ## batch_update operations should be atomic
209
+ validator = Familia::Validation::Validator.new
210
+ result = validator.validate do |expect|
211
+ expect.transaction do |tx|
212
+ tx.hset("atomictestaccount:batch1:object", "balance", "1500")
213
+ .hset("atomictestaccount:batch1:object", "status", "active")
214
+ end
215
+
216
+ account = AtomicTestAccount.new(account_id: "batch1", balance: "1000", status: "pending")
217
+ account.save
218
+
219
+ # batch_update should use transaction internally
220
+ account.batch_update(balance: "1500", status: "active")
221
+ end
222
+ result.valid?
223
+ #=> true
224
+
225
+ ## Nested transaction detection works
226
+ commands = []
227
+ # Invalid nested transaction
228
+ commands << Familia::Validation::CommandRecorder::RecordedCommand.new(
229
+ command: "MULTI", args: [], result: "OK",
230
+ timestamp: Time.now, duration_us: 100
231
+ )
232
+ commands << Familia::Validation::CommandRecorder::RecordedCommand.new(
233
+ command: "MULTI", args: [], result: "ERR",
234
+ timestamp: Time.now, duration_us: 50
235
+ )
236
+
237
+ sequence = Familia::Validation::CommandRecorder::CommandSequence.new
238
+ commands.each { |cmd| sequence.add_command(cmd) }
239
+
240
+ atomicity_validator = Familia::Validation::AtomicityValidator.new(sequence)
241
+ result = atomicity_validator.validate
242
+ result.valid?
243
+ #=> false
244
+
245
+ ## Empty transaction detection
246
+ commands = []
247
+ commands << Familia::Validation::CommandRecorder::RecordedCommand.new(
248
+ command: "MULTI", args: [], result: "OK",
249
+ timestamp: Time.now, duration_us: 100
250
+ )
251
+ commands << Familia::Validation::CommandRecorder::RecordedCommand.new(
252
+ command: "EXEC", args: [], result: [],
253
+ timestamp: Time.now, duration_us: 50
254
+ )
255
+
256
+ sequence = Familia::Validation::CommandRecorder::CommandSequence.new
257
+ sequence.start_transaction
258
+ commands.each { |cmd| sequence.add_command(cmd) }
259
+ sequence.end_transaction
260
+
261
+ result = Familia::Validation::AtomicityValidator.new(sequence).validate
262
+ # Should be valid but with warning about empty transaction
263
+ result.valid? && result.warning_messages.any?
264
+ #=> true
265
+
266
+ ## Real Familia transaction usage validates correctly
267
+ validator = Familia::Validation::Validator.new
268
+ result = validator.validate do |expect|
269
+ expect.transaction do |tx|
270
+ tx.hset("atomictestcounter:familia_tx:object", "value", "10")
271
+ end
272
+
273
+ counter = AtomicTestCounter.new(name: "familia_tx", value: "5")
274
+ counter.save
275
+
276
+ # Use Familia.transaction directly
277
+ Familia.transaction do |conn|
278
+ conn.hset(counter.dbkey, "value", "10")
279
+ end
280
+ end
281
+ result.valid?
282
+ #=> true
283
+
284
+ ## Multiple transactions in sequence validate correctly
285
+ validator = Familia::Validation::Validator.new
286
+ result = validator.validate do |expect|
287
+ expect.transaction do |tx|
288
+ tx.hset("atomictestcounter:multi_tx_1:object", "value", "5")
289
+ end
290
+
291
+ expect.transaction do |tx|
292
+ tx.hset("atomictestcounter:multi_tx_2:object", "value", "10")
293
+ end
294
+
295
+ counter1 = AtomicTestCounter.new(name: "multi_tx_1", value: "0")
296
+ counter2 = AtomicTestCounter.new(name: "multi_tx_2", value: "0")
297
+ counter1.save
298
+ counter2.save
299
+
300
+ # Two separate transactions
301
+ Familia.transaction do |conn|
302
+ conn.hset(counter1.dbkey, "value", "5")
303
+ end
304
+
305
+ Familia.transaction do |conn|
306
+ conn.hset(counter2.dbkey, "value", "10")
307
+ end
308
+ end
309
+ result.valid?
310
+ #=> true
311
+
312
+ ## Cleanup test environment
313
+ teardown_validation_test
314
+
315
+ ## Clean up test data
316
+ cleanup_keys = Familia.dbclient.keys("atomictestaccount:*")
317
+ cleanup_keys.concat(Familia.dbclient.keys("atomictestcounter:*"))
318
+ Familia.dbclient.del(*cleanup_keys) if cleanup_keys.any?
319
+ true
320
+ #=> true
@@ -0,0 +1,207 @@
1
+ # Validation testing for Redis command recording and expectation matching
2
+
3
+ require_relative '../helpers/test_helpers'
4
+ require_relative '../../lib/familia/validation'
5
+
6
+ # Test class for validation testing
7
+ class ValidationTestModel < Familia::Horreum
8
+ identifier_field :id
9
+ field :id
10
+ field :name
11
+ field :email
12
+ field :active
13
+ end
14
+
15
+ extend Familia::Validation::TestHelpers
16
+
17
+ # Initialize validation framework
18
+ setup_validation_test
19
+
20
+ ## Command recorder captures basic Redis commands
21
+ Familia::Validation::CommandRecorder.start_recording
22
+ ValidationTestModel.new(id: "test1", name: "John").save
23
+ commands = Familia::Validation::CommandRecorder.stop_recording
24
+ commands.command_count > 0
25
+ #=> true
26
+
27
+ ## Command recorder captures command details
28
+ Familia::Validation::CommandRecorder.start_recording
29
+ Familia.dbclient.hset("test:key", "field", "value")
30
+ commands = Familia::Validation::CommandRecorder.stop_recording
31
+ first_command = commands.commands.first
32
+ first_command ? [first_command.command, first_command.args] : "No commands recorded"
33
+ #=> ["HSET", ["test:key", "field", "value"]]
34
+
35
+ ## Expectation DSL allows chaining commands
36
+ expectations = Familia::Validation::CommandExpectations.new
37
+ expectations.hset("user:123", "name", "John")
38
+ .hset("user:123", "email", "john@example.com")
39
+ .incr("counter")
40
+ expectations.expected_commands.length
41
+ #=> 3
42
+
43
+ ## Basic command validation passes with exact match
44
+ validator = Familia::Validation::Validator.new
45
+ result = validator.validate do |expect|
46
+ expect.hset("validationtestmodel:test2:object", "name", "Jane")
47
+ .hset("validationtestmodel:test2:object", "id", "test2")
48
+
49
+ # Execute the actual operation
50
+ model = ValidationTestModel.new(id: "test2", name: "Jane")
51
+ model.save
52
+ end
53
+ result.valid?
54
+ #=> true
55
+
56
+ ## Command validation fails with wrong command
57
+ validator = Familia::Validation::Validator.new
58
+ result = validator.validate do |expect|
59
+ expect.get("wrong:key") # This won't match actual operation
60
+
61
+ ValidationTestModel.new(id: "test3", name: "Bob").save
62
+ end
63
+ result.valid?
64
+ #=> false
65
+
66
+ ## Command validation provides detailed error messages
67
+ validator = Familia::Validation::Validator.new
68
+ result = validator.validate do |expect|
69
+ expect.hset("wrong:key", "field", "value")
70
+
71
+ ValidationTestModel.new(id: "test4").save
72
+ end
73
+ result.error_messages.length > 0
74
+ #=> true
75
+
76
+ ## Pattern matching works for flexible validation
77
+ validator = Familia::Validation::Validator.new
78
+ result = validator.validate do |expect|
79
+ expect.match_pattern(/^HSET validationtestmodel:test5:object/)
80
+
81
+ ValidationTestModel.new(id: "test5", name: "Alice").save
82
+ end
83
+ result.valid?
84
+ #=> true
85
+
86
+ ## Any value matchers work correctly
87
+ validator = Familia::Validation::Validator.new
88
+ result = validator.validate do |expect|
89
+ expect.hset("validationtestmodel:test6:object", "name", any_string)
90
+ .hset("validationtestmodel:test6:object", "id", "test6")
91
+
92
+ ValidationTestModel.new(id: "test6", name: "Charlie").save
93
+ end
94
+ result.valid?
95
+ #=> true
96
+
97
+ ## Test helper assert_redis_commands works
98
+ model = ValidationTestModel.new(id: "test7", name: "Dave")
99
+ assert_redis_commands do |expect|
100
+ expect.hset("validationtestmodel:test7:object", "name", "Dave")
101
+ .hset("validationtestmodel:test7:object", "id", "test7")
102
+
103
+ model.save
104
+ end
105
+ #=> true
106
+
107
+ ## Test helper assert_command_count works
108
+ result = assert_command_count(2) do
109
+ Familia.dbclient.hset("test:count", "field1", "value1")
110
+ Familia.dbclient.hset("test:count", "field2", "value2")
111
+ end
112
+ result
113
+ #=> true
114
+
115
+ ## Test helper assert_no_redis_commands works
116
+ result = assert_no_redis_commands do
117
+ # Just create object, don't save
118
+ ValidationTestModel.new(id: "test8", name: "Eve")
119
+ end
120
+ result
121
+ #=> true
122
+
123
+ ## Flexible order validation works
124
+ validator = Familia::Validation::Validator.new
125
+ result = validator.validate do |expect|
126
+ expect.strict_order(false)
127
+ .hset("validationtestmodel:test9:object", "name", any_string)
128
+ .hset("validationtestmodel:test9:object", "id", "test9")
129
+
130
+ # Save in any order should work
131
+ model = ValidationTestModel.new(id: "test9", name: "Frank")
132
+ model.save
133
+ end
134
+ result.valid?
135
+ #=> true
136
+
137
+ ## Command sequence provides useful metadata
138
+ Familia::Validation::CommandRecorder.start_recording
139
+ ValidationTestModel.new(id: "test10", name: "Grace").save
140
+ commands = Familia::Validation::CommandRecorder.stop_recording
141
+ summary = {
142
+ command_count: commands.command_count,
143
+ has_commands: commands.commands.any?,
144
+ first_command_type: commands.commands.first&.command_type
145
+ }
146
+ summary[:command_count] > 0 && summary[:has_commands]
147
+ #=> true
148
+
149
+ ## Performance tracking captures timing information
150
+ validator = Familia::Validation::Validator.new(performance_tracking: true)
151
+ result = validator.validate do |expect|
152
+ expect.match_pattern(/HSET/)
153
+
154
+ ValidationTestModel.new(id: "test11", name: "Henry").save
155
+ end
156
+ result.respond_to?(:performance_metrics) && result.performance_metrics[:total_commands] > 0
157
+ #=> true
158
+
159
+ ## Validation result provides comprehensive summary
160
+ validator = Familia::Validation::Validator.new
161
+ result = validator.validate do |expect|
162
+ expect.hset("validationtestmodel:test12:object", "name", "Iris")
163
+
164
+ ValidationTestModel.new(id: "test12", name: "Iris").save
165
+ end
166
+ summary = result.summary
167
+ summary[:valid] == true && summary[:expected_commands] == 1
168
+ #=> true
169
+
170
+ ## Complex validation with multiple operations
171
+ class ComplexTestModel < Familia::Horreum
172
+ identifier_field :id
173
+ field :id, :name, :email
174
+ list :tags
175
+ set :categories
176
+ end
177
+
178
+ validator = Familia::Validation::Validator.new
179
+ result = validator.validate do |expect|
180
+ expect.hset("complextestmodel:complex1:object", "name", "Complex")
181
+ .hset("complextestmodel:complex1:object", "id", "complex1")
182
+ .lpush("complextestmodel:complex1:tags", "tag1")
183
+ .sadd("complextestmodel:complex1:categories", "cat1")
184
+
185
+ model = ComplexTestModel.new(id: "complex1", name: "Complex")
186
+ model.save
187
+ model.tags.unshift("tag1")
188
+ model.categories.add("cat1")
189
+ end
190
+ result.valid?
191
+ #=> true
192
+
193
+ ## Cleanup test environment
194
+ begin
195
+ teardown_validation_test
196
+ rescue => e
197
+ # Gracefully handle teardown issues during test development
198
+ puts "Teardown warning: #{e.message}"
199
+ end
200
+
201
+ ## Clean up test data
202
+ test_keys = Familia.dbclient.keys("validationtestmodel:*")
203
+ test_keys.concat(Familia.dbclient.keys("complextestmodel:*"))
204
+ test_keys.concat(Familia.dbclient.keys("test:*"))
205
+ Familia.dbclient.del(*test_keys) if test_keys.any?
206
+ true
207
+ #=> true