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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +4 -0
- data/COMMITS.md +196 -0
- data/README.md +485 -203
- data/docs/.keep +0 -0
- data/docs/advanced/custom-keywords.md +421 -0
- data/docs/advanced/dynamic-directives.md +535 -0
- data/docs/advanced/performance.md +612 -0
- data/docs/advanced/search-integration.md +635 -0
- data/docs/api/configuration.md +355 -0
- data/docs/api/directive-processor.md +431 -0
- data/docs/api/prompt-class.md +354 -0
- data/docs/api/storage-adapters.md +462 -0
- data/docs/assets/favicon.ico +1 -0
- data/docs/assets/logo.svg +24 -0
- data/docs/core-features/comments.md +48 -0
- data/docs/core-features/directive-processing.md +38 -0
- data/docs/core-features/erb-integration.md +68 -0
- data/docs/core-features/error-handling.md +197 -0
- data/docs/core-features/parameter-history.md +76 -0
- data/docs/core-features/parameterized-prompts.md +500 -0
- data/docs/core-features/shell-integration.md +79 -0
- data/docs/development/architecture.md +544 -0
- data/docs/development/contributing.md +425 -0
- data/docs/development/roadmap.md +234 -0
- data/docs/development/testing.md +822 -0
- data/docs/examples/advanced.md +523 -0
- data/docs/examples/basic.md +688 -0
- data/docs/examples/real-world.md +776 -0
- data/docs/examples.md +337 -0
- data/docs/getting-started/basic-concepts.md +318 -0
- data/docs/getting-started/installation.md +97 -0
- data/docs/getting-started/quick-start.md +256 -0
- data/docs/index.md +230 -0
- data/docs/migration/v0.9.0.md +459 -0
- data/docs/migration/v1.0.0.md +591 -0
- data/docs/storage/activerecord-adapter.md +348 -0
- data/docs/storage/custom-adapters.md +176 -0
- data/docs/storage/filesystem-adapter.md +236 -0
- data/docs/storage/overview.md +427 -0
- data/examples/advanced_integrations.rb +52 -0
- data/examples/prompts_dir/advanced_demo.txt +79 -0
- data/examples/prompts_dir/directive_example.json +1 -0
- data/examples/prompts_dir/directive_example.txt +8 -0
- data/examples/prompts_dir/todo.json +1 -1
- data/improvement_plan.md +996 -0
- data/lib/prompt_manager/storage/file_system_adapter.rb +8 -2
- data/lib/prompt_manager/version.rb +1 -1
- data/mkdocs.yml +146 -0
- data/prompt_manager_logo.png +0 -0
- metadata +46 -3
- 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
|