"""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") == ([], [])