zone 0.1.0 → 0.1.2

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.
data/TEST_PLAN.md ADDED
@@ -0,0 +1,911 @@
1
+ # Zone CLI - Comprehensive Test Plan
2
+
3
+ ## Overview
4
+
5
+ Based on Minitest documentation (Rule 1), this plan covers:
6
+ 1. **Unit tests** for each domain class
7
+ 2. **Integration tests** for CLI command execution
8
+ 3. **All assertion types** appropriate for each test
9
+
10
+ ## Minitest Assertions Available
11
+
12
+ From `ri Minitest::Assertions`:
13
+
14
+ ### **Equality & Comparison**
15
+ - `assert_equal(exp, act)` - Fails unless exp == act
16
+ - `refute_equal(exp, act)` - Fails if exp == act
17
+ - `assert_same(exp, act)` - Fails unless exp.equal?(act)
18
+ - `assert_nil(obj)` - Fails unless obj is nil
19
+ - `refute_nil(obj)` - Fails if obj is nil
20
+
21
+ ### **Type Checking**
22
+ - `assert_instance_of(cls, obj)` - Fails unless obj is an instance of cls
23
+ - `assert_kind_of(cls, obj)` - Fails unless obj is a kind of cls
24
+ - `assert_respond_to(obj, meth)` - Fails unless obj responds to meth
25
+
26
+ ### **Collections**
27
+ - `assert_includes(collection, obj)` - Fails unless collection includes obj
28
+ - `assert_empty(obj)` - Fails unless obj is empty
29
+ - `refute_empty(obj)` - Fails if obj is empty
30
+
31
+ ### **Pattern Matching**
32
+ - `assert_match(matcher, obj)` - Fails unless matcher =~ obj
33
+ - `refute_match(matcher, obj)` - Fails if matcher =~ obj
34
+
35
+ ### **Exceptions**
36
+ - `assert_raises(*exp) { }` - Fails unless block raises one of exp
37
+ - `assert_silent { }` - Fails if block outputs to stdout or stderr
38
+
39
+ ### **IO Capture**
40
+ - `capture_io { }` - Captures $stdout and $stderr (for in-process)
41
+ - `capture_subprocess_io { }` - Captures subprocess IO (for shell commands)
42
+
43
+ ---
44
+
45
+ ## Test File Structure
46
+
47
+ ```
48
+ test/
49
+ test_helper.rb # Common test setup
50
+ zone/
51
+ test_timestamp.rb # Zone::Timestamp unit tests
52
+ test_field_mapping.rb # Zone::FieldMapping unit tests
53
+ test_field_line.rb # Zone::FieldLine unit tests
54
+ test_zone_module.rb # Zone.find module method tests
55
+ integration/
56
+ test_cli_integration.rb # End-to-end CLI tests
57
+ ```
58
+
59
+ ---
60
+
61
+ ## 1. test/test_helper.rb
62
+
63
+ ```ruby
64
+ # frozen_string_literal: true
65
+
66
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
67
+ require "zone"
68
+ require "minitest/autorun"
69
+
70
+ # Make diffs prettier
71
+ Minitest::Test.make_my_diffs_pretty!
72
+ ```
73
+
74
+ ---
75
+
76
+ ## 2. test/zone/test_timestamp.rb
77
+
78
+ ### **Purpose**: Test Zone::Timestamp parsing, conversion, and formatting
79
+
80
+ ### **Assertions to Use**:
81
+ - `assert_instance_of` - Verify return type
82
+ - `assert_equal` - Compare formatted output
83
+ - `assert_raises` - Test error cases
84
+ - `assert_respond_to` - Verify API methods exist
85
+ - `assert_match` - Verify output format patterns
86
+
87
+ ### **Test Cases**:
88
+
89
+ ```ruby
90
+ # frozen_string_literal: true
91
+
92
+ require "test_helper"
93
+
94
+ class TestTimestamp < Minitest::Test
95
+ def test_parse_time_object
96
+ time = Time.now
97
+ timestamp = Zone::Timestamp.parse(time)
98
+
99
+ assert_instance_of Zone::Timestamp, timestamp
100
+ assert_equal time, timestamp.time
101
+ end
102
+
103
+ def test_parse_datetime_object
104
+ datetime = DateTime.now
105
+ timestamp = Zone::Timestamp.parse(datetime)
106
+
107
+ assert_instance_of Zone::Timestamp, timestamp
108
+ assert_instance_of Time, timestamp.time
109
+ end
110
+
111
+ def test_parse_date_object
112
+ date = Date.today
113
+ timestamp = Zone::Timestamp.parse(date)
114
+
115
+ assert_instance_of Zone::Timestamp, timestamp
116
+ assert_instance_of Time, timestamp.time
117
+ end
118
+
119
+ def test_parse_unix_timestamp_seconds
120
+ # Unix epoch for 2025-01-15 10:30:00 UTC
121
+ timestamp = Zone::Timestamp.parse("1736937000")
122
+
123
+ assert_equal 1736937000, timestamp.time.to_i
124
+ end
125
+
126
+ def test_parse_unix_timestamp_milliseconds
127
+ # 13 digits - milliseconds precision
128
+ timestamp = Zone::Timestamp.parse("1736937000000")
129
+
130
+ assert_equal 1736937000, timestamp.time.to_i
131
+ end
132
+
133
+ def test_parse_iso8601_string
134
+ timestamp = Zone::Timestamp.parse("2025-01-15T10:30:00Z")
135
+
136
+ assert_equal 2025, timestamp.time.year
137
+ assert_equal 1, timestamp.time.month
138
+ assert_equal 15, timestamp.time.day
139
+ assert_equal 10, timestamp.time.hour
140
+ assert_equal 30, timestamp.time.min
141
+ end
142
+
143
+ def test_parse_relative_time_ago
144
+ timestamp = Zone::Timestamp.parse("5 minutes ago")
145
+ diff = Time.now - timestamp.time
146
+
147
+ assert_in_delta 300, diff, 2 # Within 2 seconds of 5 minutes
148
+ end
149
+
150
+ def test_parse_relative_time_from_now
151
+ timestamp = Zone::Timestamp.parse("1 hour from now")
152
+ diff = timestamp.time - Time.now
153
+
154
+ assert_in_delta 3600, diff, 2 # Within 2 seconds of 1 hour
155
+ end
156
+
157
+ def test_parse_invalid_input_raises_error
158
+ error = assert_raises(ArgumentError) do
159
+ Zone::Timestamp.parse("not a valid timestamp")
160
+ end
161
+
162
+ assert_match(/Could not parse time/, error.message)
163
+ end
164
+
165
+ def test_in_zone_returns_new_timestamp
166
+ original = Zone::Timestamp.parse("2025-01-15T10:30:00Z")
167
+ tokyo = original.in_zone("Tokyo")
168
+
169
+ assert_instance_of Zone::Timestamp, tokyo
170
+ refute_same original, tokyo
171
+ assert_equal "Tokyo", tokyo.zone
172
+ end
173
+
174
+ def test_in_utc_returns_new_timestamp
175
+ local = Zone::Timestamp.parse(Time.now.to_s)
176
+ utc = local.in_utc
177
+
178
+ assert_instance_of Zone::Timestamp, utc
179
+ assert_equal "UTC", utc.zone
180
+ end
181
+
182
+ def test_in_local_returns_new_timestamp
183
+ utc = Zone::Timestamp.parse("2025-01-15T10:30:00Z")
184
+ local = utc.in_local
185
+
186
+ assert_instance_of Zone::Timestamp, local
187
+ assert_equal "local", local.zone
188
+ end
189
+
190
+ def test_to_iso8601_format
191
+ timestamp = Zone::Timestamp.parse("2025-01-15T10:30:00Z")
192
+ formatted = timestamp.to_iso8601
193
+
194
+ assert_match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/, formatted)
195
+ end
196
+
197
+ def test_to_unix_returns_integer
198
+ timestamp = Zone::Timestamp.parse("2025-01-15T10:30:00Z")
199
+ unix = timestamp.to_unix
200
+
201
+ assert_instance_of Integer, unix
202
+ assert_equal 1736937000, unix
203
+ end
204
+
205
+ def test_to_pretty_format
206
+ timestamp = Zone::Timestamp.parse("2025-01-15T10:30:00Z")
207
+ pretty = timestamp.to_pretty
208
+
209
+ assert_match(/Jan \d+/, pretty)
210
+ assert_match(/\d{1,2}:\d{2} [AP]M/, pretty)
211
+ end
212
+
213
+ def test_strftime_custom_format
214
+ timestamp = Zone::Timestamp.parse("2025-01-15T10:30:00Z")
215
+ formatted = timestamp.strftime("%Y-%m-%d")
216
+
217
+ assert_equal "2025-01-15", formatted
218
+ end
219
+
220
+ def test_chainable_operations
221
+ result = Zone::Timestamp
222
+ .parse("2025-01-15T10:30:00Z")
223
+ .in_zone("Tokyo")
224
+ .to_iso8601
225
+
226
+ assert_instance_of String, result
227
+ assert_match(/\+09:00/, result) # Tokyo timezone offset
228
+ end
229
+
230
+ def test_responds_to_all_public_methods
231
+ timestamp = Zone::Timestamp.parse("2025-01-15T10:30:00Z")
232
+
233
+ assert_respond_to timestamp, :in_zone
234
+ assert_respond_to timestamp, :in_utc
235
+ assert_respond_to timestamp, :in_local
236
+ assert_respond_to timestamp, :to_iso8601
237
+ assert_respond_to timestamp, :to_unix
238
+ assert_respond_to timestamp, :to_pretty
239
+ assert_respond_to timestamp, :strftime
240
+ end
241
+ end
242
+ ```
243
+
244
+ ---
245
+
246
+ ## 3. test/zone/test_field_mapping.rb
247
+
248
+ ### **Purpose**: Test Zone::FieldMapping index resolution
249
+
250
+ ### **Assertions to Use**:
251
+ - `assert_equal` - Verify index resolution
252
+ - `assert_raises` - Test error cases with invalid fields
253
+ - `assert_includes` - Check names array
254
+ - `assert_empty` - Verify numeric-only mappings
255
+
256
+ ### **Test Cases**:
257
+
258
+ ```ruby
259
+ # frozen_string_literal: true
260
+
261
+ require "test_helper"
262
+
263
+ class TestFieldMapping < Minitest::Test
264
+ def test_from_fields_creates_mapping
265
+ fields = ["name", "age", "city"]
266
+ mapping = Zone::FieldMapping.from_fields(fields)
267
+
268
+ assert_instance_of Zone::FieldMapping, mapping
269
+ assert_equal 0, mapping["name"]
270
+ assert_equal 1, mapping["age"]
271
+ assert_equal 2, mapping["city"]
272
+ end
273
+
274
+ def test_numeric_creates_numeric_only_mapping
275
+ mapping = Zone::FieldMapping.numeric
276
+
277
+ assert_instance_of Zone::FieldMapping, mapping
278
+ refute mapping.has_names?
279
+ assert_empty mapping.names
280
+ end
281
+
282
+ def test_resolve_with_field_name
283
+ mapping = Zone::FieldMapping.from_fields(["name", "timestamp", "value"])
284
+
285
+ assert_equal 1, mapping.resolve("timestamp")
286
+ end
287
+
288
+ def test_resolve_with_integer_converts_to_zero_based
289
+ mapping = Zone::FieldMapping.numeric
290
+
291
+ assert_equal 0, mapping.resolve(1) # 1-based to 0-based
292
+ assert_equal 1, mapping.resolve(2)
293
+ assert_equal 9, mapping.resolve(10)
294
+ end
295
+
296
+ def test_resolve_with_numeric_string
297
+ mapping = Zone::FieldMapping.numeric
298
+
299
+ assert_equal 0, mapping["1"] # String "1" -> 0
300
+ assert_equal 1, mapping["2"]
301
+ end
302
+
303
+ def test_bracket_operator_alias
304
+ mapping = Zone::FieldMapping.from_fields(["a", "b", "c"])
305
+
306
+ assert_equal 0, mapping["a"]
307
+ assert_equal 1, mapping[2]
308
+ end
309
+
310
+ def test_raises_error_for_missing_field_name
311
+ mapping = Zone::FieldMapping.from_fields(["a", "b"])
312
+
313
+ error = assert_raises(KeyError) do
314
+ mapping.resolve("nonexistent")
315
+ end
316
+
317
+ assert_match(/not found/, error.message)
318
+ end
319
+
320
+ def test_raises_error_for_invalid_key_type
321
+ mapping = Zone::FieldMapping.numeric
322
+
323
+ assert_raises(ArgumentError) do
324
+ mapping.resolve(3.14) # Float is not valid
325
+ end
326
+ end
327
+
328
+ def test_names_returns_field_names
329
+ fields = ["foo", "bar", "baz"]
330
+ mapping = Zone::FieldMapping.from_fields(fields)
331
+
332
+ assert_equal fields, mapping.names
333
+ assert_includes mapping.names, "foo"
334
+ assert_includes mapping.names, "bar"
335
+ end
336
+
337
+ def test_names_empty_for_numeric_mapping
338
+ mapping = Zone::FieldMapping.numeric
339
+
340
+ assert_empty mapping.names
341
+ end
342
+
343
+ def test_has_names_predicate
344
+ with_names = Zone::FieldMapping.from_fields(["a"])
345
+ numeric = Zone::FieldMapping.numeric
346
+
347
+ assert with_names.has_names?
348
+ refute numeric.has_names?
349
+ end
350
+ end
351
+ ```
352
+
353
+ ---
354
+
355
+ ## 4. test/zone/test_field_line.rb
356
+
357
+ ### **Purpose**: Test Zone::FieldLine parsing and transformation
358
+
359
+ ### **Assertions to Use**:
360
+ - `assert_equal` - Compare parsed fields and output
361
+ - `assert_instance_of` - Verify return types
362
+ - `assert_same` - Verify transform returns self (for chaining)
363
+ - `assert_match` - Verify delimiter inference
364
+
365
+ ### **Test Cases**:
366
+
367
+ ```ruby
368
+ # frozen_string_literal: true
369
+
370
+ require "test_helper"
371
+
372
+ class TestFieldLine < Minitest::Test
373
+ def test_parse_simple_comma_delimited
374
+ line = Zone::FieldLine.parse("foo,bar,baz")
375
+
376
+ assert_instance_of Zone::FieldLine, line
377
+ assert_equal ["foo", "bar", "baz"], line.fields
378
+ end
379
+
380
+ def test_parse_tab_delimited
381
+ line = Zone::FieldLine.parse("foo\tbar\tbaz")
382
+
383
+ assert_equal ["foo", "bar", "baz"], line.fields
384
+ end
385
+
386
+ def test_parse_whitespace_delimited
387
+ line = Zone::FieldLine.parse("foo bar baz")
388
+
389
+ assert_equal ["foo", "bar", "baz"], line.fields
390
+ end
391
+
392
+ def test_parse_with_explicit_delimiter
393
+ line = Zone::FieldLine.parse(
394
+ "foo|bar|baz",
395
+ delimiter: "|"
396
+ )
397
+
398
+ assert_equal ["foo", "bar", "baz"], line.fields
399
+ end
400
+
401
+ def test_parse_with_mapping
402
+ mapping = Zone::FieldMapping.from_fields(["name", "value"])
403
+ line = Zone::FieldLine.parse(
404
+ "test,100",
405
+ mapping: mapping
406
+ )
407
+
408
+ assert_equal "test", line["name"]
409
+ assert_equal "100", line["value"]
410
+ end
411
+
412
+ def test_bracket_access_by_index
413
+ line = Zone::FieldLine.parse("a,b,c")
414
+
415
+ assert_equal "a", line[1] # 1-based
416
+ assert_equal "b", line[2]
417
+ assert_equal "c", line[3]
418
+ end
419
+
420
+ def test_bracket_access_by_name
421
+ mapping = Zone::FieldMapping.from_fields(["x", "y"])
422
+ line = Zone::FieldLine.parse(
423
+ "10,20",
424
+ mapping: mapping
425
+ )
426
+
427
+ assert_equal "10", line["x"]
428
+ assert_equal "20", line["y"]
429
+ end
430
+
431
+ def test_transform_by_index
432
+ line = Zone::FieldLine.parse("foo,bar,baz")
433
+ result = line.transform(2) { |v| v.upcase }
434
+
435
+ assert_same line, result # Returns self for chaining
436
+ assert_equal "foo,BAR,baz", line.to_s
437
+ end
438
+
439
+ def test_transform_by_name
440
+ mapping = Zone::FieldMapping.from_fields(["name", "value"])
441
+ line = Zone::FieldLine.parse(
442
+ "test,100",
443
+ mapping: mapping
444
+ )
445
+
446
+ line.transform("value") { |v| v.to_i * 2 }
447
+
448
+ assert_equal "test,200", line.to_s
449
+ end
450
+
451
+ def test_transform_all_fields
452
+ line = Zone::FieldLine.parse("a,b,c")
453
+ line.transform_all(&:upcase)
454
+
455
+ assert_equal "A,B,C", line.to_s
456
+ end
457
+
458
+ def test_to_s_reconstructs_line
459
+ line = Zone::FieldLine.parse("foo,bar,baz")
460
+
461
+ assert_equal "foo,bar,baz", line.to_s
462
+ end
463
+
464
+ def test_to_s_single_field
465
+ line = Zone::FieldLine.parse("2025-01-15T10:30:00Z")
466
+
467
+ assert_equal "2025-01-15T10:30:00Z", line.to_s
468
+ end
469
+
470
+ def test_to_s_uses_tab_for_regex_delimiter
471
+ line = Zone::FieldLine.parse("foo bar baz") # Whitespace
472
+
473
+ assert_match(/\t/, line.to_s) # Should use tab in output
474
+ end
475
+
476
+ def test_to_a_returns_fields_array
477
+ line = Zone::FieldLine.parse("a,b,c")
478
+
479
+ assert_equal ["a", "b", "c"], line.to_a
480
+ end
481
+
482
+ def test_to_h_with_mapping
483
+ mapping = Zone::FieldMapping.from_fields(["x", "y", "z"])
484
+ line = Zone::FieldLine.parse(
485
+ "1,2,3",
486
+ mapping: mapping
487
+ )
488
+
489
+ expected = { "x" => "1", "y" => "2", "z" => "3" }
490
+ assert_equal expected, line.to_h
491
+ end
492
+
493
+ def test_to_h_without_mapping_returns_empty
494
+ line = Zone::FieldLine.parse("a,b,c")
495
+
496
+ assert_empty line.to_h
497
+ end
498
+
499
+ def test_infer_delimiter_comma
500
+ delimiter = Zone::FieldLine.infer_delimiter(
501
+ "a,b,c",
502
+ explicit: nil
503
+ )
504
+
505
+ assert_equal ",", delimiter
506
+ end
507
+
508
+ def test_infer_delimiter_tab
509
+ delimiter = Zone::FieldLine.infer_delimiter(
510
+ "a\tb\tc",
511
+ explicit: nil
512
+ )
513
+
514
+ assert_equal "\t", delimiter
515
+ end
516
+
517
+ def test_infer_delimiter_whitespace
518
+ delimiter = Zone::FieldLine.infer_delimiter(
519
+ "a b c",
520
+ explicit: nil
521
+ )
522
+
523
+ assert_instance_of Regexp, delimiter
524
+ end
525
+
526
+ def test_infer_delimiter_explicit_overrides
527
+ delimiter = Zone::FieldLine.infer_delimiter(
528
+ "a,b,c",
529
+ explicit: "|"
530
+ )
531
+
532
+ assert_equal "|", delimiter
533
+ end
534
+
535
+ def test_chainable_transformations
536
+ line = Zone::FieldLine.parse("a,b,c")
537
+
538
+ result = line
539
+ .transform(1, &:upcase)
540
+ .transform(2, &:upcase)
541
+ .transform(3, &:upcase)
542
+
543
+ assert_same line, result
544
+ assert_equal "A,B,C", line.to_s
545
+ end
546
+ end
547
+ ```
548
+
549
+ ---
550
+
551
+ ## 5. test/zone/test_zone_module.rb
552
+
553
+ ### **Purpose**: Test Zone.find module method
554
+
555
+ ### **Assertions to Use**:
556
+ - `assert_instance_of` - Verify TZInfo::Timezone returned
557
+ - `assert_raises` - Test invalid timezone
558
+ - `assert_equal` - Verify correct timezone found
559
+
560
+ ### **Test Cases**:
561
+
562
+ ```ruby
563
+ # frozen_string_literal: true
564
+
565
+ require "test_helper"
566
+
567
+ class TestZoneModule < Minitest::Test
568
+ def test_find_exact_timezone_name
569
+ tz = Zone.find("America/New_York")
570
+
571
+ assert_instance_of TZInfo::Timezone, tz
572
+ assert_equal "America/New_York", tz.identifier
573
+ end
574
+
575
+ def test_find_utc
576
+ tz = Zone.find("UTC")
577
+
578
+ assert_instance_of TZInfo::Timezone, tz
579
+ assert_equal "UTC", tz.identifier
580
+ end
581
+
582
+ def test_find_fuzzy_tokyo
583
+ tz = Zone.find("tokyo")
584
+
585
+ assert_instance_of TZInfo::Timezone, tz
586
+ assert_equal "Asia/Tokyo", tz.identifier
587
+ end
588
+
589
+ def test_find_fuzzy_new_york
590
+ tz = Zone.find("new york")
591
+
592
+ assert_instance_of TZInfo::Timezone, tz
593
+ assert_match(/New_York/, tz.identifier)
594
+ end
595
+
596
+ def test_find_us_timezone
597
+ tz = Zone.find("eastern")
598
+
599
+ assert_instance_of TZInfo::Timezone, tz
600
+ assert_match(/^US\//, tz.identifier)
601
+ end
602
+
603
+ def test_find_returns_nil_for_invalid
604
+ tz = Zone.find("not_a_real_timezone_12345")
605
+
606
+ assert_nil tz
607
+ end
608
+
609
+ def test_find_case_insensitive
610
+ tz1 = Zone.find("Tokyo")
611
+ tz2 = Zone.find("TOKYO")
612
+ tz3 = Zone.find("tokyo")
613
+
614
+ assert_equal tz1.identifier, tz2.identifier
615
+ assert_equal tz2.identifier, tz3.identifier
616
+ end
617
+ end
618
+ ```
619
+
620
+ ---
621
+
622
+ ## 6. test/integration/test_cli_integration.rb
623
+
624
+ ### **Purpose**: End-to-end testing of actual CLI commands
625
+
626
+ ### **Assertions to Use**:
627
+ - `capture_subprocess_io` - Run actual commands and capture output
628
+ - `assert_match` - Verify output format
629
+ - `assert_equal` - Verify exact output
630
+ - `refute_match` - Ensure errors don't occur
631
+
632
+ ### **Test Cases**:
633
+
634
+ ```ruby
635
+ # frozen_string_literal: true
636
+
637
+ require "test_helper"
638
+
639
+ class TestCLIIntegration < Minitest::Test
640
+ ZONE_COMMAND = File.expand_path("../../exe/zone", __dir__)
641
+
642
+ def run_zone(input, *args)
643
+ capture_subprocess_io do
644
+ IO.popen(
645
+ [ZONE_COMMAND, *args],
646
+ "r+",
647
+ err: [:child, :out]
648
+ ) do |io|
649
+ io.write(input)
650
+ io.close_write
651
+ puts io.read
652
+ end
653
+ end
654
+ end
655
+
656
+ def test_basic_timestamp_conversion
657
+ out, err = run_zone(
658
+ "2025-01-15T10:30:00Z\n",
659
+ "--utc"
660
+ )
661
+
662
+ assert_match(/2025-01-15T10:30:00Z/, out)
663
+ assert_empty err
664
+ end
665
+
666
+ def test_unix_timestamp_to_pretty
667
+ out, err = run_zone(
668
+ "1736937000\n",
669
+ "--utc",
670
+ "--pretty"
671
+ )
672
+
673
+ assert_match(/Jan 15, 2025/, out)
674
+ assert_match(/10:30 AM/, out)
675
+ assert_empty err
676
+ end
677
+
678
+ def test_timezone_conversion_tokyo
679
+ out, err = run_zone(
680
+ "2025-01-15T10:30:00Z\n",
681
+ "--zone",
682
+ "tokyo"
683
+ )
684
+
685
+ assert_match(/19:30:00\+09:00/, out)
686
+ end
687
+
688
+ def test_field_parsing_multiple_fields
689
+ out, err = run_zone(
690
+ "test 1736937000 data\n",
691
+ "--field",
692
+ "2",
693
+ "--utc"
694
+ )
695
+
696
+ assert_match(/test/, out)
697
+ assert_match(/2025-01-15T10:30:00Z/, out)
698
+ assert_match(/data/, out)
699
+ end
700
+
701
+ def test_csv_with_headers
702
+ input = <<~CSV
703
+ name,timestamp,value
704
+ foo,2025-01-15T10:30:00Z,100
705
+ bar,1736937000,200
706
+ CSV
707
+
708
+ out, err = run_zone(
709
+ input,
710
+ "--headers",
711
+ "--field",
712
+ "2",
713
+ "--zone",
714
+ "tokyo"
715
+ )
716
+
717
+ assert_match(/name,timestamp,value/, out)
718
+ assert_match(/foo/, out)
719
+ assert_match(/\+09:00/, out)
720
+ end
721
+
722
+ def test_help_output
723
+ out, err = run_zone("", "--help")
724
+
725
+ assert_match(/Usage: zone/, out)
726
+ assert_match(/--iso8601/, out)
727
+ assert_match(/--zone/, out)
728
+ assert_match(/--field/, out)
729
+ end
730
+
731
+ def test_multiple_timestamps_as_arguments
732
+ out, err = capture_subprocess_io do
733
+ system(
734
+ ZONE_COMMAND,
735
+ "--utc",
736
+ "2025-01-15T10:30:00Z",
737
+ "1736937000",
738
+ out: :out,
739
+ err: :err
740
+ )
741
+ end
742
+
743
+ lines = out.split("\n")
744
+ assert_equal 2, lines.count
745
+ assert_match(/2025-01-15/, lines[0])
746
+ assert_match(/2025-01-15/, lines[1])
747
+ end
748
+
749
+ def test_verbose_logging
750
+ out, err = run_zone(
751
+ "2025-01-15T10:30:00Z\n",
752
+ "--zone",
753
+ "tokyo",
754
+ "--verbose"
755
+ )
756
+
757
+ assert_match(/Using time zone/, err)
758
+ assert_match(/Tokyo/, err)
759
+ end
760
+
761
+ def test_strftime_format
762
+ out, err = run_zone(
763
+ "2025-01-15T10:30:00Z\n",
764
+ "--utc",
765
+ "--strftime",
766
+ "%Y-%m-%d"
767
+ )
768
+
769
+ assert_equal "2025-01-15\n", out
770
+ end
771
+
772
+ def test_unix_output_format
773
+ out, err = run_zone(
774
+ "2025-01-15T10:30:00Z\n",
775
+ "--utc",
776
+ "--unix"
777
+ )
778
+
779
+ assert_equal "1736937000\n", out
780
+ end
781
+
782
+ def test_local_timezone
783
+ out, err = run_zone(
784
+ "2025-01-15T10:30:00Z\n",
785
+ "--local"
786
+ )
787
+
788
+ # Should convert to local time (output will vary by system)
789
+ assert_match(/2025-01-15/, out)
790
+ refute_match(/\+00:00/, out) # Should not be UTC
791
+ end
792
+
793
+ def test_delimiter_inference_tab
794
+ out, err = run_zone(
795
+ "foo\t1736937000\tbar\n",
796
+ "--field",
797
+ "2",
798
+ "--utc"
799
+ )
800
+
801
+ assert_match(/foo/, out)
802
+ assert_match(/2025-01-15/, out)
803
+ assert_match(/bar/, out)
804
+ end
805
+
806
+ def test_explicit_delimiter
807
+ out, err = run_zone(
808
+ "foo|1736937000|bar\n",
809
+ "--field",
810
+ "2",
811
+ "--delimiter",
812
+ "|",
813
+ "--utc"
814
+ )
815
+
816
+ assert_match(/foo/, out)
817
+ assert_match(/2025-01-15/, out)
818
+ assert_match(/bar/, out)
819
+ end
820
+
821
+ def test_no_input_uses_current_time
822
+ out, err = capture_subprocess_io do
823
+ system(
824
+ ZONE_COMMAND,
825
+ "--utc",
826
+ in: :close,
827
+ out: :out,
828
+ err: :err
829
+ )
830
+ end
831
+
832
+ # Should output current time
833
+ assert_match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/, out)
834
+ end
835
+
836
+ def test_invalid_timestamp_logs_warning
837
+ out, err = run_zone(
838
+ "not_a_valid_timestamp\n",
839
+ "--utc",
840
+ "--verbose"
841
+ )
842
+
843
+ assert_match(/Could not parse/, err)
844
+ assert_match(/Skipping/, err)
845
+ end
846
+ end
847
+ ```
848
+
849
+ ---
850
+
851
+ ## Summary of Assertions Used
852
+
853
+ Based on Minitest documentation:
854
+
855
+ ### **Equality & Type Checks**
856
+ - ✅ `assert_equal` - Most common, verify expected values
857
+ - ✅ `refute_equal` - Verify values are different
858
+ - ✅ `assert_same` - Verify object identity (for chainable methods)
859
+ - ✅ `assert_instance_of` - Verify exact class
860
+ - ✅ `assert_nil` - Verify nil values
861
+ - ✅ `refute_nil` - Verify non-nil values
862
+
863
+ ### **Collections**
864
+ - ✅ `assert_includes` - Verify array/hash membership
865
+ - ✅ `assert_empty` - Verify empty collections
866
+ - ✅ `refute_empty` - Verify non-empty collections
867
+
868
+ ### **Pattern Matching**
869
+ - ✅ `assert_match` - Verify regex patterns in strings
870
+ - ✅ `refute_match` - Verify patterns don't match
871
+
872
+ ### **Exceptions**
873
+ - ✅ `assert_raises` - Verify error handling
874
+
875
+ ### **Numeric**
876
+ - ✅ `assert_in_delta` - Compare floats with tolerance
877
+
878
+ ### **Methods**
879
+ - ✅ `assert_respond_to` - Verify API surface
880
+
881
+ ### **IO**
882
+ - ✅ `capture_subprocess_io` - Test actual CLI commands
883
+
884
+ ---
885
+
886
+ ## Test Execution
887
+
888
+ ```bash
889
+ # Run all tests
890
+ rake test
891
+
892
+ # Run specific test file
893
+ ruby test/zone/test_timestamp.rb
894
+
895
+ # Run specific test
896
+ ruby test/zone/test_timestamp.rb --name test_parse_unix_timestamp_seconds
897
+
898
+ # Run with verbose output
899
+ ruby test/zone/test_timestamp.rb --verbose
900
+ ```
901
+
902
+ ---
903
+
904
+ ## Coverage Goals
905
+
906
+ - **Unit tests**: 100% coverage of public methods
907
+ - **Integration tests**: All CLI flags and combinations
908
+ - **Edge cases**: Invalid input, missing fields, errors
909
+ - **Documentation**: Every assertion type used appropriately
910
+
911
+ This plan follows **Rule 1** by reading all Minitest documentation and using the appropriate assertion types for each test case.