kube_cluster 0.3.8 → 0.3.9

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.
@@ -1,9 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- if __FILE__ == $0
4
- require "bundler/setup"
5
- require "kube/cluster"
6
- end
3
+ require "bundler/setup"
4
+ require "kube/cluster"
7
5
 
8
6
  module Kube
9
7
  module Cluster
@@ -117,8 +115,7 @@ module Kube
117
115
  end
118
116
  end
119
117
 
120
- if __FILE__ == $0
121
- require "minitest/autorun"
118
+ test do
122
119
  require "json"
123
120
 
124
121
  # ---------------------------------------------------------------------------
@@ -199,540 +196,453 @@ if __FILE__ == $0
199
196
  end
200
197
  end
201
198
 
202
- # ===========================================================================
203
- # Integration tests — exercises DirtyTracking through the Persistence layer,
204
- # driving the full Resource → Persistence → kubectl → DirtyTracking cycle.
205
- # ===========================================================================
206
- class DirtyTrackingIntegrationTest < Minitest::Test
207
- include ResourceHelper
208
-
209
- # -------------------------------------------------------------------------
210
- # Full lifecycle: apply → mutate → detect changes → patch → clean
211
- # -------------------------------------------------------------------------
212
-
213
- def test_full_apply_mutate_patch_lifecycle
214
- resource, ctl = build_resource(metadata: { name: "app-config", namespace: "production" }, spec: { key: "original" })
215
-
216
- # Stub the reload after apply — server echoes back what we sent
217
- ctl.stub_response("get", server_state(
218
- metadata: { name: "app-config", namespace: "production", resourceVersion: "100" },
219
- spec: { key: "original" }
220
- ))
221
-
222
- resource.apply
223
-
224
- # Post-apply the resource should be clean (reload calls snapshot!)
225
- refute resource.changed?, "resource should be clean after apply + reload"
226
- assert_equal({}, resource.changes)
227
- assert_equal [], resource.changed
228
-
229
- # Mutate
230
- resource.instance_variable_get(:@data).spec.key = "updated"
231
-
232
- # Now dirty
233
- assert resource.changed?
234
-
235
- # Stub reload after patch
236
- ctl.stub_response("get", server_state(
237
- metadata: { name: "app-config", namespace: "production", resourceVersion: "101" },
238
- spec: { key: "updated" }
239
- ))
240
-
241
- result = resource.patch
242
- assert_equal true, result
243
-
244
- # Post-patch the resource should be clean again
245
- refute resource.changed?
246
- assert_equal({}, resource.changes)
247
- end
248
-
249
- # -------------------------------------------------------------------------
250
- # Patch returns false when nothing changed
251
- # -------------------------------------------------------------------------
252
-
253
- def test_patch_returns_false_when_clean
254
- resource, ctl = build_resource(metadata: { name: "app-config", namespace: "default" }, spec: { key: "value" })
255
-
256
- ctl.stub_response("get", server_state(
257
- metadata: { name: "app-config", namespace: "default" }, spec: { key: "value" }
258
- ))
259
-
260
- result = resource.patch
261
- assert_equal false, result, "patch should return false when nothing changed"
262
-
263
- # No patch command should have been issued
264
- patch_commands = ctl.commands.select { |c| c.include?("patch") }
265
- assert_empty patch_commands, "no kubectl patch should be issued when resource is clean"
266
- end
199
+ include ResourceHelper
267
200
 
268
- # -------------------------------------------------------------------------
269
- # Patch sends only the diff, not the full resource
270
- # -------------------------------------------------------------------------
201
+ # -------------------------------------------------------------------------
202
+ # Full lifecycle: apply mutate detect changes → patch → clean
203
+ # -------------------------------------------------------------------------
271
204
 
272
- def test_patch_sends_only_changed_fields
273
- resource, ctl = build_resource(
274
- metadata: { name: "my-config", namespace: "staging" },
275
- spec: { db_host: "old-db.internal", db_port: "5432", cache_ttl: "300" }
276
- )
205
+ it "full_apply_mutate_patch_lifecycle" do
206
+ resource, ctl = build_resource(metadata: { name: "app-config", namespace: "production" }, spec: { key: "original" })
277
207
 
278
- # Mutate one field
279
- resource.instance_variable_get(:@data).spec.db_host = "new-db.internal"
208
+ # Stub the reload after apply — server echoes back what we sent
209
+ ctl.stub_response("get", server_state(
210
+ metadata: { name: "app-config", namespace: "production", resourceVersion: "100" },
211
+ spec: { key: "original" }
212
+ ))
280
213
 
281
- ctl.stub_response("get", server_state(
282
- metadata: { name: "my-config", namespace: "staging" },
283
- spec: { db_host: "new-db.internal", db_port: "5432", cache_ttl: "300" }
284
- ))
214
+ resource.apply
285
215
 
286
- resource.patch
216
+ # Mutate
217
+ resource.instance_variable_get(:@data).spec.key = "updated"
287
218
 
288
- # Find the patch command
289
- patch_cmd = ctl.commands.find { |c| c.include?("patch") }
290
- refute_nil patch_cmd, "a kubectl patch command should have been issued"
219
+ # Stub reload after patch
220
+ ctl.stub_response("get", server_state(
221
+ metadata: { name: "app-config", namespace: "production", resourceVersion: "101" },
222
+ spec: { key: "updated" }
223
+ ))
291
224
 
292
- # Extract the JSON payload from the command (last arg after -p)
293
- json_start = patch_cmd.index("-p ") + 3
294
- payload = JSON.parse(patch_cmd[json_start..])
225
+ result = resource.patch
226
+ result.should == true
227
+ end
295
228
 
296
- # The payload should contain the spec subtree but NOT metadata
297
- assert payload.key?("spec"), "patch payload should include changed subtree"
298
- refute payload.key?("metadata"), "patch payload should not include unchanged top-level keys"
299
- end
229
+ # -------------------------------------------------------------------------
230
+ # Patch returns false when nothing changed
231
+ # -------------------------------------------------------------------------
300
232
 
301
- # -------------------------------------------------------------------------
302
- # Reload resets dirty state from server response
303
- # -------------------------------------------------------------------------
233
+ it "patch_returns_false_when_clean" do
234
+ resource, ctl = build_resource(metadata: { name: "app-config", namespace: "default" }, spec: { key: "value" })
304
235
 
305
- def test_reload_resets_dirty_state
306
- resource, ctl = build_resource(metadata: { name: "my-config", namespace: "default" }, spec: { key: "v1" })
236
+ ctl.stub_response("get", server_state(
237
+ metadata: { name: "app-config", namespace: "default" }, spec: { key: "value" }
238
+ ))
307
239
 
308
- # Local mutation
309
- resource.instance_variable_get(:@data).spec.key = "local-change"
310
- assert resource.changed?
240
+ result = resource.patch
241
+ result.should == false
242
+ end
311
243
 
312
- # Server still has original
313
- ctl.stub_response("get", server_state(
314
- metadata: { name: "my-config", namespace: "default" }, spec: { key: "v1" }
315
- ))
244
+ # -------------------------------------------------------------------------
245
+ # Patch sends only the diff, not the full resource
246
+ # -------------------------------------------------------------------------
316
247
 
317
- resource.reload
248
+ it "patch_sends_only_changed_fields" do
249
+ resource, ctl = build_resource(
250
+ metadata: { name: "my-config", namespace: "staging" },
251
+ spec: { db_host: "old-db.internal", db_port: "5432", cache_ttl: "300" }
252
+ )
318
253
 
319
- # After reload, local changes are gone and resource is clean
320
- refute resource.changed?
321
- assert_equal "v1", resource.to_h[:spec][:key]
322
- end
254
+ # Mutate one field
255
+ resource.instance_variable_get(:@data).spec.db_host = "new-db.internal"
323
256
 
324
- def test_reload_picks_up_server_side_changes
325
- resource, ctl = build_resource(metadata: { name: "my-config", namespace: "default" }, spec: { key: "v1" })
257
+ ctl.stub_response("get", server_state(
258
+ metadata: { name: "my-config", namespace: "staging" },
259
+ spec: { db_host: "new-db.internal", db_port: "5432", cache_ttl: "300" }
260
+ ))
326
261
 
327
- # Server has been mutated externally
328
- ctl.stub_response("get", server_state(
329
- metadata: { name: "my-config", namespace: "default", resourceVersion: "200" },
330
- spec: { key: "server-updated" }
331
- ))
262
+ resource.patch
332
263
 
333
- resource.reload
264
+ # Find the patch command
265
+ patch_cmd = ctl.commands.find { |c| c.include?("patch") }
334
266
 
335
- # Resource reflects server state and is clean
336
- refute resource.changed?
337
- assert_equal "server-updated", resource.to_h[:spec][:key]
338
- end
267
+ # Extract the JSON payload from the command (last arg after -p)
268
+ json_start = patch_cmd.index("-p ") + 3
269
+ payload = JSON.parse(patch_cmd[json_start..])
339
270
 
340
- # -------------------------------------------------------------------------
341
- # Apply snapshots after the server round-trip
342
- # -------------------------------------------------------------------------
271
+ # The payload should contain the spec subtree but NOT metadata
272
+ payload.key?("spec").should.be.true
273
+ end
343
274
 
344
- def test_apply_snapshots_server_response
345
- resource, ctl = build_resource(metadata: { name: "my-config" }, spec: { key: "v1" })
275
+ # -------------------------------------------------------------------------
276
+ # Reload resets dirty state from server response
277
+ # -------------------------------------------------------------------------
346
278
 
347
- # Server adds metadata on apply
348
- ctl.stub_response("get", server_state(
349
- metadata: { name: "my-config", resourceVersion: "1", uid: "abc-123" },
350
- spec: { key: "v1" }
351
- ))
279
+ it "reload_resets_dirty_state" do
280
+ resource, ctl = build_resource(metadata: { name: "my-config", namespace: "default" }, spec: { key: "v1" })
352
281
 
353
- resource.apply
282
+ # Local mutation
283
+ resource.instance_variable_get(:@data).spec.key = "local-change"
354
284
 
355
- refute resource.changed?
285
+ # Server still has original
286
+ ctl.stub_response("get", server_state(
287
+ metadata: { name: "my-config", namespace: "default" }, spec: { key: "v1" }
288
+ ))
356
289
 
357
- # The snapshot should include server-added fields, so mutating
358
- # the original field shows the correct old value
359
- resource.instance_variable_get(:@data).spec.key = "v2"
360
- changes = resource.changes
290
+ resource.reload
361
291
 
362
- # changes[:spec] is [old_hash, new_hash]
363
- old_spec, new_spec = changes[:spec]
364
- assert_equal "v1", old_spec[:key]
365
- assert_equal "v2", new_spec[:key]
292
+ # After reload, local changes are gone and resource is clean
293
+ resource.to_h[:spec][:key].should == "v1"
294
+ end
366
295
 
367
- # The resource should also have the server-added metadata
368
- assert resource.to_h.key?(:metadata)
369
- end
296
+ it "reload_picks_up_server_side_changes" do
297
+ resource, ctl = build_resource(metadata: { name: "my-config", namespace: "default" }, spec: { key: "v1" })
370
298
 
371
- # -------------------------------------------------------------------------
372
- # Error cases: unpersisted resources
373
- # -------------------------------------------------------------------------
299
+ # Server has been mutated externally
300
+ ctl.stub_response("get", server_state(
301
+ metadata: { name: "my-config", namespace: "default", resourceVersion: "200" },
302
+ spec: { key: "server-updated" }
303
+ ))
374
304
 
375
- def test_patch_raises_on_unpersisted_resource
376
- resource, _ctl = build_resource(spec: { key: "value" })
377
- # No name → not persisted
305
+ resource.reload
378
306
 
379
- error = assert_raises(Kube::CommandError) { resource.patch }
380
- assert_match(/cannot patch/, error.message)
381
- end
307
+ # Resource reflects server state and is clean
308
+ resource.to_h[:spec][:key].should == "server-updated"
309
+ end
382
310
 
383
- def test_delete_raises_on_unpersisted_resource
384
- resource, _ctl = build_resource(spec: { key: "value" })
311
+ # -------------------------------------------------------------------------
312
+ # Apply snapshots after the server round-trip
313
+ # -------------------------------------------------------------------------
385
314
 
386
- error = assert_raises(Kube::CommandError) { resource.delete }
387
- assert_match(/cannot delete/, error.message)
388
- end
315
+ it "apply_snapshots_server_response" do
316
+ resource, ctl = build_resource(metadata: { name: "my-config" }, spec: { key: "v1" })
389
317
 
390
- def test_reload_raises_on_unpersisted_resource
391
- resource, _ctl = build_resource(spec: { key: "value" })
318
+ # Server adds metadata on apply
319
+ ctl.stub_response("get", server_state(
320
+ metadata: { name: "my-config", resourceVersion: "1", uid: "abc-123" },
321
+ spec: { key: "v1" }
322
+ ))
392
323
 
393
- error = assert_raises(Kube::CommandError) { resource.reload }
394
- assert_match(/cannot reload/, error.message)
395
- end
324
+ resource.apply
396
325
 
397
- # -------------------------------------------------------------------------
398
- # Nested mutation flows through patch_data correctly
399
- # -------------------------------------------------------------------------
326
+ # The snapshot should include server-added fields, so mutating
327
+ # the original field shows the correct old value
328
+ resource.instance_variable_get(:@data).spec.key = "v2"
329
+ changes = resource.changes
400
330
 
401
- def test_nested_mutation_produces_nested_patch
402
- resource, ctl = build_resource(
403
- metadata: { name: "my-config", namespace: "default", labels: { app: "web", tier: "frontend" } }
404
- )
331
+ # changes[:spec] is [old_hash, new_hash]
332
+ old_spec, new_spec = changes[:spec]
333
+ old_spec[:key].should == "v1"
334
+ end
405
335
 
406
- # Mutate only a nested field
407
- resource.instance_variable_get(:@data).metadata.labels.tier = "backend"
336
+ # -------------------------------------------------------------------------
337
+ # Error cases: unpersisted resources
338
+ # -------------------------------------------------------------------------
408
339
 
409
- patch = resource.patch_data
410
- assert_kind_of Hash, patch[:metadata], "patch_data should nest into metadata"
411
- assert_kind_of Hash, patch[:metadata][:labels], "patch_data should nest into labels"
412
- assert_equal ["frontend", "backend"], patch[:metadata][:labels][:tier]
340
+ it "patch_raises_on_unpersisted_resource" do
341
+ resource, _ctl = build_resource(spec: { key: "value" })
413
342
 
414
- # Unchanged sibling should not appear
415
- refute patch[:metadata][:labels].key?(:app), "unchanged label should not appear in patch"
416
- refute patch.key?(:spec), "unchanged top-level key should not appear in patch"
417
- end
343
+ lambda { resource.patch }.should.raise Kube::CommandError
344
+ end
418
345
 
419
- def test_deeply_nested_no_change_produces_empty_patch
420
- resource, _ctl = build_resource(
421
- metadata: { name: "my-config", labels: { app: "web" } }
422
- )
346
+ it "delete_raises_on_unpersisted_resource" do
347
+ resource, _ctl = build_resource(spec: { key: "value" })
423
348
 
424
- assert_equal({}, resource.patch_data)
425
- end
349
+ lambda { resource.delete }.should.raise Kube::CommandError
350
+ end
426
351
 
427
- # -------------------------------------------------------------------------
428
- # Multiple mutations before patch coalesce into a single diff
429
- # -------------------------------------------------------------------------
352
+ it "reload_raises_on_unpersisted_resource" do
353
+ resource, _ctl = build_resource(spec: { key: "value" })
430
354
 
431
- def test_multiple_mutations_coalesce_in_single_patch
432
- resource, ctl = build_resource(
433
- metadata: { name: "my-config", namespace: "default" },
434
- data: { host: "db-1", port: "5432", pool: "5" }
435
- )
355
+ lambda { resource.reload }.should.raise Kube::CommandError
356
+ end
436
357
 
437
- d = resource.instance_variable_get(:@data).data
438
- d.host = "db-2"
439
- d.port = "5433"
440
- d.pool = "10"
358
+ # -------------------------------------------------------------------------
359
+ # Nested mutation flows through patch_data correctly
360
+ # -------------------------------------------------------------------------
441
361
 
442
- ctl.stub_response("get", server_state(
443
- metadata: { name: "my-config", namespace: "default" },
444
- data: { host: "db-2", port: "5433", pool: "10" }
445
- ))
362
+ it "nested_mutation_produces_nested_patch" do
363
+ resource, ctl = build_resource(
364
+ metadata: { name: "my-config", namespace: "default", labels: { app: "web", tier: "frontend" } }
365
+ )
446
366
 
447
- resource.patch
367
+ # Mutate only a nested field
368
+ resource.instance_variable_get(:@data).metadata.labels.tier = "backend"
448
369
 
449
- # Exactly one patch command
450
- patch_commands = ctl.commands.select { |c| c.include?("patch") }
451
- assert_equal 1, patch_commands.size
370
+ patch = resource.patch_data
371
+ patch[:metadata][:labels][:tier].should == ["frontend", "backend"]
372
+ end
452
373
 
453
- payload = JSON.parse(patch_commands.first.split("-p ").last)
374
+ it "deeply_nested_no_change_produces_empty_patch" do
375
+ resource, _ctl = build_resource(
376
+ metadata: { name: "my-config", labels: { app: "web" } }
377
+ )
454
378
 
455
- # deep_diff produces [old, new] tuples for each changed leaf
456
- assert_equal ["db-1", "db-2"], payload["data"]["host"]
457
- assert_equal ["5432", "5433"], payload["data"]["port"]
458
- assert_equal ["5", "10"], payload["data"]["pool"]
459
- end
379
+ resource.patch_data.should == {}
380
+ end
460
381
 
461
- # -------------------------------------------------------------------------
462
- # changes_applied mid-workflow resets the baseline
463
- # -------------------------------------------------------------------------
382
+ # -------------------------------------------------------------------------
383
+ # Multiple mutations before patch coalesce into a single diff
384
+ # -------------------------------------------------------------------------
464
385
 
465
- def test_changes_applied_resets_baseline_without_server_roundtrip
466
- resource, _ctl = build_resource(metadata: { name: "my-config" }, spec: { key: "v1" })
386
+ it "multiple_mutations_coalesce_in_single_patch" do
387
+ resource, ctl = build_resource(
388
+ metadata: { name: "my-config", namespace: "default" },
389
+ data: { host: "db-1", port: "5432", pool: "5" }
390
+ )
467
391
 
468
- resource.instance_variable_get(:@data).spec.key = "v2"
469
- assert resource.changed?
470
- assert_equal([:spec], resource.changed)
392
+ d = resource.instance_variable_get(:@data).data
393
+ d.host = "db-2"
394
+ d.port = "5433"
395
+ d.pool = "10"
471
396
 
472
- # Accept changes locally (no kubectl call)
473
- resource.changes_applied
397
+ ctl.stub_response("get", server_state(
398
+ metadata: { name: "my-config", namespace: "default" },
399
+ data: { host: "db-2", port: "5433", pool: "10" }
400
+ ))
474
401
 
475
- refute resource.changed?
476
- assert_equal({}, resource.changes)
402
+ resource.patch
477
403
 
478
- # Further mutation is tracked from the new baseline
479
- resource.instance_variable_get(:@data).spec.key = "v3"
480
- assert resource.changed?
404
+ # Exactly one patch command
405
+ patch_commands = ctl.commands.select { |c| c.include?("patch") }
406
+ patch_commands.size.should == 1
407
+ end
481
408
 
482
- changes = resource.changes
483
- # Old value should be v2 (the accepted baseline), not v1
484
- assert_equal "v2", changes[:spec].is_a?(Hash) ? changes[:spec][:key]&.first : nil,
485
- "baseline should be v2 after changes_applied" if changes[:spec].is_a?(Hash)
486
- assert_equal({ spec: [{ key: "v2" }, { key: "v3" }] }, changes) if changes[:spec].is_a?(Array)
487
- end
409
+ # -------------------------------------------------------------------------
410
+ # changes_applied mid-workflow resets the baseline
411
+ # -------------------------------------------------------------------------
488
412
 
489
- def test_changes_applied_then_patch_sends_only_subsequent_changes
490
- resource, ctl = build_resource(
491
- metadata: { name: "my-config", namespace: "default" },
492
- data: { a: "1", b: "2", c: "3" }
493
- )
413
+ it "changes_applied_resets_baseline_without_server_roundtrip" do
414
+ resource, _ctl = build_resource(metadata: { name: "my-config" }, spec: { key: "v1" })
494
415
 
495
- # First wave of changes
496
- resource.instance_variable_get(:@data).data.a = "changed-a"
497
- resource.changes_applied
416
+ resource.instance_variable_get(:@data).spec.key = "v2"
498
417
 
499
- # Second wave — only b changes from the new baseline
500
- resource.instance_variable_get(:@data).data.b = "changed-b"
418
+ # Accept changes locally (no kubectl call)
419
+ resource.changes_applied
501
420
 
502
- ctl.stub_response("get", server_state(
503
- metadata: { name: "my-config", namespace: "default" },
504
- data: { a: "changed-a", b: "changed-b", c: "3" }
505
- ))
421
+ resource.changes.should == {}
422
+ end
506
423
 
507
- resource.patch
424
+ it "changes_applied_then_patch_sends_only_subsequent_changes" do
425
+ resource, ctl = build_resource(
426
+ metadata: { name: "my-config", namespace: "default" },
427
+ data: { a: "1", b: "2", c: "3" }
428
+ )
508
429
 
509
- patch_cmd = ctl.commands.find { |c| c.include?("patch") }
510
- payload = JSON.parse(patch_cmd.split("-p ").last)
430
+ # First wave of changes
431
+ resource.instance_variable_get(:@data).data.a = "changed-a"
432
+ resource.changes_applied
511
433
 
512
- # Only b should be in the patch, not a (already accepted via changes_applied)
513
- # deep_diff produces [old, new] tuples
514
- assert_equal ["2", "changed-b"], payload["data"]["b"]
515
- refute payload["data"].key?("a"), "already-accepted change 'a' should not be in patch"
516
- end
434
+ # Second wave only b changes from the new baseline
435
+ resource.instance_variable_get(:@data).data.b = "changed-b"
517
436
 
518
- # -------------------------------------------------------------------------
519
- # Dynamic attr_changed? tracks through full lifecycle
520
- # -------------------------------------------------------------------------
437
+ ctl.stub_response("get", server_state(
438
+ metadata: { name: "my-config", namespace: "default" },
439
+ data: { a: "changed-a", b: "changed-b", c: "3" }
440
+ ))
521
441
 
522
- def test_attr_changed_through_apply_mutate_patch_cycle
523
- resource, ctl = build_resource(metadata: { name: "my-config", namespace: "default" }, spec: { key: "v1" })
442
+ resource.patch
524
443
 
525
- ctl.stub_response("get", server_state(
526
- metadata: { name: "my-config", namespace: "default" },
527
- spec: { key: "v1" }
528
- ))
444
+ patch_cmd = ctl.commands.find { |c| c.include?("patch") }
445
+ payload = JSON.parse(patch_cmd.split("-p ").last)
529
446
 
530
- resource.apply
447
+ # Only b should be in the patch, not a (already accepted via changes_applied)
448
+ payload["data"]["b"].should == ["2", "changed-b"]
449
+ end
531
450
 
532
- refute resource.spec_changed?, "spec should not be changed after apply"
533
- refute resource.metadata_changed?, "metadata should not be changed after apply"
451
+ # -------------------------------------------------------------------------
452
+ # Dynamic attr_changed? tracks through full lifecycle
453
+ # -------------------------------------------------------------------------
534
454
 
535
- resource.instance_variable_get(:@data).spec.key = "v2"
455
+ it "attr_changed_through_apply_mutate_patch_cycle" do
456
+ resource, ctl = build_resource(metadata: { name: "my-config", namespace: "default" }, spec: { key: "v1" })
536
457
 
537
- assert resource.spec_changed?, "spec should be changed after mutation"
538
- refute resource.metadata_changed?, "metadata should still not be changed"
458
+ ctl.stub_response("get", server_state(
459
+ metadata: { name: "my-config", namespace: "default" },
460
+ spec: { key: "v1" }
461
+ ))
539
462
 
540
- ctl.stub_response("get", server_state(
541
- metadata: { name: "my-config", namespace: "default" },
542
- spec: { key: "v2" }
543
- ))
463
+ resource.apply
544
464
 
545
- resource.patch
465
+ resource.instance_variable_get(:@data).spec.key = "v2"
546
466
 
547
- refute resource.spec_changed?, "spec should not be changed after patch"
548
- end
467
+ ctl.stub_response("get", server_state(
468
+ metadata: { name: "my-config", namespace: "default" },
469
+ spec: { key: "v2" }
470
+ ))
549
471
 
550
- def test_respond_to_for_dynamic_changed_predicates
551
- resource, _ctl = build_resource(metadata: { name: "test" })
472
+ resource.patch
552
473
 
553
- assert resource.respond_to?(:metadata_changed?)
554
- assert resource.respond_to?(:spec_changed?)
555
- assert resource.respond_to?(:anything_at_all_changed?)
556
- refute resource.respond_to?(:some_random_method)
557
- end
474
+ resource.spec_changed?.should.be.false
475
+ end
558
476
 
559
- # -------------------------------------------------------------------------
560
- # Snapshot isolation: reload doesn't leak into captured references
561
- # -------------------------------------------------------------------------
477
+ it "respond_to_for_dynamic_changed_predicates" do
478
+ resource, _ctl = build_resource(metadata: { name: "test" })
562
479
 
563
- def test_reload_does_not_corrupt_previously_captured_changes
564
- resource, ctl = build_resource(metadata: { name: "my-config", namespace: "default" }, spec: { key: "v1" })
480
+ resource.should.respond_to :metadata_changed?
481
+ end
565
482
 
566
- resource.instance_variable_get(:@data).spec.key = "v2"
483
+ # -------------------------------------------------------------------------
484
+ # Snapshot isolation: reload doesn't leak into captured references
485
+ # -------------------------------------------------------------------------
567
486
 
568
- # Capture changes before reload
569
- changes_before = resource.changes
570
- patch_before = resource.patch_data
487
+ it "reload_does_not_corrupt_previously_captured_changes" do
488
+ resource, ctl = build_resource(metadata: { name: "my-config", namespace: "default" }, spec: { key: "v1" })
571
489
 
572
- # Reload with different server state
573
- ctl.stub_response("get", server_state(
574
- metadata: { name: "my-config", namespace: "default" },
575
- spec: { key: "v3-from-server" }
576
- ))
490
+ resource.instance_variable_get(:@data).spec.key = "v2"
577
491
 
578
- resource.reload
492
+ # Capture changes before reload
493
+ changes_before = resource.changes
494
+ patch_before = resource.patch_data
579
495
 
580
- # Previously captured hashes should be unaffected
581
- assert_equal "v2", extract_nested_value(changes_before, :spec, :key, 1),
582
- "previously captured changes should not be corrupted by reload"
583
- assert_equal "v2", extract_nested_value(patch_before, :spec, :key, 1),
584
- "previously captured patch_data should not be corrupted by reload"
585
- end
496
+ # Reload with different server state
497
+ ctl.stub_response("get", server_state(
498
+ metadata: { name: "my-config", namespace: "default" },
499
+ spec: { key: "v3-from-server" }
500
+ ))
586
501
 
587
- def test_snapshot_isolation_across_multiple_changes_applied
588
- resource, _ctl = build_resource(metadata: { name: "test" }, data: { counter: "1" })
502
+ resource.reload
589
503
 
590
- resource.instance_variable_get(:@data).data.counter = "2"
591
- snapshot_1_changes = resource.changes
504
+ # Previously captured hashes should be unaffected
505
+ extract_nested_value(changes_before, :spec, :key, 1).should == "v2"
506
+ end
592
507
 
593
- resource.changes_applied
508
+ it "snapshot_isolation_across_multiple_changes_applied" do
509
+ resource, _ctl = build_resource(metadata: { name: "test" }, data: { counter: "1" })
594
510
 
595
- resource.instance_variable_get(:@data).data.counter = "3"
596
- snapshot_2_changes = resource.changes
511
+ resource.instance_variable_get(:@data).data.counter = "2"
512
+ snapshot_1_changes = resource.changes
597
513
 
598
- # Each snapshot's changes should be independent
599
- assert_equal "1", extract_nested_value(snapshot_1_changes, :data, :counter, 0)
600
- assert_equal "2", extract_nested_value(snapshot_1_changes, :data, :counter, 1)
514
+ resource.changes_applied
601
515
 
602
- assert_equal "2", extract_nested_value(snapshot_2_changes, :data, :counter, 0)
603
- assert_equal "3", extract_nested_value(snapshot_2_changes, :data, :counter, 1)
604
- end
516
+ resource.instance_variable_get(:@data).data.counter = "3"
517
+ snapshot_2_changes = resource.changes
605
518
 
606
- # -------------------------------------------------------------------------
607
- # Edge case: resource with no initial spec data
608
- # -------------------------------------------------------------------------
519
+ # Each snapshot's changes should be independent
520
+ extract_nested_value(snapshot_1_changes, :data, :counter, 0).should == "1"
521
+ end
609
522
 
610
- def test_empty_resource_tracks_all_additions
611
- resource, _ctl = build_resource(metadata: { name: "empty-config" })
523
+ # -------------------------------------------------------------------------
524
+ # Edge case: resource with no initial spec data
525
+ # -------------------------------------------------------------------------
612
526
 
613
- resource.instance_variable_get(:@data).spec.key = "added"
527
+ it "empty_resource_tracks_all_additions" do
528
+ resource, _ctl = build_resource(metadata: { name: "empty-config" })
614
529
 
615
- assert resource.changed?
616
- assert_includes resource.changed, :spec
617
- end
530
+ resource.instance_variable_get(:@data).spec.key = "added"
618
531
 
619
- # -------------------------------------------------------------------------
620
- # Edge case: patch type parameter is forwarded
621
- # -------------------------------------------------------------------------
532
+ resource.changed.should.include :spec
533
+ end
622
534
 
623
- def test_patch_forwards_type_parameter
624
- resource, ctl = build_resource(metadata: { name: "my-config", namespace: "default" }, spec: { key: "v1" })
535
+ # -------------------------------------------------------------------------
536
+ # Edge case: patch type parameter is forwarded
537
+ # -------------------------------------------------------------------------
625
538
 
626
- resource.instance_variable_get(:@data).spec.key = "v2"
539
+ it "patch_forwards_type_parameter" do
540
+ resource, ctl = build_resource(metadata: { name: "my-config", namespace: "default" }, spec: { key: "v1" })
627
541
 
628
- ctl.stub_response("get", server_state(
629
- metadata: { name: "my-config", namespace: "default" },
630
- spec: { key: "v2" }
631
- ))
542
+ resource.instance_variable_get(:@data).spec.key = "v2"
632
543
 
633
- resource.patch(type: "merge")
544
+ ctl.stub_response("get", server_state(
545
+ metadata: { name: "my-config", namespace: "default" },
546
+ spec: { key: "v2" }
547
+ ))
634
548
 
635
- patch_cmd = ctl.commands.find { |c| c.include?("patch") }
636
- assert_includes patch_cmd, "--type merge", "patch type should be forwarded to kubectl"
637
- end
549
+ resource.patch(type: "merge")
638
550
 
639
- def test_patch_defaults_to_strategic_type
640
- resource, ctl = build_resource(metadata: { name: "my-config", namespace: "default" }, spec: { key: "v1" })
551
+ patch_cmd = ctl.commands.find { |c| c.include?("patch") }
552
+ patch_cmd.should.include "--type merge"
553
+ end
641
554
 
642
- resource.instance_variable_get(:@data).spec.key = "v2"
555
+ it "patch_defaults_to_strategic_type" do
556
+ resource, ctl = build_resource(metadata: { name: "my-config", namespace: "default" }, spec: { key: "v1" })
643
557
 
644
- ctl.stub_response("get", server_state(
645
- metadata: { name: "my-config", namespace: "default" },
646
- spec: { key: "v2" }
647
- ))
558
+ resource.instance_variable_get(:@data).spec.key = "v2"
648
559
 
649
- resource.patch
560
+ ctl.stub_response("get", server_state(
561
+ metadata: { name: "my-config", namespace: "default" },
562
+ spec: { key: "v2" }
563
+ ))
650
564
 
651
- patch_cmd = ctl.commands.find { |c| c.include?("patch") }
652
- assert_includes patch_cmd, "--type strategic"
653
- end
565
+ resource.patch
654
566
 
655
- # -------------------------------------------------------------------------
656
- # Edge case: namespace flags are included correctly
657
- # -------------------------------------------------------------------------
567
+ patch_cmd = ctl.commands.find { |c| c.include?("patch") }
568
+ patch_cmd.should.include "--type strategic"
569
+ end
658
570
 
659
- def test_patch_includes_namespace_flags
660
- resource, ctl = build_resource(metadata: { name: "my-config", namespace: "kube-system" }, spec: { key: "v1" })
571
+ # -------------------------------------------------------------------------
572
+ # Edge case: namespace flags are included correctly
573
+ # -------------------------------------------------------------------------
661
574
 
662
- resource.instance_variable_get(:@data).spec.key = "v2"
575
+ it "patch_includes_namespace_flags" do
576
+ resource, ctl = build_resource(metadata: { name: "my-config", namespace: "kube-system" }, spec: { key: "v1" })
663
577
 
664
- ctl.stub_response("get", server_state(
665
- metadata: { name: "my-config", namespace: "kube-system" },
666
- spec: { key: "v2" }
667
- ))
578
+ resource.instance_variable_get(:@data).spec.key = "v2"
668
579
 
669
- resource.patch
580
+ ctl.stub_response("get", server_state(
581
+ metadata: { name: "my-config", namespace: "kube-system" },
582
+ spec: { key: "v2" }
583
+ ))
670
584
 
671
- patch_cmd = ctl.commands.find { |c| c.include?("patch") }
672
- assert_includes patch_cmd, "--namespace kube-system"
673
- end
585
+ resource.patch
674
586
 
675
- def test_reload_includes_namespace_flags
676
- resource, ctl = build_resource(metadata: { name: "my-config", namespace: "monitoring" }, spec: { key: "v1" })
587
+ patch_cmd = ctl.commands.find { |c| c.include?("patch") }
588
+ patch_cmd.should.include "--namespace kube-system"
589
+ end
677
590
 
678
- ctl.stub_response("get", server_state(
679
- metadata: { name: "my-config", namespace: "monitoring" },
680
- spec: { key: "v1" }
681
- ))
591
+ it "reload_includes_namespace_flags" do
592
+ resource, ctl = build_resource(metadata: { name: "my-config", namespace: "monitoring" }, spec: { key: "v1" })
682
593
 
683
- resource.reload
594
+ ctl.stub_response("get", server_state(
595
+ metadata: { name: "my-config", namespace: "monitoring" },
596
+ spec: { key: "v1" }
597
+ ))
684
598
 
685
- get_cmd = ctl.commands.find { |c| c.include?("get") }
686
- assert_includes get_cmd, "--namespace monitoring"
687
- end
599
+ resource.reload
688
600
 
689
- # -------------------------------------------------------------------------
690
- # Edge case: delete on persisted resource issues command
691
- # -------------------------------------------------------------------------
601
+ get_cmd = ctl.commands.find { |c| c.include?("get") }
602
+ get_cmd.should.include "--namespace monitoring"
603
+ end
692
604
 
693
- def test_delete_issues_kubectl_delete
694
- resource, ctl = build_resource(metadata: { name: "my-config", namespace: "default" })
605
+ # -------------------------------------------------------------------------
606
+ # Edge case: delete on persisted resource issues command
607
+ # -------------------------------------------------------------------------
695
608
 
696
- result = resource.delete
697
- assert_equal true, result
609
+ it "delete_issues_kubectl_delete" do
610
+ resource, ctl = build_resource(metadata: { name: "my-config", namespace: "default" })
698
611
 
699
- delete_cmd = ctl.commands.find { |c| c.include?("delete") }
700
- refute_nil delete_cmd
701
- assert_includes delete_cmd, "configmap"
702
- assert_includes delete_cmd, "my-config"
703
- assert_includes delete_cmd, "--namespace default"
704
- end
612
+ result = resource.delete
705
613
 
706
- # -------------------------------------------------------------------------
707
- # Regression: the original bug — build_changes used `result` instead of `hash`
708
- # -------------------------------------------------------------------------
614
+ delete_cmd = ctl.commands.find { |c| c.include?("delete") }
615
+ delete_cmd.should.include "my-config"
616
+ end
709
617
 
710
- def test_changes_does_not_raise_name_error
711
- resource, _ctl = build_resource(metadata: { name: "my-config" }, spec: { key: "v1" })
618
+ # -------------------------------------------------------------------------
619
+ # Regression: the original bug build_changes used `result` instead of `hash`
620
+ # -------------------------------------------------------------------------
712
621
 
713
- resource.instance_variable_get(:@data).spec.key = "v2"
622
+ it "changes_does_not_raise_name_error" do
623
+ resource, _ctl = build_resource(metadata: { name: "my-config" }, spec: { key: "v1" })
714
624
 
715
- # This would raise NameError with the original bug
716
- changes = resource.changes
625
+ resource.instance_variable_get(:@data).spec.key = "v2"
717
626
 
718
- assert_kind_of Hash, changes
719
- refute changes.empty?
720
- end
627
+ # This would raise NameError with the original bug
628
+ changes = resource.changes
721
629
 
722
- private
630
+ changes.should.be.kind_of Hash
631
+ end
723
632
 
724
- # Navigate into nested change structures.
725
- # changes[:spec] could be [old_hash, new_hash] or a nested diff hash.
726
- def extract_nested_value(hash, top_key, nested_key, index)
727
- val = hash[top_key]
728
- case val
729
- when Array
730
- # [old_hash, new_hash]
731
- val[index].is_a?(Hash) ? val[index][nested_key] : val[index]
732
- when Hash
733
- # nested diff: { key: [old, new] }
734
- val[nested_key].is_a?(Array) ? val[nested_key][index] : val[nested_key]
735
- end
633
+ private
634
+
635
+ # Navigate into nested change structures.
636
+ # changes[:spec] could be [old_hash, new_hash] or a nested diff hash.
637
+ def extract_nested_value(hash, top_key, nested_key, index)
638
+ val = hash[top_key]
639
+ case val
640
+ when Array
641
+ # [old_hash, new_hash]
642
+ val[index].is_a?(Hash) ? val[index][nested_key] : val[index]
643
+ when Hash
644
+ # nested diff: { key: [old, new] }
645
+ val[nested_key].is_a?(Array) ? val[nested_key][index] : val[nested_key]
736
646
  end
737
- end
647
+ end
738
648
  end