appydave-tools 0.70.0 → 0.71.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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/commands/brainstorming-agent.md +227 -0
  3. data/.claude/commands/cli-test.md +251 -0
  4. data/.claude/commands/dev.md +234 -0
  5. data/.claude/commands/po.md +227 -0
  6. data/.claude/commands/progress.md +51 -0
  7. data/.claude/commands/uat.md +321 -0
  8. data/.rubocop.yml +9 -0
  9. data/AGENTS.md +43 -0
  10. data/CHANGELOG.md +12 -0
  11. data/CLAUDE.md +26 -3
  12. data/README.md +15 -0
  13. data/bin/dam +21 -1
  14. data/bin/jump.rb +29 -0
  15. data/bin/subtitle_processor.rb +54 -1
  16. data/bin/zsh_history.rb +846 -0
  17. data/docs/README.md +162 -69
  18. data/docs/architecture/cli/exe-bin-convention.md +434 -0
  19. data/docs/architecture/cli-patterns.md +631 -0
  20. data/docs/architecture/gpt-context/gpt-context-architecture.md +325 -0
  21. data/docs/architecture/gpt-context/gpt-context-implementation-guide.md +419 -0
  22. data/docs/architecture/gpt-context/gpt-context-vision.md +179 -0
  23. data/docs/architecture/testing/testing-patterns.md +762 -0
  24. data/docs/backlog.md +120 -0
  25. data/docs/cli-tests/FR-3-jump-location-tool.md +515 -0
  26. data/docs/specs/fr-002-gpt-context-help-system.md +265 -0
  27. data/docs/specs/fr-003-jump-location-tool.md +779 -0
  28. data/docs/specs/zsh-history-tool.md +820 -0
  29. data/docs/uat/FR-3-jump-location-tool.md +741 -0
  30. data/exe/jump +11 -0
  31. data/exe/{subtitle_manager → subtitle_processor} +1 -1
  32. data/exe/zsh_history +11 -0
  33. data/lib/appydave/tools/configuration/openai.rb +1 -1
  34. data/lib/appydave/tools/dam/file_helper.rb +28 -0
  35. data/lib/appydave/tools/dam/project_listing.rb +4 -30
  36. data/lib/appydave/tools/dam/s3_operations.rb +2 -1
  37. data/lib/appydave/tools/dam/ssd_status.rb +226 -0
  38. data/lib/appydave/tools/dam/status.rb +3 -51
  39. data/lib/appydave/tools/jump/cli.rb +561 -0
  40. data/lib/appydave/tools/jump/commands/add.rb +52 -0
  41. data/lib/appydave/tools/jump/commands/base.rb +43 -0
  42. data/lib/appydave/tools/jump/commands/generate.rb +153 -0
  43. data/lib/appydave/tools/jump/commands/remove.rb +58 -0
  44. data/lib/appydave/tools/jump/commands/report.rb +214 -0
  45. data/lib/appydave/tools/jump/commands/update.rb +42 -0
  46. data/lib/appydave/tools/jump/commands/validate.rb +54 -0
  47. data/lib/appydave/tools/jump/config.rb +233 -0
  48. data/lib/appydave/tools/jump/formatters/base.rb +48 -0
  49. data/lib/appydave/tools/jump/formatters/json_formatter.rb +19 -0
  50. data/lib/appydave/tools/jump/formatters/paths_formatter.rb +21 -0
  51. data/lib/appydave/tools/jump/formatters/table_formatter.rb +183 -0
  52. data/lib/appydave/tools/jump/location.rb +134 -0
  53. data/lib/appydave/tools/jump/path_validator.rb +47 -0
  54. data/lib/appydave/tools/jump/search.rb +230 -0
  55. data/lib/appydave/tools/subtitle_processor/transcript.rb +51 -0
  56. data/lib/appydave/tools/version.rb +1 -1
  57. data/lib/appydave/tools/zsh_history/command.rb +37 -0
  58. data/lib/appydave/tools/zsh_history/config.rb +235 -0
  59. data/lib/appydave/tools/zsh_history/filter.rb +184 -0
  60. data/lib/appydave/tools/zsh_history/formatter.rb +75 -0
  61. data/lib/appydave/tools/zsh_history/parser.rb +101 -0
  62. data/lib/appydave/tools.rb +25 -0
  63. data/package.json +1 -1
  64. metadata +51 -4
@@ -0,0 +1,762 @@
1
+ # Testing Patterns Guide
2
+
3
+ This guide documents the testing patterns and conventions used in appydave-tools. Follow these patterns when writing tests for new tools or maintaining existing ones.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Philosophy](#philosophy)
8
+ - [Directory Structure](#directory-structure)
9
+ - [RSpec Conventions](#rspec-conventions)
10
+ - [Test Business Logic, Not CLI](#test-business-logic-not-cli)
11
+ - [Spec Helper Configuration](#spec-helper-configuration)
12
+ - [Fixture Management](#fixture-management)
13
+ - [HTTP Mocking with VCR](#http-mocking-with-vcr)
14
+ - [Configuration in Tests](#configuration-in-tests)
15
+ - [Guard for Continuous Testing](#guard-for-continuous-testing)
16
+ - [Common Patterns](#common-patterns)
17
+ - [Anti-Patterns to Avoid](#anti-patterns-to-avoid)
18
+
19
+ ---
20
+
21
+ ## Philosophy
22
+
23
+ ### Core Principles
24
+
25
+ 1. **Test business logic, not CLI executables** - Focus on `lib/` classes, not `bin/` scripts
26
+ 2. **No require statements in specs** - `spec_helper.rb` handles all loading
27
+ 3. **Isolated tests** - Each test should be independent and not rely on external state
28
+ 4. **Mock external services** - Use VCR for HTTP calls, WebMock for network isolation
29
+ 5. **Fast feedback** - Use Guard for continuous testing during development
30
+
31
+ ### Why This Matters
32
+
33
+ ```
34
+ bin/ ← CLI layer (thin wrapper, not tested directly)
35
+ └── tool.rb
36
+
37
+ lib/appydave/tools/ ← Business logic (THIS IS WHAT WE TEST)
38
+ └── tool_name/
39
+ ├── processor.rb ← Unit tests focus here
40
+ └── validator.rb ← And here
41
+
42
+ spec/appydave/tools/ ← Tests mirror lib/ structure
43
+ └── tool_name/
44
+ ├── processor_spec.rb
45
+ └── validator_spec.rb
46
+ ```
47
+
48
+ **The CLI layer is a thin wrapper.** It parses arguments and calls business logic. Testing CLI directly is:
49
+ - Fragile (depends on exact output format)
50
+ - Slow (spawns processes)
51
+ - Unnecessary (business logic tests cover the important parts)
52
+
53
+ ---
54
+
55
+ ## Directory Structure
56
+
57
+ ### Test Files Mirror lib/ Structure
58
+
59
+ ```
60
+ lib/appydave/tools/subtitle_processor/clean.rb
61
+ spec/appydave/tools/subtitle_processor/clean_spec.rb
62
+
63
+ lib/appydave/tools/dam/s3_operations.rb
64
+ spec/appydave/tools/dam/s3_operations_spec.rb
65
+ ```
66
+
67
+ ### Complete Test Directory Layout
68
+
69
+ ```
70
+ spec/
71
+ ├── spec_helper.rb # Main configuration (loads all dependencies)
72
+ ├── support/ # Shared test helpers
73
+ │ └── dam_filesystem_helpers.rb
74
+ ├── fixtures/ # Test data files
75
+ │ ├── subtitle_processor/
76
+ │ │ ├── sample.srt
77
+ │ │ └── expected_output.srt
78
+ │ ├── zsh_history/
79
+ │ │ └── sample_history
80
+ │ └── ...
81
+ ├── vcr_cassettes/ # Recorded HTTP responses
82
+ │ └── youtube_manager/
83
+ │ └── get_video.yml
84
+ └── appydave/
85
+ └── tools/
86
+ ├── subtitle_processor/
87
+ │ ├── clean_spec.rb
88
+ │ └── join_spec.rb
89
+ ├── dam/
90
+ │ ├── s3_operations_spec.rb
91
+ │ └── brand_resolver_spec.rb
92
+ └── ...
93
+ ```
94
+
95
+ ---
96
+
97
+ ## RSpec Conventions
98
+
99
+ ### No Require Statements
100
+
101
+ **All requires are handled by `spec_helper.rb`.** Never add require statements to individual spec files.
102
+
103
+ ```ruby
104
+ # spec/appydave/tools/subtitle_processor/clean_spec.rb
105
+ # frozen_string_literal: true
106
+
107
+ # NO require statements needed - spec_helper handles everything
108
+
109
+ RSpec.describe Appydave::Tools::SubtitleProcessor::Clean do
110
+ # Tests
111
+ end
112
+ ```
113
+
114
+ ### Frozen String Literal
115
+
116
+ All spec files must start with:
117
+
118
+ ```ruby
119
+ # frozen_string_literal: true
120
+ ```
121
+
122
+ ### Describe Block Naming
123
+
124
+ Use the full module path for the describe block:
125
+
126
+ ```ruby
127
+ # Good
128
+ RSpec.describe Appydave::Tools::SubtitleProcessor::Clean do
129
+
130
+ # Bad - missing namespace
131
+ RSpec.describe Clean do
132
+ ```
133
+
134
+ ### Subject Definition
135
+
136
+ Define subject for the class under test:
137
+
138
+ ```ruby
139
+ RSpec.describe Appydave::Tools::SubtitleProcessor::Clean do
140
+ subject { described_class.new(srt_content: sample_content) }
141
+
142
+ let(:sample_content) { "1\n00:00:00,000 --> 00:00:01,000\nHello" }
143
+
144
+ describe '#clean' do
145
+ it 'processes the content' do
146
+ expect(subject.clean).to be_a(String)
147
+ end
148
+ end
149
+ end
150
+ ```
151
+
152
+ ### Let vs Instance Variables
153
+
154
+ Prefer `let` and `let!` over instance variables:
155
+
156
+ ```ruby
157
+ # Good
158
+ let(:options) { { file: 'test.srt', output: 'output.srt' } }
159
+ let(:processor) { described_class.new(**options) }
160
+
161
+ # Bad - instance variables in before blocks
162
+ before do
163
+ @options = { file: 'test.srt', output: 'output.srt' }
164
+ @processor = described_class.new(**@options)
165
+ end
166
+ ```
167
+
168
+ ### Context vs Describe
169
+
170
+ - Use `describe` for methods or logical groupings
171
+ - Use `context` for different states or conditions
172
+
173
+ ```ruby
174
+ RSpec.describe Appydave::Tools::Dam::BrandResolver do
175
+ describe '.resolve' do
176
+ context 'when brand exists' do
177
+ it 'returns the brand directory' do
178
+ # ...
179
+ end
180
+ end
181
+
182
+ context 'when brand does not exist' do
183
+ it 'raises an error' do
184
+ # ...
185
+ end
186
+ end
187
+ end
188
+ end
189
+ ```
190
+
191
+ ---
192
+
193
+ ## Test Business Logic, Not CLI
194
+
195
+ ### Good: Testing Business Logic
196
+
197
+ ```ruby
198
+ # spec/appydave/tools/subtitle_processor/clean_spec.rb
199
+ RSpec.describe Appydave::Tools::SubtitleProcessor::Clean do
200
+ subject { described_class.new(srt_content: sample_srt) }
201
+
202
+ let(:sample_srt) do
203
+ <<~SRT
204
+ 1
205
+ 00:00:00,000 --> 00:00:02,000
206
+ <u>Hello world</u>
207
+ SRT
208
+ end
209
+
210
+ describe '#clean' do
211
+ it 'removes underline HTML tags' do
212
+ result = subject.clean
213
+ expect(result).not_to include('<u>')
214
+ expect(result).not_to include('</u>')
215
+ end
216
+
217
+ it 'preserves subtitle content' do
218
+ result = subject.clean
219
+ expect(result).to include('Hello world')
220
+ end
221
+ end
222
+
223
+ describe '#write' do
224
+ let(:temp_file) { Tempfile.new(['test', '.srt']) }
225
+
226
+ after { temp_file.unlink }
227
+
228
+ it 'writes cleaned content to file' do
229
+ subject.clean
230
+ subject.write(temp_file.path)
231
+
232
+ expect(File.read(temp_file.path)).to include('Hello world')
233
+ end
234
+ end
235
+ end
236
+ ```
237
+
238
+ ### Bad: Testing CLI Executables
239
+
240
+ ```ruby
241
+ # DON'T DO THIS - fragile, slow, unnecessary
242
+ RSpec.describe 'bin/subtitle_processor.rb' do
243
+ it 'runs clean command' do
244
+ output = `bin/subtitle_processor.rb clean -f test.srt -o output.srt`
245
+ expect(output).to include('Processed')
246
+ end
247
+ end
248
+ ```
249
+
250
+ ### When CLI Testing Makes Sense
251
+
252
+ In rare cases, integration tests for CLI may be warranted:
253
+ - End-to-end smoke tests
254
+ - Verifying exit codes
255
+ - Testing CLI-specific error messages
256
+
257
+ But these should be few and clearly marked:
258
+
259
+ ```ruby
260
+ # spec/integration/cli/subtitle_processor_cli_spec.rb
261
+ RSpec.describe 'Subtitle Processor CLI', :integration do
262
+ # Integration tests here
263
+ end
264
+ ```
265
+
266
+ ---
267
+
268
+ ## Spec Helper Configuration
269
+
270
+ ### Current spec_helper.rb Setup
271
+
272
+ ```ruby
273
+ # frozen_string_literal: true
274
+
275
+ require 'pry'
276
+ require 'bundler/setup'
277
+ require 'simplecov'
278
+
279
+ SimpleCov.start
280
+
281
+ require 'appydave/tools'
282
+ require 'webmock/rspec'
283
+ require 'vcr'
284
+
285
+ # Load shared helpers
286
+ require_relative 'support/dam_filesystem_helpers'
287
+
288
+ # Configure default test configuration
289
+ Appydave::Tools::Configuration::Config.set_default do |config|
290
+ config.config_path = Dir.mktmpdir
291
+ config.register(:settings, Appydave::Tools::Configuration::Models::SettingsConfig)
292
+ config.register(:brands, Appydave::Tools::Configuration::Models::BrandsConfig)
293
+ config.register(:channels, Appydave::Tools::Configuration::Models::ChannelsConfig)
294
+ config.register(:youtube_automation, Appydave::Tools::Configuration::Models::YoutubeAutomationConfig)
295
+ end
296
+
297
+ VCR.configure do |config|
298
+ config.cassette_library_dir = 'spec/vcr_cassettes'
299
+ config.hook_into :webmock
300
+ config.configure_rspec_metadata!
301
+ config.allow_http_connections_when_no_cassette = true
302
+ end
303
+
304
+ RSpec.configure do |config|
305
+ config.example_status_persistence_file_path = '.rspec_status'
306
+ config.filter_run_when_matching :focus
307
+
308
+ # Skip tools_enabled tests unless explicitly enabled
309
+ config.filter_run_excluding :tools_enabled unless ENV['TOOLS_ENABLED'] == 'true'
310
+
311
+ config.disable_monkey_patching!
312
+
313
+ config.expect_with :rspec do |c|
314
+ c.syntax = :expect
315
+ end
316
+
317
+ config.before do
318
+ if ENV['TOOLS_ENABLED'] == 'true'
319
+ WebMock.allow_net_connect!
320
+ else
321
+ WebMock.disable_net_connect!(allow_localhost: true)
322
+ end
323
+ end
324
+ end
325
+ ```
326
+
327
+ ### Key Configuration Points
328
+
329
+ | Setting | Purpose |
330
+ |---------|---------|
331
+ | `SimpleCov.start` | Code coverage reporting |
332
+ | `config_path = Dir.mktmpdir` | Isolated temp config for each test run |
333
+ | `:tools_enabled` filter | Skip external API tests in CI |
334
+ | `WebMock.disable_net_connect!` | Block real HTTP in tests |
335
+ | `.rspec_status` | Enable `--only-failures` and `--next-failure` |
336
+
337
+ ---
338
+
339
+ ## Fixture Management
340
+
341
+ ### Location
342
+
343
+ Fixtures live in `spec/fixtures/` organized by tool:
344
+
345
+ ```
346
+ spec/fixtures/
347
+ ├── subtitle_processor/
348
+ │ ├── sample.srt
349
+ │ ├── sample_with_tags.srt
350
+ │ └── expected_clean_output.srt
351
+ ├── zsh_history/
352
+ │ └── sample_history
353
+ └── dam/
354
+ └── sample_manifest.json
355
+ ```
356
+
357
+ ### Loading Fixtures
358
+
359
+ ```ruby
360
+ RSpec.describe Appydave::Tools::SubtitleProcessor::Clean do
361
+ let(:fixture_path) { File.expand_path('../../fixtures/subtitle_processor', __dir__) }
362
+ let(:sample_srt) { File.read(File.join(fixture_path, 'sample.srt')) }
363
+
364
+ subject { described_class.new(srt_content: sample_srt) }
365
+
366
+ # ...
367
+ end
368
+ ```
369
+
370
+ ### Shared Fixture Helper
371
+
372
+ For frequently accessed fixtures, create a helper in `spec/support/`:
373
+
374
+ ```ruby
375
+ # spec/support/fixture_helpers.rb
376
+ module FixtureHelpers
377
+ def fixture_path(tool, filename)
378
+ File.expand_path("../fixtures/#{tool}/#{filename}", __dir__)
379
+ end
380
+
381
+ def load_fixture(tool, filename)
382
+ File.read(fixture_path(tool, filename))
383
+ end
384
+ end
385
+
386
+ # In spec_helper.rb
387
+ RSpec.configure do |config|
388
+ config.include FixtureHelpers
389
+ end
390
+
391
+ # In specs
392
+ let(:sample_srt) { load_fixture('subtitle_processor', 'sample.srt') }
393
+ ```
394
+
395
+ ---
396
+
397
+ ## HTTP Mocking with VCR
398
+
399
+ ### Recording Cassettes
400
+
401
+ VCR records HTTP interactions for replay in tests:
402
+
403
+ ```ruby
404
+ RSpec.describe Appydave::Tools::YouTubeManager::GetVideo do
405
+ describe '#get', :vcr do
406
+ it 'retrieves video metadata' do
407
+ video = described_class.new
408
+ video.get('dQw4w9WgXcQ')
409
+
410
+ expect(video.data).to include('title')
411
+ end
412
+ end
413
+ end
414
+ ```
415
+
416
+ ### Cassette Storage
417
+
418
+ ```
419
+ spec/vcr_cassettes/
420
+ └── Appydave_Tools_YouTubeManager_GetVideo/
421
+ └── _get/retrieves_video_metadata.yml
422
+ ```
423
+
424
+ ### Custom Cassette Names
425
+
426
+ ```ruby
427
+ it 'retrieves video metadata', vcr: { cassette_name: 'youtube/get_video_success' } do
428
+ # ...
429
+ end
430
+ ```
431
+
432
+ ### Filtering Sensitive Data
433
+
434
+ Add to VCR configuration:
435
+
436
+ ```ruby
437
+ VCR.configure do |config|
438
+ config.filter_sensitive_data('<YOUTUBE_API_KEY>') { ENV['YOUTUBE_API_KEY'] }
439
+ config.filter_sensitive_data('<OPENAI_TOKEN>') { ENV['OPENAI_ACCESS_TOKEN'] }
440
+ end
441
+ ```
442
+
443
+ ---
444
+
445
+ ## Configuration in Tests
446
+
447
+ ### Isolated Test Configuration
448
+
449
+ Tests use a temporary directory for configuration:
450
+
451
+ ```ruby
452
+ # Set up in spec_helper.rb
453
+ Appydave::Tools::Configuration::Config.set_default do |config|
454
+ config.config_path = Dir.mktmpdir
455
+ # ...
456
+ end
457
+ ```
458
+
459
+ This ensures:
460
+ - Tests don't modify real user configuration
461
+ - Tests are isolated from each other
462
+ - No leftover state between test runs
463
+
464
+ ### Creating Test Configurations
465
+
466
+ ```ruby
467
+ RSpec.describe Appydave::Tools::Dam::BrandResolver do
468
+ let(:config_path) { Dir.mktmpdir }
469
+
470
+ before do
471
+ Appydave::Tools::Configuration::Config.set_default do |config|
472
+ config.config_path = config_path
473
+ config.register(:settings, Appydave::Tools::Configuration::Models::SettingsConfig)
474
+ end
475
+
476
+ # Create test settings
477
+ File.write(
478
+ File.join(config_path, 'settings.json'),
479
+ { 'video-projects-root' => '/tmp/test-projects' }.to_json
480
+ )
481
+ end
482
+
483
+ after do
484
+ FileUtils.rm_rf(config_path)
485
+ end
486
+
487
+ # Tests...
488
+ end
489
+ ```
490
+
491
+ ---
492
+
493
+ ## Guard for Continuous Testing
494
+
495
+ ### Running Guard
496
+
497
+ ```bash
498
+ # Start Guard for auto-testing
499
+ guard
500
+
501
+ # With Ruby 3.4 warning suppression
502
+ RUBYOPT="-W0" guard
503
+ ```
504
+
505
+ ### What Guard Does
506
+
507
+ 1. **Watches file changes** in `lib/` and `spec/`
508
+ 2. **Runs relevant tests** when files change
509
+ 3. **Runs RuboCop** on changed files
510
+ 4. **Provides fast feedback** during development
511
+
512
+ ### Guardfile Configuration
513
+
514
+ The project's Guardfile configures:
515
+ - RSpec test runs on lib/spec file changes
516
+ - RuboCop linting on Ruby file changes
517
+ - Notification settings
518
+
519
+ ### Focus Tags
520
+
521
+ Use `:focus` to run specific tests during development:
522
+
523
+ ```ruby
524
+ it 'does something', :focus do
525
+ # Only this test runs when you save
526
+ end
527
+ ```
528
+
529
+ Guard respects the `filter_run_when_matching :focus` setting.
530
+
531
+ ---
532
+
533
+ ## Common Patterns
534
+
535
+ ### Testing File Operations
536
+
537
+ Use `Tempfile` and `Dir.mktmpdir` for isolated file operations:
538
+
539
+ ```ruby
540
+ RSpec.describe Appydave::Tools::SubtitleProcessor::Clean do
541
+ let(:temp_dir) { Dir.mktmpdir }
542
+ let(:input_file) { File.join(temp_dir, 'input.srt') }
543
+ let(:output_file) { File.join(temp_dir, 'output.srt') }
544
+
545
+ before do
546
+ File.write(input_file, sample_content)
547
+ end
548
+
549
+ after do
550
+ FileUtils.rm_rf(temp_dir)
551
+ end
552
+
553
+ it 'writes to output file' do
554
+ processor = described_class.new(file_path: input_file)
555
+ processor.clean
556
+ processor.write(output_file)
557
+
558
+ expect(File.exist?(output_file)).to be true
559
+ end
560
+ end
561
+ ```
562
+
563
+ ### Testing Error Handling
564
+
565
+ ```ruby
566
+ RSpec.describe Appydave::Tools::Dam::BrandResolver do
567
+ describe '.resolve' do
568
+ context 'when brand does not exist' do
569
+ it 'raises BrandNotFoundError' do
570
+ expect {
571
+ described_class.resolve('nonexistent')
572
+ }.to raise_error(Appydave::Tools::Dam::Errors::BrandNotFoundError)
573
+ end
574
+ end
575
+ end
576
+ end
577
+ ```
578
+
579
+ ### Testing with Options/Parameters
580
+
581
+ ```ruby
582
+ RSpec.describe Appydave::Tools::SubtitleProcessor::Join do
583
+ subject do
584
+ described_class.new(
585
+ folder: fixture_dir,
586
+ files: '*.srt',
587
+ sort: 'asc',
588
+ buffer: 100,
589
+ output: output_file
590
+ )
591
+ end
592
+
593
+ let(:fixture_dir) { File.expand_path('../../fixtures/subtitle_processor', __dir__) }
594
+ let(:output_file) { File.join(Dir.mktmpdir, 'merged.srt') }
595
+
596
+ describe '#join' do
597
+ it 'merges files in specified order' do
598
+ subject.join
599
+ expect(File.exist?(output_file)).to be true
600
+ end
601
+ end
602
+ end
603
+ ```
604
+
605
+ ### Shared Examples
606
+
607
+ For common behavior across classes:
608
+
609
+ ```ruby
610
+ # spec/support/shared_examples/configurable.rb
611
+ RSpec.shared_examples 'a configurable class' do
612
+ it 'responds to configuration' do
613
+ expect(described_class).to respond_to(:configuration)
614
+ end
615
+ end
616
+
617
+ # In specs
618
+ RSpec.describe Appydave::Tools::Dam::S3Operations do
619
+ it_behaves_like 'a configurable class'
620
+ end
621
+ ```
622
+
623
+ ---
624
+
625
+ ## Anti-Patterns to Avoid
626
+
627
+ ### Don't Test Private Methods Directly
628
+
629
+ ```ruby
630
+ # Bad
631
+ it 'parses timestamp correctly' do
632
+ result = subject.send(:parse_timestamp, '00:01:30,500')
633
+ expect(result).to eq(90500)
634
+ end
635
+
636
+ # Good - test through public interface
637
+ it 'handles timestamps in content' do
638
+ result = subject.process
639
+ expect(result).to include_correct_timestamps
640
+ end
641
+ ```
642
+
643
+ ### Don't Use sleep() in Tests
644
+
645
+ ```ruby
646
+ # Bad
647
+ it 'processes asynchronously' do
648
+ subject.start_processing
649
+ sleep(2) # Flaky!
650
+ expect(subject.done?).to be true
651
+ end
652
+
653
+ # Good - use proper synchronization or mock time
654
+ it 'processes asynchronously' do
655
+ expect(subject).to receive(:notify_complete)
656
+ subject.start_processing
657
+ end
658
+ ```
659
+
660
+ ### Don't Rely on Test Order
661
+
662
+ ```ruby
663
+ # Bad - depends on previous test creating file
664
+ it 'reads the created file' do
665
+ content = File.read('/tmp/test_output.txt')
666
+ expect(content).to include('data')
667
+ end
668
+
669
+ # Good - each test creates its own fixtures
670
+ let(:test_file) do
671
+ path = '/tmp/test_output.txt'
672
+ File.write(path, 'test data')
673
+ path
674
+ end
675
+
676
+ after { FileUtils.rm_f(test_file) }
677
+
678
+ it 'reads the created file' do
679
+ content = File.read(test_file)
680
+ expect(content).to include('data')
681
+ end
682
+ ```
683
+
684
+ ### Don't Test External Services Without Mocking
685
+
686
+ ```ruby
687
+ # Bad - hits real YouTube API
688
+ it 'fetches video data' do
689
+ video = YouTubeManager::GetVideo.new
690
+ video.get('dQw4w9WgXcQ')
691
+ expect(video.data).not_to be_nil
692
+ end
693
+
694
+ # Good - uses VCR cassette
695
+ it 'fetches video data', :vcr do
696
+ video = YouTubeManager::GetVideo.new
697
+ video.get('dQw4w9WgXcQ')
698
+ expect(video.data).not_to be_nil
699
+ end
700
+ ```
701
+
702
+ ---
703
+
704
+ ## Running Tests
705
+
706
+ ### Common Commands
707
+
708
+ ```bash
709
+ # Run all tests
710
+ rake spec
711
+
712
+ # Run specific test file
713
+ bundle exec rspec spec/appydave/tools/subtitle_processor/clean_spec.rb
714
+
715
+ # Run with documentation format
716
+ bundle exec rspec -f doc
717
+
718
+ # Run only failed tests from last run
719
+ bundle exec rspec --only-failures
720
+
721
+ # Run next failure (one at a time)
722
+ bundle exec rspec --next-failure
723
+
724
+ # Run with coverage report
725
+ COVERAGE=true rake spec
726
+
727
+ # Run focused tests only
728
+ bundle exec rspec --tag focus
729
+
730
+ # Enable external API tests (for development)
731
+ TOOLS_ENABLED=true bundle exec rspec
732
+ ```
733
+
734
+ ### CI/CD Testing
735
+
736
+ In CI, tests run with:
737
+ - `WebMock.disable_net_connect!` - No real HTTP calls
738
+ - VCR cassettes for recorded responses
739
+ - `TOOLS_ENABLED` not set - External API tests skipped
740
+
741
+ ---
742
+
743
+ ## Summary
744
+
745
+ | Principle | Implementation |
746
+ |-----------|----------------|
747
+ | Test business logic | Focus on `lib/` classes, not `bin/` |
748
+ | No requires | `spec_helper.rb` handles loading |
749
+ | Isolated tests | Temp directories, mocked config |
750
+ | Mock HTTP | VCR + WebMock |
751
+ | Fast feedback | Guard for continuous testing |
752
+ | Clean fixtures | `spec/fixtures/` organized by tool |
753
+
754
+ **When in doubt:**
755
+ 1. Check existing specs for patterns
756
+ 2. Test the class, not the CLI
757
+ 3. Mock external dependencies
758
+ 4. Keep tests fast and isolated
759
+
760
+ ---
761
+
762
+ **Last updated:** 2025-12-13