feat(wiki): [[wikilinks]] with redlinks + cross-project resolver (Issue B)
Last P2 from Antoine's "daily-usable" sprint. Entities referenced via [[Name]] in descriptions or mirror markdown now render as: - live wikilink if the name matches an entity in the same project - live cross-project link with "(in project X)" scope indicator if the only match is in another project - red italic redlink pointing at /wiki/new?name=... otherwise Clicking a redlink opens a pre-filled "create this entity" form that POSTs to /v1/entities and redirects to the new entity's page. - engineering/wiki.py: _wikilink_transform + _resolve_wikilink, applied in render_project (pre-markdown) and render_entity (description body). render_new_entity_form for the create page. CSS for .wikilink / .wikilink-cross / .redlink / .new-entity-form - api/routes.py: GET /wiki/new?name&project - tests/test_wikilinks.py: 12 tests including the spec regression (A references [[B]] -> redlink; create B -> link becomes live) - DEV-LEDGER.md: session log + test_count 521 -> 533 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
132
tests/test_wikilinks.py
Normal file
132
tests/test_wikilinks.py
Normal file
@@ -0,0 +1,132 @@
|
||||
"""Issue B — wikilinks with redlinks + cross-project resolution."""
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from atocore.engineering.service import (
|
||||
create_entity,
|
||||
init_engineering_schema,
|
||||
)
|
||||
from atocore.engineering.wiki import (
|
||||
_resolve_wikilink,
|
||||
_wikilink_transform,
|
||||
render_entity,
|
||||
render_new_entity_form,
|
||||
render_project,
|
||||
)
|
||||
from atocore.main import app
|
||||
from atocore.models.database import init_db
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def env(tmp_data_dir, tmp_path, monkeypatch):
|
||||
registry_path = tmp_path / "test-registry.json"
|
||||
registry_path.write_text('{"projects": []}', encoding="utf-8")
|
||||
monkeypatch.setenv("ATOCORE_PROJECT_REGISTRY_PATH", str(registry_path))
|
||||
from atocore import config
|
||||
config.settings = config.Settings()
|
||||
init_db()
|
||||
init_engineering_schema()
|
||||
yield tmp_data_dir
|
||||
|
||||
|
||||
def test_resolve_wikilink_same_project_is_live(env):
|
||||
tower = create_entity(entity_type="component", name="Tower", project="p05")
|
||||
href, cls, _ = _resolve_wikilink("Tower", current_project="p05")
|
||||
assert href == f"/wiki/entities/{tower.id}"
|
||||
assert cls == "wikilink"
|
||||
|
||||
|
||||
def test_resolve_wikilink_missing_is_redlink(env):
|
||||
href, cls, suffix = _resolve_wikilink("DoesNotExist", current_project="p05")
|
||||
assert "/wiki/new" in href
|
||||
assert "name=DoesNotExist" in href
|
||||
assert cls == "redlink"
|
||||
|
||||
|
||||
def test_resolve_wikilink_cross_project_indicator(env):
|
||||
other = create_entity(entity_type="material", name="Invar", project="p06")
|
||||
href, cls, suffix = _resolve_wikilink("Invar", current_project="p05")
|
||||
assert href == f"/wiki/entities/{other.id}"
|
||||
assert "wikilink-cross" in cls
|
||||
assert "in p06" in suffix
|
||||
|
||||
|
||||
def test_resolve_wikilink_case_insensitive(env):
|
||||
tower = create_entity(entity_type="component", name="Tower", project="p05")
|
||||
href, cls, _ = _resolve_wikilink("tower", current_project="p05")
|
||||
assert href == f"/wiki/entities/{tower.id}"
|
||||
assert cls == "wikilink"
|
||||
|
||||
|
||||
def test_transform_replaces_brackets_with_anchor(env):
|
||||
create_entity(entity_type="component", name="Base Plate", project="p05")
|
||||
out = _wikilink_transform("See [[Base Plate]] for details.", current_project="p05")
|
||||
assert '<a href="/wiki/entities/' in out
|
||||
assert 'class="wikilink"' in out
|
||||
assert "[[Base Plate]]" not in out
|
||||
|
||||
|
||||
def test_transform_redlink_for_missing(env):
|
||||
out = _wikilink_transform("Mentions [[Ghost]] nowhere.", current_project="p05")
|
||||
assert 'class="redlink"' in out
|
||||
assert "/wiki/new?name=Ghost" in out
|
||||
|
||||
|
||||
def test_transform_alias_syntax(env):
|
||||
tower = create_entity(entity_type="component", name="Tower", project="p05")
|
||||
out = _wikilink_transform("The [[Tower|big tower]] is tall.", current_project="p05")
|
||||
assert f'href="/wiki/entities/{tower.id}"' in out
|
||||
assert ">big tower<" in out
|
||||
|
||||
|
||||
def test_render_entity_description_has_redlink(env):
|
||||
a = create_entity(
|
||||
entity_type="component",
|
||||
name="EntityA",
|
||||
project="p05",
|
||||
description="This depends on [[MissingPart]] which does not exist.",
|
||||
)
|
||||
html = render_entity(a.id)
|
||||
assert 'class="redlink"' in html
|
||||
assert "/wiki/new?name=MissingPart" in html
|
||||
|
||||
|
||||
def test_regression_redlink_becomes_live_once_target_created(env):
|
||||
a = create_entity(
|
||||
entity_type="component",
|
||||
name="EntityA",
|
||||
project="p05",
|
||||
description="Connected to [[EntityB]].",
|
||||
)
|
||||
# Pre-create: redlink.
|
||||
html_before = render_entity(a.id)
|
||||
assert 'class="redlink"' in html_before
|
||||
|
||||
b = create_entity(entity_type="component", name="EntityB", project="p05")
|
||||
html_after = render_entity(a.id)
|
||||
assert 'class="redlink"' not in html_after
|
||||
assert f"/wiki/entities/{b.id}" in html_after
|
||||
|
||||
|
||||
def test_new_entity_form_prefills_name():
|
||||
html = render_new_entity_form(name="FreshEntity", project="p05")
|
||||
assert 'value="FreshEntity"' in html
|
||||
assert 'value="p05"' in html
|
||||
assert "entity_type" in html
|
||||
assert 'method="post"' not in html # JS-driven
|
||||
|
||||
|
||||
def test_wiki_new_route_renders(env):
|
||||
client = TestClient(app)
|
||||
r = client.get("/wiki/new?name=NewThing&project=p05")
|
||||
assert r.status_code == 200
|
||||
assert "NewThing" in r.text
|
||||
assert "Create entity" in r.text
|
||||
|
||||
|
||||
def test_wiki_new_url_escapes_special_chars(env):
|
||||
# "steel (likely)" is the kind of awkward name AKC produces
|
||||
href, cls, _ = _resolve_wikilink("steel (likely)", current_project="p05")
|
||||
assert cls == "redlink"
|
||||
assert "name=steel%20%28likely%29" in href
|
||||
Reference in New Issue
Block a user