#!/usr/bin/env python3 """Offline regression tests for the ECHO tooling. No network, no vault. Run: python3 test_echo_client.py (or: pytest test_echo_client.py) Covers echo.py body/scalar normalization, the routing-view consistency checker's helpers, and — as the guard for improvement #4 — asserts the shipped docs are in sync with routing.json. """ from __future__ import annotations import json import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent)) import echo # noqa: E402 import check_routing # noqa: E402 import echo_index # noqa: E402 import echo_links # noqa: E402 def test_heading_replace_body_is_newline_guarded() -> None: assert echo.normalize_patch_body(b"- [x] done\n", "replace", "heading") == b"\n- [x] done\n" def test_heading_append_body_ends_with_newline() -> None: assert echo.normalize_patch_body(b"- item", "append", "heading") == b"- item\n" def test_frontmatter_plain_string_becomes_json_scalar() -> None: assert json.loads(echo.normalize_json_scalar("Fabrication Lead")) == "Fabrication Lead" def test_frontmatter_existing_json_is_preserved() -> None: assert echo.normalize_json_scalar('"2026-06-20"') == b'"2026-06-20"' def test_read_many_dedups_and_maps_each_path() -> None: # Concurrency helper contract, no network: monkeypatch the per-file fetch. orig = echo.get_text echo.get_text = lambda p: f"T:{p}" try: assert echo.read_many(["a", "b", "a"]) == {"a": "T:a", "b": "T:b"} finally: echo.get_text = orig def test_read_many_empty_returns_empty_dict() -> None: assert echo.read_many([]) == {} def test_read_many_tolerates_unreadable_file_as_none() -> None: orig = echo.get_text echo.get_text = lambda p: None if p == "bad" else f"T:{p}" try: assert echo.read_many(["ok", "bad"]) == {"ok": "T:ok", "bad": None} finally: echo.get_text = orig def test_stem_extracts_literal_directory_prefix() -> None: assert check_routing.stem(r"^projects/active/[^/]+\.md$") == "projects/active/" assert check_routing.stem(r"^areas/(business|personal)/[^/]+\.md$") == "areas/" assert check_routing.stem(r"^journal/daily/\d{4}-\d{2}-\d{2}\.md$") == "journal/daily/" def test_concretize_substitutes_placeholders() -> None: assert check_routing.concretize("projects//.md") == "projects/active/sample.md" assert check_routing.concretize("_agent/sessions/YYYY-MM-DD-HHMM-.md") == "_agent/sessions/2026-01-02-0900-sample.md" assert check_routing.concretize("journal/weekly/YYYY-Www.md") == "journal/weekly/2026-W01.md" def test_looks_like_path_filters_non_paths() -> None: assert check_routing.looks_like_path("inbox/captures/inbox.md") assert check_routing.looks_like_path("_agent/locks/vault.lock") assert not check_routing.looks_like_path("application/vnd.olrapi.document-map+json") assert not check_routing.looks_like_path("Operator Preferences::Fact / Pattern") assert not check_routing.looks_like_path("status: active") def test_docs_are_in_sync_with_routing_json() -> None: # The guard for improvement #4: SKILL.md / routing-map.md / api-reference.md # must stay consistent with routing.json. assert check_routing.main() == 0 def test_index_slugify_and_derive_path() -> None: assert echo_index.slugify("Bob Smith!") == "bob-smith" assert echo_index.derive_path("person", "bob-smith") == "resources/people/bob-smith.md" assert echo_index.derive_path("project", "echo") == "projects/active/echo.md" assert echo_index.derive_path("area", "ops", domain="business") == "areas/business/ops.md" assert echo_index.derive_path("decision", "x", date="2026-01-02") == "decisions/by-date/2026-01-02-x.md" def test_index_resolve_matches_alias_and_title() -> None: index = {"entities": {"bob-smith": {"path": "resources/people/bob-smith.md", "kind": "person", "title": "Bob Smith", "aliases": ["bob"]}}} assert echo_index.resolve(index, "Bob Smith")[0] == "bob-smith" assert echo_index.resolve(index, "bob")[0] == "bob-smith" assert echo_index.resolve(index, "nobody")[0] is None def test_kind_for_path() -> None: assert echo_index.kind_for_path("resources/people/x.md") == "person" assert echo_index.kind_for_path("projects/on-hold/x.md") == "project" assert echo_index.kind_for_path("journal/daily/2026-01-01.md") is None def test_derive_aliases_produces_punctuation_variants() -> None: al = echo_index.derive_aliases("ECHO Memory") assert "echo-memory" in al and "echo memory" in al assert echo_index.derive_aliases("") == [] def test_upsert_folds_in_title_variants_and_drops_slug() -> None: index = {"entities": {}} e = echo_index.upsert(index, "echo-memory", "projects/active/echo-memory.md", "project", "Echo Memory") assert "echo memory" in e["aliases"] # space variant auto-derived assert "echo-memory" not in e["aliases"] # equals the slug -> not stored redundantly def test_fuzzy_candidates_finds_shortened_name() -> None: # The real bug: a project slugged "echo" must surface for the mention "echo memory". index = {"entities": {"echo": {"path": "projects/active/echo.md", "kind": "project", "title": "echo", "aliases": []}}} cands = echo_index.fuzzy_candidates(index, "echo memory") assert cands and cands[0][0] == "echo" # exact resolve still misses (it's exact-only) — fuzzy is the safety net: assert echo_index.resolve(index, "echo memory")[0] is None def test_fuzzy_candidates_ignores_common_and_short_tokens() -> None: index = {"entities": {"data": {"path": "x.md", "kind": "concept", "title": "data", "aliases": []}}} assert echo_index.fuzzy_candidates(index, "data pipeline") == [] def test_aliases_in_frontmatter_flow_and_block() -> None: flow = '---\ntype: project\naliases: ["echo memory", "echo plugin"]\n---\n# x\n' assert echo_index.aliases_in_frontmatter(flow) == ["echo memory", "echo plugin"] block = "---\ntype: project\naliases:\n - echo memory\n - echo plugin\n---\n# x\n" assert echo_index.aliases_in_frontmatter(block) == ["echo memory", "echo plugin"] assert echo_index.aliases_in_frontmatter("# no frontmatter\n") == [] def test_links_add_related_is_idempotent_and_bidirectional_token() -> None: text = "---\ntype: person\n---\n\n# Bob\n\n## Related\n" new, changed = echo_links.add_related(text, "resources/companies/mpm.md") assert changed and "[[resources/companies/mpm]]" in new again, changed2 = echo_links.add_related(new, "resources/companies/mpm.md") assert not changed2 and again == new def test_links_create_related_section_when_absent() -> None: text = "---\ntype: concept\n---\n\n# Alpha\n\nbody\n" new, changed = echo_links.add_related(text, "resources/concepts/beta.md") assert changed and "## Related" in new and "[[resources/concepts/beta]]" in new def _run_all() -> int: tests = [v for k, v in sorted(globals().items()) if k.startswith("test_") and callable(v)] failed = 0 for test in tests: try: test() print(f"ok {test.__name__}") except AssertionError as exc: failed += 1 print(f"FAIL {test.__name__}: {exc}") print(f"\n{len(tests) - failed}/{len(tests)} passed") return 1 if failed else 0 if __name__ == "__main__": raise SystemExit(_run_all())