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,902 @@
1
+ """Tests for context graph visualization module.
2
+
3
+ Tests Mermaid diagram generation, ASCII art output, HTML generation,
4
+ and the main visualization function.
5
+
6
+ Execution Context:
7
+ Test module - run via pytest
8
+
9
+ Dependencies:
10
+ - pytest: Test framework
11
+ - gitmap_core: Module under test
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import tempfile
16
+ from datetime import datetime
17
+ from datetime import timedelta
18
+ from pathlib import Path
19
+
20
+ import pytest
21
+
22
+ from gitmap_core.context import Annotation
23
+ from gitmap_core.context import ContextStore
24
+ from gitmap_core.context import Edge
25
+ from gitmap_core.context import Event
26
+ from gitmap_core.visualize import (
27
+ GraphData,
28
+ _format_event_label,
29
+ _sanitize_mermaid_text,
30
+ _wrap_text,
31
+ generate_ascii_graph,
32
+ generate_ascii_timeline,
33
+ generate_html_visualization,
34
+ generate_mermaid_flowchart,
35
+ generate_mermaid_git_graph,
36
+ generate_mermaid_timeline,
37
+ visualize_context,
38
+ )
39
+
40
+
41
+ # ---- Fixtures ------------------------------------------------------------------------------------------------
42
+
43
+
44
+ @pytest.fixture
45
+ def sample_events() -> list[Event]:
46
+ """Create sample events for testing."""
47
+ base_time = datetime.now()
48
+ return [
49
+ Event(
50
+ id="event-001-uuid",
51
+ timestamp=(base_time - timedelta(hours=2)).isoformat(),
52
+ event_type="commit",
53
+ actor="user1",
54
+ repo="/test/repo",
55
+ ref="abc12345",
56
+ payload={"message": "Initial commit", "layers": 3},
57
+ ),
58
+ Event(
59
+ id="event-002-uuid",
60
+ timestamp=(base_time - timedelta(hours=1)).isoformat(),
61
+ event_type="push",
62
+ actor="user1",
63
+ repo="/test/repo",
64
+ ref="abc12345",
65
+ payload={"remote": "portal", "status": "success"},
66
+ ),
67
+ Event(
68
+ id="event-003-uuid",
69
+ timestamp=base_time.isoformat(),
70
+ event_type="commit",
71
+ actor="user2",
72
+ repo="/test/repo",
73
+ ref="def67890",
74
+ payload={"message": "Add accessibility layer", "layers": 4},
75
+ ),
76
+ ]
77
+
78
+
79
+ @pytest.fixture
80
+ def sample_edges() -> list[Edge]:
81
+ """Create sample edges for testing."""
82
+ return [
83
+ Edge(
84
+ source_id="event-002-uuid",
85
+ target_id="event-001-uuid",
86
+ relationship="caused_by",
87
+ metadata={"auto": True},
88
+ ),
89
+ Edge(
90
+ source_id="event-003-uuid",
91
+ target_id="event-001-uuid",
92
+ relationship="related_to",
93
+ metadata=None,
94
+ ),
95
+ ]
96
+
97
+
98
+ @pytest.fixture
99
+ def sample_annotations() -> dict[str, list[Annotation]]:
100
+ """Create sample annotations for testing."""
101
+ base_time = datetime.now()
102
+ return {
103
+ "event-001-uuid": [
104
+ Annotation(
105
+ id="ann-001",
106
+ event_id="event-001-uuid",
107
+ annotation_type="rationale",
108
+ content="Setting up initial map structure",
109
+ source="user",
110
+ timestamp=base_time.isoformat(),
111
+ ),
112
+ ],
113
+ "event-003-uuid": [
114
+ Annotation(
115
+ id="ann-002",
116
+ event_id="event-003-uuid",
117
+ annotation_type="rationale",
118
+ content="Client requested accessibility features",
119
+ source="user",
120
+ timestamp=base_time.isoformat(),
121
+ ),
122
+ Annotation(
123
+ id="ann-003",
124
+ event_id="event-003-uuid",
125
+ annotation_type="lesson",
126
+ content="Always test color contrast",
127
+ source="agent",
128
+ timestamp=base_time.isoformat(),
129
+ ),
130
+ ],
131
+ }
132
+
133
+
134
+ @pytest.fixture
135
+ def graph_data(
136
+ sample_events: list[Event],
137
+ sample_edges: list[Edge],
138
+ sample_annotations: dict[str, list[Annotation]],
139
+ ) -> GraphData:
140
+ """Create GraphData instance for testing."""
141
+ return GraphData(
142
+ events=sample_events,
143
+ edges=sample_edges,
144
+ annotations=sample_annotations,
145
+ )
146
+
147
+
148
+ @pytest.fixture
149
+ def temp_db() -> Path:
150
+ """Create temporary database path."""
151
+ with tempfile.TemporaryDirectory() as tmpdir:
152
+ yield Path(tmpdir) / "context.db"
153
+
154
+
155
+ # ---- Helper Function Tests -----------------------------------------------------------------------------------
156
+
157
+
158
+ class TestSanitizeMermaidText:
159
+ """Tests for _sanitize_mermaid_text function."""
160
+
161
+ def test_removes_quotes(self) -> None:
162
+ """Test that double quotes are replaced."""
163
+ result = _sanitize_mermaid_text('Hello "world"')
164
+ assert '"' not in result
165
+ assert "'" in result
166
+
167
+ def test_removes_brackets(self) -> None:
168
+ """Test that brackets are replaced."""
169
+ result = _sanitize_mermaid_text("Test [brackets] {braces}")
170
+ assert "[" not in result
171
+ assert "]" not in result
172
+ assert "{" not in result
173
+ assert "}" not in result
174
+
175
+ def test_removes_angle_brackets(self) -> None:
176
+ """Test that angle brackets are replaced."""
177
+ result = _sanitize_mermaid_text("Test <html> tags")
178
+ assert "<" not in result
179
+ assert ">" not in result
180
+
181
+ def test_truncates_long_text(self) -> None:
182
+ """Test that long text is truncated."""
183
+ long_text = "x" * 100
184
+ result = _sanitize_mermaid_text(long_text)
185
+ assert len(result) <= 40
186
+ assert result.endswith("...")
187
+
188
+ def test_replaces_newlines(self) -> None:
189
+ """Test that newlines are replaced with spaces."""
190
+ result = _sanitize_mermaid_text("Line 1\nLine 2")
191
+ assert "\n" not in result
192
+ assert "Line 1 Line 2" == result
193
+
194
+
195
+ class TestFormatEventLabel:
196
+ """Tests for _format_event_label function."""
197
+
198
+ def test_includes_event_type(self, sample_events: list[Event]) -> None:
199
+ """Test that event type is included."""
200
+ label = _format_event_label(sample_events[0])
201
+ assert "COMMIT" in label
202
+
203
+ def test_includes_short_ref(self, sample_events: list[Event]) -> None:
204
+ """Test that ref is truncated."""
205
+ label = _format_event_label(sample_events[0])
206
+ assert "abc12345" in label
207
+
208
+ def test_hides_time_when_disabled(self, sample_events: list[Event]) -> None:
209
+ """Test that time can be hidden."""
210
+ label_with_time = _format_event_label(sample_events[0], show_time=True)
211
+ label_no_time = _format_event_label(sample_events[0], show_time=False)
212
+ assert len(label_no_time) < len(label_with_time)
213
+
214
+
215
+ class TestWrapText:
216
+ """Tests for _wrap_text function."""
217
+
218
+ def test_wraps_long_text(self) -> None:
219
+ """Test that long text is wrapped."""
220
+ text = "This is a very long line of text that should be wrapped"
221
+ result = _wrap_text(text, width=20)
222
+ assert len(result) > 1
223
+ assert all(len(line) <= 20 for line in result)
224
+
225
+ def test_preserves_short_text(self) -> None:
226
+ """Test that short text is not wrapped."""
227
+ text = "Short text"
228
+ result = _wrap_text(text, width=50)
229
+ assert len(result) == 1
230
+ assert result[0] == text
231
+
232
+ def test_handles_empty_text(self) -> None:
233
+ """Test that empty text returns single empty line."""
234
+ result = _wrap_text("", width=20)
235
+ assert result == [""]
236
+
237
+
238
+ # ---- Mermaid Generation Tests --------------------------------------------------------------------------------
239
+
240
+
241
+ class TestMermaidFlowchart:
242
+ """Tests for generate_mermaid_flowchart function."""
243
+
244
+ def test_generates_valid_mermaid(self, graph_data: GraphData) -> None:
245
+ """Test that valid Mermaid syntax is generated."""
246
+ result = generate_mermaid_flowchart(graph_data)
247
+ assert result.startswith("flowchart")
248
+ assert "TB" in result # Default direction
249
+
250
+ def test_includes_event_nodes(self, graph_data: GraphData) -> None:
251
+ """Test that events are included as nodes."""
252
+ result = generate_mermaid_flowchart(graph_data)
253
+ assert "e_event-00" in result
254
+
255
+ def test_includes_edges(self, graph_data: GraphData) -> None:
256
+ """Test that edges are included."""
257
+ result = generate_mermaid_flowchart(graph_data)
258
+ assert "-->" in result or "---" in result
259
+
260
+ def test_respects_direction(self, graph_data: GraphData) -> None:
261
+ """Test that direction parameter is respected."""
262
+ result = generate_mermaid_flowchart(graph_data, direction="LR")
263
+ assert "flowchart LR" in result
264
+
265
+ def test_includes_annotations_when_enabled(self, graph_data: GraphData) -> None:
266
+ """Test that annotations are included when enabled."""
267
+ result = generate_mermaid_flowchart(graph_data, show_annotations=True)
268
+ assert "a_" in result # Annotation node prefix
269
+
270
+ def test_excludes_annotations_when_disabled(self, graph_data: GraphData) -> None:
271
+ """Test that annotations are excluded when disabled."""
272
+ result = generate_mermaid_flowchart(graph_data, show_annotations=False)
273
+ assert "rationale" not in result.lower()
274
+
275
+ def test_includes_styling(self, graph_data: GraphData) -> None:
276
+ """Test that styling classes are included."""
277
+ result = generate_mermaid_flowchart(graph_data)
278
+ assert "classDef commit" in result
279
+ assert "classDef push" in result
280
+
281
+ def test_adds_title_comment(self, graph_data: GraphData) -> None:
282
+ """Test that title is added as comment."""
283
+ result = generate_mermaid_flowchart(graph_data, title="Test Graph")
284
+ assert "Test Graph" in result
285
+
286
+ def test_links_branch_with_source_commit(self) -> None:
287
+ """Test that branch events with source_commit link FROM that commit."""
288
+ base_time = datetime.now()
289
+ events = [
290
+ Event(
291
+ id="commit-001-uuid",
292
+ timestamp=(base_time - timedelta(hours=2)).isoformat(),
293
+ event_type="commit",
294
+ actor="user1",
295
+ repo="/test/repo",
296
+ ref="abc12345",
297
+ payload={"message": "Initial commit", "branch": "main"},
298
+ ),
299
+ Event(
300
+ id="branch-001-uuid",
301
+ timestamp=(base_time - timedelta(hours=1)).isoformat(),
302
+ event_type="branch",
303
+ actor="user1",
304
+ repo="/test/repo",
305
+ ref=None,
306
+ payload={
307
+ "action": "create",
308
+ "branch_name": "feature/test",
309
+ "commit_id": "abc12345", # Source commit
310
+ },
311
+ ),
312
+ ]
313
+ data = GraphData(events=events, edges=[], annotations={})
314
+ result = generate_mermaid_flowchart(data)
315
+ # Should have dotted link from source commit to branch
316
+ assert "e_commit-0" in result
317
+ assert "e_branch-0" in result
318
+ assert "-.->" in result # Dotted arrow for branch creation
319
+
320
+ def test_links_branch_to_first_commit_on_branch(self) -> None:
321
+ """Test that branch events link to first commit ON that branch."""
322
+ base_time = datetime.now()
323
+ events = [
324
+ Event(
325
+ id="branch-001-uuid",
326
+ timestamp=(base_time - timedelta(hours=2)).isoformat(),
327
+ event_type="branch",
328
+ actor="user1",
329
+ repo="/test/repo",
330
+ ref=None,
331
+ payload={"action": "create", "branch_name": "feature/new"},
332
+ ),
333
+ Event(
334
+ id="commit-001-uuid",
335
+ timestamp=(base_time - timedelta(hours=1)).isoformat(),
336
+ event_type="commit",
337
+ actor="user1",
338
+ repo="/test/repo",
339
+ ref="abc12345",
340
+ payload={"message": "Feature commit", "branch": "feature/new"},
341
+ ),
342
+ ]
343
+ data = GraphData(events=events, edges=[], annotations={})
344
+ result = generate_mermaid_flowchart(data)
345
+ # Branch should link to commit on that branch
346
+ assert "e_branch-0" in result
347
+ assert "e_commit-0" in result
348
+ assert "-->" in result
349
+
350
+ def test_links_initial_branch_to_next_commit(self) -> None:
351
+ """Test that initial branch without tracking links to first commit after it."""
352
+ base_time = datetime.now()
353
+ events = [
354
+ Event(
355
+ id="branch-001-uuid",
356
+ timestamp=(base_time - timedelta(hours=2)).isoformat(),
357
+ event_type="branch",
358
+ actor="user1",
359
+ repo="/test/repo",
360
+ ref=None,
361
+ payload={"action": "create", "branch_name": "main"},
362
+ ),
363
+ Event(
364
+ id="commit-001-uuid",
365
+ timestamp=(base_time - timedelta(hours=1)).isoformat(),
366
+ event_type="commit",
367
+ actor="user1",
368
+ repo="/test/repo",
369
+ ref="abc12345",
370
+ payload={"message": "Initial commit"}, # No branch tracking
371
+ ),
372
+ ]
373
+ data = GraphData(events=events, edges=[], annotations={})
374
+ result = generate_mermaid_flowchart(data)
375
+ # Branch should link to next commit
376
+ assert "e_branch-0" in result
377
+ assert "e_commit-0" in result
378
+
379
+ def test_merge_commit_connects_to_both_parents(self) -> None:
380
+ """Test that merge commits connect to both parent branches."""
381
+ base_time = datetime.now()
382
+ events = [
383
+ Event(
384
+ id="commit-001-uuid",
385
+ timestamp=(base_time - timedelta(hours=3)).isoformat(),
386
+ event_type="commit",
387
+ actor="user1",
388
+ repo="/test/repo",
389
+ ref="abc12345",
390
+ payload={"message": "Main commit", "branch": "main"},
391
+ ),
392
+ Event(
393
+ id="commit-002-uuid",
394
+ timestamp=(base_time - timedelta(hours=2)).isoformat(),
395
+ event_type="commit",
396
+ actor="user1",
397
+ repo="/test/repo",
398
+ ref="def67890",
399
+ payload={"message": "Feature commit", "branch": "feature"},
400
+ ),
401
+ Event(
402
+ id="commit-003-uuid",
403
+ timestamp=(base_time - timedelta(hours=1)).isoformat(),
404
+ event_type="commit",
405
+ actor="user1",
406
+ repo="/test/repo",
407
+ ref="merge1234",
408
+ payload={
409
+ "message": "Merge feature into main",
410
+ "parent": "abc12345",
411
+ "parent2": "def67890",
412
+ "branch": "main",
413
+ },
414
+ ),
415
+ ]
416
+ data = GraphData(events=events, edges=[], annotations={})
417
+ result = generate_mermaid_flowchart(data)
418
+ # Merge commit should show as merge shape
419
+ assert "{{" in result # Merge shape
420
+ # Should have MERGE label instead of COMMIT
421
+ assert "MERGE" in result
422
+
423
+ def test_merge_event_connects_source_and_target_branches(self) -> None:
424
+ """Test that merge events connect source and target branches."""
425
+ base_time = datetime.now()
426
+ events = [
427
+ Event(
428
+ id="commit-001-uuid",
429
+ timestamp=(base_time - timedelta(hours=3)).isoformat(),
430
+ event_type="commit",
431
+ actor="user1",
432
+ repo="/test/repo",
433
+ ref="abc12345",
434
+ payload={"message": "Main commit", "branch": "main"},
435
+ ),
436
+ Event(
437
+ id="commit-002-uuid",
438
+ timestamp=(base_time - timedelta(hours=2)).isoformat(),
439
+ event_type="commit",
440
+ actor="user1",
441
+ repo="/test/repo",
442
+ ref="def67890",
443
+ payload={"message": "Feature commit", "branch": "feature"},
444
+ ),
445
+ Event(
446
+ id="merge-001-uuid",
447
+ timestamp=(base_time - timedelta(hours=1)).isoformat(),
448
+ event_type="merge",
449
+ actor="user1",
450
+ repo="/test/repo",
451
+ ref="merge1234",
452
+ payload={
453
+ "source_branch": "feature",
454
+ "target_branch": "main",
455
+ },
456
+ ),
457
+ ]
458
+ data = GraphData(events=events, edges=[], annotations={})
459
+ result = generate_mermaid_flowchart(data)
460
+ # Merge event should use merge shape
461
+ assert "{{" in result
462
+ # Should show MERGE label
463
+ assert "MERGE" in result
464
+
465
+
466
+ class TestMermaidTimeline:
467
+ """Tests for generate_mermaid_timeline function."""
468
+
469
+ def test_generates_timeline_syntax(self, graph_data: GraphData) -> None:
470
+ """Test that valid timeline syntax is generated."""
471
+ result = generate_mermaid_timeline(graph_data)
472
+ assert result.startswith("timeline")
473
+
474
+ def test_includes_title(self, graph_data: GraphData) -> None:
475
+ """Test that title is included."""
476
+ result = generate_mermaid_timeline(graph_data, title="My Timeline")
477
+ assert "My Timeline" in result
478
+
479
+ def test_groups_by_date(self, graph_data: GraphData) -> None:
480
+ """Test that events are grouped by date."""
481
+ result = generate_mermaid_timeline(graph_data)
482
+ assert "section" in result
483
+
484
+
485
+ class TestMermaidGitGraph:
486
+ """Tests for generate_mermaid_git_graph function."""
487
+
488
+ def test_generates_git_graph_syntax(self, graph_data: GraphData) -> None:
489
+ """Test that valid gitGraph syntax is generated."""
490
+ result = generate_mermaid_git_graph(graph_data)
491
+ assert result.startswith("gitGraph")
492
+
493
+ def test_includes_commits(self, graph_data: GraphData) -> None:
494
+ """Test that commit events are included."""
495
+ result = generate_mermaid_git_graph(graph_data)
496
+ assert "commit" in result
497
+
498
+ def test_handles_empty_commits(self) -> None:
499
+ """Test handling of no commit events."""
500
+ data = GraphData(events=[], edges=[], annotations={})
501
+ result = generate_mermaid_git_graph(data)
502
+ assert "No commits yet" in result
503
+
504
+ def test_handles_merge_commits_with_parent2(self) -> None:
505
+ """Test that merge commits with parent2 are highlighted."""
506
+ base_time = datetime.now()
507
+ events = [
508
+ Event(
509
+ id="commit-001-uuid",
510
+ timestamp=(base_time - timedelta(hours=2)).isoformat(),
511
+ event_type="commit",
512
+ actor="user1",
513
+ repo="/test/repo",
514
+ ref="abc12345",
515
+ payload={"message": "Initial commit"},
516
+ ),
517
+ Event(
518
+ id="commit-002-uuid",
519
+ timestamp=(base_time - timedelta(hours=1)).isoformat(),
520
+ event_type="commit",
521
+ actor="user1",
522
+ repo="/test/repo",
523
+ ref="def67890",
524
+ payload={
525
+ "message": "Merge feature into main",
526
+ "parent": "abc12345",
527
+ "parent2": "xyz98765",
528
+ },
529
+ ),
530
+ ]
531
+ data = GraphData(events=events, edges=[], annotations={})
532
+ result = generate_mermaid_git_graph(data)
533
+ assert "HIGHLIGHT" in result
534
+
535
+ def test_handles_merge_events(self) -> None:
536
+ """Test that merge events generate proper merge commands."""
537
+ base_time = datetime.now()
538
+ events = [
539
+ Event(
540
+ id="commit-001-uuid",
541
+ timestamp=(base_time - timedelta(hours=2)).isoformat(),
542
+ event_type="commit",
543
+ actor="user1",
544
+ repo="/test/repo",
545
+ ref="abc12345",
546
+ payload={"message": "Initial commit"},
547
+ ),
548
+ Event(
549
+ id="merge-001-uuid",
550
+ timestamp=(base_time - timedelta(hours=1)).isoformat(),
551
+ event_type="merge",
552
+ actor="user1",
553
+ repo="/test/repo",
554
+ ref="def67890",
555
+ payload={
556
+ "source_branch": "feature/login",
557
+ "target_branch": "main",
558
+ "commit_id": "def67890",
559
+ },
560
+ ),
561
+ ]
562
+ data = GraphData(events=events, edges=[], annotations={})
563
+ result = generate_mermaid_git_graph(data)
564
+ assert "merge" in result
565
+ # Branch name should be sanitized (slashes become dashes)
566
+ assert "feature-login" in result
567
+
568
+ def test_handles_lsm_events(self) -> None:
569
+ """Test that LSM events are rendered as reverse commits."""
570
+ base_time = datetime.now()
571
+ events = [
572
+ Event(
573
+ id="lsm-001-uuid",
574
+ timestamp=base_time.isoformat(),
575
+ event_type="lsm",
576
+ actor="user1",
577
+ repo="/test/repo",
578
+ ref=None,
579
+ payload={
580
+ "source": "Portal",
581
+ "transferred_count": 5,
582
+ },
583
+ ),
584
+ ]
585
+ data = GraphData(events=events, edges=[], annotations={})
586
+ result = generate_mermaid_git_graph(data)
587
+ assert "REVERSE" in result
588
+ assert "LSM from Portal" in result
589
+ assert "5 transferred" in result
590
+
591
+ def test_handles_branch_creation_events(self) -> None:
592
+ """Test that branch creation events generate branch commands."""
593
+ base_time = datetime.now()
594
+ events = [
595
+ Event(
596
+ id="commit-001-uuid",
597
+ timestamp=(base_time - timedelta(hours=2)).isoformat(),
598
+ event_type="commit",
599
+ actor="user1",
600
+ repo="/test/repo",
601
+ ref="abc12345",
602
+ payload={"message": "Initial commit"},
603
+ ),
604
+ Event(
605
+ id="branch-001-uuid",
606
+ timestamp=(base_time - timedelta(hours=1)).isoformat(),
607
+ event_type="branch",
608
+ actor="user1",
609
+ repo="/test/repo",
610
+ ref=None,
611
+ payload={
612
+ "action": "create",
613
+ "branch_name": "feature/new-feature",
614
+ },
615
+ ),
616
+ ]
617
+ data = GraphData(events=events, edges=[], annotations={})
618
+ result = generate_mermaid_git_graph(data)
619
+ assert "branch feature-new-feature" in result
620
+
621
+ def test_handles_commit_without_message(self) -> None:
622
+ """Test that commits without messages get default message."""
623
+ base_time = datetime.now()
624
+ events = [
625
+ Event(
626
+ id="commit-001-uuid",
627
+ timestamp=base_time.isoformat(),
628
+ event_type="commit",
629
+ actor="user1",
630
+ repo="/test/repo",
631
+ ref="abc12345",
632
+ payload={}, # No message
633
+ ),
634
+ ]
635
+ data = GraphData(events=events, edges=[], annotations={})
636
+ result = generate_mermaid_git_graph(data)
637
+ assert "Commit abc12345" in result
638
+
639
+ def test_handles_checkout_on_merge(self) -> None:
640
+ """Test that merge to different branch triggers checkout."""
641
+ base_time = datetime.now()
642
+ events = [
643
+ Event(
644
+ id="branch-001-uuid",
645
+ timestamp=(base_time - timedelta(hours=2)).isoformat(),
646
+ event_type="branch",
647
+ actor="user1",
648
+ repo="/test/repo",
649
+ ref=None,
650
+ payload={"action": "create", "branch_name": "develop"},
651
+ ),
652
+ Event(
653
+ id="merge-001-uuid",
654
+ timestamp=base_time.isoformat(),
655
+ event_type="merge",
656
+ actor="user1",
657
+ repo="/test/repo",
658
+ ref=None,
659
+ payload={
660
+ "source_branch": "develop",
661
+ "target_branch": "main",
662
+ },
663
+ ),
664
+ ]
665
+ data = GraphData(events=events, edges=[], annotations={})
666
+ result = generate_mermaid_git_graph(data)
667
+ assert "checkout main" in result
668
+ assert "merge develop" in result
669
+
670
+
671
+ # ---- ASCII Generation Tests ----------------------------------------------------------------------------------
672
+
673
+
674
+ class TestAsciiTimeline:
675
+ """Tests for generate_ascii_timeline function."""
676
+
677
+ def test_generates_ascii_output(self, graph_data: GraphData) -> None:
678
+ """Test that ASCII output is generated."""
679
+ result = generate_ascii_timeline(graph_data)
680
+ assert isinstance(result, str)
681
+ assert len(result) > 0
682
+
683
+ def test_uses_unicode_by_default(self, graph_data: GraphData) -> None:
684
+ """Test that Unicode characters are used by default."""
685
+ result = generate_ascii_timeline(graph_data, use_unicode=True)
686
+ assert "┌" in result or "─" in result
687
+
688
+ def test_uses_simple_ascii_when_requested(self, graph_data: GraphData) -> None:
689
+ """Test that simple ASCII can be used."""
690
+ result = generate_ascii_timeline(graph_data, use_unicode=False)
691
+ assert "+" in result or "-" in result
692
+
693
+ def test_respects_width(self, graph_data: GraphData) -> None:
694
+ """Test that width parameter is respected."""
695
+ result = generate_ascii_timeline(graph_data, width=40)
696
+ lines = result.split("\n")
697
+ # Box lines should respect width
698
+ assert all(len(line) <= 42 for line in lines) # Allow slight overflow
699
+
700
+ def test_handles_empty_events(self) -> None:
701
+ """Test handling of no events."""
702
+ data = GraphData(events=[], edges=[], annotations={})
703
+ result = generate_ascii_timeline(data)
704
+ assert "No events" in result
705
+
706
+
707
+ class TestAsciiGraph:
708
+ """Tests for generate_ascii_graph function."""
709
+
710
+ def test_generates_ascii_output(self, graph_data: GraphData) -> None:
711
+ """Test that ASCII output is generated."""
712
+ result = generate_ascii_graph(graph_data)
713
+ assert isinstance(result, str)
714
+
715
+ def test_includes_legend(self, graph_data: GraphData) -> None:
716
+ """Test that legend is included."""
717
+ result = generate_ascii_graph(graph_data)
718
+ assert "Legend" in result
719
+
720
+ def test_shows_relationships(self, graph_data: GraphData) -> None:
721
+ """Test that relationships are shown."""
722
+ result = generate_ascii_graph(graph_data)
723
+ # Should include arrow or relationship indicator
724
+ assert "→" in result or "->" in result
725
+
726
+
727
+ # ---- HTML Generation Tests -----------------------------------------------------------------------------------
728
+
729
+
730
+ class TestHtmlVisualization:
731
+ """Tests for generate_html_visualization function."""
732
+
733
+ def test_generates_valid_html(self, graph_data: GraphData) -> None:
734
+ """Test that valid HTML is generated."""
735
+ result = generate_html_visualization(graph_data)
736
+ assert "<!DOCTYPE html>" in result
737
+ assert "</html>" in result
738
+
739
+ def test_includes_mermaid_script(self, graph_data: GraphData) -> None:
740
+ """Test that Mermaid.js is included."""
741
+ result = generate_html_visualization(graph_data)
742
+ assert "mermaid" in result.lower()
743
+
744
+ def test_includes_title(self, graph_data: GraphData) -> None:
745
+ """Test that title is included."""
746
+ result = generate_html_visualization(graph_data, title="My Graph")
747
+ assert "My Graph" in result
748
+
749
+ def test_respects_theme(self, graph_data: GraphData) -> None:
750
+ """Test that theme affects colors."""
751
+ light = generate_html_visualization(graph_data, theme="light")
752
+ dark = generate_html_visualization(graph_data, theme="dark")
753
+ # Dark theme should have different background color
754
+ assert "#1e1e1e" in dark
755
+ assert "#ffffff" in light
756
+
757
+ def test_includes_stats(self, graph_data: GraphData) -> None:
758
+ """Test that statistics are included."""
759
+ result = generate_html_visualization(graph_data)
760
+ assert "Events" in result
761
+ assert "Relationships" in result
762
+ assert "Annotations" in result
763
+
764
+
765
+ # ---- Main Visualization Function Tests -----------------------------------------------------------------------
766
+
767
+
768
+ class TestVisualizeContext:
769
+ """Tests for visualize_context function."""
770
+
771
+ def test_supports_mermaid_format(self, temp_db: Path) -> None:
772
+ """Test mermaid format output."""
773
+ store = ContextStore(temp_db)
774
+ store.record_event("commit", "/repo", {"message": "Test"}, ref="abc123")
775
+
776
+ result = visualize_context(store, output_format="mermaid")
777
+ assert "flowchart" in result
778
+ store.close()
779
+
780
+ def test_supports_mermaid_timeline_format(self, temp_db: Path) -> None:
781
+ """Test mermaid-timeline format output."""
782
+ store = ContextStore(temp_db)
783
+ store.record_event("commit", "/repo", {"message": "Test"})
784
+
785
+ result = visualize_context(store, output_format="mermaid-timeline")
786
+ assert "timeline" in result
787
+ store.close()
788
+
789
+ def test_supports_ascii_format(self, temp_db: Path) -> None:
790
+ """Test ascii format output."""
791
+ store = ContextStore(temp_db)
792
+ store.record_event("commit", "/repo", {"message": "Test"})
793
+
794
+ result = visualize_context(store, output_format="ascii")
795
+ assert isinstance(result, str)
796
+ store.close()
797
+
798
+ def test_supports_html_format(self, temp_db: Path) -> None:
799
+ """Test html format output."""
800
+ store = ContextStore(temp_db)
801
+ store.record_event("commit", "/repo", {"message": "Test"})
802
+
803
+ result = visualize_context(store, output_format="html")
804
+ assert "<!DOCTYPE html>" in result
805
+ store.close()
806
+
807
+ def test_raises_for_unknown_format(self, temp_db: Path) -> None:
808
+ """Test that unknown format raises ValueError."""
809
+ store = ContextStore(temp_db)
810
+
811
+ with pytest.raises(ValueError) as exc_info:
812
+ visualize_context(store, output_format="invalid")
813
+
814
+ assert "Unknown output format" in str(exc_info.value)
815
+ store.close()
816
+
817
+ def test_respects_limit(self, temp_db: Path) -> None:
818
+ """Test that limit parameter is respected."""
819
+ store = ContextStore(temp_db)
820
+ # Create many events
821
+ for i in range(10):
822
+ store.record_event("commit", "/repo", {"message": f"Commit {i}"})
823
+
824
+ result = visualize_context(store, output_format="ascii", limit=3)
825
+ # Should only show limited events
826
+ store.close()
827
+
828
+ def test_filters_by_event_type(self, temp_db: Path) -> None:
829
+ """Test that event_types filter works."""
830
+ store = ContextStore(temp_db)
831
+ store.record_event("commit", "/repo", {"message": "Commit"}, ref="c1")
832
+ store.record_event("push", "/repo", {"remote": "portal"}, ref="c1")
833
+
834
+ result = visualize_context(
835
+ store,
836
+ output_format="mermaid",
837
+ event_types=["commit"],
838
+ )
839
+ # Should only include commit events
840
+ assert "COMMIT" in result
841
+ store.close()
842
+
843
+
844
+ # ---- GraphData Tests -----------------------------------------------------------------------------------------
845
+
846
+
847
+ class TestGraphData:
848
+ """Tests for GraphData class."""
849
+
850
+ def test_from_context_store(self, temp_db: Path) -> None:
851
+ """Test building GraphData from context store."""
852
+ store = ContextStore(temp_db)
853
+ event = store.record_event(
854
+ "commit",
855
+ "/repo",
856
+ {"message": "Test"},
857
+ rationale="Test rationale",
858
+ )
859
+
860
+ data = GraphData.from_context_store(store)
861
+
862
+ assert len(data.events) == 1
863
+ assert data.events[0].id == event.id
864
+ assert event.id in data.annotations
865
+ store.close()
866
+
867
+ def test_from_context_store_with_limit(self, temp_db: Path) -> None:
868
+ """Test that limit is respected."""
869
+ store = ContextStore(temp_db)
870
+ for i in range(10):
871
+ store.record_event("commit", "/repo", {"message": f"Commit {i}"})
872
+
873
+ data = GraphData.from_context_store(store, limit=5)
874
+
875
+ assert len(data.events) == 5
876
+ store.close()
877
+
878
+ def test_from_context_store_with_event_types(self, temp_db: Path) -> None:
879
+ """Test filtering by event types."""
880
+ store = ContextStore(temp_db)
881
+ store.record_event("commit", "/repo", {"message": "Commit"})
882
+ store.record_event("push", "/repo", {"remote": "portal"})
883
+
884
+ data = GraphData.from_context_store(store, event_types=["commit"])
885
+
886
+ assert len(data.events) == 1
887
+ assert data.events[0].event_type == "commit"
888
+ store.close()
889
+
890
+ def test_from_context_store_includes_edges(self, temp_db: Path) -> None:
891
+ """Test that edges between events are included."""
892
+ store = ContextStore(temp_db)
893
+ event1 = store.record_event("commit", "/repo", {"message": "First"})
894
+ event2 = store.record_event("push", "/repo", {"remote": "portal"})
895
+ store.add_edge(event2.id, event1.id, "caused_by")
896
+
897
+ data = GraphData.from_context_store(store)
898
+
899
+ assert len(data.edges) == 1
900
+ assert data.edges[0].source_id == event2.id
901
+ assert data.edges[0].target_id == event1.id
902
+ store.close()