feat(wiki): sync upstream v2026.10.4 core maintenance

Selectively port source hashing, locked manifest/memory writes, path-aware
link lint and graph analysis, and revision-checked staging from upstream
4f457e154653756b04802d5aeb25c20f42b4770c. Record source hashes and exclusions.

Preserve local archive/catalog citations, curated manifest metadata, config
location, file-index and MCP behavior. Wire maintenance guidance into four
skills without migrating vault contents or changing running services.

Validation: 609 tests passed, including existing MCP/file-asset regressions
and imported upstream maintenance tests. Read-only vault status left shared
metadata hashes unchanged. Wiki MCP remains inactive; orders MCP is active.
This commit is contained in:
Codex
2026-10-31 12:39:48 +00:00
parent 1dd5d8d753
commit 82890606aa
35 changed files with 12632 additions and 44 deletions
+172
View File
@@ -0,0 +1,172 @@
"""Tests for bi-temporal page validity."""
from __future__ import annotations
import json
import os
import subprocess
import sys
from datetime import date
from pathlib import Path
import pytest
from obsidian_wiki.lint import lint_vault
from obsidian_wiki.temporal import (
is_current,
parse_date,
superseded_target,
validity_window,
)
def _page(vault: Path, relpath: str, *, extra: str = "", summary: str = "A page.") -> Path:
path = vault / relpath
path.parent.mkdir(parents=True, exist_ok=True)
lines = [
"---",
f"title: {path.stem}",
"category: references",
"tags: [test]",
"sources: [manual]",
"created: 2026-01-01",
"updated: 2026-01-01",
f"summary: {summary}",
]
if extra:
lines.append(extra)
lines += ["---", f"# {path.stem}"]
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
return path
# --- parsing ---------------------------------------------------------------
def test_parse_date_accepts_dates_timestamps_and_empty() -> None:
assert parse_date("2026-04-01") == date(2026, 4, 1)
assert parse_date("2026-04-01T09:30:00+00:00") == date(2026, 4, 1)
assert parse_date("2026-04-01T09:30:00Z") == date(2026, 4, 1)
assert parse_date(' "2026-04-01" ') == date(2026, 4, 1)
assert parse_date("") is None
assert parse_date(None) is None
def test_parse_date_rejects_nonsense() -> None:
with pytest.raises(ValueError):
parse_date("last tuesday")
def test_validity_window_rejects_inverted_window() -> None:
with pytest.raises(ValueError, match="precedes valid_from"):
validity_window({"valid_from": "2026-05-01", "valid_until": "2026-04-01"})
def test_superseded_target_unwraps_wikilinks_aliases_and_paths() -> None:
assert superseded_target("[[gateway-envoy]]") == "gateway-envoy"
assert superseded_target('"[[references/gateway-envoy.md|Envoy]]"') == "gateway-envoy"
assert superseded_target("[[gateway-envoy#Routes]]") == "gateway-envoy"
assert superseded_target("") == ""
# --- currency --------------------------------------------------------------
def test_page_without_temporal_fields_is_always_current() -> None:
assert is_current({}, date(1999, 1, 1)) is True
def test_valid_until_is_inclusive_and_valid_from_is_a_floor() -> None:
window = {"valid_from": "2026-04-01", "valid_until": "2026-06-30"}
assert is_current(window, date(2026, 3, 31)) is False # not yet true
assert is_current(window, date(2026, 4, 1)) is True
assert is_current(window, date(2026, 6, 30)) is True # last day counts
assert is_current(window, date(2026, 7, 1)) is False # no longer true
def test_malformed_dates_count_as_current_so_retrieval_never_silently_drops() -> None:
assert is_current({"valid_until": "whenever"}, date(2026, 6, 1)) is True
# --- retrieval -------------------------------------------------------------
# --- lint ------------------------------------------------------------------
def test_lint_fails_on_malformed_temporal_metadata(tmp_path: Path) -> None:
vault = tmp_path / "vault"
_page(vault, "references/inverted.md",
extra="valid_from: 2026-05-01\nvalid_until: 2026-04-01")
_page(vault, "references/garbage.md", extra="valid_until: soon")
report = lint_vault(vault, require_trust_ledger=False)
issues = {item["page"] for item in report["findings"]["temporal_errors"]}
assert issues == {"references/inverted.md", "references/garbage.md"}
assert report["status"] == "fail"
def test_lint_reports_dangling_superseded_by(tmp_path: Path) -> None:
vault = tmp_path / "vault"
_page(vault, "references/old.md", extra="superseded_by: \"[[does-not-exist]]\"")
_page(vault, "references/self.md", extra="superseded_by: \"[[self]]\"")
report = lint_vault(vault, require_trust_ledger=False)
assert report["findings"]["temporal_errors"] == []
assert report["findings"]["superseded_dangling"] == [
{"page": "references/old.md", "target": "does-not-exist", "issue": "missing_target"},
{"page": "references/self.md", "target": "self", "issue": "self_reference"},
]
# A bracketed value is a wikilink like any other, so the pre-existing
# broken_links check fires too and the vault fails — same as a typed
# `relationships:` target pointing at nothing.
assert report["findings"]["broken_links"] == [
{"page": "references/old.md", "target": "does-not-exist"}
]
assert report["status"] == "fail"
def test_lint_warns_on_a_dangling_unbracketed_superseded_by(tmp_path: Path) -> None:
"""Written as a plain name it isn't a wikilink, so only the specific
finding fires and the vault warns rather than fails."""
vault = tmp_path / "vault"
_page(vault, "references/old.md", extra="superseded_by: does-not-exist")
report = lint_vault(vault, require_trust_ledger=False)
assert report["findings"]["broken_links"] == []
assert report["findings"]["superseded_dangling"] == [
{"page": "references/old.md", "target": "does-not-exist", "issue": "missing_target"}
]
assert report["status"] == "warn"
def test_lint_passes_a_valid_supersession_chain(tmp_path: Path) -> None:
vault = tmp_path / "vault"
_page(vault, "references/old.md",
extra="valid_until: 2026-03-31\nsuperseded_by: \"[[new]]\"")
_page(vault, "references/new.md", extra="valid_from: 2026-04-01")
report = lint_vault(vault, require_trust_ledger=False)
assert report["findings"]["temporal_errors"] == []
assert report["findings"]["superseded_dangling"] == []
# --- CLI -------------------------------------------------------------------
def _run(*args: str) -> subprocess.CompletedProcess[str]:
env = os.environ.copy()
env["PYTHONPATH"] = str(Path(__file__).parents[1])
return subprocess.run(
[sys.executable, "-m", "obsidian_wiki.cli", *args],
capture_output=True, check=False, text=True, env=env,
)