loosecanvas / tests /test_config_extractor.py
Joshua Sundance Bailey
loosecanvas: local AI thought-mapping canvas with a trust-tagged knowledge graph
6d1438c
Raw
History Blame Contribute Delete
4.9 kB
"""M07 — Config + Docs Extractor. Red/green tests derived from the spec.
Success criteria under test (archive/plan-poc-m01-m23/modules/02-layer-1-extraction-pipeline/m07):
- Parses pyproject/requirements/GitHub Actions/README into ``Node``/``Edge`` lists.
- ``pyproject.toml`` yields a project node plus >=1 dependency node + ``depends_on`` edge.
- ``requirements.txt`` yields one node per real requirement; comments/blank lines skipped.
- ``README.md`` headings yield a document node plus section nodes + ``contains`` edges.
- A GitHub Actions workflow yields a workflow node plus job nodes (parsed via safe_load).
- Malformed TOML / YAML and a missing file degrade gracefully (no raise; empty/partial).
"""
from __future__ import annotations
from pathlib import Path
from loosecanvas.contracts import Edge, Node
from loosecanvas.extractors.config_extractor import (
extract_github_actions,
extract_pyproject,
extract_readme,
extract_requirements,
)
def test_extract_pyproject_yields_project_and_dependency_nodes(tmp_path: Path) -> None:
path = tmp_path / "pyproject.toml"
path.write_text(
"""
[project]
name = "demo-pkg"
version = "1.2.3"
dependencies = [
"requests>=2.0",
"pydantic>=2",
]
""",
encoding="utf-8",
)
nodes, edges = extract_pyproject(path)
assert all(isinstance(n, Node) for n in nodes)
assert all(isinstance(e, Edge) for e in edges)
project_nodes = [n for n in nodes if n.kind == "package"]
assert len(project_nodes) == 1
assert project_nodes[0].label == "demo-pkg"
dep_labels = {n.label for n in nodes if n.kind == "dependency"}
assert "requests>=2.0" in dep_labels
assert "pydantic>=2" in dep_labels
assert len(edges) >= 2
assert all(e.kind == "depends_on" for e in edges)
assert all(e.source == project_nodes[0].id for e in edges)
def test_extract_requirements_skips_comments_and_blanks(tmp_path: Path) -> None:
path = tmp_path / "requirements.txt"
path.write_text(
"""
# top-level comment
requests>=2.0
pydantic==2.5.0 # inline comment
numpy
""",
encoding="utf-8",
)
nodes, edges = extract_requirements(path)
labels = [n.label for n in nodes]
kinds = {n.kind for n in nodes}
assert kinds == {"dependency"}
assert len(nodes) == 3
assert "requests>=2.0" in labels
assert "pydantic==2.5.0" in labels
assert "numpy" in labels
assert edges == []
def test_extract_readme_yields_document_and_section_nodes(tmp_path: Path) -> None:
path = tmp_path / "README.md"
path.write_text(
"""# Project Title
Intro paragraph.
## Installation
Steps here.
## Usage
More text.
""",
encoding="utf-8",
)
nodes, edges = extract_readme(path)
doc_nodes = [n for n in nodes if n.kind == "document"]
assert len(doc_nodes) == 1
section_labels = [n.label for n in nodes if n.kind == "section"]
assert "Project Title" in section_labels
assert "Installation" in section_labels
assert "Usage" in section_labels
assert edges
assert all(e.kind == "contains" for e in edges)
assert all(e.source == doc_nodes[0].id for e in edges)
def test_extract_github_actions_yields_workflow_and_job_nodes(tmp_path: Path) -> None:
path = tmp_path / "ci.yml"
path.write_text(
"""
name: CI
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: echo build
test:
runs-on: ubuntu-latest
steps:
- run: echo test
""",
encoding="utf-8",
)
nodes, edges = extract_github_actions(path)
workflow_nodes = [n for n in nodes if n.kind == "workflow"]
assert len(workflow_nodes) == 1
assert workflow_nodes[0].label == "CI"
job_labels = {n.label for n in nodes if n.kind == "job"}
assert job_labels == {"build", "test"}
assert len(edges) == 2
assert all(e.kind == "contains" for e in edges)
assert all(e.source == workflow_nodes[0].id for e in edges)
def test_malformed_toml_degrades_gracefully(tmp_path: Path) -> None:
path = tmp_path / "pyproject.toml"
path.write_text("this is = = not valid toml [[[", encoding="utf-8")
nodes, edges = extract_pyproject(path)
assert nodes == []
assert edges == []
def test_malformed_yaml_degrades_gracefully(tmp_path: Path) -> None:
path = tmp_path / "ci.yml"
path.write_text("name: CI\n bad: : : indentation\n\t- nope", encoding="utf-8")
nodes, edges = extract_github_actions(path)
assert nodes == []
assert edges == []
def test_missing_file_degrades_gracefully(tmp_path: Path) -> None:
missing = tmp_path / "does_not_exist.toml"
assert extract_pyproject(missing) == ([], [])
assert extract_requirements(tmp_path / "missing.txt") == ([], [])
assert extract_readme(tmp_path / "missing.md") == ([], [])
assert extract_github_actions(tmp_path / "missing.yml") == ([], [])