| | @@ -475,5 +475,170 @@ |
| 475 | 475 | with tempfile.TemporaryDirectory() as tmpdir: |
| 476 | 476 | p = Path(tmpdir) / "bad.json" |
| 477 | 477 | p.write_text("{ not valid json }") |
| 478 | 478 | result = ingester._load_json(p) |
| 479 | 479 | assert result == {} |
| 480 | + |
| 481 | + |
| 482 | +# ── ingest_interchange relationship/source branches (lines 201, 209) ────────── |
| 483 | + |
| 484 | +class TestInterchangeRelationshipsAndSources: |
| 485 | + def test_ingests_relationships_in_interchange(self): |
| 486 | + store = _make_store() |
| 487 | + ingester = PlanopticonIngester(store) |
| 488 | + data = { |
| 489 | + "project": {"name": "Proj", "tags": []}, |
| 490 | + "entities": [], |
| 491 | + "relationships": [ |
| 492 | + {"source": "Alice", "target": "Bob", "type": "related_to"} |
| 493 | + ], |
| 494 | + "artifacts": [], |
| 495 | + "sources": [], |
| 496 | + } |
| 497 | + with tempfile.TemporaryDirectory() as tmpdir: |
| 498 | + p = Path(tmpdir) / "interchange.json" |
| 499 | + p.write_text(json.dumps(data)) |
| 500 | + stats = ingester.ingest_interchange(p) |
| 501 | + store.query.assert_called() |
| 502 | + assert stats["edges"] >= 1 |
| 503 | + |
| 504 | + def test_ingests_sources_in_interchange(self): |
| 505 | + store = _make_store() |
| 506 | + ingester = PlanopticonIngester(store) |
| 507 | + data = { |
| 508 | + "project": {"name": "Proj", "tags": []}, |
| 509 | + "entities": [], |
| 510 | + "relationships": [], |
| 511 | + "artifacts": [], |
| 512 | + "sources": [{"title": "Design Doc", "url": "http://ex.com"}], |
| 513 | + } |
| 514 | + with tempfile.TemporaryDirectory() as tmpdir: |
| 515 | + p = Path(tmpdir) / "interchange.json" |
| 516 | + p.write_text(json.dumps(data)) |
| 517 | + ingester.ingest_interchange(p) |
| 518 | + labels = [c[0][0] for c in store.create_node.call_args_list] |
| 519 | + assert NodeLabel.WikiPage in labels |
| 520 | + |
| 521 | + |
| 522 | +# ── _ingest_kg_node with domain (lines 274-275) ─────────────────────────────── |
| 523 | + |
| 524 | +class TestIngestKgNodeWithDomain: |
| 525 | + def test_concept_with_domain_creates_domain_link(self): |
| 526 | + store = _make_store() |
| 527 | + ingester = PlanopticonIngester(store) |
| 528 | + ingester._ingest_kg_node({"type": "concept", "name": "Auth", "domain": "Security"}) |
| 529 | + domain_calls = [c[0][0] for c in store.create_node.call_args_list] |
| 530 | + assert NodeLabel.Domain in domain_calls |
| 531 | + store.create_edge.assert_called() |
| 532 | + |
| 533 | + |
| 534 | +# ── _ingest_planning_entity guards and domain (lines 293, 325-326) ──────────── |
| 535 | + |
| 536 | +class TestIngestPlanningEntityBranches: |
| 537 | + def test_skips_entity_with_empty_name(self): |
| 538 | + store = _make_store() |
| 539 | + ingester = PlanopticonIngester(store) |
| 540 | + ingester._ingest_planning_entity({"planning_type": "decision", "name": ""}) |
| 541 | + store.create_node.assert_not_called() |
| 542 | + |
| 543 | + def test_entity_with_domain_creates_domain_link(self): |
| 544 | + store = _make_store() |
| 545 | + ingester = PlanopticonIngester(store) |
| 546 | + ingester._ingest_planning_entity({ |
| 547 | + "planning_type": "decision", |
| 548 | + "name": "Switch to Postgres", |
| 549 | + "domain": "Infrastructure", |
| 550 | + }) |
| 551 | + domain_calls = [c[0][0] for c in store.create_node.call_args_list] |
| 552 | + assert NodeLabel.Domain in domain_calls |
| 553 | + store.create_edge.assert_called() |
| 554 | + |
| 555 | + |
| 556 | +# ── _ingest_kg_relationship exception handler (lines 353-354) ───────────────── |
| 557 | + |
| 558 | +class TestIngestKgRelationshipException: |
| 559 | + def test_exception_in_query_is_swallowed(self): |
| 560 | + store = _make_store() |
| 561 | + store.query.side_effect = Exception("graph error") |
| 562 | + ingester = PlanopticonIngester(store) |
| 563 | + # Should not raise |
| 564 | + ingester._ingest_kg_relationship({"source": "A", "target": "B", "type": "related_to"}) |
| 565 | + assert ingester._stats.get("edges", 0) == 0 |
| 566 | + |
| 567 | + |
| 568 | +# ── _ingest_key_points empty-point skip (line 360) ─────────────────────────── |
| 569 | + |
| 570 | +class TestIngestKeyPointsEmptySkip: |
| 571 | + def test_skips_empty_point(self): |
| 572 | + store = _make_store() |
| 573 | + ingester = PlanopticonIngester(store) |
| 574 | + ingester._ingest_key_points([{"point": "", "topic": "foo"}], "source") |
| 575 | + store.create_node.assert_not_called() |
| 576 | + |
| 577 | + |
| 578 | +# ── _ingest_action_items empty-action skip (line 383) ──────────────────────── |
| 579 | + |
| 580 | +class TestIngestActionItemsEmptySkip: |
| 581 | + def test_skips_empty_action(self): |
| 582 | + store = _make_store() |
| 583 | + ingester = PlanopticonIngester(store) |
| 584 | + ingester._ingest_action_items([{"action": "", "assignee": "Bob"}], "source") |
| 585 | + store.create_node.assert_not_called() |
| 586 | + |
| 587 | + |
| 588 | +# ── diagram element empty-string skip (line 426) ───────────────────────────── |
| 589 | + |
| 590 | +class TestDiagramElementEmptySkip: |
| 591 | + def test_skips_empty_diagram_element(self): |
| 592 | + store = _make_store() |
| 593 | + ingester = PlanopticonIngester(store) |
| 594 | + with tempfile.TemporaryDirectory() as tmpdir: |
| 595 | + p = Path(tmpdir) / "manifest.json" |
| 596 | + p.write_text(json.dumps({ |
| 597 | + "video": {"title": "T", "url": "http://x.com"}, |
| 598 | + "key_points": [], |
| 599 | + "action_items": [], |
| 600 | + "diagrams": [{ |
| 601 | + "diagram_type": "sequence", |
| 602 | + "timestamp": 0, |
| 603 | + "description": "D", |
| 604 | + "mermaid": "", |
| 605 | + "elements": ["", " "], # all empty/whitespace |
| 606 | + }], |
| 607 | + })) |
| 608 | + ingester.ingest_manifest(p) |
| 609 | + # Only WikiPage for the diagram itself; no Concept for elements |
| 610 | + concept_calls = [c for c in store.create_node.call_args_list |
| 611 | + if c[0][0] == NodeLabel.Concept] |
| 612 | + assert len(concept_calls) == 0 |
| 613 | + |
| 614 | + |
| 615 | +# ── _ingest_source empty name guard (line 440) ─────────────────────────────── |
| 616 | + |
| 617 | +class TestIngestSourceEmptyName: |
| 618 | + def test_skips_source_with_no_name(self): |
| 619 | + store = _make_store() |
| 620 | + ingester = PlanopticonIngester(store) |
| 621 | + ingester._ingest_source({"title": "", "source_id": None, "url": ""}) |
| 622 | + store.create_node.assert_not_called() |
| 623 | + |
| 624 | + |
| 625 | +# ── _ingest_artifact empty name guard (line 453) ───────────────────────────── |
| 626 | + |
| 627 | +class TestIngestArtifactEmptyName: |
| 628 | + def test_skips_artifact_with_no_name(self): |
| 629 | + store = _make_store() |
| 630 | + ingester = PlanopticonIngester(store) |
| 631 | + ingester._ingest_artifact({"name": ""}, "project") |
| 632 | + store.create_node.assert_not_called() |
| 633 | + |
| 634 | + |
| 635 | +# ── _lazy_wiki_link exception handler (lines 476-477) ──────────────────────── |
| 636 | + |
| 637 | +class TestLazyWikiLinkException: |
| 638 | + def test_exception_in_create_edge_is_swallowed(self): |
| 639 | + from navegador.graph.schema import NodeLabel |
| 640 | + store = _make_store() |
| 641 | + store.create_edge.side_effect = Exception("no such node") |
| 642 | + ingester = PlanopticonIngester(store) |
| 643 | + # Should not raise |
| 644 | + ingester._lazy_wiki_link("AuthService", NodeLabel.Concept, "source-123") |
| 480 | 645 | |