119 lines
4.8 KiB
Python
119 lines
4.8 KiB
Python
#!/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_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/<lifecycle>/<slug>.md") == "projects/active/sample.md"
|
|
assert check_routing.concretize("_agent/sessions/YYYY-MM-DD-HHMM-<slug>.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_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())
|