Navegador

navegador / tests / test_ingestion_planopticon.py
Blame History Raw 647 lines
1
"""Tests for navegador.ingestion.planopticon — PlanopticonIngester."""
2
3
import json
4
import tempfile
5
from pathlib import Path
6
from unittest.mock import MagicMock
7
8
import pytest
9
10
from navegador.graph.schema import NodeLabel
11
from navegador.ingestion.planopticon import (
12
EDGE_MAP,
13
NODE_TYPE_MAP,
14
PLANNING_TYPE_MAP,
15
PlanopticonIngester,
16
)
17
18
19
def _make_store():
20
store = MagicMock()
21
store.query.return_value = MagicMock(result_set=[])
22
return store
23
24
25
# ── Fixtures ──────────────────────────────────────────────────────────────────
26
27
KG_DATA = {
28
"nodes": [
29
{"id": "n1", "type": "concept", "name": "Payment Gateway",
30
"description": "Handles payments"},
31
{"id": "n2", "type": "person", "name": "Carol", "email": "[email protected]"},
32
{"id": "n3", "type": "technology", "name": "PostgreSQL", "description": "DB"},
33
{"id": "n4", "type": "decision", "name": "Use Redis"},
34
{"id": "n5", "type": "unknown_type", "name": "Misc"},
35
{"id": "n6", "type": "diagram", "name": "Service Map", "source": "http://img.png"},
36
],
37
"relationships": [
38
{"source": "Payment Gateway", "target": "PostgreSQL", "type": "uses"},
39
{"source": "Carol", "target": "Payment Gateway", "type": "assigned_to"},
40
{"source": "", "target": "nope", "type": "related_to"}, # bad rel — no source
41
],
42
"sources": [
43
{"title": "Meeting 2024", "url": "https://ex.com", "source_type": "meeting"},
44
],
45
}
46
47
INTERCHANGE_DATA = {
48
"project": {"name": "MyProject", "tags": ["backend", "payments"]},
49
"entities": [
50
{
51
"planning_type": "decision",
52
"name": "Adopt microservices",
53
"description": "Split the monolith",
54
"status": "accepted",
55
"rationale": "Scale independently",
56
},
57
{
58
"planning_type": "requirement",
59
"name": "PCI compliance",
60
"description": "Must comply with PCI-DSS",
61
"priority": "high",
62
},
63
{
64
"planning_type": "goal",
65
"name": "Increase uptime",
66
"description": "99.9% SLA",
67
},
68
{
69
# no planning_type → falls through to _ingest_kg_node
70
"type": "concept",
71
"name": "Event Sourcing",
72
},
73
],
74
"relationships": [],
75
"artifacts": [
76
{"name": "Architecture Diagram", "content": "mermaid content here"},
77
],
78
"sources": [],
79
}
80
81
MANIFEST_DATA = {
82
"video": {"title": "Sprint Planning", "url": "https://example.com/video/1"},
83
"key_points": [
84
{"point": "Use async everywhere", "topic": "Architecture", "details": "For scale"},
85
],
86
"action_items": [
87
{"action": "Refactor auth service", "assignee": "Bob", "context": "High priority"},
88
],
89
"diagrams": [
90
{
91
"diagram_type": "sequence",
92
"timestamp": 120,
93
"description": "Auth flow",
94
"mermaid": "sequenceDiagram ...",
95
"elements": ["User", "Auth"],
96
}
97
],
98
}
99
100
101
# ── Maps ──────────────────────────────────────────────────────────────────────
102
103
class TestMaps:
104
def test_node_type_map_coverage(self):
105
assert NODE_TYPE_MAP["concept"] == NodeLabel.Concept
106
assert NODE_TYPE_MAP["technology"] == NodeLabel.Concept
107
assert NODE_TYPE_MAP["organization"] == NodeLabel.Concept
108
assert NODE_TYPE_MAP["person"] == NodeLabel.Person
109
assert NODE_TYPE_MAP["diagram"] == NodeLabel.WikiPage
110
111
def test_planning_type_map_coverage(self):
112
assert PLANNING_TYPE_MAP["decision"] == NodeLabel.Decision
113
assert PLANNING_TYPE_MAP["requirement"] == NodeLabel.Rule
114
assert PLANNING_TYPE_MAP["constraint"] == NodeLabel.Rule
115
assert PLANNING_TYPE_MAP["risk"] == NodeLabel.Rule
116
assert PLANNING_TYPE_MAP["goal"] == NodeLabel.Concept
117
118
def test_edge_map_coverage(self):
119
from navegador.graph.schema import EdgeType
120
assert EDGE_MAP["uses"] == EdgeType.DEPENDS_ON
121
assert EDGE_MAP["related_to"] == EdgeType.RELATED_TO
122
assert EDGE_MAP["assigned_to"] == EdgeType.ASSIGNED_TO
123
assert EDGE_MAP["governs"] == EdgeType.GOVERNS
124
assert EDGE_MAP["implements"] == EdgeType.IMPLEMENTS
125
126
127
# ── ingest_kg ─────────────────────────────────────────────────────────────────
128
129
class TestIngestKg:
130
def test_ingests_concept_nodes(self):
131
store = _make_store()
132
ingester = PlanopticonIngester(store)
133
with tempfile.TemporaryDirectory() as tmpdir:
134
p = Path(tmpdir) / "kg.json"
135
p.write_text(json.dumps(KG_DATA))
136
stats = ingester.ingest_kg(p)
137
assert stats["nodes"] >= 1
138
139
def test_ingests_person_nodes(self):
140
store = _make_store()
141
ingester = PlanopticonIngester(store)
142
with tempfile.TemporaryDirectory() as tmpdir:
143
p = Path(tmpdir) / "kg.json"
144
p.write_text(json.dumps(KG_DATA))
145
ingester.ingest_kg(p)
146
labels = [c[0][0] for c in store.create_node.call_args_list]
147
assert NodeLabel.Person in labels
148
149
def test_ingests_technology_as_concept(self):
150
store = _make_store()
151
ingester = PlanopticonIngester(store)
152
data = {"nodes": [{"type": "technology", "name": "PostgreSQL"}],
153
"relationships": [], "sources": []}
154
with tempfile.TemporaryDirectory() as tmpdir:
155
p = Path(tmpdir) / "kg.json"
156
p.write_text(json.dumps(data))
157
ingester.ingest_kg(p)
158
labels = [c[0][0] for c in store.create_node.call_args_list]
159
assert NodeLabel.Concept in labels
160
161
def test_ingests_diagram_as_wiki_page(self):
162
store = _make_store()
163
ingester = PlanopticonIngester(store)
164
data = {"nodes": [{"type": "diagram", "name": "Arch Diagram", "source": "http://x.com"}],
165
"relationships": [], "sources": []}
166
with tempfile.TemporaryDirectory() as tmpdir:
167
p = Path(tmpdir) / "kg.json"
168
p.write_text(json.dumps(data))
169
ingester.ingest_kg(p)
170
labels = [c[0][0] for c in store.create_node.call_args_list]
171
assert NodeLabel.WikiPage in labels
172
173
def test_skips_nodes_without_name(self):
174
store = _make_store()
175
ingester = PlanopticonIngester(store)
176
data = {"nodes": [{"type": "concept", "name": ""}], "relationships": [], "sources": []}
177
with tempfile.TemporaryDirectory() as tmpdir:
178
p = Path(tmpdir) / "kg.json"
179
p.write_text(json.dumps(data))
180
stats = ingester.ingest_kg(p)
181
assert stats["nodes"] == 0
182
183
def test_ingests_sources_as_wiki_pages(self):
184
store = _make_store()
185
ingester = PlanopticonIngester(store)
186
data = {
187
"nodes": [], "relationships": [],
188
"sources": [
189
{"title": "Meeting 2024", "url": "https://ex.com", "source_type": "meeting"},
190
],
191
}
192
with tempfile.TemporaryDirectory() as tmpdir:
193
p = Path(tmpdir) / "kg.json"
194
p.write_text(json.dumps(data))
195
stats = ingester.ingest_kg(p)
196
assert stats["nodes"] >= 1
197
labels = [c[0][0] for c in store.create_node.call_args_list]
198
assert NodeLabel.WikiPage in labels
199
200
def test_ingests_relationships(self):
201
store = _make_store()
202
ingester = PlanopticonIngester(store)
203
with tempfile.TemporaryDirectory() as tmpdir:
204
p = Path(tmpdir) / "kg.json"
205
p.write_text(json.dumps(KG_DATA))
206
stats = ingester.ingest_kg(p)
207
assert stats["edges"] >= 1
208
store.query.assert_called()
209
210
def test_skips_bad_relationships(self):
211
store = _make_store()
212
ingester = PlanopticonIngester(store)
213
data = {"nodes": [], "relationships": [{"source": "", "target": "x", "type": "related_to"}],
214
"sources": []}
215
with tempfile.TemporaryDirectory() as tmpdir:
216
p = Path(tmpdir) / "kg.json"
217
p.write_text(json.dumps(data))
218
stats = ingester.ingest_kg(p)
219
assert stats["edges"] == 0
220
221
def test_missing_file_raises(self):
222
store = _make_store()
223
ingester = PlanopticonIngester(store)
224
with pytest.raises(FileNotFoundError):
225
ingester.ingest_kg("/nonexistent/kg.json")
226
227
def test_returns_stats_dict(self):
228
store = _make_store()
229
ingester = PlanopticonIngester(store)
230
with tempfile.TemporaryDirectory() as tmpdir:
231
p = Path(tmpdir) / "kg.json"
232
p.write_text(json.dumps({"nodes": [], "relationships": [], "sources": []}))
233
stats = ingester.ingest_kg(p)
234
assert "nodes" in stats
235
assert "edges" in stats
236
237
238
# ── ingest_interchange ────────────────────────────────────────────────────────
239
240
class TestIngestInterchange:
241
def test_ingests_decision_entities(self):
242
store = _make_store()
243
ingester = PlanopticonIngester(store)
244
with tempfile.TemporaryDirectory() as tmpdir:
245
p = Path(tmpdir) / "interchange.json"
246
p.write_text(json.dumps(INTERCHANGE_DATA))
247
ingester.ingest_interchange(p)
248
labels = [c[0][0] for c in store.create_node.call_args_list]
249
assert NodeLabel.Decision in labels
250
251
def test_ingests_requirement_as_rule(self):
252
store = _make_store()
253
ingester = PlanopticonIngester(store)
254
with tempfile.TemporaryDirectory() as tmpdir:
255
p = Path(tmpdir) / "interchange.json"
256
p.write_text(json.dumps(INTERCHANGE_DATA))
257
ingester.ingest_interchange(p)
258
labels = [c[0][0] for c in store.create_node.call_args_list]
259
assert NodeLabel.Rule in labels
260
261
def test_creates_domain_nodes_from_project_tags(self):
262
store = _make_store()
263
ingester = PlanopticonIngester(store)
264
with tempfile.TemporaryDirectory() as tmpdir:
265
p = Path(tmpdir) / "interchange.json"
266
p.write_text(json.dumps(INTERCHANGE_DATA))
267
ingester.ingest_interchange(p)
268
labels = [c[0][0] for c in store.create_node.call_args_list]
269
assert NodeLabel.Domain in labels
270
271
def test_ingests_artifacts_as_wiki_pages(self):
272
store = _make_store()
273
ingester = PlanopticonIngester(store)
274
with tempfile.TemporaryDirectory() as tmpdir:
275
p = Path(tmpdir) / "interchange.json"
276
p.write_text(json.dumps(INTERCHANGE_DATA))
277
ingester.ingest_interchange(p)
278
labels = [c[0][0] for c in store.create_node.call_args_list]
279
assert NodeLabel.WikiPage in labels
280
281
def test_empty_entities_returns_empty_stats(self):
282
store = _make_store()
283
ingester = PlanopticonIngester(store)
284
with tempfile.TemporaryDirectory() as tmpdir:
285
p = Path(tmpdir) / "interchange.json"
286
p.write_text(json.dumps({"project": {}, "entities": [], "relationships": [],
287
"artifacts": [], "sources": []}))
288
stats = ingester.ingest_interchange(p)
289
assert stats["nodes"] == 0
290
291
def test_returns_stats_dict(self):
292
store = _make_store()
293
ingester = PlanopticonIngester(store)
294
with tempfile.TemporaryDirectory() as tmpdir:
295
p = Path(tmpdir) / "interchange.json"
296
p.write_text(json.dumps(INTERCHANGE_DATA))
297
stats = ingester.ingest_interchange(p)
298
assert "nodes" in stats and "edges" in stats
299
300
301
# ── ingest_manifest ────────────────────────────────────────────────────────────
302
303
class TestIngestManifest:
304
def test_ingests_key_points_as_concepts(self):
305
store = _make_store()
306
ingester = PlanopticonIngester(store)
307
with tempfile.TemporaryDirectory() as tmpdir:
308
p = Path(tmpdir) / "manifest.json"
309
p.write_text(json.dumps(MANIFEST_DATA))
310
ingester.ingest_manifest(p)
311
labels = [c[0][0] for c in store.create_node.call_args_list]
312
assert NodeLabel.Concept in labels
313
314
def test_ingests_action_items_as_rules(self):
315
store = _make_store()
316
ingester = PlanopticonIngester(store)
317
with tempfile.TemporaryDirectory() as tmpdir:
318
p = Path(tmpdir) / "manifest.json"
319
p.write_text(json.dumps(MANIFEST_DATA))
320
ingester.ingest_manifest(p)
321
labels = [c[0][0] for c in store.create_node.call_args_list]
322
assert NodeLabel.Rule in labels
323
324
def test_ingests_action_item_assignee_as_person(self):
325
store = _make_store()
326
ingester = PlanopticonIngester(store)
327
with tempfile.TemporaryDirectory() as tmpdir:
328
p = Path(tmpdir) / "manifest.json"
329
p.write_text(json.dumps(MANIFEST_DATA))
330
ingester.ingest_manifest(p)
331
labels = [c[0][0] for c in store.create_node.call_args_list]
332
assert NodeLabel.Person in labels
333
334
def test_ingests_diagrams_as_wiki_pages(self):
335
store = _make_store()
336
ingester = PlanopticonIngester(store)
337
with tempfile.TemporaryDirectory() as tmpdir:
338
p = Path(tmpdir) / "manifest.json"
339
p.write_text(json.dumps(MANIFEST_DATA))
340
ingester.ingest_manifest(p)
341
labels = [c[0][0] for c in store.create_node.call_args_list]
342
assert NodeLabel.WikiPage in labels
343
344
def test_diagram_elements_become_concepts(self):
345
store = _make_store()
346
ingester = PlanopticonIngester(store)
347
with tempfile.TemporaryDirectory() as tmpdir:
348
p = Path(tmpdir) / "manifest.json"
349
p.write_text(json.dumps(MANIFEST_DATA))
350
ingester.ingest_manifest(p)
351
# "User" and "Auth" are diagram elements → Concept nodes
352
names = [c[0][1].get("name") for c in store.create_node.call_args_list
353
if isinstance(c[0][1], dict)]
354
assert "User" in names or "Auth" in names
355
356
def test_loads_external_kg_json(self):
357
store = _make_store()
358
ingester = PlanopticonIngester(store)
359
with tempfile.TemporaryDirectory() as tmpdir:
360
kg = {"nodes": [{"type": "concept", "name": "External Concept"}],
361
"relationships": [], "sources": []}
362
(Path(tmpdir) / "kg.json").write_text(json.dumps(kg))
363
manifest = dict(MANIFEST_DATA)
364
manifest["knowledge_graph_json"] = "kg.json"
365
p = Path(tmpdir) / "manifest.json"
366
p.write_text(json.dumps(manifest))
367
ingester.ingest_manifest(p)
368
names = [c[0][1].get("name") for c in store.create_node.call_args_list
369
if isinstance(c[0][1], dict)]
370
assert "External Concept" in names
371
372
def test_empty_manifest_no_crash(self):
373
store = _make_store()
374
ingester = PlanopticonIngester(store)
375
with tempfile.TemporaryDirectory() as tmpdir:
376
p = Path(tmpdir) / "manifest.json"
377
p.write_text(json.dumps({}))
378
stats = ingester.ingest_manifest(p)
379
assert "nodes" in stats
380
381
382
# ── ingest_batch ──────────────────────────────────────────────────────────────
383
384
class TestIngestBatch:
385
def test_processes_merged_kg_if_present(self):
386
store = _make_store()
387
ingester = PlanopticonIngester(store)
388
with tempfile.TemporaryDirectory() as tmpdir:
389
kg = {"nodes": [{"type": "concept", "name": "Merged"}],
390
"relationships": [], "sources": []}
391
(Path(tmpdir) / "merged.json").write_text(json.dumps(kg))
392
batch = {"merged_knowledge_graph_json": "merged.json"}
393
p = Path(tmpdir) / "batch.json"
394
p.write_text(json.dumps(batch))
395
ingester.ingest_batch(p)
396
names = [c[0][1].get("name") for c in store.create_node.call_args_list
397
if isinstance(c[0][1], dict)]
398
assert "Merged" in names
399
400
def test_processes_completed_videos(self):
401
store = _make_store()
402
ingester = PlanopticonIngester(store)
403
with tempfile.TemporaryDirectory() as tmpdir:
404
(Path(tmpdir) / "vid1.json").write_text(json.dumps(MANIFEST_DATA))
405
batch = {
406
"videos": [
407
{"status": "completed", "manifest_path": "vid1.json"},
408
{"status": "pending", "manifest_path": "vid1.json"}, # skipped
409
]
410
}
411
p = Path(tmpdir) / "batch.json"
412
p.write_text(json.dumps(batch))
413
stats = ingester.ingest_batch(p)
414
assert "nodes" in stats
415
416
def test_missing_manifest_raises(self):
417
store = _make_store()
418
ingester = PlanopticonIngester(store)
419
with tempfile.TemporaryDirectory() as tmpdir:
420
batch = {
421
"videos": [
422
{"status": "completed", "manifest_path": "nonexistent.json"},
423
]
424
}
425
p = Path(tmpdir) / "batch.json"
426
p.write_text(json.dumps(batch))
427
with pytest.raises(FileNotFoundError):
428
ingester.ingest_batch(p)
429
430
def test_merges_stats_across_videos(self):
431
store = _make_store()
432
ingester = PlanopticonIngester(store)
433
with tempfile.TemporaryDirectory() as tmpdir:
434
(Path(tmpdir) / "v1.json").write_text(json.dumps(MANIFEST_DATA))
435
(Path(tmpdir) / "v2.json").write_text(json.dumps(MANIFEST_DATA))
436
batch = {
437
"videos": [
438
{"status": "completed", "manifest_path": "v1.json"},
439
{"status": "completed", "manifest_path": "v2.json"},
440
]
441
}
442
p = Path(tmpdir) / "batch.json"
443
p.write_text(json.dumps(batch))
444
stats = ingester.ingest_batch(p)
445
# Should have processed both, stats should be non-zero
446
assert stats.get("nodes", 0) >= 0 # at least doesn't crash
447
448
449
# ── _reset_stats / _merge_stats ───────────────────────────────────────────────
450
451
class TestInternalHelpers:
452
def test_reset_stats(self):
453
store = _make_store()
454
ingester = PlanopticonIngester(store)
455
ingester._stats = {"nodes": 5, "edges": 3}
456
stats = ingester._reset_stats()
457
assert stats == {"nodes": 0, "edges": 0}
458
459
def test_merge_stats(self):
460
store = _make_store()
461
ingester = PlanopticonIngester(store)
462
ingester._stats = {"nodes": 2, "edges": 1}
463
ingester._merge_stats({"nodes": 3, "edges": 2, "pages": 1})
464
assert ingester._stats["nodes"] == 5
465
assert ingester._stats["edges"] == 3
466
assert ingester._stats["pages"] == 1
467
468
def test_load_json_missing_file_raises(self):
469
store = _make_store()
470
ingester = PlanopticonIngester(store)
471
with pytest.raises(FileNotFoundError):
472
ingester._load_json(Path("/nonexistent/file.json"))
473
474
def test_load_json_invalid_json_raises(self):
475
store = _make_store()
476
ingester = PlanopticonIngester(store)
477
with tempfile.TemporaryDirectory() as tmpdir:
478
p = Path(tmpdir) / "bad.json"
479
p.write_text("{ not valid json }")
480
with pytest.raises((json.JSONDecodeError, ValueError)):
481
ingester._load_json(p)
482
483
484
# ── ingest_interchange relationship/source branches (lines 201, 209) ──────────
485
486
class TestInterchangeRelationshipsAndSources:
487
def test_ingests_relationships_in_interchange(self):
488
store = _make_store()
489
ingester = PlanopticonIngester(store)
490
data = {
491
"project": {"name": "Proj", "tags": []},
492
"entities": [],
493
"relationships": [
494
{"source": "Alice", "target": "Bob", "type": "related_to"}
495
],
496
"artifacts": [],
497
"sources": [],
498
}
499
with tempfile.TemporaryDirectory() as tmpdir:
500
p = Path(tmpdir) / "interchange.json"
501
p.write_text(json.dumps(data))
502
stats = ingester.ingest_interchange(p)
503
store.query.assert_called()
504
assert stats["edges"] >= 1
505
506
def test_ingests_sources_in_interchange(self):
507
store = _make_store()
508
ingester = PlanopticonIngester(store)
509
data = {
510
"project": {"name": "Proj", "tags": []},
511
"entities": [],
512
"relationships": [],
513
"artifacts": [],
514
"sources": [{"title": "Design Doc", "url": "http://ex.com"}],
515
}
516
with tempfile.TemporaryDirectory() as tmpdir:
517
p = Path(tmpdir) / "interchange.json"
518
p.write_text(json.dumps(data))
519
ingester.ingest_interchange(p)
520
labels = [c[0][0] for c in store.create_node.call_args_list]
521
assert NodeLabel.WikiPage in labels
522
523
524
# ── _ingest_kg_node with domain (lines 274-275) ───────────────────────────────
525
526
class TestIngestKgNodeWithDomain:
527
def test_concept_with_domain_creates_domain_link(self):
528
store = _make_store()
529
ingester = PlanopticonIngester(store)
530
ingester._ingest_kg_node({"type": "concept", "name": "Auth", "domain": "Security"})
531
domain_calls = [c[0][0] for c in store.create_node.call_args_list]
532
assert NodeLabel.Domain in domain_calls
533
store.create_edge.assert_called()
534
535
536
# ── _ingest_planning_entity guards and domain (lines 293, 325-326) ────────────
537
538
class TestIngestPlanningEntityBranches:
539
def test_skips_entity_with_empty_name(self):
540
store = _make_store()
541
ingester = PlanopticonIngester(store)
542
ingester._ingest_planning_entity({"planning_type": "decision", "name": ""})
543
store.create_node.assert_not_called()
544
545
def test_entity_with_domain_creates_domain_link(self):
546
store = _make_store()
547
ingester = PlanopticonIngester(store)
548
ingester._ingest_planning_entity({
549
"planning_type": "decision",
550
"name": "Switch to Postgres",
551
"domain": "Infrastructure",
552
})
553
domain_calls = [c[0][0] for c in store.create_node.call_args_list]
554
assert NodeLabel.Domain in domain_calls
555
store.create_edge.assert_called()
556
557
558
# ── _ingest_kg_relationship exception handler (lines 353-354) ─────────────────
559
560
class TestIngestKgRelationshipException:
561
def test_exception_in_query_is_swallowed(self):
562
store = _make_store()
563
store.query.side_effect = Exception("graph error")
564
ingester = PlanopticonIngester(store)
565
# Should not raise
566
ingester._ingest_kg_relationship({"source": "A", "target": "B", "type": "related_to"})
567
assert ingester._stats.get("edges", 0) == 0
568
569
570
# ── _ingest_key_points empty-point skip (line 360) ───────────────────────────
571
572
class TestIngestKeyPointsEmptySkip:
573
def test_skips_empty_point(self):
574
store = _make_store()
575
ingester = PlanopticonIngester(store)
576
ingester._ingest_key_points([{"point": "", "topic": "foo"}], "source")
577
store.create_node.assert_not_called()
578
579
580
# ── _ingest_action_items empty-action skip (line 383) ────────────────────────
581
582
class TestIngestActionItemsEmptySkip:
583
def test_skips_empty_action(self):
584
store = _make_store()
585
ingester = PlanopticonIngester(store)
586
ingester._ingest_action_items([{"action": "", "assignee": "Bob"}], "source")
587
store.create_node.assert_not_called()
588
589
590
# ── diagram element empty-string skip (line 426) ─────────────────────────────
591
592
class TestDiagramElementEmptySkip:
593
def test_skips_empty_diagram_element(self):
594
store = _make_store()
595
ingester = PlanopticonIngester(store)
596
with tempfile.TemporaryDirectory() as tmpdir:
597
p = Path(tmpdir) / "manifest.json"
598
p.write_text(json.dumps({
599
"video": {"title": "T", "url": "http://x.com"},
600
"key_points": [],
601
"action_items": [],
602
"diagrams": [{
603
"diagram_type": "sequence",
604
"timestamp": 0,
605
"description": "D",
606
"mermaid": "",
607
"elements": ["", " "], # all empty/whitespace
608
}],
609
}))
610
ingester.ingest_manifest(p)
611
# Only WikiPage for the diagram itself; no Concept for elements
612
concept_calls = [c for c in store.create_node.call_args_list
613
if c[0][0] == NodeLabel.Concept]
614
assert len(concept_calls) == 0
615
616
617
# ── _ingest_source empty name guard (line 440) ───────────────────────────────
618
619
class TestIngestSourceEmptyName:
620
def test_skips_source_with_no_name(self):
621
store = _make_store()
622
ingester = PlanopticonIngester(store)
623
ingester._ingest_source({"title": "", "source_id": None, "url": ""})
624
store.create_node.assert_not_called()
625
626
627
# ── _ingest_artifact empty name guard (line 453) ─────────────────────────────
628
629
class TestIngestArtifactEmptyName:
630
def test_skips_artifact_with_no_name(self):
631
store = _make_store()
632
ingester = PlanopticonIngester(store)
633
ingester._ingest_artifact({"name": ""}, "project")
634
store.create_node.assert_not_called()
635
636
637
# ── _lazy_wiki_link exception handler (lines 476-477) ────────────────────────
638
639
class TestLazyWikiLinkException:
640
def test_exception_in_create_edge_is_swallowed(self):
641
from navegador.graph.schema import NodeLabel
642
store = _make_store()
643
store.create_edge.side_effect = Exception("no such node")
644
ingester = PlanopticonIngester(store)
645
# Should not raise
646
ingester._lazy_wiki_link("AuthService", NodeLabel.Concept, "source-123")
647

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button