appydave-tools 0.76.6 → 0.76.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 +15 -0
- data/bin/dam +8 -134
- data/docs/planning/AGENTS.md +9 -3
- data/docs/planning/BACKLOG.md +7 -3
- data/docs/planning/extract-vat-cli/IMPLEMENTATION_PLAN.md +3 -3
- data/docs/planning/extract-vat-cli/assessment.md +107 -0
- data/docs/planning/extract-vat-cli/learnings/wave-learnings.md +36 -0
- data/docs/planning/library-boundary-cleanup/AGENTS.md +653 -0
- data/docs/planning/library-boundary-cleanup/IMPLEMENTATION_PLAN.md +61 -0
- data/lib/appydave/tools/dam/errors.rb +3 -0
- data/lib/appydave/tools/dam/s3_arg_parser.rb +111 -0
- data/lib/appydave/tools/dam/s3_scan_command.rb +2 -4
- data/lib/appydave/tools/version.rb +1 -1
- data/lib/appydave/tools.rb +1 -0
- data/package.json +1 -1
- metadata +6 -1
|
@@ -0,0 +1,653 @@
|
|
|
1
|
+
# AGENTS.md — AppyDave Tools / library-boundary-cleanup campaign
|
|
2
|
+
|
|
3
|
+
> Operational knowledge for every background agent. Self-contained — you receive only this file + your work unit prompt.
|
|
4
|
+
> Inherited from: extract-vat-cli campaign (2026-03-19)
|
|
5
|
+
> Updated for: library-boundary-cleanup campaign (2026-03-19)
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Project Overview
|
|
10
|
+
|
|
11
|
+
**What:** Ruby gem providing CLI productivity tools for AppyDave's YouTube content creation workflow.
|
|
12
|
+
**Stack:** Ruby 3.4.2, Bundler 2.6.2, RSpec, RuboCop, semantic-release CI/CD.
|
|
13
|
+
**Active campaign:** library-boundary-cleanup — fixing two boundary violations left by extract-vat-cli:
|
|
14
|
+
1. `exit 1` calls inside library code (S3ScanCommand + S3ArgParser)
|
|
15
|
+
2. `ENV['BRAND_PATH']` side-effect inside library argument parser (S3ArgParser)
|
|
16
|
+
**Commits:** Trigger automated semantic versioning via GitHub Actions. Always use `kfeat`/`kfix` — never `git commit`.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## ⚠️ Pre-Commit Check (Mandatory Every Commit)
|
|
21
|
+
|
|
22
|
+
Before running `kfix`, always run:
|
|
23
|
+
```bash
|
|
24
|
+
git status
|
|
25
|
+
```
|
|
26
|
+
Confirm ONLY the files you intentionally changed are staged. If unexpected files appear, run `git diff` to investigate before proceeding. Never commit files you didn't intentionally change.
|
|
27
|
+
|
|
28
|
+
**Why:** Prior campaign accidentally staged a pre-existing uncommitted change when running `kfix`. Required a follow-up fix commit.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Build & Run Commands
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# Initialize rbenv (required if rbenv not in PATH)
|
|
36
|
+
eval "$(rbenv init -)"
|
|
37
|
+
|
|
38
|
+
# Run tests
|
|
39
|
+
bundle exec rspec # All tests
|
|
40
|
+
bundle exec rspec spec/path/to/file_spec.rb # Single file
|
|
41
|
+
RUBYOPT="-W0" bundle exec rspec # Suppress Ruby 3.4 platform warnings
|
|
42
|
+
|
|
43
|
+
# Lint
|
|
44
|
+
bundle exec rubocop --format clang # Standard lint check (matches CI)
|
|
45
|
+
|
|
46
|
+
# Commit (never use git commit directly)
|
|
47
|
+
kfeat "add feature description" # Minor version bump
|
|
48
|
+
kfix "fix bug description" # Patch version bump
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**Baseline (start of library-boundary-cleanup):** 847 examples, 0 failures, ~85.92% line coverage
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Directory Structure
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
bin/
|
|
59
|
+
dam Main DAM CLI entry point (VatCLI class — 1,600 lines, being reduced)
|
|
60
|
+
exe/ Thin wrappers for gem installation (no .rb extension)
|
|
61
|
+
lib/appydave/tools/
|
|
62
|
+
dam/ Digital Asset Management — main active area
|
|
63
|
+
brand_resolver.rb Centralizes ALL brand name transformations (appydave ↔ v-appydave)
|
|
64
|
+
errors.rb Custom exception hierarchy — ADD UsageError here in B034
|
|
65
|
+
fuzzy_matcher.rb Levenshtein distance for "did you mean?" suggestions
|
|
66
|
+
git_helper.rb Extracted git command wrappers
|
|
67
|
+
file_helper.rb File utility methods (format_size, calculate_directory_size, format_age)
|
|
68
|
+
config.rb Delegates brand resolution to BrandResolver; memoized Config loading
|
|
69
|
+
project_resolver.rb Project name resolution with regex pattern matching
|
|
70
|
+
project_listing.rb Table display for `dam list` command
|
|
71
|
+
s3_operations.rb S3 upload/download/status with MD5 comparison
|
|
72
|
+
s3_scanner.rb S3 bucket scanner for s3-scan command
|
|
73
|
+
status.rb Project git/S3 status display
|
|
74
|
+
manifest_generator.rb Video project manifest
|
|
75
|
+
sync_from_ssd.rb SSD sync operations
|
|
76
|
+
ssd_status.rb SSD backup status
|
|
77
|
+
share_operations.rb Pre-signed URL generation
|
|
78
|
+
config_loader.rb Loads .video-tools.env per brand
|
|
79
|
+
local_sync_status.rb Enriches project data with local s3-staging sync status
|
|
80
|
+
s3_scan_command.rb S3 scan orchestration + display — HAS exit 1 AT LINE 55 (B034 target)
|
|
81
|
+
s3_arg_parser.rb CLI argument parsing for S3 commands — HAS exit 1 + ENV side-effects (B034/B035 targets)
|
|
82
|
+
lib/appydave/tools.rb Require file — already includes s3_scan_command and s3_arg_parser
|
|
83
|
+
spec/
|
|
84
|
+
appydave/tools/dam/ One spec file per dam/ class
|
|
85
|
+
support/
|
|
86
|
+
dam_filesystem_helpers.rb Shared contexts for DAM filesystem testing
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## This Campaign: What We're Fixing
|
|
92
|
+
|
|
93
|
+
### The Two Problems
|
|
94
|
+
|
|
95
|
+
**Problem 1: `exit 1` in library code** — Library classes must not terminate the process. VatCLI's `rescue StandardError` already catches and prints errors. Fix: replace `exit 1` with typed exceptions.
|
|
96
|
+
|
|
97
|
+
**Problem 2: `ENV['BRAND_PATH']` side-effect in library** — `S3ArgParser` sets a process-wide env var during argument parsing. Unsafe for parallelism. Fix: return `brand_path:` in result hash; move the `ENV[]=` call to VatCLI.
|
|
98
|
+
|
|
99
|
+
### Work Unit Dependencies
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
B034: extract-exit-calls (no deps — do first)
|
|
103
|
+
B035: extract-env-side-effect (DEPENDS ON B034 — both touch s3_arg_parser.rb)
|
|
104
|
+
B036: tests-s3-scan-command (DEPENDS ON B034 — can't test exceptions until they exist)
|
|
105
|
+
B037: tests-local-sync-status (no deps — but run after B035 for clean baseline)
|
|
106
|
+
|
|
107
|
+
Wave 1: B034
|
|
108
|
+
Wave 2: B035
|
|
109
|
+
Wave 3: B036 + B037 (parallel — different spec files)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## B034: extract-exit-calls
|
|
115
|
+
|
|
116
|
+
**Files touched:** `lib/appydave/tools/dam/errors.rb`, `lib/appydave/tools/dam/s3_scan_command.rb`, `lib/appydave/tools/dam/s3_arg_parser.rb`
|
|
117
|
+
|
|
118
|
+
### Step 1 — Add UsageError to errors.rb
|
|
119
|
+
|
|
120
|
+
After the existing `GitOperationError` line, add:
|
|
121
|
+
|
|
122
|
+
```ruby
|
|
123
|
+
# Raised when CLI arguments are invalid or missing
|
|
124
|
+
class UsageError < DamError; end
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Step 2 — Fix S3ScanCommand (line 55)
|
|
128
|
+
|
|
129
|
+
Replace:
|
|
130
|
+
```ruby
|
|
131
|
+
unless File.exist?(manifest_path)
|
|
132
|
+
puts "❌ Manifest not found: #{manifest_path}"
|
|
133
|
+
puts " Run: dam manifest #{brand_key}"
|
|
134
|
+
puts " Then retry: dam s3-scan #{brand_key}"
|
|
135
|
+
exit 1
|
|
136
|
+
end
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
With:
|
|
140
|
+
```ruby
|
|
141
|
+
unless File.exist?(manifest_path)
|
|
142
|
+
raise Appydave::Tools::Dam::ConfigurationError,
|
|
143
|
+
"Manifest not found: #{manifest_path}. Run: dam manifest #{brand_key}"
|
|
144
|
+
end
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Remove the `puts` lines above — the exception message carries the information. VatCLI's rescue prints it.
|
|
148
|
+
|
|
149
|
+
### Step 3 — Fix S3ArgParser (4 exit locations)
|
|
150
|
+
|
|
151
|
+
**parse_s3 — PWD auto-detect fail (around line 25):**
|
|
152
|
+
|
|
153
|
+
Replace:
|
|
154
|
+
```ruby
|
|
155
|
+
if brand.nil? || project_id.nil?
|
|
156
|
+
puts '❌ Could not auto-detect brand/project from current directory'
|
|
157
|
+
puts "Usage: dam #{command} <brand> <project> [--dry-run]"
|
|
158
|
+
exit 1
|
|
159
|
+
end
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
With:
|
|
163
|
+
```ruby
|
|
164
|
+
raise Appydave::Tools::Dam::UsageError,
|
|
165
|
+
"Could not auto-detect brand/project from current directory. Usage: dam #{command} <brand> <project> [--dry-run]" if brand.nil? || project_id.nil?
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
**parse_s3 — invalid brand (around line 43):**
|
|
169
|
+
|
|
170
|
+
Replace:
|
|
171
|
+
```ruby
|
|
172
|
+
unless valid_brand?(brand_arg)
|
|
173
|
+
puts "❌ Invalid brand: '#{brand_arg}'"
|
|
174
|
+
puts ''
|
|
175
|
+
puts 'Valid brands:'
|
|
176
|
+
# ... puts lines ...
|
|
177
|
+
puts "Usage: dam #{command} <brand> <project> [--dry-run]"
|
|
178
|
+
exit 1
|
|
179
|
+
end
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
With:
|
|
183
|
+
```ruby
|
|
184
|
+
unless valid_brand?(brand_arg)
|
|
185
|
+
raise Appydave::Tools::Dam::UsageError,
|
|
186
|
+
"Invalid brand: '#{brand_arg}'. Valid brands: appydave, voz, aitldr, kiros, joy, ss. Usage: dam #{command} <brand> <project> [--dry-run]"
|
|
187
|
+
end
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
**parse_discover — missing args (around line 100):**
|
|
191
|
+
|
|
192
|
+
Replace:
|
|
193
|
+
```ruby
|
|
194
|
+
if brand_arg.nil? || project_arg.nil?
|
|
195
|
+
puts 'Usage: dam s3-discover <brand> <project> [--shareable]'
|
|
196
|
+
# ... puts lines ...
|
|
197
|
+
exit 1
|
|
198
|
+
end
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
With:
|
|
202
|
+
```ruby
|
|
203
|
+
raise Appydave::Tools::Dam::UsageError,
|
|
204
|
+
'Usage: dam s3-discover <brand> <project> [--shareable]' if brand_arg.nil? || project_arg.nil?
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
**show_share_usage_and_exit — rename and replace:**
|
|
208
|
+
|
|
209
|
+
Rename the method to `raise_share_usage_error`. Replace:
|
|
210
|
+
```ruby
|
|
211
|
+
def show_share_usage_and_exit
|
|
212
|
+
puts 'Usage: dam s3-share ...'
|
|
213
|
+
# ... puts lines ...
|
|
214
|
+
exit 1
|
|
215
|
+
end
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
With:
|
|
219
|
+
```ruby
|
|
220
|
+
def raise_share_usage_error
|
|
221
|
+
raise Appydave::Tools::Dam::UsageError,
|
|
222
|
+
'Usage: dam s3-share <brand> <project> <file> [--expires 7d] [--download]'
|
|
223
|
+
end
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Update the caller in `parse_share`:
|
|
227
|
+
```ruby
|
|
228
|
+
# Before:
|
|
229
|
+
show_share_usage_and_exit if brand_arg.nil? || project_arg.nil? || file_arg.nil?
|
|
230
|
+
# After:
|
|
231
|
+
raise_share_usage_error if brand_arg.nil? || project_arg.nil? || file_arg.nil?
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Done when B034 is complete
|
|
235
|
+
- `errors.rb` contains `UsageError < DamError`
|
|
236
|
+
- 0 occurrences of `exit 1` in `s3_scan_command.rb` or `s3_arg_parser.rb`
|
|
237
|
+
- `show_share_usage_and_exit` renamed to `raise_share_usage_error`
|
|
238
|
+
- `bundle exec rubocop --format clang` → 0 offenses
|
|
239
|
+
- `RUBYOPT="-W0" bundle exec rspec` → 847 examples, 0 failures (no new specs in this WU)
|
|
240
|
+
- `git status` clean before `kfix`
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## B035: extract-env-side-effect
|
|
245
|
+
|
|
246
|
+
**Files touched:** `lib/appydave/tools/dam/s3_arg_parser.rb`, `bin/dam`
|
|
247
|
+
|
|
248
|
+
**Prerequisite:** B034 complete.
|
|
249
|
+
|
|
250
|
+
### Step 1 — Remove ENV side-effects from S3ArgParser
|
|
251
|
+
|
|
252
|
+
In `parse_s3` (around line 51): remove `ENV['BRAND_PATH'] = ...`. Add `brand_path:` to returned hash:
|
|
253
|
+
|
|
254
|
+
```ruby
|
|
255
|
+
# Before:
|
|
256
|
+
ENV['BRAND_PATH'] = Appydave::Tools::Dam::Config.brand_path(brand)
|
|
257
|
+
{ brand: brand_key, project: project_id, dry_run: dry_run, force: force }
|
|
258
|
+
|
|
259
|
+
# After (auto-detect path):
|
|
260
|
+
brand_path = Appydave::Tools::Dam::Config.brand_path(brand_key)
|
|
261
|
+
{ brand: brand_key, project: project_id, dry_run: dry_run, force: force, brand_path: brand_path }
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Note: In the `brand_arg.nil?` branch, `brand` is set from `ProjectResolver.detect_from_pwd` — use `brand` not `brand_arg` for path resolution there.
|
|
265
|
+
|
|
266
|
+
In `parse_share` (around line 82): remove `ENV['BRAND_PATH'] = ...`. Add `brand_path:` to returned hash:
|
|
267
|
+
|
|
268
|
+
```ruby
|
|
269
|
+
# Before:
|
|
270
|
+
ENV['BRAND_PATH'] = Appydave::Tools::Dam::Config.brand_path(brand)
|
|
271
|
+
{ brand: brand_key, project: project_id, file: file_arg, expires: expires, download: download }
|
|
272
|
+
|
|
273
|
+
# After:
|
|
274
|
+
brand_path = Appydave::Tools::Dam::Config.brand_path(brand)
|
|
275
|
+
{ brand: brand_key, project: project_id, file: file_arg, expires: expires, download: download, brand_path: brand_path }
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
In `parse_discover` (around line 108): same pattern:
|
|
279
|
+
|
|
280
|
+
```ruby
|
|
281
|
+
# Before:
|
|
282
|
+
ENV['BRAND_PATH'] = Appydave::Tools::Dam::Config.brand_path(brand)
|
|
283
|
+
{ brand_key: brand_key, project_id: project_id, shareable: shareable }
|
|
284
|
+
|
|
285
|
+
# After:
|
|
286
|
+
brand_path = Appydave::Tools::Dam::Config.brand_path(brand)
|
|
287
|
+
{ brand_key: brand_key, project_id: project_id, shareable: shareable, brand_path: brand_path }
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### Step 2 — Update VatCLI callers in bin/dam
|
|
291
|
+
|
|
292
|
+
Search for every call to `S3ArgParser.parse_s3`, `S3ArgParser.parse_share`, `S3ArgParser.parse_discover` in `bin/dam`. After each call, add:
|
|
293
|
+
|
|
294
|
+
```ruby
|
|
295
|
+
options = Appydave::Tools::Dam::S3ArgParser.parse_s3(args, 's3-up')
|
|
296
|
+
ENV['BRAND_PATH'] = options[:brand_path]
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
Same pattern for `parse_share` and `parse_discover` callers.
|
|
300
|
+
|
|
301
|
+
**Affected VatCLI methods** (grep `S3ArgParser` in bin/dam to confirm):
|
|
302
|
+
- `s3_up_command` → uses `parse_s3`
|
|
303
|
+
- `s3_down_command` → uses `parse_s3`
|
|
304
|
+
- `s3_status_command` → uses `parse_s3`
|
|
305
|
+
- `s3_cleanup_remote_command` → uses `parse_s3`
|
|
306
|
+
- `s3_cleanup_local_command` → uses `parse_s3`
|
|
307
|
+
- `archive_command` → uses `parse_s3`
|
|
308
|
+
- `s3_share_command` → uses `parse_share`
|
|
309
|
+
- `s3_discover_command` → uses `parse_discover`
|
|
310
|
+
|
|
311
|
+
### Done when B035 is complete
|
|
312
|
+
- 0 occurrences of `ENV['BRAND_PATH'] =` in `s3_arg_parser.rb`
|
|
313
|
+
- All 3 parse methods return `brand_path:` in their result hash
|
|
314
|
+
- All VatCLI callers set `ENV['BRAND_PATH'] = options[:brand_path]` after parsing
|
|
315
|
+
- `bundle exec rubocop --format clang` → 0 offenses
|
|
316
|
+
- `RUBYOPT="-W0" bundle exec rspec` → 847 examples, 0 failures
|
|
317
|
+
- `git status` clean before `kfix`
|
|
318
|
+
|
|
319
|
+
---
|
|
320
|
+
|
|
321
|
+
## B036: tests-s3-scan-command
|
|
322
|
+
|
|
323
|
+
**File touched:** `spec/appydave/tools/dam/s3_scan_command_spec.rb`
|
|
324
|
+
|
|
325
|
+
**Prerequisite:** B034 complete (exceptions must exist before testing them).
|
|
326
|
+
|
|
327
|
+
Current state: 2 smoke tests (`respond_to` checks). Replace with real behaviour tests.
|
|
328
|
+
|
|
329
|
+
### Target: 8–10 examples covering
|
|
330
|
+
|
|
331
|
+
```ruby
|
|
332
|
+
RSpec.describe Appydave::Tools::Dam::S3ScanCommand do
|
|
333
|
+
include_context 'with vat filesystem and brands', brands: %w[appydave]
|
|
334
|
+
|
|
335
|
+
let(:scanner) { described_class.new }
|
|
336
|
+
let(:brand_key) { 'appydave' }
|
|
337
|
+
|
|
338
|
+
before do
|
|
339
|
+
allow(Appydave::Tools::Dam::S3Scanner).to receive(:new).and_return(mock_s3_scanner)
|
|
340
|
+
allow(Appydave::Tools::Configuration::Config).to receive(:configure)
|
|
341
|
+
allow(Appydave::Tools::Configuration::Config).to receive(:brands).and_return(mock_brands_config)
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
describe '#scan_single' do
|
|
345
|
+
context 'when manifest exists and S3 returns results' do
|
|
346
|
+
# Write a fixture projects.json to brand_path
|
|
347
|
+
# Mock scanner.scan_all_projects to return { 'b65-test' => { file_count: 3, total_bytes: 1000, last_modified: '...' } }
|
|
348
|
+
# Expect: manifest updated, updated_count == 1, no error
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
context 'when manifest not found' do
|
|
352
|
+
# Do NOT write projects.json
|
|
353
|
+
# Mock scanner to return results
|
|
354
|
+
# expect { scanner.scan_single(brand_key) }.to raise_error(Appydave::Tools::Dam::ConfigurationError, /Manifest not found/)
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
context 'when S3 returns empty results' do
|
|
358
|
+
# Mock scanner.scan_all_projects to return {}
|
|
359
|
+
# Expect: early return (no raise, no manifest write)
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
context 'when S3 has orphaned projects' do
|
|
363
|
+
# projects.json has ['b65-test'], S3 returns { 'b65-test' => ..., 'b99-orphan' => ... }
|
|
364
|
+
# Expect: orphaned_projects contains 'b99-orphan', matched_projects contains 'b65-test'
|
|
365
|
+
end
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
describe '#scan_all' do
|
|
369
|
+
context 'when one brand fails' do
|
|
370
|
+
# brands_config.brands returns [appydave, voz]
|
|
371
|
+
# scan_single raises for voz
|
|
372
|
+
# Expect: results array has appydave success: true, voz success: false
|
|
373
|
+
# Expect: no re-raise (scan_all rescues per-brand)
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
**Fixture pattern for projects.json:**
|
|
380
|
+
```ruby
|
|
381
|
+
manifest = {
|
|
382
|
+
config: { last_updated: Time.now.utc.iso8601, note: 'test' },
|
|
383
|
+
projects: [{ id: 'b65-test-project', storage: { s3: {} } }]
|
|
384
|
+
}
|
|
385
|
+
File.write(File.join(brand_path, 'projects.json'), JSON.generate(manifest))
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
### Done when B036 is complete
|
|
389
|
+
- `spec/appydave/tools/dam/s3_scan_command_spec.rb` has 8+ real behaviour examples
|
|
390
|
+
- `respond_to` smoke tests removed or replaced
|
|
391
|
+
- `RUBYOPT="-W0" bundle exec rspec` → 855+ examples, 0 failures
|
|
392
|
+
- `bundle exec rubocop --format clang` → 0 offenses
|
|
393
|
+
- `git status` clean before `kfix`
|
|
394
|
+
|
|
395
|
+
---
|
|
396
|
+
|
|
397
|
+
## B037: tests-local-sync-status
|
|
398
|
+
|
|
399
|
+
**File touched:** `spec/appydave/tools/dam/local_sync_status_spec.rb`
|
|
400
|
+
|
|
401
|
+
**No prerequisites** — but run after B035 for clean baseline.
|
|
402
|
+
|
|
403
|
+
### Gaps to fill
|
|
404
|
+
|
|
405
|
+
**1. `:partial` case in `#enrich!`**
|
|
406
|
+
|
|
407
|
+
Set up `s3-staging` folder with 2 files but `file_count: 3` in S3 data:
|
|
408
|
+
```ruby
|
|
409
|
+
context 'when local s3-staging has fewer files than S3' do
|
|
410
|
+
before do
|
|
411
|
+
FileUtils.mkdir_p(File.join(appydave_path, 'b65-test', 's3-staging'))
|
|
412
|
+
FileUtils.touch(File.join(appydave_path, 'b65-test', 's3-staging', 'file1.mp4'))
|
|
413
|
+
FileUtils.touch(File.join(appydave_path, 'b65-test', 's3-staging', 'file2.mp4'))
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
it 'sets status to :partial' do
|
|
417
|
+
matched = { 'b65-test' => { file_count: 3 } }
|
|
418
|
+
described_class.enrich!(matched, 'appydave')
|
|
419
|
+
expect(matched['b65-test'][:local_status]).to eq(:partial)
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
it 'sets local_file_count to 2' do
|
|
423
|
+
matched = { 'b65-test' => { file_count: 3 } }
|
|
424
|
+
described_class.enrich!(matched, 'appydave')
|
|
425
|
+
expect(matched['b65-test'][:local_file_count]).to eq(2)
|
|
426
|
+
end
|
|
427
|
+
end
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
**2. `local_file_count` assertion in existing `:synced` test**
|
|
431
|
+
|
|
432
|
+
Find the existing synced context and add:
|
|
433
|
+
```ruby
|
|
434
|
+
it 'sets local_file_count correctly' do
|
|
435
|
+
expect(data[:local_file_count]).to eq(3) # adjust to match your fixture
|
|
436
|
+
end
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
**3. Zone.Identifier exclusion**
|
|
440
|
+
|
|
441
|
+
Windows NTFS streams appear as `filename:Zone.Identifier` in samba mounts. These must NOT count as local files:
|
|
442
|
+
```ruby
|
|
443
|
+
context 'when s3-staging contains Zone.Identifier files' do
|
|
444
|
+
before do
|
|
445
|
+
FileUtils.mkdir_p(File.join(appydave_path, 'b65-test', 's3-staging'))
|
|
446
|
+
FileUtils.touch(File.join(appydave_path, 'b65-test', 's3-staging', 'file1.mp4'))
|
|
447
|
+
FileUtils.touch(File.join(appydave_path, 'b65-test', 's3-staging', 'file1.mp4:Zone.Identifier'))
|
|
448
|
+
FileUtils.touch(File.join(appydave_path, 'b65-test', 's3-staging', 'file2.mp4'))
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
it 'excludes Zone.Identifier files from local_file_count' do
|
|
452
|
+
matched = { 'b65-test' => { file_count: 2 } }
|
|
453
|
+
described_class.enrich!(matched, 'appydave')
|
|
454
|
+
expect(matched['b65-test'][:local_file_count]).to eq(2)
|
|
455
|
+
expect(matched['b65-test'][:local_status]).to eq(:synced)
|
|
456
|
+
end
|
|
457
|
+
end
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
**4. Unknown status guard in `#format`**
|
|
461
|
+
|
|
462
|
+
```ruby
|
|
463
|
+
describe '.format' do
|
|
464
|
+
it 'returns Unknown for unrecognised status' do
|
|
465
|
+
expect(described_class.format(:unknown, nil, 0)).to eq('Unknown')
|
|
466
|
+
end
|
|
467
|
+
end
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
Check the actual `format` implementation first — if it already has an `else` branch returning `'Unknown'`, this is a documentation test. If it has no `else`, add one to `local_sync_status.rb` before writing this spec.
|
|
471
|
+
|
|
472
|
+
### Done when B037 is complete
|
|
473
|
+
- `:partial` case tested with `local_file_count` assertion
|
|
474
|
+
- `:synced` case has `local_file_count` assertion
|
|
475
|
+
- `Zone.Identifier` exclusion tested
|
|
476
|
+
- `format(:unknown, ...)` tested
|
|
477
|
+
- `RUBYOPT="-W0" bundle exec rspec` → 851+ examples, 0 failures
|
|
478
|
+
- `bundle exec rubocop --format clang` → 0 offenses
|
|
479
|
+
- `git status` clean before `kfix`
|
|
480
|
+
|
|
481
|
+
---
|
|
482
|
+
|
|
483
|
+
## Success Criteria (Every Work Unit)
|
|
484
|
+
|
|
485
|
+
Every work unit must satisfy ALL of the following before marking `[x]`:
|
|
486
|
+
|
|
487
|
+
- [ ] `RUBYOPT="-W0" bundle exec rspec` — 847+ examples, 0 failures (rising with each WU)
|
|
488
|
+
- [ ] `bundle exec rubocop --format clang` — 0 offenses
|
|
489
|
+
- [ ] Line coverage stays ≥ 85.92%
|
|
490
|
+
- [ ] Any new `.rb` files start with `# frozen_string_literal: true`
|
|
491
|
+
- [ ] `git status` confirmed clean before `kfix`
|
|
492
|
+
|
|
493
|
+
---
|
|
494
|
+
|
|
495
|
+
## Reference Patterns
|
|
496
|
+
|
|
497
|
+
### Shared Context for DAM Specs — THE STANDARD PATTERN
|
|
498
|
+
|
|
499
|
+
```ruby
|
|
500
|
+
# spec/appydave/tools/dam/some_class_spec.rb
|
|
501
|
+
# frozen_string_literal: true
|
|
502
|
+
|
|
503
|
+
require 'spec_helper'
|
|
504
|
+
|
|
505
|
+
RSpec.describe Appydave::Tools::Dam::SomeClass do
|
|
506
|
+
include_context 'with vat filesystem and brands', brands: %w[appydave voz]
|
|
507
|
+
|
|
508
|
+
before do
|
|
509
|
+
FileUtils.mkdir_p(File.join(appydave_path, 'b65-test-project'))
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
describe '.some_method' do
|
|
513
|
+
it 'does the thing' do
|
|
514
|
+
expect(described_class.some_method('appydave')).to eq('expected')
|
|
515
|
+
end
|
|
516
|
+
end
|
|
517
|
+
end
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
**Available from shared context:**
|
|
521
|
+
- `temp_folder` — temp root (auto-cleaned after each example)
|
|
522
|
+
- `projects_root` — `/path/to/temp/video-projects`
|
|
523
|
+
- `appydave_path`, `voz_path`, etc. — brand dirs (created on demand)
|
|
524
|
+
- `SettingsConfig#video_projects_root` is mocked to return `projects_root`
|
|
525
|
+
|
|
526
|
+
### FileHelper — Use These, Don't Duplicate
|
|
527
|
+
|
|
528
|
+
```ruby
|
|
529
|
+
Appydave::Tools::Dam::FileHelper.format_size(bytes) # "1.5 GB"
|
|
530
|
+
Appydave::Tools::Dam::FileHelper.calculate_directory_size(path) # Integer bytes
|
|
531
|
+
Appydave::Tools::Dam::FileHelper.format_age(time) # "3d", "2w", etc.
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
### BrandResolver — All Brand Name Transformations
|
|
535
|
+
|
|
536
|
+
```ruby
|
|
537
|
+
BrandResolver.expand('appydave') # => 'v-appydave'
|
|
538
|
+
BrandResolver.expand('ad') # => 'v-appydave' (shortcut)
|
|
539
|
+
BrandResolver.normalize('v-appydave') # => 'appydave'
|
|
540
|
+
BrandResolver.validate('appydave') # => 'appydave' or raises BrandNotFoundError
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
### Typed Exception Pattern
|
|
544
|
+
|
|
545
|
+
```ruby
|
|
546
|
+
raise Appydave::Tools::Dam::ConfigurationError, 'Manifest not found: /path'
|
|
547
|
+
raise Appydave::Tools::Dam::UsageError, 'Usage: dam s3-up <brand> <project>'
|
|
548
|
+
raise ProjectNotFoundError, 'Project name is required' if project_hint.nil?
|
|
549
|
+
raise BrandNotFoundError.new(brand, available_brands, fuzzy_suggestions)
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
### Config.brand_path — Correct Call
|
|
553
|
+
|
|
554
|
+
```ruby
|
|
555
|
+
# brand_key is the short form ('appydave'), brand is expanded ('v-appydave')
|
|
556
|
+
brand_path = Appydave::Tools::Dam::Config.brand_path(brand_key)
|
|
557
|
+
# OR:
|
|
558
|
+
brand_path = Appydave::Tools::Dam::Config.brand_path(brand)
|
|
559
|
+
# Both work — Config.brand_path resolves via BrandResolver internally
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
---
|
|
563
|
+
|
|
564
|
+
## Anti-Patterns to Avoid
|
|
565
|
+
|
|
566
|
+
- ❌ `exit 1` in library code — use typed exceptions from `errors.rb` instead
|
|
567
|
+
- ❌ `ENV['BRAND_PATH'] = ...` in library code — return `brand_path:` in result hash; set ENV in VatCLI
|
|
568
|
+
- ❌ Inline brand transformations — never write `"v-#{brand}"` outside BrandResolver
|
|
569
|
+
- ❌ `format_bytes` — does not exist; use `FileHelper.format_size`
|
|
570
|
+
- ❌ Mocking Config class methods in DAM specs — use shared filesystem context instead
|
|
571
|
+
- ❌ Multiple `before` blocks in same RSpec context — merge them (triggers RSpec/ScatteredSetup)
|
|
572
|
+
- ❌ `$?` for subprocess status — use `$CHILD_STATUS` (rubocop Style/SpecialGlobalVars)
|
|
573
|
+
- ❌ `raise 'string error'` in DAM module — use typed exceptions from `errors.rb`
|
|
574
|
+
- ❌ `include FileUtils` — use dam's `FileHelper` instead
|
|
575
|
+
- ❌ Hardcoded header strings for table output — always use `format()` matching data row format
|
|
576
|
+
- ❌ Adding new `Config.configure` calls — memoized but called redundantly; don't spread further
|
|
577
|
+
|
|
578
|
+
---
|
|
579
|
+
|
|
580
|
+
## Mock Patterns
|
|
581
|
+
|
|
582
|
+
### ENV Stubbing (if needed in specs)
|
|
583
|
+
|
|
584
|
+
```ruby
|
|
585
|
+
allow(ENV).to receive(:[]).and_call_original
|
|
586
|
+
allow(ENV).to receive(:[]).with('BRAND_PATH').and_return('/tmp/test/v-appydave')
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
**Do NOT use climate_control gem** — project doesn't have it.
|
|
590
|
+
|
|
591
|
+
### S3Scanner Mocking (for B036)
|
|
592
|
+
|
|
593
|
+
```ruby
|
|
594
|
+
let(:mock_s3_scanner) { instance_double(Appydave::Tools::Dam::S3Scanner) }
|
|
595
|
+
|
|
596
|
+
before do
|
|
597
|
+
allow(Appydave::Tools::Dam::S3Scanner).to receive(:new).with(brand_key).and_return(mock_s3_scanner)
|
|
598
|
+
allow(mock_s3_scanner).to receive(:scan_all_projects).and_return({
|
|
599
|
+
'b65-test-project' => { file_count: 3, total_bytes: 1_500_000, last_modified: '2025-01-01T00:00:00Z' }
|
|
600
|
+
})
|
|
601
|
+
end
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
### Config::Brands Mocking (for B036 scan_all)
|
|
605
|
+
|
|
606
|
+
```ruby
|
|
607
|
+
let(:mock_brands_config) { instance_double(Appydave::Tools::Configuration::BrandsConfig) }
|
|
608
|
+
let(:mock_brand_info) { instance_double(Appydave::Tools::Configuration::BrandInfo, key: 'appydave') }
|
|
609
|
+
|
|
610
|
+
before do
|
|
611
|
+
allow(mock_brands_config).to receive(:brands).and_return([mock_brand_info])
|
|
612
|
+
end
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
---
|
|
616
|
+
|
|
617
|
+
## Quality Gates
|
|
618
|
+
|
|
619
|
+
- **Tests:** `RUBYOPT="-W0" bundle exec rspec` — 847+ examples, 0 failures
|
|
620
|
+
- **Lint:** `bundle exec rubocop --format clang` — 0 offenses (CI will reject)
|
|
621
|
+
- **Coverage:** ≥ 85.92% line coverage
|
|
622
|
+
- **frozen_string_literal:** Required on every new `.rb` file
|
|
623
|
+
- **Commit format:** `kfeat`/`kfix` only — triggers semantic versioning + CI wait
|
|
624
|
+
- **Pre-commit:** Always run `git status` before `kfix` — confirm staged files
|
|
625
|
+
|
|
626
|
+
---
|
|
627
|
+
|
|
628
|
+
## Learnings
|
|
629
|
+
|
|
630
|
+
### From DAM Enhancement Sprint (Jan 2025)
|
|
631
|
+
|
|
632
|
+
- **BrandResolver is the critical path.** All `dam` commands flow through it. Any change to brand resolution must be tested with all shortcuts.
|
|
633
|
+
- **`Regexp.last_match` is reset by `.sub()` calls.** Always capture regex groups BEFORE any string transformation.
|
|
634
|
+
- **`Config.configure` is memoized but called redundantly.** Don't add new calls.
|
|
635
|
+
- **Table format() pattern is non-obvious.** Headers misaligned 3 times in UAT. Always verify with real data.
|
|
636
|
+
|
|
637
|
+
### From micro-cleanup (2026-03-19)
|
|
638
|
+
|
|
639
|
+
- **Dirty working tree + kfix = accidental staging.** Always run `git status` before committing.
|
|
640
|
+
- **Pre-existing "already fixed" items:** Check B-items aren't already done before acting (B031, B033, B015, B019 were all already committed).
|
|
641
|
+
|
|
642
|
+
### From Architectural Review (2026-03-19)
|
|
643
|
+
|
|
644
|
+
- **bin/dam is the primary DAM CLI entry point.** Regressions here affect real workflows. Test every command path after extraction.
|
|
645
|
+
- **`ENV['BRAND_PATH']` is set in 5 places in bin/dam.** Three are in parse methods (being extracted to B035). Two remain in `generate_single_manifest` and `sync_ssd_command` — out of scope for this campaign.
|
|
646
|
+
- **Do NOT attempt B020 (split S3Operations) in this campaign.** Different class, different risk profile.
|
|
647
|
+
|
|
648
|
+
### From extract-vat-cli (2026-03-19)
|
|
649
|
+
|
|
650
|
+
- **valid_brand? needs Config.brands mock.** The shared filesystem context only mocks `SettingsConfig`. When testing code that calls `Config.brands`, mock it separately: `allow(Appydave::Tools::Configuration::Config).to receive(:brands).and_return(...)`.
|
|
651
|
+
- **rubocop-disable directives become redundant when methods move.** After extracting to a library class, check the VatCLI file for orphaned disable/enable pairs and remove them.
|
|
652
|
+
- **VatCLI rescue blocks already catch StandardError.** No changes to VatCLI rescue needed when replacing `exit 1` with exceptions — the existing rescue chain handles it.
|
|
653
|
+
- **S3ScanCommand#scan_all already rescues per-brand.** The `rescue StandardError` inside `scan_all` means per-brand failures are isolated. Once `scan_single` raises instead of calling `exit`, `scan_all` catches it correctly — no structural change needed.
|