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.
- checksums.yaml +4 -4
- data/.claude/commands/brainstorming-agent.md +227 -0
- data/.claude/commands/cli-test.md +251 -0
- data/.claude/commands/dev.md +234 -0
- data/.claude/commands/po.md +227 -0
- data/.claude/commands/progress.md +51 -0
- data/.claude/commands/uat.md +321 -0
- data/.rubocop.yml +9 -0
- data/AGENTS.md +43 -0
- data/CHANGELOG.md +12 -0
- data/CLAUDE.md +26 -3
- data/README.md +15 -0
- data/bin/dam +21 -1
- data/bin/jump.rb +29 -0
- data/bin/subtitle_processor.rb +54 -1
- data/bin/zsh_history.rb +846 -0
- data/docs/README.md +162 -69
- data/docs/architecture/cli/exe-bin-convention.md +434 -0
- data/docs/architecture/cli-patterns.md +631 -0
- data/docs/architecture/gpt-context/gpt-context-architecture.md +325 -0
- data/docs/architecture/gpt-context/gpt-context-implementation-guide.md +419 -0
- data/docs/architecture/gpt-context/gpt-context-vision.md +179 -0
- data/docs/architecture/testing/testing-patterns.md +762 -0
- data/docs/backlog.md +120 -0
- data/docs/cli-tests/FR-3-jump-location-tool.md +515 -0
- data/docs/specs/fr-002-gpt-context-help-system.md +265 -0
- data/docs/specs/fr-003-jump-location-tool.md +779 -0
- data/docs/specs/zsh-history-tool.md +820 -0
- data/docs/uat/FR-3-jump-location-tool.md +741 -0
- data/exe/jump +11 -0
- data/exe/{subtitle_manager → subtitle_processor} +1 -1
- data/exe/zsh_history +11 -0
- data/lib/appydave/tools/configuration/openai.rb +1 -1
- data/lib/appydave/tools/dam/file_helper.rb +28 -0
- data/lib/appydave/tools/dam/project_listing.rb +4 -30
- data/lib/appydave/tools/dam/s3_operations.rb +2 -1
- data/lib/appydave/tools/dam/ssd_status.rb +226 -0
- data/lib/appydave/tools/dam/status.rb +3 -51
- data/lib/appydave/tools/jump/cli.rb +561 -0
- data/lib/appydave/tools/jump/commands/add.rb +52 -0
- data/lib/appydave/tools/jump/commands/base.rb +43 -0
- data/lib/appydave/tools/jump/commands/generate.rb +153 -0
- data/lib/appydave/tools/jump/commands/remove.rb +58 -0
- data/lib/appydave/tools/jump/commands/report.rb +214 -0
- data/lib/appydave/tools/jump/commands/update.rb +42 -0
- data/lib/appydave/tools/jump/commands/validate.rb +54 -0
- data/lib/appydave/tools/jump/config.rb +233 -0
- data/lib/appydave/tools/jump/formatters/base.rb +48 -0
- data/lib/appydave/tools/jump/formatters/json_formatter.rb +19 -0
- data/lib/appydave/tools/jump/formatters/paths_formatter.rb +21 -0
- data/lib/appydave/tools/jump/formatters/table_formatter.rb +183 -0
- data/lib/appydave/tools/jump/location.rb +134 -0
- data/lib/appydave/tools/jump/path_validator.rb +47 -0
- data/lib/appydave/tools/jump/search.rb +230 -0
- data/lib/appydave/tools/subtitle_processor/transcript.rb +51 -0
- data/lib/appydave/tools/version.rb +1 -1
- data/lib/appydave/tools/zsh_history/command.rb +37 -0
- data/lib/appydave/tools/zsh_history/config.rb +235 -0
- data/lib/appydave/tools/zsh_history/filter.rb +184 -0
- data/lib/appydave/tools/zsh_history/formatter.rb +75 -0
- data/lib/appydave/tools/zsh_history/parser.rb +101 -0
- data/lib/appydave/tools.rb +25 -0
- data/package.json +1 -1
- 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
|