gitmap-core 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,694 @@
1
+ """Tests for gitmap_core.merge module.
2
+
3
+ Tests the layer-level merge logic for GitMap including:
4
+ - Basic merging without conflicts
5
+ - Three-way merge with conflict detection
6
+ - Conflict resolution
7
+ - Table merging
8
+ - Summary formatting
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import pytest
13
+
14
+ from gitmap_core.merge import (
15
+ apply_resolution,
16
+ format_merge_summary,
17
+ merge_maps,
18
+ MergeConflict,
19
+ MergeResult,
20
+ resolve_conflict,
21
+ )
22
+
23
+
24
+ # ---- Fixtures -----------------------------------------------------------------------------------------------
25
+
26
+
27
+ @pytest.fixture
28
+ def base_map() -> dict:
29
+ """Base map state for three-way merges."""
30
+ return {
31
+ "operationalLayers": [
32
+ {"id": "layer1", "title": "Layer One", "url": "http://example.com/1"},
33
+ {"id": "layer2", "title": "Layer Two", "url": "http://example.com/2"},
34
+ ],
35
+ "tables": [
36
+ {"id": "table1", "title": "Table One", "url": "http://example.com/t1"},
37
+ ],
38
+ }
39
+
40
+
41
+ @pytest.fixture
42
+ def our_map() -> dict:
43
+ """Our modified map state."""
44
+ return {
45
+ "operationalLayers": [
46
+ {"id": "layer1", "title": "Layer One Modified", "url": "http://example.com/1"},
47
+ {"id": "layer2", "title": "Layer Two", "url": "http://example.com/2"},
48
+ {"id": "layer3", "title": "Layer Three (New)", "url": "http://example.com/3"},
49
+ ],
50
+ "tables": [
51
+ {"id": "table1", "title": "Table One", "url": "http://example.com/t1"},
52
+ ],
53
+ }
54
+
55
+
56
+ @pytest.fixture
57
+ def their_map() -> dict:
58
+ """Their modified map state."""
59
+ return {
60
+ "operationalLayers": [
61
+ {"id": "layer1", "title": "Layer One", "url": "http://example.com/1"},
62
+ {"id": "layer2", "title": "Layer Two Updated", "url": "http://example.com/2-new"},
63
+ {"id": "layer4", "title": "Layer Four (New)", "url": "http://example.com/4"},
64
+ ],
65
+ "tables": [
66
+ {"id": "table1", "title": "Table One Updated", "url": "http://example.com/t1-new"},
67
+ ],
68
+ }
69
+
70
+
71
+ # ---- MergeConflict Tests ------------------------------------------------------------------------------------
72
+
73
+
74
+ class TestMergeConflict:
75
+ """Tests for MergeConflict dataclass."""
76
+
77
+ def test_create_conflict(self):
78
+ """Test basic conflict creation."""
79
+ conflict = MergeConflict(
80
+ layer_id="layer1",
81
+ layer_title="Test Layer",
82
+ ours={"id": "layer1", "title": "Ours"},
83
+ theirs={"id": "layer1", "title": "Theirs"},
84
+ )
85
+ assert conflict.layer_id == "layer1"
86
+ assert conflict.layer_title == "Test Layer"
87
+ assert conflict.ours == {"id": "layer1", "title": "Ours"}
88
+ assert conflict.theirs == {"id": "layer1", "title": "Theirs"}
89
+ assert conflict.base is None
90
+
91
+ def test_conflict_with_base(self):
92
+ """Test conflict with base version."""
93
+ conflict = MergeConflict(
94
+ layer_id="layer1",
95
+ layer_title="Test Layer",
96
+ ours={"id": "layer1", "title": "Ours"},
97
+ theirs={"id": "layer1", "title": "Theirs"},
98
+ base={"id": "layer1", "title": "Base"},
99
+ )
100
+ assert conflict.base == {"id": "layer1", "title": "Base"}
101
+
102
+
103
+ # ---- MergeResult Tests --------------------------------------------------------------------------------------
104
+
105
+
106
+ class TestMergeResult:
107
+ """Tests for MergeResult dataclass."""
108
+
109
+ def test_default_result(self):
110
+ """Test default merge result values."""
111
+ result = MergeResult()
112
+ assert result.success is True
113
+ assert result.merged_data == {}
114
+ assert result.conflicts == []
115
+ assert result.added_layers == []
116
+ assert result.removed_layers == []
117
+ assert result.modified_layers == []
118
+
119
+ def test_has_conflicts_false(self):
120
+ """Test has_conflicts property when no conflicts."""
121
+ result = MergeResult()
122
+ assert result.has_conflicts is False
123
+
124
+ def test_has_conflicts_true(self):
125
+ """Test has_conflicts property when conflicts exist."""
126
+ result = MergeResult()
127
+ result.conflicts.append(MergeConflict(
128
+ layer_id="layer1",
129
+ layer_title="Test",
130
+ ours={},
131
+ theirs={},
132
+ ))
133
+ assert result.has_conflicts is True
134
+
135
+
136
+ # ---- merge_maps Tests ---------------------------------------------------------------------------------------
137
+
138
+
139
+ class TestMergeMaps:
140
+ """Tests for merge_maps function."""
141
+
142
+ def test_merge_identical_maps(self):
143
+ """Test merging identical maps produces no conflicts."""
144
+ map_data = {
145
+ "operationalLayers": [
146
+ {"id": "layer1", "title": "Layer One"},
147
+ ],
148
+ "tables": [],
149
+ }
150
+ result = merge_maps(map_data, map_data)
151
+ assert result.success is True
152
+ assert result.has_conflicts is False
153
+ assert len(result.merged_data["operationalLayers"]) == 1
154
+
155
+ def test_merge_empty_maps(self):
156
+ """Test merging empty maps."""
157
+ result = merge_maps({}, {})
158
+ assert result.success is True
159
+ assert result.merged_data.get("operationalLayers", []) == []
160
+ assert result.merged_data.get("tables", []) == []
161
+
162
+ def test_merge_adds_their_new_layer(self):
163
+ """Test merging adds layers only in theirs."""
164
+ ours = {
165
+ "operationalLayers": [
166
+ {"id": "layer1", "title": "Layer One"},
167
+ ],
168
+ }
169
+ theirs = {
170
+ "operationalLayers": [
171
+ {"id": "layer1", "title": "Layer One"},
172
+ {"id": "layer2", "title": "Layer Two"},
173
+ ],
174
+ }
175
+ result = merge_maps(ours, theirs)
176
+ assert result.success is True
177
+ assert len(result.merged_data["operationalLayers"]) == 2
178
+ assert "layer2" in result.added_layers
179
+
180
+ def test_merge_keeps_our_new_layer(self):
181
+ """Test merging keeps layers only in ours."""
182
+ ours = {
183
+ "operationalLayers": [
184
+ {"id": "layer1", "title": "Layer One"},
185
+ {"id": "layer2", "title": "Layer Two"},
186
+ ],
187
+ }
188
+ theirs = {
189
+ "operationalLayers": [
190
+ {"id": "layer1", "title": "Layer One"},
191
+ ],
192
+ }
193
+ result = merge_maps(ours, theirs)
194
+ assert result.success is True
195
+ assert len(result.merged_data["operationalLayers"]) == 2
196
+
197
+ def test_three_way_merge_theirs_modified(self, base_map, our_map, their_map):
198
+ """Test three-way merge when they modified a layer we didn't."""
199
+ # Revert our modification to layer1 to test "theirs wins"
200
+ our_map["operationalLayers"][0]["title"] = "Layer One" # Same as base
201
+
202
+ result = merge_maps(our_map, their_map, base_map)
203
+
204
+ # Layer2 should use their version since we didn't change it
205
+ layer2 = next(
206
+ (l for l in result.merged_data["operationalLayers"] if l["id"] == "layer2"),
207
+ None
208
+ )
209
+ assert layer2 is not None
210
+ assert layer2["title"] == "Layer Two Updated"
211
+ assert "layer2" in result.modified_layers
212
+
213
+ def test_three_way_merge_ours_modified(self, base_map):
214
+ """Test three-way merge when we modified a layer they didn't."""
215
+ ours = {
216
+ "operationalLayers": [
217
+ {"id": "layer1", "title": "Layer One Modified", "url": "http://example.com/1"},
218
+ ],
219
+ }
220
+ theirs = {
221
+ "operationalLayers": [
222
+ {"id": "layer1", "title": "Layer One", "url": "http://example.com/1"},
223
+ ],
224
+ }
225
+ base = {
226
+ "operationalLayers": [
227
+ {"id": "layer1", "title": "Layer One", "url": "http://example.com/1"},
228
+ ],
229
+ }
230
+
231
+ result = merge_maps(ours, theirs, base)
232
+
233
+ # Our modification should be kept
234
+ layer1 = result.merged_data["operationalLayers"][0]
235
+ assert layer1["title"] == "Layer One Modified"
236
+ assert result.success is True
237
+
238
+ def test_three_way_merge_both_modified_conflict(self, base_map):
239
+ """Test three-way merge creates conflict when both modify same layer."""
240
+ ours = {
241
+ "operationalLayers": [
242
+ {"id": "layer1", "title": "Layer One - Ours", "url": "http://example.com/1"},
243
+ ],
244
+ }
245
+ theirs = {
246
+ "operationalLayers": [
247
+ {"id": "layer1", "title": "Layer One - Theirs", "url": "http://example.com/1"},
248
+ ],
249
+ }
250
+ base = {
251
+ "operationalLayers": [
252
+ {"id": "layer1", "title": "Layer One", "url": "http://example.com/1"},
253
+ ],
254
+ }
255
+
256
+ result = merge_maps(ours, theirs, base)
257
+
258
+ assert result.success is False
259
+ assert result.has_conflicts is True
260
+ assert len(result.conflicts) == 1
261
+ assert result.conflicts[0].layer_id == "layer1"
262
+
263
+ def test_merge_conflict_without_base(self):
264
+ """Test conflict when same layer differs without base."""
265
+ ours = {
266
+ "operationalLayers": [
267
+ {"id": "layer1", "title": "Layer One - Ours"},
268
+ ],
269
+ }
270
+ theirs = {
271
+ "operationalLayers": [
272
+ {"id": "layer1", "title": "Layer One - Theirs"},
273
+ ],
274
+ }
275
+
276
+ result = merge_maps(ours, theirs)
277
+
278
+ assert result.success is False
279
+ assert len(result.conflicts) == 1
280
+
281
+ def test_merge_they_deleted_we_kept(self, base_map):
282
+ """Test merging when they deleted a layer we kept."""
283
+ ours = {
284
+ "operationalLayers": [
285
+ {"id": "layer1", "title": "Layer One", "url": "http://example.com/1"},
286
+ {"id": "layer2", "title": "Layer Two", "url": "http://example.com/2"},
287
+ ],
288
+ }
289
+ theirs = {
290
+ "operationalLayers": [
291
+ {"id": "layer1", "title": "Layer One", "url": "http://example.com/1"},
292
+ # layer2 deleted
293
+ ],
294
+ }
295
+
296
+ result = merge_maps(ours, theirs, base_map)
297
+
298
+ # We kept layer2, it should be in merged result
299
+ layer_ids = [l["id"] for l in result.merged_data["operationalLayers"]]
300
+ assert "layer2" in layer_ids
301
+
302
+ def test_merge_we_deleted_they_modified_conflict(self, base_map):
303
+ """Test conflict when we deleted and they modified."""
304
+ ours = {
305
+ "operationalLayers": [
306
+ {"id": "layer1", "title": "Layer One", "url": "http://example.com/1"},
307
+ # layer2 deleted
308
+ ],
309
+ }
310
+ theirs = {
311
+ "operationalLayers": [
312
+ {"id": "layer1", "title": "Layer One", "url": "http://example.com/1"},
313
+ {"id": "layer2", "title": "Layer Two Modified", "url": "http://example.com/2-new"},
314
+ ],
315
+ }
316
+
317
+ result = merge_maps(ours, theirs, base_map)
318
+
319
+ # Conflict: we deleted, they modified
320
+ assert result.has_conflicts is True
321
+ conflict = result.conflicts[0]
322
+ assert conflict.layer_id == "layer2"
323
+ assert conflict.ours == {} # We deleted
324
+
325
+ def test_merge_tables_conflict(self, base_map):
326
+ """Test table merge creates conflicts correctly."""
327
+ ours = {
328
+ "operationalLayers": [],
329
+ "tables": [
330
+ {"id": "table1", "title": "Table One - Ours", "url": "http://example.com/t1"},
331
+ ],
332
+ }
333
+ theirs = {
334
+ "operationalLayers": [],
335
+ "tables": [
336
+ {"id": "table1", "title": "Table One - Theirs", "url": "http://example.com/t1"},
337
+ ],
338
+ }
339
+
340
+ result = merge_maps(ours, theirs, base_map)
341
+
342
+ assert result.has_conflicts is True
343
+ conflict = result.conflicts[0]
344
+ assert conflict.layer_id == "table1"
345
+
346
+ def test_merge_adds_their_new_table(self):
347
+ """Test merging adds tables only in theirs."""
348
+ ours = {
349
+ "operationalLayers": [],
350
+ "tables": [],
351
+ }
352
+ theirs = {
353
+ "operationalLayers": [],
354
+ "tables": [
355
+ {"id": "table1", "title": "New Table"},
356
+ ],
357
+ }
358
+
359
+ result = merge_maps(ours, theirs)
360
+
361
+ assert len(result.merged_data["tables"]) == 1
362
+ assert "table1" in result.added_layers
363
+
364
+ def test_merge_tables_conflict_two_way(self):
365
+ """Test two-way table merge (no base) creates conflict when both differ."""
366
+ ours = {
367
+ "operationalLayers": [],
368
+ "tables": [
369
+ {"id": "table1", "title": "Table One - Ours", "url": "http://example.com/t1-ours"},
370
+ ],
371
+ }
372
+ theirs = {
373
+ "operationalLayers": [],
374
+ "tables": [
375
+ {"id": "table1", "title": "Table One - Theirs", "url": "http://example.com/t1-theirs"},
376
+ ],
377
+ }
378
+
379
+ # Two-way merge: no base argument
380
+ result = merge_maps(ours, theirs)
381
+
382
+ assert result.has_conflicts is True
383
+ assert len(result.conflicts) == 1
384
+ conflict = result.conflicts[0]
385
+ assert conflict.layer_id == "table1"
386
+ assert conflict.layer_title == "Table One - Ours"
387
+ assert conflict.ours == ours["tables"][0]
388
+ assert conflict.theirs == theirs["tables"][0]
389
+ assert conflict.base is None # No base in two-way merge
390
+
391
+ def test_merge_they_modified_table_we_deleted(self, base_map):
392
+ """Test conflict when they modify a table we deleted."""
393
+ # We deleted table1 (not in our map)
394
+ ours = {
395
+ "operationalLayers": base_map["operationalLayers"],
396
+ "tables": [], # We deleted table1
397
+ }
398
+ # They modified table1
399
+ theirs = {
400
+ "operationalLayers": base_map["operationalLayers"],
401
+ "tables": [
402
+ {"id": "table1", "title": "Table One Modified By Them", "url": "http://example.com/t1-modified"},
403
+ ],
404
+ }
405
+
406
+ result = merge_maps(ours, theirs, base_map)
407
+
408
+ # Should create a conflict: they modified, we deleted
409
+ assert result.has_conflicts is True
410
+ conflict = next((c for c in result.conflicts if c.layer_id == "table1"), None)
411
+ assert conflict is not None
412
+ assert conflict.ours == {} # We deleted it
413
+ assert conflict.theirs["title"] == "Table One Modified By Them"
414
+ assert conflict.base == base_map["tables"][0]
415
+
416
+ def test_merge_they_kept_table_we_deleted_unchanged(self, base_map):
417
+ """Test that when they don't change a table we deleted, we respect our deletion."""
418
+ # We deleted table1
419
+ ours = {
420
+ "operationalLayers": base_map["operationalLayers"],
421
+ "tables": [],
422
+ }
423
+ # They didn't modify table1 (same as base)
424
+ theirs = {
425
+ "operationalLayers": base_map["operationalLayers"],
426
+ "tables": base_map["tables"].copy(), # Same as base
427
+ }
428
+
429
+ result = merge_maps(ours, theirs, base_map)
430
+
431
+ # No conflict - we deleted, they didn't change, so deletion is respected
432
+ table_conflicts = [c for c in result.conflicts if c.layer_id == "table1"]
433
+ assert len(table_conflicts) == 0
434
+ # Table should not be in merged result
435
+ merged_table_ids = [t["id"] for t in result.merged_data.get("tables", [])]
436
+ assert "table1" not in merged_table_ids
437
+
438
+ def test_merge_preserves_non_layer_properties(self):
439
+ """Test that merge preserves map properties outside layers."""
440
+ ours = {
441
+ "mapTitle": "My Map",
442
+ "basemap": "topo",
443
+ "operationalLayers": [],
444
+ }
445
+ theirs = {
446
+ "mapTitle": "Their Map",
447
+ "basemap": "satellite",
448
+ "operationalLayers": [],
449
+ }
450
+
451
+ result = merge_maps(ours, theirs)
452
+
453
+ # Should keep ours since we start with our map
454
+ assert result.merged_data["mapTitle"] == "My Map"
455
+ assert result.merged_data["basemap"] == "topo"
456
+
457
+
458
+ # ---- resolve_conflict Tests ---------------------------------------------------------------------------------
459
+
460
+
461
+ class TestResolveConflict:
462
+ """Tests for resolve_conflict function."""
463
+
464
+ @pytest.fixture
465
+ def conflict(self):
466
+ """Create a test conflict."""
467
+ return MergeConflict(
468
+ layer_id="layer1",
469
+ layer_title="Test Layer",
470
+ ours={"id": "layer1", "title": "Ours"},
471
+ theirs={"id": "layer1", "title": "Theirs"},
472
+ base={"id": "layer1", "title": "Base"},
473
+ )
474
+
475
+ def test_resolve_ours(self, conflict):
476
+ """Test resolving conflict with 'ours'."""
477
+ result = resolve_conflict(conflict, "ours")
478
+ assert result == {"id": "layer1", "title": "Ours"}
479
+
480
+ def test_resolve_theirs(self, conflict):
481
+ """Test resolving conflict with 'theirs'."""
482
+ result = resolve_conflict(conflict, "theirs")
483
+ assert result == {"id": "layer1", "title": "Theirs"}
484
+
485
+ def test_resolve_base(self, conflict):
486
+ """Test resolving conflict with 'base'."""
487
+ result = resolve_conflict(conflict, "base")
488
+ assert result == {"id": "layer1", "title": "Base"}
489
+
490
+ def test_resolve_base_not_available(self):
491
+ """Test error when resolving with base but no base exists."""
492
+ conflict = MergeConflict(
493
+ layer_id="layer1",
494
+ layer_title="Test",
495
+ ours={},
496
+ theirs={},
497
+ base=None,
498
+ )
499
+ with pytest.raises(ValueError, match="No base version available"):
500
+ resolve_conflict(conflict, "base")
501
+
502
+ def test_resolve_invalid_strategy(self, conflict):
503
+ """Test error with invalid resolution strategy."""
504
+ with pytest.raises(ValueError, match="Invalid resolution strategy"):
505
+ resolve_conflict(conflict, "invalid")
506
+
507
+
508
+ # ---- apply_resolution Tests ---------------------------------------------------------------------------------
509
+
510
+
511
+ class TestApplyResolution:
512
+ """Tests for apply_resolution function."""
513
+
514
+ def test_apply_layer_resolution(self):
515
+ """Test applying resolution to a layer conflict."""
516
+ merge_result = MergeResult(
517
+ merged_data={
518
+ "operationalLayers": [
519
+ {"id": "layer1", "title": "Old"},
520
+ ],
521
+ "tables": [],
522
+ },
523
+ conflicts=[
524
+ MergeConflict(
525
+ layer_id="layer1",
526
+ layer_title="Test",
527
+ ours={"id": "layer1", "title": "Old"},
528
+ theirs={"id": "layer1", "title": "New"},
529
+ ),
530
+ ],
531
+ )
532
+
533
+ result = apply_resolution(
534
+ merge_result,
535
+ "layer1",
536
+ {"id": "layer1", "title": "Resolved"},
537
+ )
538
+
539
+ assert len(result.conflicts) == 0
540
+ assert result.success is True
541
+ assert result.merged_data["operationalLayers"][0]["title"] == "Resolved"
542
+
543
+ def test_apply_table_resolution(self):
544
+ """Test applying resolution to a table conflict."""
545
+ merge_result = MergeResult(
546
+ merged_data={
547
+ "operationalLayers": [],
548
+ "tables": [
549
+ {"id": "table1", "title": "Old"},
550
+ ],
551
+ },
552
+ conflicts=[
553
+ MergeConflict(
554
+ layer_id="table1",
555
+ layer_title="Test",
556
+ ours={},
557
+ theirs={},
558
+ ),
559
+ ],
560
+ )
561
+
562
+ result = apply_resolution(
563
+ merge_result,
564
+ "table1",
565
+ {"id": "table1", "title": "Resolved"},
566
+ )
567
+
568
+ assert len(result.conflicts) == 0
569
+ assert result.merged_data["tables"][0]["title"] == "Resolved"
570
+
571
+ def test_apply_resolution_deletes_layer(self):
572
+ """Test that empty resolution deletes the layer."""
573
+ merge_result = MergeResult(
574
+ merged_data={
575
+ "operationalLayers": [
576
+ {"id": "layer1", "title": "To Delete"},
577
+ ],
578
+ "tables": [],
579
+ },
580
+ conflicts=[
581
+ MergeConflict(
582
+ layer_id="layer1",
583
+ layer_title="Test",
584
+ ours={},
585
+ theirs={},
586
+ ),
587
+ ],
588
+ )
589
+
590
+ result = apply_resolution(merge_result, "layer1", {})
591
+
592
+ assert len(result.merged_data["operationalLayers"]) == 0
593
+
594
+ def test_apply_resolution_adds_missing_layer(self):
595
+ """Test adding a layer that wasn't in merged data."""
596
+ merge_result = MergeResult(
597
+ merged_data={
598
+ "operationalLayers": [],
599
+ "tables": [],
600
+ },
601
+ conflicts=[
602
+ MergeConflict(
603
+ layer_id="layer1",
604
+ layer_title="Test",
605
+ ours={},
606
+ theirs={"id": "layer1", "title": "New"},
607
+ ),
608
+ ],
609
+ )
610
+
611
+ result = apply_resolution(
612
+ merge_result,
613
+ "layer1",
614
+ {"id": "layer1", "title": "New"},
615
+ )
616
+
617
+ assert len(result.merged_data["operationalLayers"]) == 1
618
+ assert result.merged_data["operationalLayers"][0]["title"] == "New"
619
+
620
+
621
+ # ---- format_merge_summary Tests -----------------------------------------------------------------------------
622
+
623
+
624
+ class TestFormatMergeSummary:
625
+ """Tests for format_merge_summary function."""
626
+
627
+ def test_format_successful_merge(self):
628
+ """Test formatting a successful merge."""
629
+ result = MergeResult(success=True)
630
+ summary = format_merge_summary(result)
631
+ assert "Merge completed successfully" in summary
632
+
633
+ def test_format_merge_with_conflicts(self):
634
+ """Test formatting a merge with conflicts."""
635
+ result = MergeResult(
636
+ success=False,
637
+ conflicts=[
638
+ MergeConflict(
639
+ layer_id="layer1",
640
+ layer_title="Problem Layer",
641
+ ours={},
642
+ theirs={},
643
+ ),
644
+ ],
645
+ )
646
+ summary = format_merge_summary(result)
647
+ assert "1 conflict" in summary
648
+ assert "Problem Layer" in summary
649
+ assert "layer1" in summary
650
+
651
+ def test_format_with_added_layers(self):
652
+ """Test formatting shows added layers."""
653
+ result = MergeResult(
654
+ success=True,
655
+ added_layers=["layer1", "layer2"],
656
+ )
657
+ summary = format_merge_summary(result)
658
+ assert "Added layers: 2" in summary
659
+ assert "+ layer1" in summary
660
+ assert "+ layer2" in summary
661
+
662
+ def test_format_with_removed_layers(self):
663
+ """Test formatting shows removed layers."""
664
+ result = MergeResult(
665
+ success=True,
666
+ removed_layers=["layer1"],
667
+ )
668
+ summary = format_merge_summary(result)
669
+ assert "Removed layers: 1" in summary
670
+ assert "- layer1" in summary
671
+
672
+ def test_format_with_modified_layers(self):
673
+ """Test formatting shows modified layers."""
674
+ result = MergeResult(
675
+ success=True,
676
+ modified_layers=["layer1"],
677
+ )
678
+ summary = format_merge_summary(result)
679
+ assert "Modified layers: 1" in summary
680
+ assert "~ layer1" in summary
681
+
682
+ def test_format_with_tables(self):
683
+ """Test formatting shows merged tables count."""
684
+ result = MergeResult(
685
+ success=True,
686
+ merged_data={
687
+ "tables": [
688
+ {"id": "table1"},
689
+ {"id": "table2"},
690
+ ],
691
+ },
692
+ )
693
+ summary = format_merge_summary(result)
694
+ assert "Merged tables: 2" in summary