prompt_manager 0.5.7 → 0.5.8

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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +4 -0
  3. data/COMMITS.md +196 -0
  4. data/README.md +485 -203
  5. data/docs/.keep +0 -0
  6. data/docs/advanced/custom-keywords.md +421 -0
  7. data/docs/advanced/dynamic-directives.md +535 -0
  8. data/docs/advanced/performance.md +612 -0
  9. data/docs/advanced/search-integration.md +635 -0
  10. data/docs/api/configuration.md +355 -0
  11. data/docs/api/directive-processor.md +431 -0
  12. data/docs/api/prompt-class.md +354 -0
  13. data/docs/api/storage-adapters.md +462 -0
  14. data/docs/assets/favicon.ico +1 -0
  15. data/docs/assets/logo.svg +24 -0
  16. data/docs/core-features/comments.md +48 -0
  17. data/docs/core-features/directive-processing.md +38 -0
  18. data/docs/core-features/erb-integration.md +68 -0
  19. data/docs/core-features/error-handling.md +197 -0
  20. data/docs/core-features/parameter-history.md +76 -0
  21. data/docs/core-features/parameterized-prompts.md +500 -0
  22. data/docs/core-features/shell-integration.md +79 -0
  23. data/docs/development/architecture.md +544 -0
  24. data/docs/development/contributing.md +425 -0
  25. data/docs/development/roadmap.md +234 -0
  26. data/docs/development/testing.md +822 -0
  27. data/docs/examples/advanced.md +523 -0
  28. data/docs/examples/basic.md +688 -0
  29. data/docs/examples/real-world.md +776 -0
  30. data/docs/examples.md +337 -0
  31. data/docs/getting-started/basic-concepts.md +318 -0
  32. data/docs/getting-started/installation.md +97 -0
  33. data/docs/getting-started/quick-start.md +256 -0
  34. data/docs/index.md +230 -0
  35. data/docs/migration/v0.9.0.md +459 -0
  36. data/docs/migration/v1.0.0.md +591 -0
  37. data/docs/storage/activerecord-adapter.md +348 -0
  38. data/docs/storage/custom-adapters.md +176 -0
  39. data/docs/storage/filesystem-adapter.md +236 -0
  40. data/docs/storage/overview.md +427 -0
  41. data/examples/advanced_integrations.rb +52 -0
  42. data/examples/prompts_dir/advanced_demo.txt +79 -0
  43. data/examples/prompts_dir/directive_example.json +1 -0
  44. data/examples/prompts_dir/directive_example.txt +8 -0
  45. data/examples/prompts_dir/todo.json +1 -1
  46. data/improvement_plan.md +996 -0
  47. data/lib/prompt_manager/storage/file_system_adapter.rb +8 -2
  48. data/lib/prompt_manager/version.rb +1 -1
  49. data/mkdocs.yml +146 -0
  50. data/prompt_manager_logo.png +0 -0
  51. metadata +46 -3
  52. data/LICENSE.txt +0 -21
@@ -0,0 +1,822 @@
1
+ # Testing Guide
2
+
3
+ This guide covers testing strategies, patterns, and best practices for PromptManager development.
4
+
5
+ ## Test Framework Setup
6
+
7
+ ### RSpec Configuration
8
+
9
+ ```ruby
10
+ # spec/spec_helper.rb
11
+ require 'simplecov'
12
+ SimpleCov.start do
13
+ add_filter '/spec/'
14
+ minimum_coverage 90
15
+ end
16
+
17
+ require 'prompt_manager'
18
+ require 'rspec'
19
+ require 'webmock/rspec'
20
+
21
+ RSpec.configure do |config|
22
+ # Use expect syntax only
23
+ config.expect_with :rspec do |expectations|
24
+ expectations.syntax = :expect
25
+ end
26
+
27
+ # Mock framework
28
+ config.mock_with :rspec do |mocks|
29
+ mocks.verify_partial_doubles = true
30
+ end
31
+
32
+ # Shared examples and helpers
33
+ config.shared_context_metadata_behavior = :apply_to_host_groups
34
+
35
+ # Test isolation
36
+ config.before(:each) do
37
+ # Reset PromptManager configuration
38
+ PromptManager.reset_configuration!
39
+
40
+ # Clear any cached data
41
+ if defined?(Rails) && Rails.cache
42
+ Rails.cache.clear
43
+ end
44
+ end
45
+
46
+ config.after(:each) do
47
+ # Clean up test files
48
+ FileUtils.rm_rf('tmp/test_prompts') if Dir.exist?('tmp/test_prompts')
49
+ end
50
+ end
51
+
52
+ # Load support files
53
+ Dir[File.join(__dir__, 'support', '**', '*.rb')].each { |f| require f }
54
+ ```
55
+
56
+ ### Test Support Files
57
+
58
+ ```ruby
59
+ # spec/support/shared_examples/storage_adapter.rb
60
+ RSpec.shared_examples 'a storage adapter' do
61
+ let(:prompt_id) { 'test_prompt' }
62
+ let(:content) { 'Hello [NAME]!' }
63
+ let(:updated_content) { 'Updated: [NAME]!' }
64
+
65
+ describe 'required interface' do
66
+ it 'implements all required methods' do
67
+ expect(adapter).to respond_to(:read)
68
+ expect(adapter).to respond_to(:write)
69
+ expect(adapter).to respond_to(:exist?)
70
+ expect(adapter).to respond_to(:delete)
71
+ expect(adapter).to respond_to(:list)
72
+ end
73
+ end
74
+
75
+ describe '#write and #read' do
76
+ it 'stores and retrieves content' do
77
+ expect(adapter.write(prompt_id, content)).to be true
78
+ expect(adapter.read(prompt_id)).to eq content
79
+ end
80
+
81
+ it 'overwrites existing content' do
82
+ adapter.write(prompt_id, content)
83
+ adapter.write(prompt_id, updated_content)
84
+ expect(adapter.read(prompt_id)).to eq updated_content
85
+ end
86
+
87
+ it 'raises PromptNotFoundError for non-existent prompts' do
88
+ expect {
89
+ adapter.read('non_existent')
90
+ }.to raise_error(PromptManager::PromptNotFoundError)
91
+ end
92
+ end
93
+
94
+ describe '#exist?' do
95
+ it 'returns false for non-existent prompts' do
96
+ expect(adapter.exist?('non_existent')).to be false
97
+ end
98
+
99
+ it 'returns true for existing prompts' do
100
+ adapter.write(prompt_id, content)
101
+ expect(adapter.exist?(prompt_id)).to be true
102
+ end
103
+ end
104
+
105
+ describe '#delete' do
106
+ context 'when prompt exists' do
107
+ before { adapter.write(prompt_id, content) }
108
+
109
+ it 'removes the prompt' do
110
+ expect(adapter.delete(prompt_id)).to be true
111
+ expect(adapter.exist?(prompt_id)).to be false
112
+ end
113
+ end
114
+
115
+ context 'when prompt does not exist' do
116
+ it 'returns false' do
117
+ expect(adapter.delete('non_existent')).to be false
118
+ end
119
+ end
120
+ end
121
+
122
+ describe '#list' do
123
+ it 'returns empty array when no prompts exist' do
124
+ expect(adapter.list).to eq []
125
+ end
126
+
127
+ it 'returns all prompt IDs' do
128
+ adapter.write('prompt1', 'content1')
129
+ adapter.write('prompt2', 'content2')
130
+
131
+ expect(adapter.list).to contain_exactly('prompt1', 'prompt2')
132
+ end
133
+ end
134
+ end
135
+
136
+ # spec/support/helpers/prompt_helpers.rb
137
+ module PromptHelpers
138
+ def create_test_prompt(id, content, storage: nil)
139
+ storage ||= test_storage
140
+ storage.write(id, content)
141
+
142
+ PromptManager::Prompt.new(id: id, storage: storage)
143
+ end
144
+
145
+ def test_storage
146
+ @test_storage ||= PromptManager::Storage::FileSystemAdapter.new(
147
+ prompts_dir: 'tmp/test_prompts'
148
+ )
149
+ end
150
+
151
+ def create_test_storage_with_prompts(prompts = {})
152
+ storage = test_storage
153
+ prompts.each { |id, content| storage.write(id, content) }
154
+ storage
155
+ end
156
+ end
157
+
158
+ RSpec.configure do |config|
159
+ config.include PromptHelpers
160
+ end
161
+ ```
162
+
163
+ ## Unit Testing
164
+
165
+ ### Testing the Prompt Class
166
+
167
+ ```ruby
168
+ # spec/prompt_manager/prompt_spec.rb
169
+ RSpec.describe PromptManager::Prompt do
170
+ let(:prompt_id) { 'test_prompt' }
171
+ let(:storage) { instance_double(PromptManager::Storage::Base) }
172
+ let(:prompt) { described_class.new(id: prompt_id, storage: storage) }
173
+
174
+ describe '#initialize' do
175
+ it 'requires an id parameter' do
176
+ expect {
177
+ described_class.new
178
+ }.to raise_error(ArgumentError)
179
+ end
180
+
181
+ it 'accepts optional parameters' do
182
+ prompt = described_class.new(
183
+ id: 'test',
184
+ erb_flag: true,
185
+ envar_flag: true
186
+ )
187
+
188
+ expect(prompt.id).to eq 'test'
189
+ expect(prompt.erb_flag).to be true
190
+ expect(prompt.envar_flag).to be true
191
+ end
192
+ end
193
+
194
+ describe '#render' do
195
+ context 'with simple parameter substitution' do
196
+ let(:content) { 'Hello [NAME]!' }
197
+
198
+ before do
199
+ allow(storage).to receive(:read).with(prompt_id).and_return(content)
200
+ end
201
+
202
+ it 'substitutes parameters' do
203
+ result = prompt.render(name: 'World')
204
+ expect(result).to eq 'Hello World!'
205
+ end
206
+
207
+ it 'handles missing parameters' do
208
+ expect {
209
+ prompt.render
210
+ }.to raise_error(PromptManager::MissingParametersError) do |error|
211
+ expect(error.missing_parameters).to contain_exactly('NAME')
212
+ end
213
+ end
214
+
215
+ it 'preserves case in parameter names' do
216
+ content = 'Hello [name] and [NAME]!'
217
+ allow(storage).to receive(:read).with(prompt_id).and_return(content)
218
+
219
+ result = prompt.render(name: 'Alice', NAME: 'BOB')
220
+ expect(result).to eq 'Hello Alice and BOB!'
221
+ end
222
+ end
223
+
224
+ context 'with nested parameters' do
225
+ let(:content) { 'User: [USER.NAME] ([USER.EMAIL])' }
226
+
227
+ before do
228
+ allow(storage).to receive(:read).with(prompt_id).and_return(content)
229
+ end
230
+
231
+ it 'handles nested hash parameters' do
232
+ result = prompt.render(
233
+ user: {
234
+ name: 'John Doe',
235
+ email: 'john@example.com'
236
+ }
237
+ )
238
+
239
+ expect(result).to eq 'User: John Doe (john@example.com)'
240
+ end
241
+
242
+ it 'handles missing nested parameters' do
243
+ expect {
244
+ prompt.render(user: { name: 'John' })
245
+ }.to raise_error(PromptManager::MissingParametersError) do |error|
246
+ expect(error.missing_parameters).to include('USER.EMAIL')
247
+ end
248
+ end
249
+ end
250
+
251
+ context 'with array parameters' do
252
+ let(:content) { 'Items: [ITEMS]' }
253
+
254
+ before do
255
+ allow(storage).to receive(:read).with(prompt_id).and_return(content)
256
+ end
257
+
258
+ it 'joins array elements with commas' do
259
+ result = prompt.render(items: ['Apple', 'Banana', 'Cherry'])
260
+ expect(result).to eq 'Items: Apple, Banana, Cherry'
261
+ end
262
+
263
+ it 'handles empty arrays' do
264
+ result = prompt.render(items: [])
265
+ expect(result).to eq 'Items: '
266
+ end
267
+ end
268
+
269
+ context 'with ERB processing' do
270
+ let(:prompt) { described_class.new(id: prompt_id, storage: storage, erb_flag: true) }
271
+ let(:content) { 'Today is <%= Date.today.strftime("%B %d, %Y") %>' }
272
+
273
+ before do
274
+ allow(storage).to receive(:read).with(prompt_id).and_return(content)
275
+ allow(Date).to receive(:today).and_return(Date.new(2024, 1, 15))
276
+ end
277
+
278
+ it 'processes ERB templates' do
279
+ result = prompt.render
280
+ expect(result).to eq 'Today is January 15, 2024'
281
+ end
282
+ end
283
+ end
284
+
285
+ describe '#parameters' do
286
+ before do
287
+ allow(storage).to receive(:read)
288
+ .with(prompt_id)
289
+ .and_return('Hello [NAME], your order [ORDER_ID] is ready!')
290
+ end
291
+
292
+ it 'extracts parameter names from content' do
293
+ expect(prompt.parameters).to contain_exactly('NAME', 'ORDER_ID')
294
+ end
295
+ end
296
+
297
+ describe '#content' do
298
+ let(:expected_content) { 'Raw prompt content' }
299
+
300
+ before do
301
+ allow(storage).to receive(:read).with(prompt_id).and_return(expected_content)
302
+ end
303
+
304
+ it 'returns raw content from storage' do
305
+ expect(prompt.content).to eq expected_content
306
+ end
307
+ end
308
+
309
+ describe 'error handling' do
310
+ it 'raises PromptNotFoundError when prompt does not exist' do
311
+ allow(storage).to receive(:read)
312
+ .with(prompt_id)
313
+ .and_raise(PromptManager::PromptNotFoundError.new("Prompt not found"))
314
+
315
+ expect {
316
+ prompt.render
317
+ }.to raise_error(PromptManager::PromptNotFoundError)
318
+ end
319
+ end
320
+ end
321
+ ```
322
+
323
+ ### Testing Storage Adapters
324
+
325
+ ```ruby
326
+ # spec/prompt_manager/storage/file_system_adapter_spec.rb
327
+ RSpec.describe PromptManager::Storage::FileSystemAdapter do
328
+ let(:test_dir) { 'tmp/test_prompts' }
329
+ let(:adapter) { described_class.new(prompts_dir: test_dir) }
330
+
331
+ before do
332
+ FileUtils.mkdir_p(test_dir)
333
+ end
334
+
335
+ after do
336
+ FileUtils.rm_rf(test_dir)
337
+ end
338
+
339
+ include_examples 'a storage adapter'
340
+
341
+ describe 'file system specific behavior' do
342
+ describe '#initialize' do
343
+ it 'creates directory if it does not exist' do
344
+ new_dir = 'tmp/new_prompts'
345
+ expect(Dir.exist?(new_dir)).to be false
346
+
347
+ described_class.new(prompts_dir: new_dir)
348
+ expect(Dir.exist?(new_dir)).to be true
349
+
350
+ FileUtils.rm_rf(new_dir)
351
+ end
352
+
353
+ it 'accepts multiple directories' do
354
+ dirs = ['tmp/prompts1', 'tmp/prompts2']
355
+ adapter = described_class.new(prompts_dir: dirs)
356
+
357
+ dirs.each do |dir|
358
+ expect(Dir.exist?(dir)).to be true
359
+ FileUtils.rm_rf(dir)
360
+ end
361
+ end
362
+ end
363
+
364
+ describe 'file extensions' do
365
+ it 'finds .txt files' do
366
+ File.write(File.join(test_dir, 'test.txt'), 'content')
367
+ expect(adapter.exist?('test')).to be true
368
+ end
369
+
370
+ it 'finds .md files' do
371
+ File.write(File.join(test_dir, 'test.md'), 'content')
372
+ expect(adapter.exist?('test')).to be true
373
+ end
374
+
375
+ it 'prioritizes .txt over .md' do
376
+ File.write(File.join(test_dir, 'test.txt'), 'txt content')
377
+ File.write(File.join(test_dir, 'test.md'), 'md content')
378
+
379
+ expect(adapter.read('test')).to eq 'txt content'
380
+ end
381
+ end
382
+
383
+ describe 'subdirectories' do
384
+ it 'handles nested prompt IDs' do
385
+ subdir = File.join(test_dir, 'emails')
386
+ FileUtils.mkdir_p(subdir)
387
+ File.write(File.join(subdir, 'welcome.txt'), 'Welcome!')
388
+
389
+ expect(adapter.read('emails/welcome')).to eq 'Welcome!'
390
+ end
391
+ end
392
+ end
393
+ end
394
+
395
+ # spec/prompt_manager/storage/active_record_adapter_spec.rb
396
+ RSpec.describe PromptManager::Storage::ActiveRecordAdapter do
397
+ # Mock ActiveRecord model
398
+ let(:model_class) do
399
+ Class.new do
400
+ def self.name
401
+ 'TestPrompt'
402
+ end
403
+
404
+ attr_accessor :prompt_id, :content
405
+
406
+ def initialize(attributes = {})
407
+ attributes.each { |key, value| send("#{key}=", value) }
408
+ end
409
+
410
+ def save!
411
+ # Mock save
412
+ end
413
+
414
+ def destroy!
415
+ # Mock destroy
416
+ end
417
+
418
+ # Mock class methods
419
+ def self.find_by(conditions)
420
+ # Override in tests
421
+ end
422
+
423
+ def self.where(conditions)
424
+ # Override in tests
425
+ end
426
+
427
+ def self.pluck(*columns)
428
+ # Override in tests
429
+ end
430
+ end
431
+ end
432
+
433
+ let(:adapter) { described_class.new(model_class: model_class) }
434
+
435
+ include_examples 'a storage adapter' do
436
+ # Setup mock expectations for shared examples
437
+ before do
438
+ @records = {}
439
+
440
+ allow(model_class).to receive(:find_by) do |conditions|
441
+ id = conditions[:prompt_id]
442
+ record_data = @records[id]
443
+ record_data ? model_class.new(record_data) : nil
444
+ end
445
+
446
+ allow(model_class).to receive(:create!) do |attributes|
447
+ @records[attributes[:prompt_id]] = attributes
448
+ model_class.new(attributes)
449
+ end
450
+
451
+ allow_any_instance_of(model_class).to receive(:update!) do |instance, attributes|
452
+ @records[instance.prompt_id].merge!(attributes)
453
+ end
454
+
455
+ allow_any_instance_of(model_class).to receive(:destroy!) do |instance|
456
+ @records.delete(instance.prompt_id)
457
+ end
458
+
459
+ allow(model_class).to receive(:pluck) do |*columns|
460
+ @records.values.map { |record| columns.map { |col| record[col] } }
461
+ end
462
+ end
463
+ end
464
+ end
465
+ ```
466
+
467
+ ## Integration Testing
468
+
469
+ ### Full Stack Integration Tests
470
+
471
+ ```ruby
472
+ # spec/integration/prompt_rendering_spec.rb
473
+ RSpec.describe 'Prompt Rendering Integration' do
474
+ let(:test_dir) { 'tmp/integration_test' }
475
+ let(:storage) { PromptManager::Storage::FileSystemAdapter.new(prompts_dir: test_dir) }
476
+
477
+ before do
478
+ FileUtils.mkdir_p(test_dir)
479
+ FileUtils.mkdir_p(File.join(test_dir, 'common'))
480
+
481
+ # Create test prompts
482
+ File.write(
483
+ File.join(test_dir, 'common', 'header.txt'),
484
+ 'Company: [COMPANY_NAME]'
485
+ )
486
+
487
+ File.write(
488
+ File.join(test_dir, 'email_template.txt'),
489
+ "//include common/header.txt\n\nDear [CUSTOMER_NAME],\nYour order [ORDER_ID] is ready!"
490
+ )
491
+
492
+ File.write(
493
+ File.join(test_dir, 'erb_template.txt'),
494
+ "<%= erb_flag = true %>\nGenerated at: <%= Time.current.strftime('%Y-%m-%d') %>\nHello [NAME]!"
495
+ )
496
+
497
+ PromptManager.configure do |config|
498
+ config.storage = storage
499
+ end
500
+ end
501
+
502
+ after do
503
+ FileUtils.rm_rf(test_dir)
504
+ end
505
+
506
+ describe 'directive processing' do
507
+ it 'processes includes and parameter substitution' do
508
+ prompt = PromptManager::Prompt.new(id: 'email_template')
509
+
510
+ result = prompt.render(
511
+ company_name: 'Acme Corp',
512
+ customer_name: 'John Doe',
513
+ order_id: 'ORD-123'
514
+ )
515
+
516
+ expect(result).to eq "Company: Acme Corp\n\nDear John Doe,\nYour order ORD-123 is ready!"
517
+ end
518
+ end
519
+
520
+ describe 'ERB processing' do
521
+ it 'processes ERB templates with parameters' do
522
+ prompt = PromptManager::Prompt.new(id: 'erb_template', erb_flag: true)
523
+
524
+ # Mock Time.current for consistent testing
525
+ allow(Time).to receive(:current).and_return(Time.parse('2024-01-15 10:00:00'))
526
+
527
+ result = prompt.render(name: 'Alice')
528
+
529
+ expect(result).to eq "Generated at: 2024-01-15\nHello Alice!"
530
+ end
531
+ end
532
+
533
+ describe 'error scenarios' do
534
+ it 'handles missing includes gracefully' do
535
+ File.write(
536
+ File.join(test_dir, 'broken_template.txt'),
537
+ "//include non_existent.txt\nContent"
538
+ )
539
+
540
+ prompt = PromptManager::Prompt.new(id: 'broken_template')
541
+
542
+ expect {
543
+ prompt.render
544
+ }.to raise_error(PromptManager::DirectiveProcessingError)
545
+ end
546
+ end
547
+ end
548
+ ```
549
+
550
+ ## Performance Testing
551
+
552
+ ### Benchmark Tests
553
+
554
+ ```ruby
555
+ # spec/performance/rendering_performance_spec.rb
556
+ require 'benchmark/ips'
557
+
558
+ RSpec.describe 'Rendering Performance' do
559
+ let(:storage) { create_test_storage_with_prompts(test_prompts) }
560
+
561
+ let(:test_prompts) do
562
+ {
563
+ 'simple' => 'Hello [NAME]!',
564
+ 'complex' => (1..100).map { |i| "Line #{i}: [PARAM_#{i}]" }.join("\n"),
565
+ 'with_include' => "//include simple\nAdditional content: [VALUE]"
566
+ }
567
+ end
568
+
569
+ let(:simple_params) { { name: 'John' } }
570
+ let(:complex_params) do
571
+ (1..100).each_with_object({}) { |i, hash| hash["param_#{i}".to_sym] = "value_#{i}" }
572
+ end
573
+
574
+ describe 'simple prompt rendering' do
575
+ it 'renders efficiently' do
576
+ prompt = PromptManager::Prompt.new(id: 'simple', storage: storage)
577
+
578
+ expect {
579
+ prompt.render(simple_params)
580
+ }.to perform_under(0.001).sec
581
+ end
582
+ end
583
+
584
+ describe 'complex prompt rendering' do
585
+ it 'handles many parameters efficiently' do
586
+ prompt = PromptManager::Prompt.new(id: 'complex', storage: storage)
587
+
588
+ expect {
589
+ prompt.render(complex_params)
590
+ }.to perform_under(0.01).sec
591
+ end
592
+ end
593
+
594
+ describe 'bulk rendering performance' do
595
+ it 'processes multiple prompts efficiently' do
596
+ prompts = Array.new(100) { PromptManager::Prompt.new(id: 'simple', storage: storage) }
597
+
598
+ expect {
599
+ prompts.each { |prompt| prompt.render(simple_params) }
600
+ }.to perform_under(0.1).sec
601
+ end
602
+ end
603
+
604
+ # Benchmark comparison
605
+ it 'compares different rendering strategies', :benchmark do
606
+ prompt = PromptManager::Prompt.new(id: 'simple', storage: storage)
607
+
608
+ Benchmark.ips do |x|
609
+ x.config(time: 2, warmup: 1)
610
+
611
+ x.report('direct render') do
612
+ prompt.render(simple_params)
613
+ end
614
+
615
+ x.report('cached render') do
616
+ CachedPromptManager.render('simple', simple_params)
617
+ end
618
+
619
+ x.compare!
620
+ end
621
+ end
622
+ end
623
+ ```
624
+
625
+ ### Memory Usage Tests
626
+
627
+ ```ruby
628
+ # spec/performance/memory_usage_spec.rb
629
+ RSpec.describe 'Memory Usage' do
630
+ def measure_memory_usage
631
+ GC.start
632
+ before = GC.stat[:heap_allocated_pages]
633
+
634
+ yield
635
+
636
+ GC.start
637
+ after = GC.stat[:heap_allocated_pages]
638
+
639
+ (after - before) * GC::INTERNAL_CONSTANTS[:HEAP_PAGE_SIZE]
640
+ end
641
+
642
+ it 'does not leak memory during rendering' do
643
+ storage = create_test_storage_with_prompts({
644
+ 'test' => 'Hello [NAME]!'
645
+ })
646
+
647
+ prompt = PromptManager::Prompt.new(id: 'test', storage: storage)
648
+
649
+ memory_used = measure_memory_usage do
650
+ 1000.times do
651
+ prompt.render(name: 'Test')
652
+ end
653
+ end
654
+
655
+ # Should not use excessive memory (adjust threshold as needed)
656
+ expect(memory_used).to be < 10 * 1024 * 1024 # 10MB
657
+ end
658
+ end
659
+ ```
660
+
661
+ ## Mock and Stub Patterns
662
+
663
+ ### Storage Mocking
664
+
665
+ ```ruby
666
+ # spec/support/storage_mocks.rb
667
+ module StorageMocks
668
+ def mock_storage_with_prompts(prompts = {})
669
+ storage = instance_double(PromptManager::Storage::Base)
670
+
671
+ # Mock read method
672
+ allow(storage).to receive(:read) do |prompt_id|
673
+ content = prompts[prompt_id]
674
+ if content
675
+ content
676
+ else
677
+ raise PromptManager::PromptNotFoundError.new("Prompt '#{prompt_id}' not found")
678
+ end
679
+ end
680
+
681
+ # Mock exist? method
682
+ allow(storage).to receive(:exist?) do |prompt_id|
683
+ prompts.key?(prompt_id)
684
+ end
685
+
686
+ # Mock write method
687
+ allow(storage).to receive(:write) do |prompt_id, content|
688
+ prompts[prompt_id] = content
689
+ true
690
+ end
691
+
692
+ # Mock delete method
693
+ allow(storage).to receive(:delete) do |prompt_id|
694
+ prompts.delete(prompt_id) ? true : false
695
+ end
696
+
697
+ # Mock list method
698
+ allow(storage).to receive(:list) { prompts.keys }
699
+
700
+ storage
701
+ end
702
+ end
703
+
704
+ RSpec.configure do |config|
705
+ config.include StorageMocks
706
+ end
707
+ ```
708
+
709
+ ### External Service Mocking
710
+
711
+ ```ruby
712
+ # For testing prompts that make external API calls
713
+ RSpec.describe 'API Integration Prompts' do
714
+ before do
715
+ # Mock HTTP calls
716
+ stub_request(:get, 'https://api.example.com/users/123')
717
+ .to_return(
718
+ status: 200,
719
+ body: { name: 'John Doe', email: 'john@example.com' }.to_json,
720
+ headers: { 'Content-Type' => 'application/json' }
721
+ )
722
+ end
723
+
724
+ it 'handles API responses in prompts' do
725
+ # Test prompt that makes API calls
726
+ end
727
+ end
728
+ ```
729
+
730
+ ## Test Data Management
731
+
732
+ ### Fixtures
733
+
734
+ ```ruby
735
+ # spec/fixtures/prompts.yml
736
+ simple_greeting:
737
+ id: 'simple_greeting'
738
+ content: 'Hello [NAME]!'
739
+
740
+ complex_email:
741
+ id: 'complex_email'
742
+ content: |
743
+ //include headers/email_header.txt
744
+
745
+ Dear [CUSTOMER.NAME],
746
+
747
+ Your order #[ORDER.ID] has been processed.
748
+
749
+ //include footers/email_footer.txt
750
+
751
+ # spec/support/fixture_helpers.rb
752
+ module FixtureHelpers
753
+ def load_prompt_fixtures
754
+ YAML.load_file(File.join(__dir__, '..', 'fixtures', 'prompts.yml'))
755
+ end
756
+
757
+ def create_prompt_from_fixture(fixture_name)
758
+ fixtures = load_prompt_fixtures
759
+ fixture = fixtures[fixture_name.to_s]
760
+
761
+ storage = mock_storage_with_prompts(fixture['id'] => fixture['content'])
762
+ PromptManager::Prompt.new(id: fixture['id'], storage: storage)
763
+ end
764
+ end
765
+ ```
766
+
767
+ ## Continuous Integration
768
+
769
+ ### GitHub Actions Test Configuration
770
+
771
+ ```yaml
772
+ # .github/workflows/test.yml
773
+ name: Tests
774
+
775
+ on: [push, pull_request]
776
+
777
+ jobs:
778
+ test:
779
+ runs-on: ubuntu-latest
780
+
781
+ strategy:
782
+ matrix:
783
+ ruby-version: ['3.0', '3.1', '3.2']
784
+
785
+ steps:
786
+ - uses: actions/checkout@v3
787
+
788
+ - name: Set up Ruby ${{ matrix.ruby-version }}
789
+ uses: ruby/setup-ruby@v1
790
+ with:
791
+ ruby-version: ${{ matrix.ruby-version }}
792
+ bundler-cache: true
793
+
794
+ - name: Run tests
795
+ run: |
796
+ bundle exec rspec --format progress --format RspecJunitFormatter --out tmp/test-results.xml
797
+
798
+ - name: Check test coverage
799
+ run: |
800
+ bundle exec rspec
801
+ if [ -f coverage/.resultset.json ]; then
802
+ echo "Coverage report generated"
803
+ fi
804
+
805
+ - name: Upload test results
806
+ uses: actions/upload-artifact@v3
807
+ if: always()
808
+ with:
809
+ name: test-results-${{ matrix.ruby-version }}
810
+ path: tmp/test-results.xml
811
+ ```
812
+
813
+ ## Best Practices
814
+
815
+ 1. **Test Isolation**: Each test should be independent and not rely on other tests
816
+ 2. **Clear Naming**: Test names should clearly describe what is being tested
817
+ 3. **Arrange-Act-Assert**: Structure tests with clear setup, action, and verification phases
818
+ 4. **Mock External Dependencies**: Don't rely on external services in unit tests
819
+ 5. **Test Edge Cases**: Include tests for error conditions and edge cases
820
+ 6. **Performance Testing**: Include performance benchmarks for critical paths
821
+ 7. **Documentation**: Use tests as documentation of expected behavior
822
+ 8. **Continuous Integration**: Run tests automatically on all changes