Files
chemenu/tools/chemenu/tests/test_index_build.py
T
torben 54d9540c08
CI / verify (push) Successful in 56s
Release / release (push) Successful in 36s
stack: Konfidenz-Mechanismus ersatzlos entfernt, Korpus migriert (schliesst #60, #86)
Files changed:
- .wikitool-kb.json
- AGENTS.md
- CHANGES.md
- INSTALL-MCP.md
- INSTALL.md
- README.md
- VERSION
- instructions/capture-session.md
- instructions/dev/issue-tracking.md
- instructions/german-terminology.md
- instructions/kb-profiles.md
- instructions/migrate-corpus.md
- instructions/migrations/5.0.0-confidence-removal.md
- instructions/private-instance.md
- instructions/setup-instance.md
- instructions/wiki-lint/SKILL.md
- instructions/wiki-manage/SKILL.md
- instructions/wiki-query/SKILL.md
- kb/CONTRACT.md
- kb/CONVENTIONS.md
- kb/CONVENTIONS.md.template
- kb/concepts/architectures/Consolidation Tiers.md
- kb/concepts/architectures/Context Isolation.md
- kb/concepts/architectures/Cross-platform Agent Skills.md
- kb/concepts/architectures/Episodic Memory.md
- kb/concepts/architectures/Hybrid Search.md
- kb/concepts/architectures/Implementation Spectrum.md
- kb/concepts/architectures/Knowledge Graph.md
- kb/concepts/architectures/LLM Wiki Pattern.md
- kb/concepts/architectures/MCP-Leseserver.md
- kb/concepts/architectures/Memory Lifecycle.md
- kb/concepts/architectures/OKF Compatibility.md
- kb/concepts/architectures/Optional Instance Context File.md
- kb/concepts/architectures/Personalization Plane.md
- kb/concepts/architectures/Procedural Memory.md
- kb/concepts/architectures/RAG.md
- kb/concepts/architectures/Scale Ceiling.md
- kb/concepts/architectures/Semantic Memory.md
- kb/concepts/architectures/Three-Layer Architecture.md
- kb/concepts/architectures/Token Economics.md
- kb/concepts/architectures/Working Memory.md
- kb/concepts/decisions/Delete Rather Than Anonymize.md
- kb/concepts/decisions/Denylist over Allowlist.md
- kb/concepts/decisions/Diff-Reviewable Agent Edits.md
- kb/concepts/decisions/Dual Licensing by File Plan.md
- kb/concepts/decisions/Issue Label Scheme.md
- kb/concepts/decisions/KB Stack Versioning.md
- kb/concepts/decisions/Structural Enforcement over Documented Rule.md
- kb/concepts/patterns/Audit Trail.md
- kb/concepts/patterns/BM25.md
- kb/concepts/patterns/Command Round-Trip Integrity.md
- kb/concepts/patterns/Confidence Scoring.md
- kb/concepts/patterns/Contradiction Resolution.md
- kb/concepts/patterns/Entity Extraction.md
- kb/concepts/patterns/Filter on Ingest.md
- kb/concepts/patterns/Forgetting.md
- kb/concepts/patterns/Graph Traversal.md
- kb/concepts/patterns/Mesh Sync.md
- kb/concepts/patterns/Quality Scoring.md
- kb/concepts/patterns/Reciprocal Rank Fusion.md
- kb/concepts/patterns/Self-Healing.md
- kb/concepts/patterns/Shared vs Private.md
- kb/concepts/patterns/Typed Relationships.md
- kb/concepts/patterns/Vector Search.md
- kb/concepts/patterns/Work Coordination.md
- kb/concepts/problems/Ambient Environment Dependency.md
- kb/concepts/problems/Detect-Repair Asymmetry.md
- kb/concepts/problems/Green Suite Blind Spot.md
- kb/concepts/problems/Naming Convention Conflict.md
- kb/concepts/problems/Write-Once Frontmatter Fields.md
- kb/concepts/protocols/CPPC.md
- kb/concepts/protocols/Modbus.md
- kb/concepts/protocols/SSD TRIM.md
- kb/concepts/workflows/Anti-Cramming Heuristic.md
- kb/concepts/workflows/Bulk Operations.md
- kb/concepts/workflows/CI Integration.md
- kb/concepts/workflows/Checkpoint Audit.md
- kb/concepts/workflows/Claude Code Auto Mode.md
- kb/concepts/workflows/Content Quality Control.md
- kb/concepts/workflows/Crystallization.md
- kb/concepts/workflows/Event-Driven Automation.md
- kb/concepts/workflows/Hooks.md
- kb/concepts/workflows/Index Scaling.md
- kb/concepts/workflows/Iteration and Cost Limits.md
- kb/concepts/workflows/KB Migration.md
- kb/concepts/workflows/Knowledge Compounding.md
- kb/concepts/workflows/Lint Workflow.md
- kb/concepts/workflows/Mass-Update Gate.md
- kb/concepts/workflows/Multi-Agent Collaboration.md
- kb/concepts/workflows/Privacy and Governance.md
- kb/concepts/workflows/Publish-Remote Gate.md
- kb/concepts/workflows/Quality and Self-Correction.md
- kb/concepts/workflows/Semantic Lint Automation.md
- kb/concepts/workflows/Session Orientation.md
- kb/concepts/workflows/Split Merge Reclassify.md
- kb/concepts/workflows/Split Threshold.md
- kb/concepts/workflows/Stub Threshold.md
- kb/concepts/workflows/Supersession.md
- kb/concepts/workflows/User Management.md
- kb/concepts/workflows/Workflow Extraction.md
- kb/concepts/workflows/Workflow Orchestration.md
- kb/entities/people/Andrej Karpathy.md
- kb/entities/people/E3DC GmbH.md
- kb/entities/people/Rohit Gupta.md
- kb/entities/people/Vannevar Bush.md
- kb/entities/projects/BCDModule.md
- kb/entities/projects/Chemenu.md
- kb/entities/projects/andybalholm-edl.md
- kb/entities/projects/goresponsiveness.md
- kb/entities/projects/ha-core.md
- kb/entities/projects/hacs-e3dc.md
- kb/entities/projects/hacs-integration-blueprint.md
- kb/entities/projects/llm-wiki-skills.md
- kb/entities/projects/plugnburn-edl.md
- kb/entities/projects/wiki-skills-vanillaflava.md
- kb/entities/projects/wiki-skills.md
- kb/entities/systems/AGENTS.md.md
- kb/entities/systems/CLAUDE.md.md
- kb/entities/systems/E3DC.md
- kb/entities/systems/ENVIRONMENT.md.md
- kb/entities/systems/Memex.md
- kb/entities/systems/Tolkien Gateway.md
- kb/entities/technologies/Arch Linux.md
- kb/entities/technologies/Disk Encryption.md
- kb/entities/technologies/Docker.md
- kb/entities/technologies/GRUB.md
- kb/entities/technologies/Gitea Actions.md
- kb/entities/technologies/Gitea.md
- kb/entities/technologies/Go.md
- kb/entities/technologies/Home Assistant.md
- kb/entities/technologies/Kernel PM Governors.md
- kb/entities/technologies/LVM.md
- kb/entities/technologies/Linux Kernel.md
- kb/entities/technologies/MQTT.md
- kb/entities/technologies/OPC UA.md
- kb/entities/technologies/Python.md
- kb/entities/technologies/Rust.md
- kb/entities/technologies/Wine GE.md
- kb/entities/technologies/Wine-Staging.md
- kb/entities/technologies/acpi-cpufreq.md
- kb/entities/technologies/amd-pstate.md
- kb/entities/technologies/iii Engine.md
- kb/entities/tools/AUR.md
- kb/entities/tools/Act Runner.md
- kb/entities/tools/Agent Memory.md
- kb/entities/tools/Aura.md
- kb/entities/tools/Bottles.md
- kb/entities/tools/ChatGPT.md
- kb/entities/tools/Claude Code.md
- kb/entities/tools/Codex CLI.md
- kb/entities/tools/Dataview.md
- kb/entities/tools/GPG.md
- kb/entities/tools/GitHub Copilot.md
- kb/entities/tools/Gitea MCP Server.md
- kb/entities/tools/Lutris.md
- kb/entities/tools/Marp.md
- kb/entities/tools/Mistral Vibe.md
- kb/entities/tools/NotebookLM.md
- kb/entities/tools/Obsidian Web Clipper.md
- kb/entities/tools/Obsidian.md
- kb/entities/tools/OpenAI Codex.md
- kb/entities/tools/OpenCode.md
- kb/entities/tools/Pi.md
- kb/entities/tools/Proton.md
- kb/entities/tools/Steam.md
- kb/entities/tools/Wine.md
- kb/entities/tools/awesome-llm-wiki.md
- kb/entities/tools/farzaa gist.md
- kb/entities/tools/gdeploy.md
- kb/entities/tools/makepkg.md
- kb/entities/tools/pascalandy schema.md
- kb/entities/tools/qmd.md
- kb/entities/tools/wikitool.md
- kb/index.md
- kb/log.md
- raw/CONTRACT.md
- tools/CONTRACT.md
- tools/README.md
- tools/chemenu/api.py
- tools/chemenu/cli.py
- tools/chemenu/commands/confidence_decay.py
- tools/chemenu/commands/docs_verify.py
- tools/chemenu/commands/doctor.py
- tools/chemenu/commands/index_build.py
- tools/chemenu/commands/new_page.py
- tools/chemenu/commands/search.py
- tools/chemenu/commands/touch.py
- tools/chemenu/commands/version_cmd.py
- tools/chemenu/conventions.py
- tools/chemenu/corpus_diff.py
- tools/chemenu/frontmatter_io.py
- tools/chemenu/lint_core.py
- tools/chemenu/mcp/server.py
- tools/chemenu/page.py
- tools/chemenu/search/base.py
- tools/chemenu/search/filters.py
- tools/chemenu/search/ripgrep.py
- tools/chemenu/search/service.py
- tools/chemenu/search/types.py
- tools/chemenu/tests/conftest.py
- tools/chemenu/tests/test_api.py
- tools/chemenu/tests/test_confidence_decay.py
- tools/chemenu/tests/test_corpus_diff.py
- tools/chemenu/tests/test_docs_verify.py
- tools/chemenu/tests/test_frontmatter_io.py
- tools/chemenu/tests/test_index_build.py
- tools/chemenu/tests/test_kb_scan.py
- tools/chemenu/tests/test_lint.py
- tools/chemenu/tests/test_mcp_server.py
- tools/chemenu/tests/test_new_page.py
- tools/chemenu/tests/test_page_ops.py
- tools/chemenu/tests/test_provenance.py
- tools/chemenu/tests/test_raw_cmd.py
- tools/chemenu/tests/test_search.py
- tools/chemenu/tests/test_touch.py
- tools/chemenu/tests/test_type_resolver.py
- tools/chemenu/tests/test_version_cmd.py
- tools/chemenu/tests/test_xref.py
- tools/chemenu/version.py
- types/concept.md
- types/concept.schema.yaml
- types/entity.md
- types/entity.schema.yaml
- types/instruction.md
- types/type-spec.md
2026-09-10 19:51:48 +02:00

250 lines
9.1 KiB
Python

from pathlib import Path
import pytest
from chemenu.commands.index_build import (
SHARD_THRESHOLD,
_anchor,
build_index,
index_rebuild,
plan_index,
stale_shards,
)
from chemenu.frontmatter_io import write_page
from chemenu.kb_scan import GENERATED_INDEX, iter_kb_pages
from chemenu.type_resolver import resolver
def _area_title(subtype: str, type_path: str = "types/entity.md") -> str:
"""The display title `index rebuild` will use for a subtype's area.
Read from the type-spec rather than written out, because these titles follow
the KB language: hard-coding them made translating the wiki fail tests that
are not about wording at all.
"""
return resolver.get_layout(type_path)[subtype]["title"]
@pytest.fixture
def plan(kb_dir: Path):
return plan_index(kb_dir)
def _shard(plan: dict, kb_dir: Path, *parts: str) -> str:
return plan[kb_dir.joinpath(*parts, GENERATED_INDEX)]
def _section(content: str, heading: str) -> str:
"""Text from a heading line up to the next heading of any level."""
start = content.index(heading) + len(heading)
rest = content[start:]
for line in rest.splitlines(keepends=True):
if line.startswith("#"):
return rest[: rest.index(line)]
return rest
# --- the map ----------------------------------------------------------------
def test_map_reports_totals_per_collection(kb_dir):
content = build_index(kb_dir)
assert "**Total Pages:** 5" in content
assert "**Entities:** 3" in content
assert "**Concepts:** 1" in content
assert "**Sources:** 1" in content
def test_map_lists_an_empty_collection_rather_than_hiding_it(kb_dir):
"""comparisons/ holds no pages but is a real collection - a reader must
still be able to see that it exists."""
assert "| `comparisons/` | 0 |" in build_index(kb_dir)
def test_map_lists_every_area_with_its_count(kb_dir):
content = build_index(kb_dir)
assert f"| {_area_title('system')} | 2 |" in content
assert f"| {_area_title('tool')} | 1 |" in content
# Areas that exist as directories but hold no pages are simply absent.
assert f"| {_area_title('person')} |" not in content
def test_map_carries_no_page_rows(kb_dir):
"""The whole point of the map: reading it must not cost one row per page."""
content = build_index(kb_dir)
assert "[[aurora]]" not in content
assert "[[Modbus]]" not in content
assert len(content.splitlines()) < 60
def test_map_points_at_search_first(kb_dir):
assert "wikitool search" in build_index(kb_dir)
def test_map_links_to_each_collection_shard(kb_dir):
content = build_index(kb_dir)
assert f"[entities/{GENERATED_INDEX}](entities/{GENERATED_INDEX})" in content
def test_map_deep_links_an_inlined_area_by_anchor(kb_dir):
# The anchor is derived from the area's display title, so it follows the KB
# language along with it. Both sides of the link are generated in the same
# run, so they stay consistent; only a bookmark to an old anchor would break.
anchor = _anchor(_area_title("system"))
assert f"entities/{GENERATED_INDEX}#{anchor}" in build_index(kb_dir)
# --- collection shards ------------------------------------------------------
def test_collection_shard_holds_the_page_rows(plan, kb_dir):
entities = _shard(plan, kb_dir, "entities")
assert "[[aurora]]" in entities
assert "[[Borealis]]" in entities
assert "[[gdeploy]]" in entities
def test_pages_are_grouped_by_area_and_sorted_case_insensitively(plan, kb_dir):
systems = _section(_shard(plan, kb_dir, "entities"), f"## {_area_title('system')}")
assert systems.index("[[aurora]]") < systems.index("[[Borealis]]")
assert "[[gdeploy]]" not in systems
def test_area_titles_come_from_the_entity_type_spec_layout(plan, kb_dir):
entities = _shard(plan, kb_dir, "entities")
assert f"## {_area_title('system')}" in entities
assert f"## {_area_title('tool')}" in entities
def test_anchor_keeps_non_ascii_letters():
"""An area title follows the KB language, so it may carry a letter outside
`a-z`. Deleting it - which is what the old `[^a-z0-9\\s-]` did - produced a
map link (`#ablufe`) that pointed at no heading in the shard it named."""
assert _anchor("Abläufe") == "abläufe"
assert _anchor("Größere Muster") == "größere-muster"
# Punctuation is still dropped and any run of whitespace still collapses to
# a single hyphen.
assert _anchor("Tools & Utilities (v2)") == "tools-utilities-v2"
def test_area_titles_are_read_from_every_type_spec_not_only_entity(plan, kb_dir):
"""Gitea #59: the title lookup used to resolve `entity` by name and read
only its `layout:`, so a second type declaring one got `.title()` on its
directory name (`Protocols`) instead of the title it declared. The concept
areas are the first case; nothing about them is special."""
concepts = _shard(plan, kb_dir, "concepts")
declared = _area_title("protocol", "types/concept.md")
assert f"## {declared}" in concepts
assert "## Protocols" not in concepts
def test_summary_prefers_frontmatter_then_falls_back_to_body(plan, kb_dir):
entities = _shard(plan, kb_dir, "entities")
assert "Server hosting DocStore with ZFS storage" in entities # frontmatter
assert "Deploy tool." in entities # first line of ## Description
def test_shards_are_marked_generated(plan, kb_dir):
assert all(content.startswith("<!-- Generated by") for content in plan.values())
# --- sharding threshold -----------------------------------------------------
def test_area_over_the_threshold_gets_its_own_shard(kb_dir):
for n in range(SHARD_THRESHOLD + 1):
write_page(
kb_dir / f"entities/tools/tool-{n:03d}.md",
{
"type": "types/entity.md", "entity_type": "tool",
"created": "2026-08-01", "modified": "2026-08-01",
"summary": f"Tool {n}",
},
f"\n# tool-{n:03d}\n",
)
plan = plan_index(kb_dir)
tools_shard = kb_dir / "entities" / "tools" / GENERATED_INDEX
assert tools_shard in plan
assert "[[tool-000]]" in plan[tools_shard]
# The collection shard links to it instead of inlining the rows again.
entities = _shard(plan, kb_dir, "entities")
assert "[[tool-000]]" not in entities
assert f"tools/{GENERATED_INDEX}" in entities
# ...and the map points straight at the area's own shard.
assert f"entities/tools/{GENERATED_INDEX}" in plan[kb_dir / "index.md"]
def test_area_at_the_threshold_stays_inlined(kb_dir):
for n in range(SHARD_THRESHOLD - 1): # +1 existing tool = exactly threshold
write_page(
kb_dir / f"entities/tools/tool-{n:03d}.md",
{
"type": "types/entity.md", "entity_type": "tool",
"created": "2026-08-01", "modified": "2026-08-01", "summary": "x",
},
f"\n# tool-{n:03d}\n",
)
plan = plan_index(kb_dir)
assert kb_dir / "entities" / "tools" / GENERATED_INDEX not in plan
assert "[[tool-000]]" in _shard(plan, kb_dir, "entities")
# --- stale shards -----------------------------------------------------------
def test_stale_shard_is_detected(kb_dir, plan):
orphan = kb_dir / "entities" / "people" / GENERATED_INDEX
orphan.write_text("# leftover\n", encoding="utf-8")
assert stale_shards(kb_dir, plan) == [orphan]
def test_planned_shards_are_not_stale(kb_dir, plan):
for path, content in plan.items():
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content, encoding="utf-8")
assert stale_shards(kb_dir, plan_index(kb_dir)) == []
# --- interaction with the scanner -------------------------------------------
def test_scanner_ignores_generated_shards_at_any_depth(kb_dir, plan):
"""A shard lists every page in its subtree as a wikilink. Counted as a page
it would make every page look linked-to and silence the orphan check."""
for path, content in plan.items():
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content, encoding="utf-8")
(kb_dir / "entities" / "systems" / GENERATED_INDEX).write_text("# x\n", encoding="utf-8")
scanned = {p.name for p in iter_kb_pages(kb_dir)}
assert GENERATED_INDEX not in scanned
assert "COLLECTION.md" not in scanned
def test_rebuild_is_idempotent(kb_dir):
assert plan_index(kb_dir) == plan_index(kb_dir)
def test_rebuild_warns_about_a_nested_page_without_failing(kb_dir, capsys):
"""#57: `group_pages` still folds a nested page into its area silently -
that part is unchanged, reporting is the fix - so `index rebuild` warns
rather than refusing, and must not raise."""
write_page(
kb_dir / "entities/projects/someowner/nested-tool.md",
{"type": "types/entity.md", "entity_type": "project", "tags": [],
"created": "2026-07-25", "modified": "2026-07-25",
"related": [], "sources": []},
"\n# nested-tool\n",
)
index_rebuild(dry_run=False)
out = capsys.readouterr().out
assert "WARNING" in out
assert "nested-tool" in out
def test_rebuild_is_silent_about_a_healthy_tree(kb_dir, capsys):
index_rebuild(dry_run=False)
assert "WARNING" not in capsys.readouterr().out