feat: implement GEWU service and local agent workflows
CI / quality (push) Canceled after 0s

This commit is contained in:
vasily committed 2026-10-14 19:21:35 +08:00
1 parent 4db3f45164
commit d25c04cdd2
175 files changed
+56243 -15

No files matched your search

+1
View File
@@ -0,0 +1 @@
"""GEWU test suite."""
+1
View File
@@ -0,0 +1 @@
"""Infrastructure adapter tests."""
@@ -0,0 +1,60 @@
"""Exit abruptly from inside a SQLite FTS rebuild transaction."""
from __future__ import annotations
import os
import sqlite3
import sys
from pathlib import Path
from gewu.adapters.git_repository import GitCanonicalRepository
from gewu.adapters.sqlite_search import SQLiteSearchIndex
class CrashConnection(sqlite3.Connection):
def __init__(self, *args: object, **kwargs: object) -> None:
super().__init__(*args, **kwargs) # type: ignore[arg-type]
self._fts_inserts = 0
def execute(
self,
sql: str,
parameters: tuple[object, ...] = (),
/,
) -> sqlite3.Cursor:
cursor = super().execute(sql, parameters)
if sql.lstrip().upper().startswith("INSERT INTO NODE_FTS"):
self._fts_inserts += 1
if self._fts_inserts == 5:
os._exit(74)
return cursor
class CrashDuringIndexRebuild(SQLiteSearchIndex):
def _connect(self) -> sqlite3.Connection:
connection = sqlite3.connect(
self.path,
timeout=5,
isolation_level=None,
factory=CrashConnection,
)
connection.execute("PRAGMA foreign_keys=ON")
connection.execute("PRAGMA busy_timeout=5000")
connection.execute("PRAGMA synchronous=NORMAL")
return connection
def main() -> None:
repository = GitCanonicalRepository(Path(sys.argv[1]))
index = CrashDuringIndexRebuild(Path(sys.argv[2]), repository)
index.search(
query="visitors",
project_ids=("dome-performance",),
include_shared=False,
limit=20,
)
raise RuntimeError("SQLite rebuild did not reach the requested crash point")
if __name__ == "__main__":
main()
+92
View File
@@ -0,0 +1,92 @@
from __future__ import annotations
import hashlib
from pathlib import Path
from typing import Any
import pytest
from gewu.adapters.embeddings import (
EmbeddingProviderError,
LocalSentenceTransformer,
model_artifact_sha256,
)
class FakeEncoder:
def __init__(self, rows: list[list[float]]) -> None:
self.rows = rows
self.seen: list[str] = []
self.calls = 0
def encode(
self,
sentences: list[str],
*,
normalize_embeddings: bool,
convert_to_numpy: bool,
show_progress_bar: bool,
) -> Any:
assert normalize_embeddings and convert_to_numpy and not show_progress_bar
self.seen.extend(sentences)
result = self.rows[self.calls : self.calls + len(sentences)]
self.calls += len(sentences)
return result
def _model_path(tmp_path: Path) -> Path:
path = tmp_path / "model"
path.mkdir()
(path / "config.json").write_text('{"dim":2}', encoding="utf-8")
return path
def test_local_sentence_transformer_applies_e5_prefixes_and_metadata(tmp_path: Path) -> None:
path = _model_path(tmp_path)
encoder = FakeEncoder([[0.6, 0.8], [1.0, 0.0]])
artifact = model_artifact_sha256(path)
provider = LocalSentenceTransformer(
path,
model_id="intfloat/multilingual-e5-small@pinned",
expected_sha256=artifact,
dimensions=2,
encoder=encoder,
)
assert provider.embed_queries(("question",)) == ((0.6, 0.8),)
assert provider.embed_passages(("evidence",)) == ((1.0, 0.0),)
assert encoder.seen == ["query: question", "passage: evidence"]
assert provider.model_id == "intfloat/multilingual-e5-small@pinned"
assert provider.artifact_sha256 == artifact
assert len(provider.artifact_sha256) == 64
def test_local_sentence_transformer_rejects_changed_artifact(tmp_path: Path) -> None:
path = _model_path(tmp_path)
(path / "config.json").write_text("changed", encoding="utf-8")
with pytest.raises(EmbeddingProviderError, match="digest"):
LocalSentenceTransformer(
path,
model_id="model",
expected_sha256=hashlib.sha256(b"wrong").hexdigest(),
dimensions=2,
encoder=FakeEncoder([[1.0, 0.0]]),
)
@pytest.mark.parametrize("rows", [[[2.0, 0.0]], [[float("nan"), 0.0]], [[1.0]]])
def test_local_sentence_transformer_rejects_invalid_vectors(
tmp_path: Path, rows: list[list[float]]
) -> None:
path = _model_path(tmp_path)
provider = LocalSentenceTransformer(
path,
model_id="model",
expected_sha256=model_artifact_sha256(path),
dimensions=2,
encoder=FakeEncoder(rows),
)
with pytest.raises(EmbeddingProviderError):
provider.embed_passages(("text",))
+92
View File
@@ -0,0 +1,92 @@
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
from threading import Barrier
import pytest
from gewu.adapters.git_repository import (
GitCanonicalRepository,
GitConflictError,
GitRepositoryError,
)
def test_bare_repository_commits_only_explicit_files_and_checks_expected_head(
tmp_path: Path,
) -> None:
repository = GitCanonicalRepository(tmp_path / "private" / "knowledge.git")
base = repository.head()
record_path = "projects/project-a/nodes/node-a.yaml"
commit = repository.publish_files(
expected_head=base,
files={record_path: b"title: accepted\n"},
message="Accept fixture",
index_directory=tmp_path / "staging" / "publishing",
)
assert repository.read_file(commit, record_path) == b"title: accepted\n"
assert repository.list_files(commit, "projects") == (record_path,)
assert repository.list_files(commit, "sources") == ()
assert repository.read_file(commit, "unrelated/user-file.txt") is None
assert repository.introducing_commit(commit, record_path) == commit
with pytest.raises(GitConflictError, match="Canonical branch changed"):
repository.publish_files(
expected_head=base,
files={"projects/project-b/nodes/node-b.yaml": b"title: stale\n"},
message="Stale writer",
index_directory=tmp_path / "staging" / "publishing",
)
def test_record_paths_reject_traversal_and_noncanonical_separators(tmp_path: Path) -> None:
repository = GitCanonicalRepository(tmp_path / "knowledge.git")
for unsafe in ("../escape", "/absolute", "projects//node", "projects/./node", "a\\b"):
with pytest.raises(GitRepositoryError, match="record path is invalid"):
repository.read_file(repository.head(), unsafe)
def test_independent_repository_adapters_allow_one_compare_and_swap_winner(
tmp_path: Path,
) -> None:
repository_path = tmp_path / "knowledge.git"
first_adapter = GitCanonicalRepository(repository_path)
second_adapter = GitCanonicalRepository(repository_path)
base = first_adapter.head()
start = Barrier(2)
index_directory = tmp_path / "staging" / "publishing"
def publish(repository: GitCanonicalRepository, name: str) -> tuple[str, str | None]:
path = f"projects/{name}/nodes/node.yaml"
start.wait(timeout=5)
try:
revision = repository.publish_files(
expected_head=base,
files={path: f"title: {name}\n".encode()},
message=f"Publish {name}",
index_directory=index_directory,
)
except GitConflictError:
return name, None
return name, revision
with ThreadPoolExecutor(max_workers=2) as workers:
results = list(
workers.map(
lambda args: publish(*args),
((first_adapter, "project-a"), (second_adapter, "project-b")),
)
)
winners = [(name, revision) for name, revision in results if revision is not None]
losers = [name for name, revision in results if revision is None]
assert len(winners) == len(losers) == 1
winner, winning_revision = winners[0]
assert winning_revision is not None
assert first_adapter.head() == winning_revision
assert second_adapter.head() == winning_revision
assert (
first_adapter.read_file(winning_revision, f"projects/{winner}/nodes/node.yaml")
== f"title: {winner}\n".encode()
)
loser = losers[0]
assert first_adapter.read_file(winning_revision, f"projects/{loser}/nodes/node.yaml") is None
+113
View File
@@ -0,0 +1,113 @@
from __future__ import annotations
import shutil
import subprocess
import tempfile
from pathlib import Path
import pytest
from gewu.adapters.pdf_extractor import PdfTextExtractor
def _pdf(pages: tuple[str, ...]) -> bytes:
objects: list[bytes] = []
page_ids = [4 + 2 * index for index in range(len(pages))]
objects.extend(
[
b"<< /Type /Catalog /Pages 2 0 R >>",
(
f"<< /Type /Pages /Kids [{' '.join(f'{page_id} 0 R' for page_id in page_ids)}] "
f"/Count {len(pages)} >>"
).encode(),
b"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>",
]
)
for page_id, text in zip(page_ids, pages, strict=True):
content_id = page_id + 1
escaped = text.replace("\\", "\\\\").replace("(", "\\(").replace(")", "\\)")
stream = f"BT /F1 12 Tf 72 720 Td ({escaped}) Tj ET".encode()
page = (
f"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] "
f"/Resources << /Font << /F1 3 0 R >> >> /Contents {content_id} 0 R >>"
)
objects.append(page.encode())
objects.append(
b"<< /Length " + str(len(stream)).encode() + b" >>\nstream\n" + stream + b"\nendstream"
)
output = bytearray(b"%PDF-1.4\n")
offsets = [0]
for number, obj in enumerate(objects, 1):
offsets.append(len(output))
output.extend(f"{number} 0 obj\n".encode() + obj + b"\nendobj\n")
xref = len(output)
output.extend(f"xref\n0 {len(offsets)}\n0000000000 65535 f \n".encode())
for offset in offsets[1:]:
output.extend(f"{offset:010d} 00000 n \n".encode())
output.extend(
f"trailer\n<< /Size {len(offsets)} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n".encode()
)
return bytes(output)
@pytest.mark.skipif(
not all(shutil.which(item) for item in ("bwrap", "pdftotext", "prlimit")),
reason="Linux PDF sandbox tools unavailable",
)
def test_pdf_extraction_preserves_page_locations_and_parser_identity() -> None:
record = PdfTextExtractor().extract(
"project:source:r1",
_pdf(("First page evidence.", "Second page evidence.")),
"application/pdf",
)
assert record.status == "complete"
assert record.parser_name == "gewu-poppler-pdftotext"
assert [block.page for block in record.blocks] == [1, 2]
assert [block.original_text.strip() for block in record.blocks] == [
"First page evidence.",
"Second page evidence.",
]
@pytest.mark.skipif(
not all(shutil.which(item) for item in ("bwrap", "pdftotext", "prlimit")),
reason="Linux PDF sandbox tools unavailable",
)
def test_empty_pdf_page_is_reported_as_partial() -> None:
record = PdfTextExtractor().extract(
"project:source:r1", _pdf(("Text page.", "")), "application/pdf"
)
assert record.status == "partial"
assert record.blocks[0].page == 1
assert record.warnings
def test_pdf_parser_rejects_bad_signature_without_launching_parser() -> None:
with pytest.raises(ValueError, match="signature"):
PdfTextExtractor().extract("project:source:r1", b"not a pdf", "application/pdf")
@pytest.mark.skipif(
not shutil.which("bwrap") or not shutil.which("prlimit"), reason="bubblewrap unavailable"
)
def test_sandbox_cannot_read_host_temporary_file(tmp_path: Path) -> None:
sentinel = tmp_path / "gewu-host-secret-fixture"
sentinel.write_text("must-not-be-visible", encoding="utf-8")
extractor = PdfTextExtractor()
if not extractor._pdftotext:
pytest.skip("pdftotext unavailable")
command = extractor._command()
command[-6:] = ["/usr/bin/cat", str(sentinel)]
with tempfile.TemporaryFile() as output, tempfile.TemporaryFile() as errors:
result = subprocess.run(
command,
stdin=subprocess.DEVNULL,
stdout=output,
stderr=errors,
env={},
timeout=10,
check=False,
)
assert result.returncode != 0
output.seek(0)
assert b"must-not-be-visible" not in output.read()
+513
View File
@@ -0,0 +1,513 @@
import hashlib
import json
import time
from pathlib import Path
import pyotp
import pytest
from cryptography.fernet import Fernet
from gewu.adapters.sqlite_identity import SQLiteIdentityStore
def issue_challenge(
store: SQLiteIdentityStore,
*,
principal_id: str,
challenge_id: str,
domain: str,
now: int,
) -> None:
store.issue_owner_challenge(
challenge_id=challenge_id,
owner_principal_id=principal_id,
proposal_id="proposal-one",
proposal_revision=1,
payload_digest="a" * 64,
diff_digest="b" * 64,
domains=(domain,),
expires_at=now + 5 * 60,
now=now,
)
def test_credentials_are_hashed_revocable_and_project_scoped(tmp_path: Path) -> None:
store = SQLiteIdentityStore(tmp_path / "private" / "identity.sqlite3")
token = store.provision(
credential_id="agent-key-1",
principal_id="agent-pilot",
roles=("agent",),
project_ids=("dome-performance",),
)
principal = store.authenticate(token)
assert principal is not None
assert principal.principal_id == "agent-pilot"
assert principal.credential_id == "agent-key-1"
assert principal.can_read_project("dome-performance")
assert not principal.can_read_project("interactive-video")
assert token.encode() not in (tmp_path / "private" / "identity.sqlite3").read_bytes()
assert store.revoke("agent-key-1")
assert store.authenticate(token) is None
assert not store.revoke("agent-key-1")
def test_admin_role_can_read_without_project_grant(tmp_path: Path) -> None:
store = SQLiteIdentityStore(tmp_path / "identity.sqlite3")
token = store.provision(
credential_id="admin-key",
principal_id="operator-1",
roles=("admin",),
)
principal = store.authenticate(token)
assert principal is not None
assert principal.can_read_project("dome-performance")
def test_publication_authority_snapshot_detects_grant_or_role_change(tmp_path: Path) -> None:
store = SQLiteIdentityStore(tmp_path / "identity.sqlite3")
token = store.provision(
credential_id="agent-key",
principal_id="agent-one",
roles=("agent",),
project_ids=("dome-performance", "shared"),
)
principal = store.authenticate(token)
assert principal is not None
assert store.is_current(
principal.principal_id,
principal.credential_id,
roles=principal.roles,
project_ids=principal.project_ids,
)
assert not store.is_current(
principal.principal_id,
principal.credential_id,
roles=principal.roles,
project_ids=frozenset({"dome-performance"}),
)
def test_rotation_revokes_old_key_and_preserves_attribution_and_grants(tmp_path: Path) -> None:
store = SQLiteIdentityStore(tmp_path / "identity.sqlite3")
old_token = store.provision(
credential_id="agent-key-old",
principal_id="agent-pilot",
roles=("agent",),
project_ids=("dome-performance",),
)
new_token = store.rotate("agent-key-old", "agent-key-new")
assert store.authenticate(old_token) is None
principal = store.authenticate(new_token)
assert principal is not None
assert principal.principal_id == "agent-pilot"
assert principal.credential_id == "agent-key-new"
assert principal.can_read_project("dome-performance")
def test_browser_sessions_are_human_only_csrf_protected_and_revocable(tmp_path: Path) -> None:
store = SQLiteIdentityStore(tmp_path / "identity.sqlite3")
viewer_key = store.provision(
credential_id="viewer-key",
principal_id="viewer-one",
roles=("viewer",),
project_ids=("dome-performance",),
)
agent_key = store.provision(
credential_id="agent-key",
principal_id="agent-one",
roles=("agent",),
project_ids=("dome-performance",),
)
with pytest.raises(PermissionError, match="human browser identity"):
store.create_browser_session(agent_key)
session = store.create_browser_session(viewer_key)
principal = store.authenticate_browser_session(session.session_token)
assert principal is not None
assert principal.auth_method == "browser_session"
assert principal.can_read_project("dome-performance")
assert not principal.can_submit_project("dome-performance")
assert store.browser_csrf_valid(session.session_token, session.csrf_token)
assert not store.browser_csrf_valid(session.session_token, "gwc_invalid")
assert store.revoke_browser_session(session.session_token)
assert store.authenticate_browser_session(session.session_token) is None
assert not store.browser_csrf_valid(session.session_token, session.csrf_token)
admin_key = store.provision(
credential_id="human-admin-key",
principal_id="human-admin",
roles=("admin",),
project_ids=("dome-performance",),
)
admin_session = store.create_browser_session(admin_key)
browser_admin = store.authenticate_browser_session(admin_session.session_token)
assert browser_admin is not None and not browser_admin.can_submit_project("dome-performance")
def test_revoking_browser_credential_invalidates_its_sessions(tmp_path: Path) -> None:
store = SQLiteIdentityStore(tmp_path / "identity.sqlite3")
token = store.provision(
credential_id="human-key",
principal_id="human-one",
roles=("editor",),
project_ids=("dome-performance",),
)
session = store.create_browser_session(token)
assert store.authenticate_browser_session(session.session_token) is not None
assert store.revoke("human-key")
assert store.authenticate_browser_session(session.session_token) is None
def test_identity_store_rejects_symlinked_database_path(tmp_path: Path) -> None:
target = tmp_path / "real"
target.mkdir()
link = tmp_path / "linked"
link.symlink_to(target, target_is_directory=True)
with pytest.raises(ValueError, match="symbolic links"):
SQLiteIdentityStore(link / "identity.sqlite3")
def test_owner_session_is_challenge_bound_short_lived_and_separate_from_agent_auth(
tmp_path: Path,
) -> None:
store = SQLiteIdentityStore(
tmp_path / "identity.sqlite3", owner_encryption_key=Fernet.generate_key()
)
secret = pyotp.random_base32()
store.provision_owner(
principal_id="project-owner",
password="correct horse battery staple",
totp_secret=secret,
project_ids=("dome-performance",),
)
database_bytes = (tmp_path / "identity.sqlite3").read_bytes()
assert secret.encode() not in database_bytes
assert b"correct horse battery staple" not in database_bytes
now = int(time.time())
issue_challenge(
store,
principal_id="project-owner",
challenge_id="challenge-1",
domain="dome-performance",
now=now,
)
session = store.authenticate_owner(
principal_id="project-owner",
password="correct horse battery staple",
totp_code=pyotp.TOTP(secret).at(now),
challenge_id="challenge-1",
audience="https://gewu.example",
now=now,
)
assert session is not None
assert session.expires_at.timestamp() == now + 5 * 60
principal = store.authenticate_owner_session(
session.session_token,
challenge_id="challenge-1",
audience="https://gewu.example",
)
assert principal is not None
assert principal.roles == frozenset({"owner"})
assert principal.auth_method == "owner_session"
assert principal.can_read_project("dome-performance")
assert not principal.can_submit_project("dome-performance")
assert store.authenticate(session.session_token) is None
assert (
store.authenticate_owner_session(
session.session_token,
challenge_id="challenge-2",
audience="https://gewu.example",
)
is None
)
assert (
store.authenticate_owner_session(
session.session_token,
challenge_id="challenge-1",
audience="https://other.example",
)
is None
)
def test_owner_totp_cannot_be_replayed_and_login_failures_are_rate_limited(
tmp_path: Path,
) -> None:
store = SQLiteIdentityStore(
tmp_path / "identity.sqlite3", owner_encryption_key=Fernet.generate_key()
)
secret = pyotp.random_base32()
store.provision_owner(
principal_id="project-owner",
password="correct horse battery staple",
totp_secret=secret,
project_ids=("dome-performance",),
)
now = int(time.time())
issue_challenge(
store,
principal_id="project-owner",
challenge_id="challenge-1",
domain="dome-performance",
now=now,
)
code = pyotp.TOTP(secret).at(now)
args = {
"principal_id": "project-owner",
"password": "correct horse battery staple",
"totp_code": code,
"challenge_id": "challenge-1",
"audience": "https://gewu.example",
"now": now,
}
assert store.authenticate_owner(**args) is not None
assert store.authenticate_owner(**args) is None
for attempt in range(5):
challenge_id = f"challenge-bad-{attempt}"
issue_challenge(
store,
principal_id="project-owner",
challenge_id=challenge_id,
domain="dome-performance",
now=now,
)
assert (
store.authenticate_owner(
principal_id="project-owner",
password="incorrect password",
totp_code="000000",
challenge_id=challenge_id,
audience="https://gewu.example",
now=now,
)
is None
)
issue_challenge(
store,
principal_id="project-owner",
challenge_id="challenge-final",
domain="dome-performance",
now=now,
)
assert (
store.authenticate_owner(
principal_id="project-owner",
password="incorrect password",
totp_code="000000",
challenge_id="challenge-final",
audience="https://gewu.example",
now=now,
)
is None
)
def test_owner_operations_stay_disabled_without_encryption_key(tmp_path: Path) -> None:
store = SQLiteIdentityStore(tmp_path / "identity.sqlite3")
with pytest.raises(RuntimeError, match="encryption key is not configured"):
store.provision_owner(
principal_id="project-owner",
password="correct horse battery staple",
totp_secret=pyotp.random_base32(),
project_ids=(),
)
assert (
store.authenticate_owner(
principal_id="project-owner",
password="correct horse battery staple",
totp_code="000000",
challenge_id="challenge-1",
audience="https://gewu.example",
)
is None
)
def test_owner_challenge_retry_is_bound_to_payload_diff_and_granted_domains(
tmp_path: Path,
) -> None:
store = SQLiteIdentityStore(
tmp_path / "identity.sqlite3", owner_encryption_key=Fernet.generate_key()
)
store.provision_owner(
principal_id="project-owner",
password="correct horse battery staple",
totp_secret=pyotp.random_base32(),
project_ids=("dome-performance",),
)
now = int(time.time())
issue_challenge(
store,
principal_id="project-owner",
challenge_id="challenge-1",
domain="dome-performance",
now=now,
)
issue_challenge(
store,
principal_id="project-owner",
challenge_id="challenge-1",
domain="dome-performance",
now=now,
)
with pytest.raises(ValueError, match="different content"):
store.issue_owner_challenge(
challenge_id="challenge-1",
owner_principal_id="project-owner",
proposal_id="proposal-one",
proposal_revision=1,
payload_digest="c" * 64,
diff_digest="b" * 64,
domains=("dome-performance",),
expires_at=now + 300,
now=now,
)
def test_owner_decision_is_exact_diff_bound_and_idempotent(tmp_path: Path) -> None:
store = SQLiteIdentityStore(
tmp_path / "identity.sqlite3", owner_encryption_key=Fernet.generate_key()
)
secret = pyotp.random_base32()
store.provision_owner(
principal_id="project-owner",
password="correct horse battery staple",
totp_secret=secret,
project_ids=("dome-performance",),
)
now = int(time.time())
diff_json = json.dumps({"files": [{"path": "projects/dome/nodes/a.yaml"}]})
diff_digest = hashlib.sha256(diff_json.encode()).hexdigest()
store.issue_owner_challenge(
challenge_id="challenge-bound",
owner_principal_id="project-owner",
proposal_id="proposal-one",
proposal_revision=1,
payload_digest="a" * 64,
diff_digest=diff_digest,
domains=("dome-performance",),
expires_at=now + 300,
diff_json=diff_json,
now=now,
)
token = store.authenticate_owner(
principal_id="project-owner",
password="correct horse battery staple",
totp_code=pyotp.TOTP(secret).at(now),
challenge_id="challenge-bound",
audience="gewu-test",
now=now,
)
assert token is not None
assert (
store.authenticate_owner_session(
token.session_token, challenge_id="challenge-bound", audience="gewu-test"
)
is not None
)
challenge = store.get_owner_challenge("challenge-bound", principal_id="project-owner")
assert challenge is not None
assert challenge["diff_json"] == diff_json
arguments = {
"challenge_id": "challenge-bound",
"principal_id": "project-owner",
"proposal_revision": 1,
"payload_digest": "a" * 64,
"diff_digest": diff_digest,
"decision": "authorize",
"rationale": "Reviewed exact proposed files and approve the policy exception.",
"owner_session_id": hashlib.sha256(token.session_token.encode()).hexdigest(),
"audience": "gewu-test",
"now": now,
}
with pytest.raises(ValueError, match="does not match"):
store.decide_owner_challenge(**{**arguments, "diff_digest": "b" * 64})
with pytest.raises(PermissionError, match="authority is unavailable"):
store.decide_owner_challenge(**{**arguments, "owner_session_id": "0" * 64})
first = store.decide_owner_challenge(**arguments)
assert first["status"] == "authorized"
assert store.decide_owner_challenge(**arguments) == first
with pytest.raises(ValueError, match="different decision"):
store.decide_owner_challenge(**{**arguments, "decision": "reject"})
with pytest.raises(PermissionError, match="every affected domain"):
store.issue_owner_challenge(
challenge_id="challenge-out-of-scope",
owner_principal_id="project-owner",
proposal_id="proposal-one",
proposal_revision=1,
payload_digest="a" * 64,
diff_digest="b" * 64,
domains=("interactive-video",),
expires_at=now + 300,
now=now,
)
def test_owner_grant_change_and_revocation_invalidate_sessions(tmp_path: Path) -> None:
store = SQLiteIdentityStore(
tmp_path / "identity.sqlite3", owner_encryption_key=Fernet.generate_key()
)
secret = pyotp.random_base32()
store.provision_owner(
principal_id="project-owner",
password="correct horse battery staple",
totp_secret=secret,
project_ids=("dome-performance",),
)
now = int(time.time())
issue_challenge(
store,
principal_id="project-owner",
challenge_id="challenge-1",
domain="dome-performance",
now=now,
)
first = store.authenticate_owner(
principal_id="project-owner",
password="correct horse battery staple",
totp_code=pyotp.TOTP(secret).at(now),
challenge_id="challenge-1",
audience="https://gewu.example",
now=now,
)
assert first is not None
assert store.update_owner_grants("project-owner", ("interactive-video",))
assert (
store.authenticate_owner_session(
first.session_token,
challenge_id="challenge-1",
audience="https://gewu.example",
)
is None
)
second_counter = now // 30 + 1
second_now = second_counter * 30
issue_challenge(
store,
principal_id="project-owner",
challenge_id="challenge-2",
domain="interactive-video",
now=second_now,
)
second = store.authenticate_owner(
principal_id="project-owner",
password="correct horse battery staple",
totp_code=pyotp.TOTP(secret).generate_otp(second_counter),
challenge_id="challenge-2",
audience="https://gewu.example",
now=second_now,
)
assert second is not None
assert store.revoke_owner("project-owner")
assert (
store.authenticate_owner_session(
second.session_token,
challenge_id="challenge-2",
audience="https://gewu.example",
)
is None
)
+702
View File
@@ -0,0 +1,702 @@
import hashlib
import sqlite3
import subprocess
import sys
from collections.abc import Callable
from datetime import UTC, date, datetime
from pathlib import Path
from typing import cast
import pytest
import yaml
from gewu.adapters.git_repository import GitCanonicalRepository
from gewu.adapters.sqlite_search import SQLiteSearchIndex
from gewu.application.identity import Principal
from gewu.application.search import CanonicalSearchService, SearchError
from gewu.domain.contracts import (
Confidence,
EdgeEndpoint,
KnowledgeNode,
NeighborRequest,
NodeType,
Scope,
SearchRequest,
TypedEdge,
)
from gewu.domain.validation import assertion_digest, evidence_digest
from tests.domain.proposal_fixtures import NODE, PROJECTS, SOURCE_REVISION
def _node_path(node: KnowledgeNode) -> str:
project_key = hashlib.sha256(node.project_id.encode()).hexdigest()
node_key = hashlib.sha256(node.node_id.encode()).hexdigest()
return f"projects/{project_key}/nodes/{node_key}.yaml"
def _publish_nodes(
repository: GitCanonicalRepository, directory: Path, nodes: tuple[KnowledgeNode, ...]
) -> str:
return repository.publish_files(
expected_head=repository.head(),
files={
_node_path(node): yaml.safe_dump(
node.model_dump(mode="json"), allow_unicode=True
).encode()
for node in nodes
},
message="Publish search fixtures",
index_directory=directory,
)
def _service(
repository: GitCanonicalRepository,
index_path: Path,
*,
head: Callable[[], str] | None = None,
) -> CanonicalSearchService:
return CanonicalSearchService(
index=SQLiteSearchIndex(index_path, repository),
project_ids_provider=lambda: PROJECTS.keys(),
canonical_head=head or repository.head,
)
def test_fts_search_enforces_explicit_project_and_shared_scope(tmp_path: Path) -> None:
repository = GitCanonicalRepository(tmp_path / "knowledge.git")
stale = NODE.model_copy(
update={
"as_of": datetime(2024, 1, 1, tzinfo=UTC),
"stale_after": datetime(2025, 1, 1, tzinfo=UTC),
}
)
video_node = NODE.model_copy(
update={
"node_id": "video:branching-workflow",
"project_id": "interactive-video",
"title": "Interactive branching workflow",
"summary": "Video authors control branching and state growth.",
}
)
shared_node = NODE.model_copy(
update={
"node_id": "shared:visitor-benchmark",
"project_id": "shared",
"title": "Shared visitor benchmark",
}
)
_publish_nodes(repository, tmp_path / "publishing", (stale, video_node, shared_node))
service = _service(repository, tmp_path / "search.sqlite3")
dome_reader = Principal(
"reader-dome", "reader-key", frozenset({"viewer"}), frozenset({"dome-performance"})
)
dome_result = service.search(
SearchRequest(
query="visitors",
scope=Scope(projects=("dome-performance",), include_shared=False),
budget_tokens=5000,
),
principal=dome_reader,
)
assert [cast(dict[str, object], row["node"])["node_id"] for row in dome_result.data] == [
NODE.node_id
]
assert any(warning.code == "STALE_NODE" for warning in dome_result.warnings)
assert dome_result.retrieval_mode == "keyword"
assert dome_result.canonical_revision == dome_result.index_revision
assert dome_result.tokens_used > 0
bounded = service.search(
SearchRequest(
query="visitors",
scope=Scope(projects=("dome-performance",), include_shared=False),
budget_tokens=1500,
),
principal=dome_reader,
)
assert bounded.data == ()
assert bounded.citations == ()
assert bounded.omissions
assert bounded.warnings
assert bounded.next_cursor is not None
recovered = service.search(
SearchRequest(
query="visitors",
scope=Scope(projects=("dome-performance",), include_shared=False),
cursor=bounded.next_cursor,
budget_tokens=5000,
),
principal=dome_reader,
)
recovered_node = cast(dict[str, object], recovered.data[0]["node"])
assert recovered_node["node_id"] == NODE.node_id
with pytest.raises(SearchError, match="too small"):
service.search(
SearchRequest(
query="visitors",
scope=Scope(projects=("dome-performance",), include_shared=False),
budget_tokens=256,
),
principal=dome_reader,
)
with pytest.raises(SearchError, match="shared knowledge"):
service.search(
SearchRequest(
query="visitors",
scope=Scope(projects=("dome-performance",)),
budget_tokens=5000,
),
principal=dome_reader,
)
with pytest.raises(SearchError, match="every requested project"):
service.search(
SearchRequest(
query='visitors" OR *',
scope=Scope(
projects=("dome-performance", "interactive-video"), include_shared=False
),
budget_tokens=5000,
),
principal=dome_reader,
)
def test_assumption_and_limitation_claims_are_searchable_and_cited(tmp_path: Path) -> None:
original_claim = NODE.claims[0]
statement = "The assumptionspecific condition affects seasonal visitor counts."
statement_digest = assertion_digest(statement)
evidence = original_claim.evidence[0].model_copy(
update={
"assertion_path": "assumptions[0].text",
"assertion_digest": statement_digest,
}
)
assessment = original_claim.assessment.model_copy(
update={
"assertion_digest": statement_digest,
"evidence_digests": (evidence_digest(evidence),),
}
)
context_claim = original_claim.model_copy(
update={
"claim_id": "dome:assumption-search-test",
"text": statement,
"evidence": (evidence,),
"assessment": assessment,
}
)
limitation_evidence = evidence.model_copy(update={"assertion_path": "limitations[0].text"})
limitation_claim = context_claim.model_copy(
update={
"claim_id": "dome:limitation-search-test",
"evidence": (limitation_evidence,),
"assessment": assessment.model_copy(
update={"evidence_digests": (evidence_digest(limitation_evidence),)}
),
}
)
contextual_node = NODE.model_copy(
update={"assumptions": (context_claim,), "limitations": (limitation_claim,)}
)
repository = GitCanonicalRepository(tmp_path / "knowledge.git")
_publish_nodes(repository, tmp_path / "publishing", (contextual_node,))
service = _service(repository, tmp_path / "search.sqlite3")
reader = Principal(
"reader-dome", "reader-key", frozenset({"viewer"}), frozenset({"dome-performance"})
)
result = service.search(
SearchRequest(
query="assumptionspecific",
scope=Scope(projects=("dome-performance",), include_shared=False),
budget_tokens=50_000,
),
principal=reader,
)
found = cast(dict[str, object], result.data[0]["node"])
assert found["assumptions"][0]["text"] == statement
assert found["limitations"][0]["text"] == statement
assert any(item.quote == evidence.quote for item in result.citations)
def test_fts_search_indexes_entity_aliases(tmp_path: Path) -> None:
repository = GitCanonicalRepository(tmp_path / "knowledge.git")
entity = NODE.model_copy(
update={
"node_type": NodeType.ENTITY,
"node_id": "dome:planetarium-entity",
"title": "Dome installation",
"aliases": ("Planetarium",),
}
)
_publish_nodes(repository, tmp_path / "publishing", (entity,))
service = _service(repository, tmp_path / "search.sqlite3")
reader = Principal(
"reader-dome", "reader-key", frozenset({"viewer"}), frozenset({"dome-performance"})
)
result = service.search(
SearchRequest(
query="Planetarium",
scope=Scope(projects=("dome-performance",), include_shared=False),
budget_tokens=5000,
),
principal=reader,
)
assert [cast(dict[str, object], row["node"])["node_id"] for row in result.data] == [
entity.node_id
]
def test_fts_search_applies_node_type_and_freshness_facets_in_sql(tmp_path: Path) -> None:
repository = GitCanonicalRepository(tmp_path / "knowledge.git")
stale_problem = NODE.model_copy(
update={
"node_id": "dome:stale-visitor-problem",
"node_type": NodeType.PROBLEM,
"title": "Visitor count reporting problem",
"as_of": datetime(2023, 1, 1, tzinfo=UTC),
"stale_after": datetime(2024, 1, 1, tzinfo=UTC),
"value": None,
"unit": None,
}
)
fresh_finding = NODE.model_copy(
update={
"node_id": "dome:fresh-visitor-finding",
"node_type": NodeType.FINDING,
"title": "Visitor count finding",
"as_of": None,
"stale_after": None,
"value": None,
"unit": None,
"sample_size": None,
"reporting_year": None,
"tags": ("demo-only",),
"audience": ("sales",),
"use_for": ("demo",),
"domain": ("video-production",),
}
)
_publish_nodes(repository, tmp_path / "publishing", (stale_problem, fresh_finding))
index = SQLiteSearchIndex(tmp_path / "search.sqlite3", repository)
problem_hits = index.search(
query="visitor",
project_ids=("dome-performance",),
include_shared=False,
limit=10,
node_types=("problem",),
)
assert [item.node.node_id for item in problem_hits.nodes] == [stale_problem.node_id]
browse_hits = index.search(
query="",
project_ids=("dome-performance",),
include_shared=False,
limit=10,
node_types=("problem",),
)
assert [item.node.node_id for item in browse_hits.nodes] == [stale_problem.node_id]
stale_hits = index.search(
query="visitor",
project_ids=("dome-performance",),
include_shared=False,
limit=10,
stale_only=True,
)
assert [item.node.node_id for item in stale_hits.nodes] == [stale_problem.node_id]
scoped_facet_hits = index.search(
query="visitor",
project_ids=("dome-performance",),
include_shared=False,
limit=10,
tags=("attendance",),
audience=("product",),
use_for=("comparison",),
domain=("production",),
)
assert [item.node.node_id for item in scoped_facet_hits.nodes] == [stale_problem.node_id]
or_facet_hits = index.search(
query="visitor",
project_ids=("dome-performance",),
include_shared=False,
limit=10,
tags=("attendance", "demo-only"),
)
assert {item.node.node_id for item in or_facet_hits.nodes} == {
stale_problem.node_id,
fresh_finding.node_id,
}
confidence_date_hits = index.search(
query="visitor",
project_ids=("dome-performance",),
include_shared=False,
limit=10,
confidences=(Confidence.MEDIUM,),
as_of_from=date(2023, 1, 1),
as_of_to=date(2023, 12, 31),
)
assert [item.node.node_id for item in confidence_date_hits.nodes] == [stale_problem.node_id]
mismatched_confidence_hits = index.search(
query="visitor",
project_ids=("dome-performance",),
include_shared=False,
limit=10,
confidences=(Confidence.LOW,),
as_of_from=date(2023, 1, 1),
as_of_to=date(2023, 12, 31),
)
assert not mismatched_confidence_hits.nodes
def test_typed_browse_uses_scoped_non_overlapping_cursor_pages(tmp_path: Path) -> None:
repository = GitCanonicalRepository(tmp_path / "knowledge.git")
first = NODE.model_copy(
update={
"node_id": "dome:problem-a",
"node_type": NodeType.PROBLEM,
"title": "Problem A",
"value": None,
"unit": None,
}
)
second = NODE.model_copy(
update={
"node_id": "dome:problem-b",
"node_type": NodeType.PROBLEM,
"title": "Problem B",
"value": None,
"unit": None,
}
)
other_project = NODE.model_copy(
update={
"node_id": "video:problem-c",
"project_id": "interactive-video",
"node_type": NodeType.PROBLEM,
"title": "Problem C",
"value": None,
"unit": None,
}
)
_publish_nodes(repository, tmp_path / "publishing", (first, second, other_project))
service = _service(repository, tmp_path / "search.sqlite3")
reader = Principal(
"reader-dome", "reader-key", frozenset({"viewer"}), frozenset({"dome-performance"})
)
request = SearchRequest(
query="",
scope=Scope(projects=("dome-performance",), include_shared=False),
node_types=(NodeType.PROBLEM,),
limit=1,
budget_tokens=5000,
)
page_one = service.search(request, principal=reader)
page_two = service.search(
request.model_copy(update={"cursor": page_one.next_cursor}), principal=reader
)
ids = [
cast(dict[str, object], row["node"])["node_id"] for row in (*page_one.data, *page_two.data)
]
assert ids == [first.node_id, second.node_id]
assert page_one.next_cursor is not None
assert page_two.next_cursor is None
def test_search_index_rebuilds_from_git_after_index_deletion_and_new_revision(
tmp_path: Path,
) -> None:
repository = GitCanonicalRepository(tmp_path / "knowledge.git")
_publish_nodes(repository, tmp_path / "publishing", (NODE,))
index_path = tmp_path / "search.sqlite3"
index = SQLiteSearchIndex(index_path, repository)
first = index.search(
query="visitors",
project_ids=("dome-performance",),
include_shared=False,
limit=10,
)
assert [item.node.node_id for item in first.nodes] == [NODE.node_id]
video_node = NODE.model_copy(
update={
"node_id": "video:branching-workflow",
"project_id": "interactive-video",
"title": "Interactive video branches",
"summary": "Branching is costly for video authors.",
}
)
new_revision = _publish_nodes(repository, tmp_path / "publishing", (video_node,))
second = index.search(
query="branches",
project_ids=("interactive-video",),
include_shared=False,
limit=10,
)
assert second.index_revision == new_revision
assert [item.node.node_id for item in second.nodes] == [video_node.node_id]
rebuilt = SQLiteSearchIndex(tmp_path / "rebuilt-search.sqlite3", repository).search(
query="visitors",
project_ids=("dome-performance",),
include_shared=False,
limit=10,
)
assert rebuilt.index_revision == new_revision
assert [item.node.node_id for item in rebuilt.nodes] == [NODE.node_id]
def test_os_termination_inside_index_rebuild_rolls_back_before_retrying(
tmp_path: Path,
) -> None:
repository = GitCanonicalRepository(tmp_path / "knowledge.git")
nodes = tuple(
NODE.model_copy(
update={
"node_id": f"dome:rebuild-{index:02d}",
"title": f"Visitor evidence {index:02d}",
"summary": f"The installation hosted visitors in fixture record {index:02d}.",
}
)
for index in range(8)
)
first_revision = _publish_nodes(repository, tmp_path / "publishing", nodes)
index_path = tmp_path / "search.sqlite3"
index = SQLiteSearchIndex(index_path, repository)
initial = index.search(
query="visitors",
project_ids=("dome-performance",),
include_shared=False,
limit=20,
)
assert len(initial.nodes) == len(nodes)
added_nodes = tuple(
NODE.model_copy(
update={
"node_id": f"dome:rebuild-{index:02d}",
"title": f"Visitor evidence {index:02d}",
"summary": f"The installation hosted visitors in fixture record {index:02d}.",
}
)
for index in range(8, 12)
)
all_nodes = (*nodes, *added_nodes)
latest_revision = _publish_nodes(repository, tmp_path / "publishing", added_nodes)
crashed = subprocess.run(
[
sys.executable,
"-m",
"tests.adapters.crash_sqlite_rebuild_worker",
str(repository.path),
str(index_path),
],
cwd=Path(__file__).parents[2],
capture_output=True,
check=False,
timeout=60,
)
assert crashed.returncode == 74, crashed.stderr.decode("utf-8", errors="replace")
with sqlite3.connect(index_path) as connection:
assert connection.execute("SELECT count(*) FROM indexed_nodes").fetchone() == (len(nodes),)
assert connection.execute("SELECT count(*) FROM node_fts").fetchone() == (len(nodes),)
prior_revision = connection.execute("SELECT revision FROM index_state").fetchone()
assert prior_revision == (first_revision,)
recovered = SQLiteSearchIndex(index_path, repository)
result = recovered.search(
query="visitors",
project_ids=("dome-performance",),
include_shared=False,
limit=20,
)
assert {hit.node.node_id for hit in result.nodes} == {node.node_id for node in all_nodes}
assert result.index_revision == latest_revision
assert recovered.diagnostic_status()[0] == latest_revision
def test_search_discloses_canonical_revision_advancing_during_the_query(
tmp_path: Path,
) -> None:
repository = GitCanonicalRepository(tmp_path / "knowledge.git")
_publish_nodes(repository, tmp_path / "publishing", (NODE,))
video_node = NODE.model_copy(
update={
"node_id": "video:branching-workflow",
"project_id": "interactive-video",
"title": "Interactive video branching",
"summary": "Authors control video branches.",
}
)
advanced = False
def canonical_head_after_index() -> str:
nonlocal advanced
if not advanced:
advanced = True
_publish_nodes(repository, tmp_path / "publishing", (video_node,))
return repository.head()
service = _service(
repository,
tmp_path / "search.sqlite3",
head=canonical_head_after_index,
)
principal = Principal(
"reader-dome", "reader-key", frozenset({"viewer"}), frozenset({"dome-performance"})
)
response = service.search(
SearchRequest(
query="visitors",
scope=Scope(projects=("dome-performance",), include_shared=False),
budget_tokens=5000,
),
principal=principal,
)
assert response.index_revision != response.canonical_revision
assert any(item.code == "INDEX_LAG" for item in response.warnings)
def test_search_cursor_expires_when_the_index_revision_changes(tmp_path: Path) -> None:
repository = GitCanonicalRepository(tmp_path / "knowledge.git")
second = NODE.model_copy(update={"node_id": "z:season-visitors"})
_publish_nodes(repository, tmp_path / "publishing", (NODE, second))
service = _service(repository, tmp_path / "search.sqlite3")
reader = Principal("reader", "key", frozenset({"viewer"}), frozenset({NODE.project_id}))
request = SearchRequest(
query="visitors",
scope=Scope(projects=(NODE.project_id,), include_shared=False),
limit=1,
budget_tokens=5000,
)
first_page = service.search(request, principal=reader)
assert first_page.next_cursor is not None
third = NODE.model_copy(update={"node_id": "zz:season-visitors"})
_publish_nodes(repository, tmp_path / "publishing", (third,))
with pytest.raises(SearchError, match="cursor expired") as error:
service.search(
request.model_copy(update={"cursor": first_page.next_cursor}), principal=reader
)
assert error.value.status_code == 409
def test_neighbors_are_rebuilt_and_hide_cross_project_nodes(tmp_path: Path) -> None:
repository = GitCanonicalRepository(tmp_path / "knowledge.git")
other = NODE.model_copy(update={"node_id": "video:other", "project_id": "interactive-video"})
edge = TypedEdge(
edge_id="edge:cross-project",
edge_type="compared_with",
source=EdgeEndpoint(kind="node", id=NODE.node_id),
target=EdgeEndpoint(kind="node", id=other.node_id),
)
files = {
_node_path(node): yaml.safe_dump(node.model_dump(mode="json")).encode()
for node in (NODE, other)
}
files[f"edges/{hashlib.sha256(edge.edge_id.encode()).hexdigest()}.yaml"] = yaml.safe_dump(
edge.model_dump(mode="json")
).encode()
repository.publish_files(
expected_head=repository.head(),
files=files,
message="Publish typed relationship",
index_directory=tmp_path / "publishing",
)
index = SQLiteSearchIndex(tmp_path / "search.sqlite3", repository)
hits = index.neighbors(
node_id=NODE.node_id,
edge_types=(),
project_ids=(NODE.project_id, other.project_id),
include_shared=False,
limit=10,
)
assert hits.origin is not None and hits.origin.node_id == NODE.node_id
assert len(hits.edges) == 1
assert hits.edges[0].neighbor_node is not None
service = _service(repository, tmp_path / "search2.sqlite3")
reader = Principal("reader", "key", frozenset({"viewer"}), frozenset({NODE.project_id}))
response = service.neighbors(
NeighborRequest(
node_id=NODE.node_id,
scope=Scope(projects=(NODE.project_id,), include_shared=False),
budget_tokens=5000,
),
principal=reader,
)
assert response.data == ()
assert response.citations == ()
def test_neighbor_source_metadata_requires_an_ingested_for_scope(tmp_path: Path) -> None:
repository = GitCanonicalRepository(tmp_path / "knowledge.git")
edge = TypedEdge(
edge_id="edge:zzz-visible-source",
edge_type="derived_from",
source=EdgeEndpoint(kind="node", id=NODE.node_id),
target=EdgeEndpoint(kind="source", id=SOURCE_REVISION.revision_id),
)
private_source = SOURCE_REVISION.model_copy(
update={
"source_id": "video:private",
"revision_id": "video:private:r1",
"ingested_for": ("interactive-video",),
}
)
private_edge = TypedEdge(
edge_id="edge:aaa-private-source",
edge_type="derived_from",
source=EdgeEndpoint(kind="node", id=NODE.node_id),
target=EdgeEndpoint(kind="source", id=private_source.revision_id),
)
source_key = hashlib.sha256(SOURCE_REVISION.source_id.encode()).hexdigest()
revision_key = hashlib.sha256(SOURCE_REVISION.revision_id.encode()).hexdigest()
private_source_key = hashlib.sha256(private_source.source_id.encode()).hexdigest()
private_revision_key = hashlib.sha256(private_source.revision_id.encode()).hexdigest()
files = {
_node_path(NODE): yaml.safe_dump(NODE.model_dump(mode="json")).encode(),
f"sources/{source_key}/{revision_key}.yaml": yaml.safe_dump(
SOURCE_REVISION.model_dump(mode="json")
).encode(),
f"sources/{private_source_key}/{private_revision_key}.yaml": yaml.safe_dump(
private_source.model_dump(mode="json")
).encode(),
f"edges/{hashlib.sha256(edge.edge_id.encode()).hexdigest()}.yaml": yaml.safe_dump(
edge.model_dump(mode="json")
).encode(),
f"edges/{hashlib.sha256(private_edge.edge_id.encode()).hexdigest()}.yaml": yaml.safe_dump(
private_edge.model_dump(mode="json")
).encode(),
}
repository.publish_files(
expected_head=repository.head(),
files=files,
message="Publish source relationship",
index_directory=tmp_path / "publishing",
)
service = _service(repository, tmp_path / "search.sqlite3")
reader = Principal("reader", "key", frozenset({"viewer"}), frozenset({NODE.project_id}))
response = service.neighbors(
NeighborRequest(
node_id=NODE.node_id,
scope=Scope(projects=(NODE.project_id,), include_shared=False),
limit=1,
budget_tokens=5000,
),
principal=reader,
)
assert len(response.data) == 1
assert response.next_cursor is None
neighbor = cast(dict[str, object], response.data[0]["neighbor"])
assert neighbor["kind"] == "source"
assert cast(dict[str, object], neighbor["record"])["revision_id"] == SOURCE_REVISION.revision_id
+108
View File
@@ -0,0 +1,108 @@
from pathlib import Path
import pytest
from gewu.adapters.sqlite_vectors import SQLiteVectorIndex, VectorIndexError
def _vectors() -> tuple[tuple[str, str, tuple[float, ...]], ...]:
return (
("dome-performance", "dome:closest", (1.0, 0.0, 0.0)),
("interactive-video", "video:closest", (0.0, 1.0, 0.0)),
("shared", "shared:closest", (0.9701425001, 0.242535625, 0.0)),
)
def _index(path: Path) -> SQLiteVectorIndex:
pytest.importorskip("sqlite_vec")
return SQLiteVectorIndex(path)
def _build(index: SQLiteVectorIndex, generation_id: str, vectors=None) -> None:
index.build_generation(
generation_id=generation_id,
revision="a" * 40,
model_id="test-model@revision",
artifact_sha256="b" * 64,
dimensions=3,
vectors=_vectors() if vectors is None else vectors,
)
def test_vector_generation_is_scoped_and_switches_atomically(tmp_path: Path) -> None:
index = _index(tmp_path / "vectors.sqlite3")
_build(index, "generation-1")
assert index.activate("generation-1", expected_active=None) is None
active = index.active_generation()
assert active is not None
assert active.generation_id == "generation-1"
assert active.revision == "a" * 40
assert active.model_id == "test-model@revision"
assert active.artifact_sha256 == "b" * 64
assert active.dimensions == 3
assert active.node_count == 3
scoped = index.search(
query=(1.0, 0.0, 0.0),
project_ids=("dome-performance",),
include_shared=False,
limit=10,
)
assert [hit.node_id for hit in scoped] == ["dome:closest"]
with_shared = index.search(
query=(1.0, 0.0, 0.0),
project_ids=("dome-performance",),
include_shared=True,
limit=10,
)
assert {hit.node_id for hit in with_shared} == {"dome:closest", "shared:closest"}
_build(
index, "generation-2", vectors=(("dome-performance", "dome:replacement", (1.0, 0.0, 0.0)),)
)
with pytest.raises(VectorIndexError, match="changed"):
index.activate("generation-2", expected_active=None)
active = index.active_generation()
assert active is not None and active.generation_id == "generation-1"
assert index.activate("generation-2", expected_active="generation-1") == "generation-1"
assert [
hit.node_id
for hit in index.search(
query=(1.0, 0.0, 0.0),
project_ids=("dome-performance",),
include_shared=False,
limit=10,
)
] == ["dome:replacement"]
assert index.activate("generation-1", expected_active="generation-2") == "generation-2"
assert [
hit.node_id
for hit in index.search(
query=(1.0, 0.0, 0.0),
project_ids=("dome-performance",),
include_shared=False,
limit=10,
)
] == ["dome:closest"]
with pytest.raises(VectorIndexError, match="Active vector generation"):
index.delete_generation("generation-1")
index.delete_generation("generation-2")
_build(index, "generation-2")
def test_vector_generation_rejects_invalid_vectors_before_publish(tmp_path: Path) -> None:
index = _index(tmp_path / "vectors.sqlite3")
with pytest.raises(VectorIndexError, match="normalized"):
_build(index, "bad", vectors=(("dome-performance", "dome:bad", (2.0, 0.0, 0.0)),))
assert index.active_generation() is None
assert (
index.search(
query=(1.0, 0.0, 0.0),
project_ids=("dome-performance",),
include_shared=False,
limit=10,
)
== ()
)
+117
View File
@@ -0,0 +1,117 @@
import pytest
from gewu.adapters.text_extractor import TextExtractionError, TextMarkdownExtractor
from gewu.domain.validation import extraction_digest
def test_markdown_extraction_preserves_unicode_and_original_location_maps() -> None:
source = "# Evidence\r\nCafe\u0301 has 400 visitors.\r\n"
extraction = TextMarkdownExtractor().extract(
"shared:source:r1", source.encode("utf-8"), "text/markdown"
)
assert extraction.extraction_id == extraction_digest(extraction)
block = extraction.blocks[0]
assert "Café has 400 visitors." in block.normalized_text
normalized_start = block.normalized_text.index("Café")
normalized_end = normalized_start + len("Café")
original_spans = block.normalization_map[normalized_start:normalized_end]
original_quote = block.original_text[
min(span[0] for span in original_spans) : max(span[1] for span in original_spans)
]
assert original_quote == "Cafe\u0301"
def test_html_extraction_keeps_visible_text_and_excludes_active_content() -> None:
source = (
"<html><head><title>Fixture title</title>"
"<style>.hide { display:none }</style></head><body>"
"<h1>Evidence</h1><p>Cafe&#x301; has 400 visitors.</p>"
"<script>ignoreThisSecret()</script><noscript>fallback text</noscript>"
"<p>Second paragraph.</p></body></html>"
)
extraction = TextMarkdownExtractor().extract(
"shared:html:r1", source.encode("utf-8"), "text/html"
)
assert extraction.parser_version == "0.3.0"
text = "\n".join(block.original_text for block in extraction.blocks)
assert "Evidence" in text
assert "Café has 400 visitors." in "\n".join(
block.normalized_text for block in extraction.blocks
)
assert "ignoreThisSecret" not in text
assert "fallback text" not in text
assert "display:none" not in text
block = extraction.blocks[0]
normalized_start = block.normalized_text.index("Café")
original_spans = block.normalization_map[normalized_start : normalized_start + 4]
original_quote = block.original_text[
min(span[0] for span in original_spans) : max(span[1] for span in original_spans)
]
assert original_quote == "Cafe\u0301"
def test_extraction_splits_bounded_blocks_without_losing_text() -> None:
source = ("a" * 80_000 + "\n" + "b" * 200).encode("utf-8")
extraction = TextMarkdownExtractor().extract("shared:large:r1", source, "text/plain")
assert len(extraction.blocks) == 2
assert "".join(block.original_text for block in extraction.blocks) == source.decode("utf-8")
assert all(len(block.normalized_text) <= 80_000 for block in extraction.blocks)
def test_webvtt_extraction_records_exact_cue_timestamps_and_text_offsets() -> None:
source = (
"WEBVTT\n\n"
"cue-1\n00:14:32.000 --> 00:14:36.500 align:start\n"
"The audience entered the room.\n\n"
"cue-2\n00:14:40.000 --> 00:14:42.000\n"
"The host introduced the film.\n"
)
extraction = TextMarkdownExtractor().extract(
"shared:talk-transcript:r1", source.encode(), "text/vtt"
)
cues = [block for block in extraction.blocks if block.time_start_ms is not None]
assert [(cue.time_start_ms, cue.time_end_ms) for cue in cues] == [
(872_000, 876_500),
(880_000, 882_000),
]
quote = "The audience entered the room."
assert quote in cues[0].normalized_text
start = cues[0].normalized_text.index(quote)
location = cues[0].normalization_map[start : start + len(quote)]
original = cues[0].original_text[
min(span[0] for span in location) : max(span[1] for span in location)
]
assert original == quote
def test_subrip_transcript_extracts_comma_millisecond_timestamps() -> None:
extraction = TextMarkdownExtractor().extract(
"shared:interview:r1",
b"1\n00:01:02,250 --> 00:01:03,750\nA precise quote.\n",
"application/x-subrip",
)
cue = next(block for block in extraction.blocks if block.time_start_ms is not None)
assert (cue.time_start_ms, cue.time_end_ms) == (62_250, 63_750)
assert "A precise quote." in cue.normalized_text
@pytest.mark.parametrize(
"content",
[
b"WEBVTT\n\n00:00:05.000 --> 00:00:02.000\nReversed time.\n",
b"WEBVTT\n\n00:00:01.000 --> invalid\nMalformed time.\n",
b"WEBVTT\n\nNo timestamped cue.\n",
],
)
def test_transcript_extraction_rejects_invalid_or_missing_timestamps(content: bytes) -> None:
with pytest.raises(TextExtractionError):
TextMarkdownExtractor().extract("shared:bad-transcript:r1", content, "text/vtt")
@pytest.mark.parametrize(
("content", "media_type"),
[(b"\xff", "text/plain"), (b"payload", "application/pdf"), (b"", "text/plain")],
)
def test_extractor_rejects_invalid_or_unsupported_sources(content: bytes, media_type: str) -> None:
with pytest.raises(TextExtractionError):
TextMarkdownExtractor().extract("shared:source:r1", content, media_type)
+189
View File
@@ -0,0 +1,189 @@
from __future__ import annotations
import socket
import time
from collections import deque
from typing import Any
import pytest
import gewu.adapters.url_fetch as url_fetch
from gewu.adapters.url_fetch import (
MAX_CAPTURE_BYTES,
PublicUrlFetcher,
UrlCaptureError,
)
class FakeResponse:
def __init__(self, status: int, headers: dict[str, str], body: bytes = b"") -> None:
self.status = status
self.headers = headers
self.body = body
self.read_calls = 0
def getheader(self, name: str) -> str | None:
return self.headers.get(name)
def read(self, amount: int) -> bytes:
self.read_calls += 1
block, self.body = self.body[:amount], self.body[amount:]
return block
class FakeConnection:
def __init__(self, response: FakeResponse) -> None:
self.response = response
self.request_data: tuple[str, str, dict[str, str]] | None = None
self.closed = False
def request(self, method: str, path: str, headers: dict[str, str]) -> None:
self.request_data = (method, path, headers)
def getresponse(self) -> FakeResponse:
return self.response
def close(self) -> None:
self.closed = True
def public_resolver(host: str, port: int, **_kwargs: Any) -> list[tuple[Any, ...]]:
return [(socket.AF_INET, socket.SOCK_STREAM, 6, "", ("93.184.216.34", port))]
def test_capture_requires_exact_host_allowlist_and_pins_validated_dns_address() -> None:
response = FakeResponse(
200,
{"Content-Type": "text/plain; charset=utf-8", "X-Source-Title": "Fixture title"},
b"fixture source",
)
connections: list[FakeConnection] = []
pins: list[tuple[str, str, int, str, int]] = []
def factory(scheme: str, host: str, port: int, address: str, timeout: int) -> FakeConnection:
pins.append((scheme, host, port, address, timeout))
connection = FakeConnection(response)
connections.append(connection)
return connection
fetcher = PublicUrlFetcher(
{"research.example"}, resolver=public_resolver, connection_factory=factory
)
captured = fetcher.fetch("https://research.example/reference?q=1#ignored")
assert captured.original_url == "https://research.example/reference?q=1#ignored"
assert captured.final_url == "https://research.example/reference?q=1#ignored"
assert captured.media_type == "text/plain"
assert captured.title == "Fixture title"
assert captured.content == b"fixture source"
assert pins[0][1:4] == ("research.example", 443, "93.184.216.34")
assert connections[0].request_data == (
"GET",
"/reference?q=1",
{
"Host": "research.example",
"User-Agent": "GEWU-source-capture/1.0",
"Accept": "application/pdf, text/html, text/plain, text/markdown",
"Accept-Encoding": "identity",
"Connection": "close",
},
)
assert connections[0].closed
def test_private_dns_result_is_rejected_before_opening_a_connection() -> None:
factory_called = False
def resolver(host: str, port: int, **_kwargs: Any) -> list[tuple[Any, ...]]:
return [(socket.AF_INET, socket.SOCK_STREAM, 6, "", ("127.0.0.1", port))]
def factory(*_args: Any) -> FakeConnection:
nonlocal factory_called
factory_called = True
return FakeConnection(FakeResponse(200, {"Content-Type": "text/plain"}))
fetcher = PublicUrlFetcher({"research.example"}, resolver=resolver, connection_factory=factory)
with pytest.raises(UrlCaptureError) as error:
fetcher.fetch("http://research.example/source")
assert error.value.code == "FETCH_BLOCKED"
assert not factory_called
def test_dns_resolution_has_a_bounded_wait(monkeypatch: pytest.MonkeyPatch) -> None:
def slow_resolver(host: str, port: int, **_kwargs: Any) -> list[tuple[Any, ...]]:
time.sleep(0.03)
return public_resolver(host, port)
monkeypatch.setattr(url_fetch, "DNS_TIMEOUT_SECONDS", 0.001)
fetcher = PublicUrlFetcher({"research.example"}, resolver=slow_resolver)
with pytest.raises(UrlCaptureError) as error:
fetcher.fetch("https://research.example/source")
assert error.value.code == "FETCH_TIMEOUT"
def test_redirect_is_revalidated_against_allowlist_before_next_connection() -> None:
first = FakeResponse(302, {"Location": "http://127.0.0.1/metadata"})
connection_count = 0
def factory(*_args: Any) -> FakeConnection:
nonlocal connection_count
connection_count += 1
return FakeConnection(first)
fetcher = PublicUrlFetcher(
{"research.example"}, resolver=public_resolver, connection_factory=factory
)
with pytest.raises(UrlCaptureError) as error:
fetcher.fetch("http://research.example/start")
assert error.value.code == "FETCH_BLOCKED"
assert connection_count == 1
def test_capture_rejects_unsupported_types_encoding_and_oversize() -> None:
responses = deque(
[
FakeResponse(200, {"Content-Type": "application/zip"}),
FakeResponse(
200,
{"Content-Type": "text/plain", "Content-Encoding": "gzip"},
),
FakeResponse(
200,
{
"Content-Type": "text/plain",
"Content-Length": str(MAX_CAPTURE_BYTES + 1),
},
),
]
)
def factory(*_args: Any) -> FakeConnection:
return FakeConnection(responses.popleft())
fetcher = PublicUrlFetcher(
{"research.example"}, resolver=public_resolver, connection_factory=factory
)
for expected_code in (
"UNSUPPORTED_MEDIA_TYPE",
"UNSUPPORTED_ENCODING",
"CAPTURE_TOO_LARGE",
):
with pytest.raises(UrlCaptureError) as error:
fetcher.fetch("http://research.example/source")
assert error.value.code == expected_code
@pytest.mark.parametrize(
"url",
[
"file:///etc/passwd",
"http://user:password@research.example/source",
"http://research.example:8080/source",
"http://research.example/line\nInjected: header",
],
)
def test_capture_rejects_unsafe_url_forms(url: str) -> None:
fetcher = PublicUrlFetcher({"research.example"}, resolver=public_resolver)
with pytest.raises(UrlCaptureError) as error:
fetcher.fetch(url)
assert error.value.code == "URL_INVALID"
+1
View File
@@ -0,0 +1 @@
"""Transport boundary tests."""
+264
View File
@@ -0,0 +1,264 @@
import asyncio
import sqlite3
from pathlib import Path
import httpx2
from gewu.adapters.git_repository import GitCanonicalRepository
from gewu.adapters.sqlite_identity import SQLiteIdentityStore
from gewu.application.reading import CanonicalNodeReader
from gewu.bootstrap.app import create_app
PASSWORD = "initial-password-fixture"
def test_password_accounts_owned_agent_keys_and_session_lifecycle(tmp_path: Path) -> None:
async def exercise() -> None:
store = SQLiteIdentityStore(tmp_path / "identity.sqlite3")
operator = store.provision(
credential_id="operator", principal_id="operator", roles=("admin",)
)
app = create_app(
identity_provider=store,
account_identity=store,
credential_administration=store,
node_reader=CanonicalNodeReader(GitCanonicalRepository(tmp_path / "knowledge.git")),
)
transport = httpx2.ASGITransport(app=app)
operator_headers = {"Authorization": f"Bearer {operator}"}
async with httpx2.AsyncClient(transport=transport, base_url="https://gewu.test") as admin:
body = {
"username": "alice",
"password": PASSWORD,
"role": "editor",
"project_ids": ["pilot"],
}
assert (await admin.post("/v1/admin/users", json=body)).status_code == 401
created = await admin.post("/v1/admin/users", json=body, headers=operator_headers)
assert created.status_code == 201 and PASSWORD not in created.text
assert (
await admin.post("/v1/admin/users", json=body, headers=operator_headers)
).status_code == 409
for username, role in (("bob", "editor"), ("viewer", "viewer"), ("admin", "admin")):
assert (
await admin.post(
"/v1/admin/users",
headers=operator_headers,
json={**body, "username": username, "role": role},
)
).status_code == 201
short_secret = "shortsecret"
invalid = await admin.post(
"/v1/admin/users",
headers=operator_headers,
json={**body, "username": "invalid", "password": short_secret},
)
assert invalid.status_code == 422 and short_secret not in invalid.text
listing = await admin.get("/v1/admin/users", headers=operator_headers)
assert "password_hash" not in listing.text and PASSWORD not in listing.text
async with httpx2.AsyncClient(
transport=transport, base_url="https://gewu.test"
) as alice:
login = await alice.post(
"/v1/users/session",
json={"username": "alice", "password": PASSWORD},
headers={"Origin": "https://gewu.test"},
)
assert login.status_code == 200
assert "secure" in login.headers["set-cookie"].lower()
csrf = {"X-GEWU-CSRF": login.json()["csrf_token"]}
assert (await alice.get("/v1/me")).json()["principal_id"] == "user:alice"
assert (await alice.get("/v1/admin/users")).status_code == 403
assert (
await alice.post("/v1/admin/users", headers=csrf, json=body)
).status_code == 403
agent_body = {"name": "codex-laptop", "project_ids": ["pilot"]}
assert (
await alice.post("/v1/me/agent-credentials", json=agent_body)
).status_code == 403
assert (
await alice.post(
"/v1/me/agent-credentials",
headers={**csrf, "Origin": "https://attacker"},
json=agent_body,
)
).status_code == 403
assert (
await alice.post(
"/v1/me/agent-credentials",
headers=csrf,
json={**agent_body, "project_ids": ["other"]},
)
).status_code == 403
assert (
await alice.post(
"/v1/me/agent-credentials",
headers=csrf,
json={**agent_body, "role": "admin"},
)
).status_code == 422
agent = await alice.post("/v1/me/agent-credentials", headers=csrf, json=agent_body)
assert agent.status_code == 201
key_id, token = agent.json()["credential_id"], agent.json()["token"]
agent_principal = store.authenticate(token)
assert agent_principal and agent_principal.can_submit_project("pilot")
assert not agent_principal.can_submit_project("other")
assert (await alice.get("/v1/me/agent-credentials")).json()[0][
"credential_id"
] == key_id
assert token not in (await alice.get("/v1/me/agent-credentials")).text
assert (
await alice.get("/v1/me", headers={"Authorization": f"Bearer {token}"})
).status_code == 403
async with httpx2.AsyncClient(
transport=transport, base_url="https://gewu.test"
) as bob:
bob_login = await bob.post(
"/v1/users/session", json={"username": "bob", "password": PASSWORD}
)
bob_csrf = {"X-GEWU-CSRF": bob_login.json()["csrf_token"]}
assert (await bob.get("/v1/me/agent-credentials")).json() == []
assert (
await bob.delete(f"/v1/me/agent-credentials/{key_id}", headers=bob_csrf)
).status_code == 404
assert store.authenticate(token) is not None
bob_agent = await bob.post(
"/v1/me/agent-credentials", headers=bob_csrf, json=agent_body
)
rotated = store.rotate(bob_agent.json()["credential_id"], "rotated-bob-key")
assert (await bob.get("/v1/me/agent-credentials")).json()[-1][
"owner_principal_id"
] == "user:bob"
assert (
await admin.delete(
"/v1/admin/credentials/account:bob", headers=operator_headers
)
).status_code == 200
assert store.authenticate(rotated) is None
assert (await bob.get("/v1/me")).status_code == 401
password_body = {
"current_password": PASSWORD,
"new_password": "changed-password-fixture",
}
assert (await alice.post("/v1/me/password", json=password_body)).status_code == 403
assert (
await alice.post(
"/v1/me/password",
headers=csrf,
json={**password_body, "current_password": "wrong"},
)
).status_code == 403
second_session = store.login_account("alice", PASSWORD)
assert second_session
changed = await alice.post("/v1/me/password", headers=csrf, json=password_body)
assert changed.status_code == 200
assert store.authenticate_browser_session(second_session.session_token) is None
assert (await alice.get("/v1/me")).status_code == 401
assert store.login_account("alice", PASSWORD) is None
assert store.authenticate(token) is not None
relogin = await alice.post(
"/v1/users/session",
json={"username": "alice", "password": password_body["new_password"]},
)
csrf = {"X-GEWU-CSRF": relogin.json()["csrf_token"]}
assert (
await alice.delete(f"/v1/me/agent-credentials/{key_id}", headers=csrf)
).status_code == 200
assert store.authenticate(token) is None
another = await alice.post(
"/v1/me/agent-credentials", headers=csrf, json=agent_body
)
assert (
await admin.delete("/v1/admin/users/alice", headers=operator_headers)
).status_code == 200
assert store.authenticate(another.json()["token"]) is None
assert (await alice.get("/v1/me")).status_code == 401
assert store.login_account("alice", password_body["new_password"]) is None
async with httpx2.AsyncClient(
transport=transport, base_url="https://gewu.test"
) as viewer:
login = await viewer.post(
"/v1/users/session", json={"username": "viewer", "password": PASSWORD}
)
csrf = {"X-GEWU-CSRF": login.json()["csrf_token"]}
assert (
await viewer.post(
"/v1/me/agent-credentials",
headers=csrf,
json={"name": "agent", "project_ids": ["pilot"]},
)
).status_code == 403
async with httpx2.AsyncClient(
transport=transport, base_url="https://gewu.test"
) as human_admin:
login = await human_admin.post(
"/v1/users/session", json={"username": "admin", "password": PASSWORD}
)
csrf = {"X-GEWU-CSRF": login.json()["csrf_token"]}
assert (
await human_admin.delete("/v1/admin/users/admin", headers=csrf)
).status_code == 403
with sqlite3.connect(store.database) as db:
assert (
db.execute("SELECT password_hash FROM user_accounts WHERE username='bob'")
.fetchone()[0]
.startswith("$argon2id$")
)
assert (
PASSWORD not in (await admin.get("/v1/admin/events", headers=operator_headers)).text
)
assert len(SQLiteIdentityStore(store.database).list_accounts(limit=50, offset=0)) == 4
asyncio.run(exercise())
def test_password_login_origin_tls_size_and_throttle(tmp_path: Path) -> None:
async def exercise() -> None:
store = SQLiteIdentityStore(tmp_path / "identity.sqlite3")
store.create_account(
username="alice",
password=PASSWORD,
role="editor",
project_ids=("pilot",),
actor="operator",
)
app = create_app(
identity_provider=store,
account_identity=store,
node_reader=CanonicalNodeReader(GitCanonicalRepository(tmp_path / "knowledge.git")),
)
async with httpx2.AsyncClient(
transport=httpx2.ASGITransport(app=app), base_url="http://gewu.test"
) as http:
body = {"username": "alice", "password": PASSWORD}
assert (await http.post("/v1/users/session", json=body)).status_code == 403
assert (
await http.post(
"https://gewu.test/v1/users/session",
json=body,
headers={"Origin": "https://attacker.invalid"},
)
).status_code == 403
assert (
await http.post("https://gewu.test/v1/users/session", content=b" " * 4097)
).status_code == 413
for _ in range(5):
assert (
await http.post(
"https://gewu.test/v1/users/session", json={**body, "password": "wrong"}
)
).status_code == 401
assert (
await http.post("https://gewu.test/v1/users/session", json=body)
).status_code == 401
with store._connect() as db:
db.execute("UPDATE account_login_failures SET failed_at=0")
assert (
await http.post("https://gewu.test/v1/users/session", json=body)
).status_code == 200
asyncio.run(exercise())
+31
View File
@@ -0,0 +1,31 @@
import json
from pathlib import Path
from scripts.export_agent_contract import PACK, build_contract
def test_pack_mcp_contract_matches_the_live_server_and_generated_schemas(
tmp_path: Path,
) -> None:
snapshot_path = PACK / "contracts" / "service-v1.json"
snapshot = json.loads(snapshot_path.read_text(encoding="utf-8"))
actual = build_contract(tmp_path)
assert actual == snapshot
assert (PACK / "contracts" / "catalog-v1.json").read_bytes() == (
Path(__file__).resolve().parents[2] / "schema" / "v1" / "catalog.json"
).read_bytes()
packaged_schemas = PACK / "contracts"
for schema in snapshot["record_schemas"]:
schema_path = packaged_schemas / schema["path"]
assert schema_path.is_file()
assert (
schema_path.read_bytes()
== (
Path(__file__).resolve().parents[2]
/ "schema"
/ "v1"
/ "records"
/ Path(schema["path"]).name
).read_bytes()
)
+273
View File
@@ -0,0 +1,273 @@
import asyncio
import hashlib
from pathlib import Path
import httpx
import yaml
from gewu.adapters.git_repository import GitCanonicalRepository
from gewu.adapters.sqlite_identity import SQLiteIdentityStore
from gewu.adapters.text_extractor import TextMarkdownExtractor
from gewu.application.reading import CanonicalNodeReader
from gewu.application.uploads import LocalArtifactStore
from gewu.bootstrap.app import create_app
from tests.domain.proposal_fixtures import NODE, SOURCE_REVISION
def test_authenticated_scoped_canonical_node_read(tmp_path: Path) -> None:
async def exercise() -> None:
repository = GitCanonicalRepository(tmp_path / "knowledge.git")
project_key = hashlib.sha256(NODE.project_id.encode()).hexdigest()
node_key = hashlib.sha256(NODE.node_id.encode()).hexdigest()
node_path = f"projects/{project_key}/nodes/{node_key}.yaml"
source_path = (
f"sources/{hashlib.sha256(SOURCE_REVISION.source_id.encode()).hexdigest()}/"
f"{hashlib.sha256(SOURCE_REVISION.revision_id.encode()).hexdigest()}.yaml"
)
artifact_store = LocalArtifactStore(tmp_path / "artifacts")
source_bytes = b"# Archived source\nA precise citation.\n"
source_digest = hashlib.sha256(source_bytes).hexdigest()
upload_id = artifact_store.begin(
filename="source.md",
media_type="text/markdown",
byte_length=len(source_bytes),
expected_sha256=source_digest,
owner_id="agent-pilot",
project_id=NODE.project_id,
).upload_id
artifact_store.put_chunk(
upload_id,
offset=0,
content=source_bytes,
chunk_sha256=source_digest,
owner_id="agent-pilot",
)
artifact_store.finalize(upload_id, owner_id="agent-pilot")
extraction = TextMarkdownExtractor().extract(
SOURCE_REVISION.revision_id, source_bytes, "text/markdown"
)
artifact_store.save_extraction(
extraction, owner_id="agent-pilot", project_id=NODE.project_id
)
archived_source = SOURCE_REVISION.model_copy(
update={"artifact_sha256": source_digest, "byte_length": len(source_bytes)}
)
repository.publish_files(
expected_head=repository.head(),
files={node_path: yaml.safe_dump(NODE.model_dump(mode="json")).encode()},
message="Publish test node",
index_directory=tmp_path / "publishing",
)
updated_node = NODE.model_copy(update={"summary": "A revised accepted summary."})
restricted_source = SOURCE_REVISION.model_copy(
update={
"source_id": "video:private",
"revision_id": "video:private:r1",
"ingested_for": ("interactive-video",),
}
)
restricted_source_path = (
f"sources/{hashlib.sha256(restricted_source.source_id.encode()).hexdigest()}/"
f"{hashlib.sha256(restricted_source.revision_id.encode()).hexdigest()}.yaml"
)
reference_source = SOURCE_REVISION.model_copy(
update={
"source_id": "dome:reference-only",
"revision_id": "dome:reference-only:r1",
"artifact_sha256": None,
"media_type": "application/octet-stream",
"byte_length": 0,
"original_url": "https://research.example/unavailable-paper",
"ingested_for": (NODE.project_id,),
"archive_status": "reference-only",
}
)
reference_source_path = (
f"sources/{hashlib.sha256(reference_source.source_id.encode()).hexdigest()}/"
f"{hashlib.sha256(reference_source.revision_id.encode()).hexdigest()}.yaml"
)
repository.publish_files(
expected_head=repository.head(),
files={
node_path: yaml.safe_dump(updated_node.model_dump(mode="json")).encode(),
source_path: yaml.safe_dump(archived_source.model_dump(mode="json")).encode(),
restricted_source_path: yaml.safe_dump(
restricted_source.model_dump(mode="json")
).encode(),
reference_source_path: yaml.safe_dump(
reference_source.model_dump(mode="json")
).encode(),
},
message="Revise node and register source",
index_directory=tmp_path / "publishing",
)
repository.publish_files(
expected_head=repository.head(),
files={"metadata/unrelated.yaml": b"accepted but unrelated\n"},
message="Advance repository without changing the node",
index_directory=tmp_path / "publishing",
)
identities = SQLiteIdentityStore(tmp_path / "identity.sqlite3")
token = identities.provision(
credential_id="agent-key",
principal_id="agent-pilot",
roles=("agent",),
project_ids=(NODE.project_id,),
)
app = create_app(
identity_provider=identities,
node_reader=CanonicalNodeReader(repository),
artifact_store=artifact_store,
)
async with httpx.AsyncClient(
transport=httpx.ASGITransport(app=app), base_url="http://gewu.test"
) as client:
path = f"/v1/projects/{NODE.project_id}/nodes/{NODE.node_id}"
assert (await client.get(path)).status_code == 401
malformed_id = await client.get(
f"/v1/projects/{NODE.project_id}/nodes/not%20a%20record-id",
headers={"Authorization": f"Bearer {token}"},
)
assert malformed_id.status_code == 422
denied = await client.get(
f"/v1/projects/interactive-video/nodes/{NODE.node_id}",
headers={"Authorization": f"Bearer {token}"},
)
assert denied.status_code == 403
accepted = await client.get(path, headers={"Authorization": f"Bearer {token}"})
assert accepted.status_code == 200
envelope = accepted.json()
assert envelope["retrieval_mode"] == "canonical"
assert envelope["canonical_revision"] == repository.head()
assert envelope["data"][0]["node_id"] == NODE.node_id
assert envelope["citations"][0]["quote"] == NODE.claims[0].evidence[0].quote
history = await client.get(
f"{path}/history", headers={"Authorization": f"Bearer {token}"}
)
assert history.status_code == 200
history_data = history.json()["data"]
assert len(history_data) == 2
assert history_data[0]["node"]["summary"] == updated_node.summary
latest_node_revision = history_data[0]["revision"]
assert latest_node_revision != repository.head()
latest_file_version = await client.get(
f"{path}?revision={latest_node_revision}",
headers={"Authorization": f"Bearer {token}"},
)
assert latest_file_version.status_code == 200
assert latest_file_version.json()["canonical_revision"] == latest_node_revision
assert latest_file_version.json()["data"][0]["summary"] == updated_node.summary
historical_revision = history_data[-1]["revision"]
historical = await client.get(
f"{path}?revision={historical_revision}",
headers={"Authorization": f"Bearer {token}"},
)
assert historical.status_code == 200, historical.text
assert historical.json()["canonical_revision"] == historical_revision
assert historical.json()["data"][0]["summary"] == NODE.summary
assert historical.json()["citations"][0]["quote"] == NODE.claims[0].evidence[0].quote
malformed_revision = await client.get(
f"{path}?revision=not-a-commit",
headers={"Authorization": f"Bearer {token}"},
)
assert malformed_revision.status_code == 422
unknown_revision = await client.get(
f"{path}?revision={'a' * 40}",
headers={"Authorization": f"Bearer {token}"},
)
assert unknown_revision.status_code == 404
source = await client.get(
f"/v1/projects/{NODE.project_id}/sources/{SOURCE_REVISION.revision_id}",
headers={"Authorization": f"Bearer {token}"},
)
assert source.status_code == 200
assert source.json()["data"][0]["revision_id"] == SOURCE_REVISION.revision_id
reference_path = (
f"/v1/projects/{NODE.project_id}/sources/{reference_source.revision_id}"
)
reference = await client.get(
reference_path, headers={"Authorization": f"Bearer {token}"}
)
assert reference.status_code == 200
assert reference.json()["data"][0]["artifact_sha256"] is None
diagnostics_path = "/v1/diagnostics?projects=dome-performance"
assert (await client.get("/v1/diagnostics")).status_code == 401
diagnostic_response = await client.get(
diagnostics_path, headers={"Authorization": f"Bearer {token}"}
)
assert diagnostic_response.status_code == 200
diagnostic_body = diagnostic_response.json()
catalog_path = Path(__file__).resolve().parents[2] / "schema/v1/catalog.json"
assert (
diagnostic_body["catalog_sha256"]
== hashlib.sha256(catalog_path.read_bytes()).hexdigest()
)
assert diagnostic_body["selected_scope"]["projects"] == [NODE.project_id]
assert diagnostic_body["index_state"] == "not_configured"
assert diagnostic_body["max_upload_bytes"] == 50 * 1024 * 1024
assert "get_source_artifact" in diagnostic_body["authorized_operations"]
assert token not in diagnostic_response.text
assert "principal_id" not in diagnostic_body
forbidden_diagnostic_scope = await client.get(
"/v1/diagnostics?projects=interactive-video",
headers={"Authorization": f"Bearer {token}"},
)
assert forbidden_diagnostic_scope.status_code == 403
forbidden_shared_scope = await client.get(
diagnostics_path + "&include_shared=true",
headers={"Authorization": f"Bearer {token}"},
)
assert forbidden_shared_scope.status_code == 403
artifact_path = (
f"/v1/projects/{NODE.project_id}/sources/{SOURCE_REVISION.revision_id}/artifact"
)
assert (await client.get(artifact_path)).status_code == 401
archived = await client.get(artifact_path, headers={"Authorization": f"Bearer {token}"})
assert archived.status_code == 200
assert archived.content == source_bytes
assert archived.headers["etag"] == f'"{source_digest}"'
assert archived.headers["x-content-type-options"] == "nosniff"
assert archived.headers["cache-control"] == "private, no-store"
assert archived.headers["content-security-policy"] == "default-src 'none'; sandbox"
missing_artifact = await client.get(
f"{reference_path}/artifact", headers={"Authorization": f"Bearer {token}"}
)
assert missing_artifact.status_code == 404
block = extraction.blocks[0]
offset = block.normalized_text.index("precise")
location_path = (
f"/v1/projects/{NODE.project_id}/sources/{SOURCE_REVISION.revision_id}"
f"/extractions/{extraction.extraction_id}/blocks/{block.block_id}"
)
location = await client.get(
location_path,
params={"offset": offset, "length": len("precise")},
headers={"Authorization": f"Bearer {token}"},
)
assert location.status_code == 200
assert location.json()["normalized_text"] == "precise"
assert location.json()["original_text"] == "precise"
denied_location = await client.get(
location_path,
params={"offset": offset, "length": len("precise")},
)
assert denied_location.status_code == 401
restricted = await client.get(
f"/v1/projects/{NODE.project_id}/sources/{restricted_source.revision_id}",
headers={"Authorization": f"Bearer {token}"},
)
assert restricted.status_code == 404
restricted_artifact = await client.get(
f"/v1/projects/{NODE.project_id}/sources/{restricted_source.revision_id}/artifact",
headers={"Authorization": f"Bearer {token}"},
)
assert restricted_artifact.status_code == 404
assert identities.revoke("agent-key")
assert (
await client.get(path, headers={"Authorization": f"Bearer {token}"})
).status_code == 401
assert (
await client.get(artifact_path, headers={"Authorization": f"Bearer {token}"})
).status_code == 401
asyncio.run(exercise())
+189
View File
@@ -0,0 +1,189 @@
import asyncio
from pathlib import Path
import httpx2
from gewu.adapters.git_repository import GitCanonicalRepository
from gewu.adapters.sqlite_identity import SQLiteIdentityStore
from gewu.adapters.sqlite_search import SQLiteSearchIndex
from gewu.application.reading import CanonicalNodeReader
from gewu.application.search import CanonicalSearchService
from gewu.bootstrap.app import create_app
from tests.domain.proposal_fixtures import PROJECTS
def test_browser_session_is_http_only_human_only_and_csrf_protected(tmp_path: Path) -> None:
async def exercise() -> None:
repository = GitCanonicalRepository(tmp_path / "knowledge.git")
identities = SQLiteIdentityStore(tmp_path / "identity.sqlite3")
viewer_key = identities.provision(
credential_id="browser-viewer-key",
principal_id="browser-viewer",
roles=("viewer",),
project_ids=("dome-performance",),
)
agent_key = identities.provision(
credential_id="browser-agent-key",
principal_id="browser-agent",
roles=("agent",),
project_ids=("dome-performance",),
)
search = CanonicalSearchService(
index=SQLiteSearchIndex(tmp_path / "search.sqlite3", repository),
project_ids_provider=lambda: PROJECTS.keys(),
canonical_head=repository.head,
)
app = create_app(
identity_provider=identities,
node_reader=CanonicalNodeReader(repository),
search_service=search,
)
async with httpx2.AsyncClient(
transport=httpx2.ASGITransport(app=app),
base_url="http://gewu.test",
) as client:
agent_session = await client.post(
"/v1/session",
headers={
"Authorization": f"Bearer {agent_key}",
"Origin": "http://gewu.test",
},
)
assert agent_session.status_code == 403
cross_origin = await client.post(
"/v1/session",
headers={
"Authorization": f"Bearer {viewer_key}",
"Origin": "https://attacker.invalid",
},
)
assert cross_origin.status_code == 403
created = await client.post(
"/v1/session",
headers={
"Authorization": f"Bearer {viewer_key}",
"Origin": "http://gewu.test",
},
)
assert created.status_code == 200, created.text
session = created.json()
cookie = created.cookies.get("gewu_session")
assert cookie is not None
assert "httponly" in created.headers["set-cookie"].lower()
assert "samesite=strict" in created.headers["set-cookie"].lower()
assert "max-age" not in created.headers["set-cookie"].lower()
projects = await client.get("/v1/projects")
assert projects.status_code == 200
assert projects.json() == ["dome-performance"]
denied_csrf = await client.post(
"/v1/search",
json={
"query": "visitors",
"scope": {"projects": ["dome-performance"], "include_shared": False},
"budget_tokens": 5000,
},
)
assert denied_csrf.status_code == 403
searched = await client.post(
"/v1/search",
headers={"X-GEWU-CSRF": session["csrf_token"]},
json={
"query": "visitors",
"scope": {"projects": ["dome-performance"], "include_shared": False},
"budget_tokens": 5000,
},
)
assert searched.status_code == 200, searched.text
signed_out = await client.delete(
"/v1/session", headers={"X-GEWU-CSRF": session["csrf_token"]}
)
assert signed_out.status_code == 200
assert (await client.get("/v1/projects")).status_code == 401
assert identities.authenticate_browser_session(cookie) is None
asyncio.run(exercise())
def test_session_restoration_preserves_other_tabs_and_original_expiry(tmp_path: Path) -> None:
async def exercise() -> None:
identities = SQLiteIdentityStore(tmp_path / "identity.sqlite3")
token = identities.provision(
credential_id="admin", principal_id="operator", roles=("admin",)
)
app = create_app(
identity_provider=identities,
node_reader=CanonicalNodeReader(GitCanonicalRepository(tmp_path / "knowledge.git")),
)
async with httpx2.AsyncClient(
transport=httpx2.ASGITransport(app=app), base_url="https://gewu.test"
) as http:
headers = {"X-GEWU-Session": "restore", "Origin": "https://gewu.test"}
assert (await http.get("/v1/session", headers=headers)).status_code == 401
login = await http.post("/v1/session", headers={"Authorization": f"Bearer {token}"})
original_csrf = login.json()["csrf_token"]
cookie = login.cookies.get("gewu_session")
assert cookie
assert (await http.get("/v1/session")).status_code == 403
assert (
await http.get(
"/v1/session", headers={**headers, "Origin": "https://attacker.invalid"}
)
).status_code == 403
assert (
await http.get("/v1/session", headers={**headers, "Sec-Fetch-Site": "cross-site"})
).status_code == 403
restored = await http.get("/v1/session", headers=headers)
assert restored.status_code == 200
assert restored.json()["expires_at"] == login.json()["expires_at"]
assert restored.json()["password_account"] is False
assert restored.headers["cache-control"] == "private, no-store"
assert "set-cookie" not in restored.headers
assert cookie not in restored.text and token not in restored.text
restored_csrf = restored.json()["csrf_token"]
assert identities.browser_csrf_valid(cookie, original_csrf)
assert identities.browser_csrf_valid(cookie, restored_csrf)
second = await http.get("/v1/session", headers=headers)
assert second.json()["csrf_token"] == restored_csrf
assert identities.browser_csrf_valid(cookie, original_csrf)
assert not identities.browser_csrf_valid(cookie, "gwc_invalid")
signed_out = await http.delete("/v1/session", headers={"X-GEWU-CSRF": restored_csrf})
assert signed_out.status_code == 200
http.cookies.set("gewu_session", cookie)
assert (await http.get("/v1/session", headers=headers)).status_code == 401
next_login = identities.create_browser_session(token)
http.cookies.clear()
http.cookies.set("gewu_session", next_login.session_token)
identities.revoke("admin")
assert (await http.get("/v1/session", headers=headers)).status_code == 401
asyncio.run(exercise())
def test_password_session_restore_and_pre_upgrade_session_migration(tmp_path: Path) -> None:
identities = SQLiteIdentityStore(tmp_path / "identity.sqlite3")
identities.create_account(
username="admin",
password="fixture-password",
role="admin",
project_ids=(),
actor="operator",
)
session = identities.login_account("admin", "fixture-password")
assert session
# Reconstruct the pre-upgrade schema with an issued session, then run the migration.
with identities._connect() as db:
db.execute("ALTER TABLE browser_sessions DROP COLUMN restored_csrf_digest")
reopened = SQLiteIdentityStore(identities.database)
restored = reopened.restore_browser_session(session.session_token)
assert restored and restored.expires_at == session.expires_at
assert reopened.browser_csrf_valid(session.session_token, session.csrf_token)
assert reopened.browser_csrf_valid(session.session_token, restored.csrf_token)
with reopened._connect() as db:
db.execute("UPDATE browser_sessions SET expires_at=0")
assert reopened.restore_browser_session(session.session_token) is None
+103
View File
@@ -0,0 +1,103 @@
import asyncio
from pathlib import Path
import httpx2
from gewu.adapters.git_repository import GitCanonicalRepository
from gewu.adapters.sqlite_identity import SQLiteIdentityStore
from gewu.application.reading import CanonicalNodeReader
from gewu.bootstrap.app import create_app
def test_admin_credentials_sessions_and_secret_free_audit(tmp_path: Path) -> None:
async def exercise() -> None:
store = SQLiteIdentityStore(tmp_path / "identity.sqlite3")
admin = store.provision(credential_id="admin", principal_id="operator", roles=("admin",))
viewer = store.provision(credential_id="viewer", principal_id="reader", roles=("viewer",))
mixed = store.provision(
credential_id="mixed", principal_id="agent", roles=("agent", "admin")
)
app = create_app(
identity_provider=store,
credential_administration=store,
node_reader=CanonicalNodeReader(GitCanonicalRepository(tmp_path / "knowledge.git")),
)
async with httpx2.AsyncClient(
transport=httpx2.ASGITransport(app=app), base_url="http://test"
) as http:
assert (await http.get("/v1/admin/credentials")).status_code == 401
for token in (viewer, mixed):
headers = {"Authorization": f"Bearer {token}"}
assert (await http.get("/v1/admin/credentials", headers=headers)).status_code == 403
assert (await http.get("/v1/admin/events", headers=headers)).status_code == 403
assert (
await http.post(
"/v1/admin/credentials",
headers=headers,
json={
"credential_id": "denied",
"principal_id": "denied",
"role": "admin",
},
)
).status_code == 403
assert (
await http.delete("/v1/admin/credentials/viewer", headers=headers)
).status_code == 403
session = await http.post("/v1/session", headers={"Authorization": f"Bearer {admin}"})
csrf = {"X-GEWU-CSRF": session.json()["csrf_token"]}
body = {
"credential_id": "new",
"principal_id": "researcher",
"role": "agent",
"project_ids": ["pilot"],
}
assert (await http.post("/v1/admin/credentials", json=body)).status_code == 403
assert (
await http.post(
"/v1/admin/credentials", json=body, headers={**csrf, "Origin": "https://other"}
)
).status_code == 403
assert (
await http.post("/v1/admin/credentials", headers=csrf, content=b" " * 4097)
).status_code == 413
assert (
await http.post(
"/v1/admin/credentials", headers=csrf, json={**body, "project_ids": []}
)
).status_code == 422
created = await http.post("/v1/admin/credentials", headers=csrf, json=body)
assert created.status_code == 201
token = created.json()["token"]
principal = store.authenticate(token)
assert principal and principal.project_ids == frozenset({"pilot"})
assert (
await http.post("/v1/admin/credentials", headers=csrf, json=body)
).status_code == 409
listing = await http.get("/v1/admin/credentials")
assert listing.headers["cache-control"] == "private, no-store"
assert token not in listing.text and "token_digest" not in listing.text
assert (
await http.delete("/v1/admin/credentials/admin", headers=csrf)
).status_code == 409
assert (await http.delete("/v1/admin/credentials/new")).status_code == 403
assert (await http.delete("/v1/admin/credentials/new", headers=csrf)).status_code == 200
assert store.authenticate(token) is None
assert (await http.delete("/v1/admin/credentials/new", headers=csrf)).status_code == 404
# Revocation invalidates already-issued browser sessions as well as bearer keys.
viewer_session = store.create_browser_session(viewer)
assert (
await http.delete("/v1/admin/credentials/viewer", headers=csrf)
).status_code == 200
assert store.authenticate_browser_session(viewer_session.session_token) is None
events = await http.get("/v1/admin/events")
assert [e["action"] for e in events.json()] == [
"credential.revoked",
"credential.revoked",
"credential.created",
]
assert token not in events.text and admin not in events.text
reopened = SQLiteIdentityStore(store.database)
assert len(reopened.list_admin_events(limit=50)) == 3
asyncio.run(exercise())
+82
View File
@@ -0,0 +1,82 @@
import asyncio
from pathlib import Path
import httpx
import pytest
from _pytest.monkeypatch import MonkeyPatch
from fastapi.routing import APIRoute
from gewu.bootstrap.app import create_app, create_local_app
def test_health_is_available_without_exposing_mutation_routes() -> None:
async def request_health() -> httpx.Response:
async with httpx.AsyncClient(
transport=httpx.ASGITransport(app=create_app()),
base_url="http://gewu.test",
) as client:
return await client.get("/health")
response = asyncio.run(request_health())
assert response.status_code == 200
assert response.json() == {"status": "ok", "service": "gewu"}
routes = {
f"{method} {route.path}"
for route in create_app().routes
if isinstance(route, APIRoute)
for method in route.methods or ()
}
assert routes == {"GET /health"}
def test_local_factory_composes_private_persistent_read_service(
monkeypatch: MonkeyPatch, tmp_path: Path
) -> None:
data_root = tmp_path / "private-gewu"
monkeypatch.setenv("GEWU_DATA_ROOT", str(data_root))
app = create_local_app()
route_paths = {route.path for route in app.routes if isinstance(route, APIRoute)}
assert route_paths == {
"/health",
"/v1/diagnostics",
"/v1/admin/projects",
"/v1/admin/credentials",
"/v1/admin/credentials/{credential_id}",
"/v1/admin/events",
"/v1/admin/users",
"/v1/admin/users/{username}",
"/v1/users/session",
"/v1/me",
"/v1/me/password",
"/v1/me/agent-credentials",
"/v1/me/agent-credentials/{credential_id}",
"/v1/admin/projects/{project_id}/archive",
"/v1/projects/{project_id}/nodes/{node_id}",
"/v1/projects/{project_id}/nodes/{node_id}/history",
"/v1/projects/{project_id}/sources/{revision_id}",
"/v1/projects/{project_id}/sources/{revision_id}/artifact",
"/v1/projects/{project_id}/sources/{revision_id}/extractions/{extraction_id}/blocks/{block_id}",
"/v1/neighbors",
"/v1/projects",
"/v1/search",
"/v1/sources",
"/v1/session",
"/auth/owner-session",
}
assert (data_root / "knowledge.git" / "HEAD").is_file()
assert (data_root / "identity.sqlite3").is_file()
assert (data_root / "search.sqlite3").is_file()
assert (data_root / "search.sqlite3").stat().st_mode & 0o077 == 0
assert (data_root.stat().st_mode & 0o777) == 0o700
def test_local_factory_rejects_symlinked_data_root(
monkeypatch: MonkeyPatch, tmp_path: Path
) -> None:
target = tmp_path / "real-root"
target.mkdir()
link = tmp_path / "linked-root"
link.symlink_to(target, target_is_directory=True)
monkeypatch.setenv("GEWU_DATA_ROOT", str(link))
with pytest.raises(ValueError, match="symbolic links"):
create_local_app()
+951
View File
@@ -0,0 +1,951 @@
import asyncio
import base64
import hashlib
import json
import socket
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from pathlib import Path
from typing import Any
import httpx2
from mcp import Client
from mcp.client.streamable_http import streamable_http_client
from mcp.types import TextContent
from gewu.adapters.git_repository import GitCanonicalRepository
from gewu.adapters.source_extractor import LocalSourceExtractor
from gewu.adapters.sqlite_identity import SQLiteIdentityStore
from gewu.adapters.url_fetch import PublicUrlFetcher
from gewu.application.acceptance import AcceptanceService
from gewu.application.context import ValidationContext
from gewu.application.reading import CanonicalNodeReader
from gewu.application.uploads import LocalArtifactStore
from gewu.bootstrap.app import create_app
from gewu.domain.contracts import ProposalOperation, ProposalRevision, Scope, SourceRevisionRecord
from gewu.domain.validation import evidence_digest, proposal_payload_digest
from gewu.transports.stdio_connector import SelectedFile, create_connector_server
from tests.domain.proposal_fixtures import (
BLOCK_TEXT,
NODE,
POLICIES,
PROJECTS,
SOURCE_REVISION,
proposal,
)
class _FixtureResponse:
def __init__(self, media_type: str, content: bytes, status: int = 200) -> None:
self.media_type = media_type
self.content = content
self.status = status
self.offset = 0
def getheader(self, name: str) -> str | None:
return {
"Content-Type": f"{self.media_type}; charset=utf-8",
"X-Source-Title": "Allowlisted fixture",
}.get(name)
def read(self, amount: int) -> bytes:
result = self.content[self.offset : self.offset + amount]
self.offset += len(result)
return result
class _FixtureConnection:
def __init__(self) -> None:
self.closed = False
self.path = ""
def request(self, _method: str, path: str, headers: dict[str, str]) -> None:
self.path = path
assert headers["Accept-Encoding"] == "identity"
return None
def getresponse(self) -> _FixtureResponse:
if self.path.startswith("/missing"):
return _FixtureResponse("text/plain", b"unavailable", status=404)
if self.path.startswith("/source.md?rev=2"):
return _FixtureResponse("text/markdown", b"# Captured fixture\nRevised evidence.\n")
if self.path.startswith("/page.html"):
return _FixtureResponse(
"text/html",
b"<article><h1>Web evidence</h1><p>Exact HTML statement.</p>"
b"<script>ignoreInjection()</script></article>",
)
if self.path.startswith("/paper.pdf"):
return _FixtureResponse("application/pdf", _pdf_fixture())
if self.path.startswith("/malformed.pdf"):
return _FixtureResponse("application/pdf", b"%PDF-1.4\nnot a valid PDF")
return _FixtureResponse(
"text/markdown", b"# Captured fixture\nAn exact source statement.\n"
)
def close(self) -> None:
self.closed = True
def _fixture_fetcher() -> PublicUrlFetcher:
def resolver(host: str, port: int, **_kwargs: Any) -> list[tuple[Any, ...]]:
return [(socket.AF_INET, socket.SOCK_STREAM, 6, "", ("93.184.216.34", port))]
return PublicUrlFetcher(
{"research.example"},
resolver=resolver,
connection_factory=lambda *_args: _FixtureConnection(),
)
def _text_payload(result: Any) -> str:
assert result.content and isinstance(result.content[0], TextContent)
return result.content[0].text
def _pdf_fixture() -> bytes:
stream = b"BT /F1 12 Tf 72 720 Td (Archived PDF evidence.) Tj ET"
objects = [
b"<< /Type /Catalog /Pages 2 0 R >>",
b"<< /Type /Pages /Kids [4 0 R] /Count 1 >>",
b"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>",
b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] "
b"/Resources << /Font << /F1 3 0 R >> >> /Contents 5 0 R >>",
b"<< /Length " + str(len(stream)).encode() + b" >>\nstream\n" + stream + b"\nendstream",
]
result = bytearray(b"%PDF-1.4\n")
offsets = [0]
for number, item in enumerate(objects, start=1):
offsets.append(len(result))
result.extend(f"{number} 0 obj\n".encode() + item + b"\nendobj\n")
xref = len(result)
result.extend(f"xref\n0 {len(offsets)}\n0000000000 65535 f \n".encode())
for offset in offsets[1:]:
result.extend(f"{offset:010d} 00000 n \n".encode())
result.extend(
f"trailer\n<< /Size {len(offsets)} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n".encode()
)
return bytes(result)
def test_mcp_source_to_citation_vertical_slice(tmp_path: Path) -> None:
async def exercise() -> None:
repository = GitCanonicalRepository(tmp_path / "knowledge.git")
artifacts = LocalArtifactStore(tmp_path)
extractor = LocalSourceExtractor()
context = ValidationContext(
projects=PROJECTS,
policies=POLICIES,
sources={},
extractions={},
existing_nodes={},
)
identities = SQLiteIdentityStore(
tmp_path / "identity.sqlite3",
authority_lock_path=tmp_path / "workflow/locks/canonical-writer.lock",
)
token = identities.provision(
credential_id="pilot-agent-key",
principal_id="agent-pilot",
roles=("agent",),
project_ids=("dome-performance", "shared"),
)
viewer_token = identities.provision(
credential_id="pilot-viewer-key",
principal_id="pilot-viewer",
roles=("viewer",),
project_ids=("dome-performance", "shared"),
)
acceptance = AcceptanceService(
repository=repository,
artifact_store=artifacts,
workflow_root=tmp_path / "workflow",
context_provider=lambda: context,
actor_is_current=lambda actor: identities.is_current(
actor.principal_id,
actor.credential_id,
roles=actor.roles,
project_ids=actor.project_ids,
),
)
app = create_app(
identity_provider=identities,
node_reader=CanonicalNodeReader(repository),
artifact_store=artifacts,
extractor=extractor,
acceptance_service=acceptance,
context_provider=lambda: context,
url_fetcher=_fixture_fetcher(),
)
async with app.router.lifespan_context(app):
async with httpx2.AsyncClient(
transport=httpx2.ASGITransport(app=app),
base_url="http://localhost",
headers={"Authorization": f"Bearer {token}"},
) as http:
async with Client(
streamable_http_client("http://localhost/mcp", http_client=http)
) as mcp:
tools = await mcp.list_tools()
assert {tool.name for tool in tools.tools} == {
"diagnostics",
"get_node",
"node_history",
"get_project_context",
"get_source",
"source_upload_begin",
"source_capture_text",
"source_capture_url",
"source_upload_status",
"source_upload_chunk",
"source_upload_finalize",
"source_extract",
"source_extraction_block",
"proposal_evaluate",
"proposal_submit",
"proposal_status",
"proposal_withdraw",
"run_create",
"run_checkpoint",
"run_status",
}
project_context = await mcp.call_tool(
"get_project_context", {"project_id": "dome-performance"}
)
assert not project_context.is_error
lens_result = json.loads(_text_payload(project_context))
assert (
lens_result["lens"]["north_star"] == PROJECTS["dome-performance"].north_star
)
assert lens_result["lens"]["key_questions"] == list(
PROJECTS["dome-performance"].key_questions
)
assert "autonomy_policy_id" not in lens_result["lens"]
denied_lens = await mcp.call_tool(
"get_project_context", {"project_id": "interactive-video"}
)
assert denied_lens.is_error
async with httpx2.AsyncClient(
transport=httpx2.ASGITransport(app=app),
base_url="http://localhost",
headers={"Authorization": f"Bearer {viewer_token}"},
) as viewer_http:
async with Client(
streamable_http_client("http://localhost/mcp", http_client=viewer_http)
) as viewer_mcp:
viewer_proposal = await viewer_mcp.call_tool(
"proposal_submit",
{"proposal_json": proposal().model_dump_json()},
)
assert viewer_proposal.is_error
viewer_upload = await viewer_mcp.call_tool(
"source_upload_begin",
{
"project_id": "dome-performance",
"filename": "viewer.md",
"media_type": "text/markdown",
"byte_length": 4,
"expected_sha256": hashlib.sha256(b"data").hexdigest(),
},
)
assert viewer_upload.is_error
viewer_paste = await viewer_mcp.call_tool(
"source_capture_text",
{
"capture_id": "viewer-capture-1",
"project_id": "dome-performance",
"source_key": "viewer-paste",
"text": "Viewer supplied text",
},
)
assert viewer_paste.is_error
diagnostics_result = await mcp.call_tool(
"diagnostics",
{"projects": ["dome-performance"], "include_shared": True},
)
assert not diagnostics_result.is_error
diagnostics = json.loads(_text_payload(diagnostics_result))
assert diagnostics["workflow_status"] == "not_verified"
assert diagnostics["selected_scope"]["include_shared"] is True
assert "proposal_submit" in diagnostics["authorized_operations"]
assert "run_checkpoint" in diagnostics["authorized_operations"]
assert token not in _text_payload(diagnostics_result)
run_request = {
"run_id": "mcp-run-pilot",
"objective": "Compare audience response",
"scope": {
"projects": ["dome-performance"],
"include_shared": False,
"all_projects": False,
},
"lens": "Audience behavior",
"pack_id": "gewu",
"pack_version": "0.3.0",
"pack_snapshot_digest": "a" * 64,
"module_pins": [
{
"module_id": "profile",
"implementation_version": "1.0.0",
"sha256": "b" * 64,
}
],
}
created_run = await mcp.call_tool(
"run_create", {"request_json": json.dumps(run_request)}
)
assert not created_run.is_error
run_record = json.loads(_text_payload(created_run))
assert run_record["revision"] == 0
checkpoint = {
"run_id": "mcp-run-pilot",
"expected_revision": 0,
"stage": "source-review",
}
saved_run = await mcp.call_tool(
"run_checkpoint", {"request_json": json.dumps(checkpoint)}
)
assert not saved_run.is_error
assert json.loads(_text_payload(saved_run))["revision"] == 1
read_run = await mcp.call_tool("run_status", {"run_id": "mcp-run-pilot"})
assert not read_run.is_error
assert json.loads(_text_payload(read_run))["revision"] == 1
denied = await mcp.call_tool(
"source_upload_begin",
{
"project_id": "interactive-video",
"filename": "private.md",
"media_type": "text/markdown",
"byte_length": 4,
"expected_sha256": "0" * 64,
},
)
assert denied.is_error
forbidden_capture = await mcp.call_tool(
"source_capture_url",
{
"project_id": "interactive-video",
"url": "https://research.example/source.md",
},
)
assert forbidden_capture.is_error
forbidden_paste = await mcp.call_tool(
"source_capture_text",
{
"capture_id": "out-of-scope-paste-1",
"project_id": "interactive-video",
"source_key": "out-of-scope-paste",
"text": "Must not be uploaded",
},
)
assert forbidden_paste.is_error
unallowlisted_capture = await mcp.call_tool(
"source_capture_url",
{
"project_id": "dome-performance",
"url": "https://internal.example/private",
},
)
assert unallowlisted_capture.is_error
captured = await mcp.call_tool(
"source_capture_url",
{
"project_id": "dome-performance",
"url": "https://research.example/source.md?token=do-not-echo",
},
)
assert not captured.is_error
capture_result = json.loads(_text_payload(captured))
assert capture_result["title"] == "Allowlisted fixture"
assert capture_result["media_type"] == "text/markdown"
assert "do-not-echo" not in _text_payload(captured)
assert capture_result["original_url"] == ("https://research.example/source.md")
captured_source = SourceRevisionRecord.model_validate_json(
json.dumps(capture_result["source_candidate"]), strict=True
)
assert captured_source.archive_status == "archived"
assert captured_source.final_url == "https://research.example/source.md"
assert captured_source.artifact_sha256 == capture_result["sha256"]
assert captured_source.capture_method == "url-fetch"
assert f":r:{captured_source.artifact_sha256}:" in captured_source.revision_id
pasted_text = "## Meeting note\nPasted evidence: 400 visitors.\n"
pasted_arguments = {
"capture_id": "interview-paste-1",
"project_id": "dome-performance",
"source_key": "research-interview-2026-10-14",
"text": pasted_text,
"title": "Research interview notes",
"original_url": "https://research.example/interview?token=secret",
"media_type": "text/markdown",
"source_type": "internal",
}
pasted = await mcp.call_tool("source_capture_text", pasted_arguments)
assert not pasted.is_error
pasted_retry = await mcp.call_tool("source_capture_text", pasted_arguments)
assert not pasted_retry.is_error
assert json.loads(_text_payload(pasted_retry)) == json.loads(
_text_payload(pasted)
)
changed_paste_retry = await mcp.call_tool(
"source_capture_text",
{**pasted_arguments, "text": pasted_text + "changed"},
)
assert changed_paste_retry.is_error
invalid_capture_id = await mcp.call_tool(
"source_capture_text",
{**pasted_arguments, "capture_id": "invalid capture id"},
)
assert invalid_capture_id.is_error
pasted_result = json.loads(_text_payload(pasted))
pasted_source = SourceRevisionRecord.model_validate_json(
json.dumps(pasted_result["source_candidate"]), strict=True
)
assert pasted_source.capture_method == "pasted-text"
assert pasted_source.original_url == "https://research.example/interview"
assert "secret" not in _text_payload(pasted)
capture_journal = tmp_path / "staging" / "capture-receipts"
assert all(
pasted_text.encode() not in path.read_bytes()
for path in capture_journal.glob("*.json")
)
assert (
pasted_source.artifact_sha256
== hashlib.sha256(pasted_text.encode()).hexdigest()
)
assert pasted_result["byte_length"] == len(pasted_text.encode())
assert pasted_result["extraction_id"]
pasted_extraction = artifacts.get_extraction(
pasted_result["extraction_id"],
owner_id="agent-pilot",
project_id="dome-performance",
)
assert pasted_extraction is not None
assert pasted_extraction.source_revision_id == pasted_source.revision_id
assert (
"Pasted evidence: 400 visitors."
in pasted_extraction.blocks[0].original_text
)
oversized_paste = await mcp.call_tool(
"source_capture_text",
{
"capture_id": "oversized-paste-1",
"project_id": "dome-performance",
"source_key": "oversized-paste",
"text": "x" * 1_000_001,
},
)
assert oversized_paste.is_error
control_character_paste = await mcp.call_tool(
"source_capture_text",
{
"capture_id": "invalid-paste-1",
"project_id": "dome-performance",
"source_key": "invalid-paste",
"text": "unsafe\x00content",
},
)
assert control_character_paste.is_error
oversized_author_paste = await mcp.call_tool(
"source_capture_text",
{
"capture_id": "invalid-author-paste-1",
"project_id": "dome-performance",
"source_key": "invalid-author-paste",
"text": "valid body",
"authors": ["a" * 301],
},
)
assert oversized_author_paste.is_error
changed_capture = await mcp.call_tool(
"source_capture_url",
{
"project_id": "dome-performance",
"url": "https://research.example/source.md?rev=2",
},
)
assert not changed_capture.is_error
changed_source = SourceRevisionRecord.model_validate_json(
json.dumps(json.loads(_text_payload(changed_capture))["source_candidate"]),
strict=True,
)
assert changed_source.source_id == captured_source.source_id
assert changed_source.revision_id != captured_source.revision_id
with artifacts.open_artifact(
capture_result["sha256"], project_id="dome-performance"
) as artifact:
assert artifact.read() == (
b"# Captured fixture\nAn exact source statement.\n"
)
failed_capture = await mcp.call_tool(
"source_capture_url",
{
"project_id": "dome-performance",
"url": "https://research.example/missing?access-token=must-not-leak",
"title": "Temporarily unavailable paper",
"source_type": "paper",
},
)
assert not failed_capture.is_error
failed_result = json.loads(_text_payload(failed_capture))
failed_source = SourceRevisionRecord.model_validate_json(
json.dumps(failed_result["source_candidate"]), strict=True
)
assert failed_result["status"] == "capture_failed"
assert failed_source.archive_status == "capture-failed"
assert failed_source.artifact_sha256 is None
assert "access-token" not in _text_payload(failed_capture)
failed_proposal = ProposalRevision(
proposal_id="proposal-failed-url-capture",
revision=1,
initiating_project_id="dome-performance",
scope=Scope(projects=("dome-performance",), include_shared=True),
base_commit=repository.head(),
payload_digest="0" * 64,
proposer_id="agent-pilot",
run_id="run-failed-url-capture",
operations=(
ProposalOperation(operation="register_source", source=failed_source),
),
)
failed_proposal = failed_proposal.model_copy(
update={"payload_digest": proposal_payload_digest(failed_proposal)}
)
registered_failure = await mcp.call_tool(
"proposal_submit",
{"proposal_json": failed_proposal.model_dump_json()},
)
assert not registered_failure.is_error
assert (
json.loads(_text_payload(registered_failure))["receipt"]["state"]
== "accepted"
)
captured_html = await mcp.call_tool(
"source_capture_url",
{
"project_id": "dome-performance",
"url": "https://research.example/page.html",
},
)
assert not captured_html.is_error
html_result = json.loads(_text_payload(captured_html))
assert html_result["media_type"] == "text/html"
html_extraction_result = await mcp.call_tool(
"source_extract",
{
"project_id": "dome-performance",
"artifact_sha256": html_result["sha256"],
"source_revision_id": "shared:web-page:r1",
},
)
assert not html_extraction_result.is_error
html_extraction = json.loads(_text_payload(html_extraction_result))
html_block_result = await mcp.call_tool(
"source_extraction_block",
{
"project_id": "dome-performance",
"extraction_id": html_extraction["extraction_id"],
"block_id": html_extraction["blocks"][0]["block_id"],
"offset": 0,
"length": 500,
},
)
assert not html_block_result.is_error
html_block = _text_payload(html_block_result)
assert "Exact HTML statement." in html_block
assert "ignoreInjection" not in html_block
captured_pdf = await mcp.call_tool(
"source_capture_url",
{
"project_id": "dome-performance",
"url": "https://research.example/paper.pdf",
},
)
assert not captured_pdf.is_error
pdf_result = json.loads(_text_payload(captured_pdf))
assert pdf_result["media_type"] == "application/pdf"
with artifacts.open_artifact(
pdf_result["sha256"], project_id="dome-performance"
) as artifact:
assert artifact.read() == _pdf_fixture()
pdf_extracted = await mcp.call_tool(
"source_extract",
{
"project_id": "dome-performance",
"artifact_sha256": pdf_result["sha256"],
"source_revision_id": "shared:pdf:r1",
},
)
assert not pdf_extracted.is_error
pdf_record = json.loads(_text_payload(pdf_extracted))
assert pdf_record["blocks"][0]["page"] == 1
pdf_block_result = await mcp.call_tool(
"source_extraction_block",
{
"project_id": "dome-performance",
"extraction_id": pdf_record["extraction_id"],
"block_id": pdf_record["blocks"][0]["block_id"],
"offset": 0,
"length": 500,
},
)
assert not pdf_block_result.is_error
pdf_block = json.loads(_text_payload(pdf_block_result))
assert pdf_block["page"] == 1
assert "Archived PDF evidence." in pdf_block["normalized_text"]
malformed_pdf = await mcp.call_tool(
"source_capture_url",
{
"project_id": "dome-performance",
"url": "https://research.example/malformed.pdf",
},
)
assert not malformed_pdf.is_error
malformed_result = json.loads(_text_payload(malformed_pdf))
malformed_extraction = await mcp.call_tool(
"source_extract",
{
"project_id": "dome-performance",
"artifact_sha256": malformed_result["sha256"],
"source_revision_id": malformed_result["source_candidate"][
"revision_id"
],
},
)
assert not malformed_extraction.is_error
unavailable = json.loads(_text_payload(malformed_extraction))
assert unavailable["status"] == "unavailable"
assert unavailable["error_code"] == "PDF_UNSUPPORTED"
content = BLOCK_TEXT.encode("utf-8")
digest = hashlib.sha256(content).hexdigest()
interrupted_begin = await mcp.call_tool(
"source_upload_begin",
{
"project_id": "dome-performance",
"filename": "interrupted.md",
"media_type": "text/markdown",
"byte_length": len(content),
"expected_sha256": digest,
},
)
assert not interrupted_begin.is_error
interrupted_upload = json.loads(_text_payload(interrupted_begin))
interrupted_finalize = await mcp.call_tool(
"source_upload_finalize",
{"upload_id": interrupted_upload["upload_id"]},
)
assert interrupted_finalize.is_error
uncitable = await mcp.call_tool(
"source_extract",
{
"project_id": "dome-performance",
"artifact_sha256": digest,
"source_revision_id": SOURCE_REVISION.revision_id,
},
)
assert uncitable.is_error
begun = await mcp.call_tool(
"source_upload_begin",
{
"project_id": "dome-performance",
"filename": "pilot.md",
"media_type": "text/markdown",
"byte_length": len(content),
"expected_sha256": digest,
},
)
assert not begun.is_error
upload = json.loads(_text_payload(begun))
wrong_digest_chunk = await mcp.call_tool(
"source_upload_chunk",
{
"upload_id": upload["upload_id"],
"offset": 0,
"content_base64": base64.b64encode(content).decode(),
"chunk_sha256": "0" * 64,
},
)
assert wrong_digest_chunk.is_error
chunked = await mcp.call_tool(
"source_upload_chunk",
{
"upload_id": upload["upload_id"],
"offset": 0,
"content_base64": base64.b64encode(content).decode(),
"chunk_sha256": digest,
},
)
assert not chunked.is_error
finalized = await mcp.call_tool(
"source_upload_finalize", {"upload_id": upload["upload_id"]}
)
assert not finalized.is_error
artifact = json.loads(_text_payload(finalized))
extracted = await mcp.call_tool(
"source_extract",
{
"project_id": "dome-performance",
"artifact_sha256": artifact["sha256"],
"source_revision_id": SOURCE_REVISION.revision_id,
},
)
assert not extracted.is_error
extraction_info = json.loads(_text_payload(extracted))
record = extractor.extract(
SOURCE_REVISION.revision_id, content, "text/markdown"
)
assert extraction_info["extraction_id"] == record.extraction_id
evidence = (
NODE.claims[0]
.evidence[0]
.model_copy(
update={
"extraction_id": record.extraction_id,
"locator": NODE.claims[0]
.evidence[0]
.locator.model_copy(update={"block_id": record.blocks[0].block_id}),
}
)
)
claim = NODE.claims[0].model_copy(
update={
"evidence": (evidence,),
"assessment": NODE.claims[0].assessment.model_copy(
update={"evidence_digests": (evidence_digest(evidence),)}
),
}
)
node = NODE.model_copy(update={"claims": (claim,)})
source = SOURCE_REVISION.model_copy(
update={"artifact_sha256": artifact["sha256"], "byte_length": len(content)}
)
candidate = proposal(node=node, source=source, base_commit=repository.head())
candidate = candidate.model_copy(update={"payload_digest": "0" * 64})
candidate = candidate.model_copy(
update={"payload_digest": proposal_payload_digest(candidate)}
)
wrong_location_evidence = evidence.model_copy(
update={
"quote": "400 visitors",
"locator": evidence.locator.model_copy(update={"start": 0, "end": 13}),
}
)
wrong_location_claim = claim.model_copy(
update={
"evidence": (wrong_location_evidence,),
"assessment": claim.assessment.model_copy(
update={
"evidence_digests": (evidence_digest(wrong_location_evidence),)
}
),
}
)
wrong_location_node = node.model_copy(
update={"claims": (wrong_location_claim,)}
)
wrong_location = proposal(
node=wrong_location_node,
source=source,
base_commit=repository.head(),
)
wrong_location = wrong_location.model_copy(
update={
"proposal_id": "proposal-wrong-location",
"payload_digest": "0" * 64,
}
)
wrong_location = wrong_location.model_copy(
update={"payload_digest": proposal_payload_digest(wrong_location)}
)
head_before_evaluate = repository.head()
invalid_evaluation = await mcp.call_tool(
"proposal_evaluate",
{"proposal_json": wrong_location.model_dump_json()},
)
assert not invalid_evaluation.is_error, invalid_evaluation.content
invalid_evaluation_payload = json.loads(_text_payload(invalid_evaluation))
assert invalid_evaluation_payload["status"] == "invalid"
assert "EVIDENCE_MISMATCH" in {
finding["code"] for finding in invalid_evaluation_payload["findings"]
}
assert repository.head() == head_before_evaluate
rejected = await mcp.call_tool(
"proposal_submit",
{"proposal_json": wrong_location.model_dump_json()},
)
assert not rejected.is_error, rejected.content
rejected_outcome = json.loads(_text_payload(rejected))
assert rejected_outcome["status"] == "invalid"
assert "EVIDENCE_MISMATCH" in {
finding["code"] for finding in rejected_outcome["findings"]
}
rejected_status = await mcp.call_tool(
"proposal_status",
{"proposal_id": "proposal-wrong-location", "revision": 1},
)
assert not rejected_status.is_error, rejected_status.content
rejected_status_payload = json.loads(_text_payload(rejected_status))
assert rejected_status_payload["lifecycle"] == "evaluated"
assert rejected_status_payload["validation_status"] == "invalid"
assert rejected_status_payload["receipt"] is None
assert "quote" not in _text_payload(rejected_status)
withdrawn = await mcp.call_tool(
"proposal_withdraw",
{"proposal_id": "proposal-wrong-location", "revision": 1},
)
assert not withdrawn.is_error, withdrawn.content
assert json.loads(_text_payload(withdrawn))["lifecycle"] == "withdrawn"
resubmitted = await mcp.call_tool(
"proposal_submit",
{"proposal_json": wrong_location.model_dump_json()},
)
assert resubmitted.is_error
candidate_head = repository.head()
valid_evaluation = await mcp.call_tool(
"proposal_evaluate",
{"proposal_json": candidate.model_dump_json()},
)
assert not valid_evaluation.is_error, valid_evaluation.content
valid_evaluation_payload = json.loads(_text_payload(valid_evaluation))
assert valid_evaluation_payload["status"] == "eligible"
assert valid_evaluation_payload["eligible_for_auto_accept"]
assert repository.head() == candidate_head
spoofed_assessment = claim.assessment.model_copy(
update={"assessor_id": "agent-other"}
)
spoofed_claim = claim.model_copy(update={"assessment": spoofed_assessment})
spoofed_node = node.model_copy(update={"claims": (spoofed_claim,)})
spoofed = proposal(
node=spoofed_node,
source=source,
base_commit=repository.head(),
)
spoofed = spoofed.model_copy(update={"payload_digest": "0" * 64})
spoofed = spoofed.model_copy(
update={"payload_digest": proposal_payload_digest(spoofed)}
)
spoofed_result = await mcp.call_tool(
"proposal_evaluate",
{"proposal_json": spoofed.model_dump_json()},
)
assert not spoofed_result.is_error, spoofed_result.content
assert "ASSESSOR_ATTRIBUTION_MISMATCH" in {
finding["code"]
for finding in json.loads(_text_payload(spoofed_result))["findings"]
}
unstored = await mcp.call_tool(
"proposal_status",
{"proposal_id": candidate.proposal_id, "revision": candidate.revision},
)
assert unstored.is_error
submitted = await mcp.call_tool(
"proposal_submit",
{"proposal_json": candidate.model_dump_json()},
)
assert not submitted.is_error, submitted.content
outcome = json.loads(_text_payload(submitted))
assert outcome["status"] == "accepted"
assert outcome["validation_status"] == "eligible"
assert outcome["receipt"] is not None
accepted_head = repository.head()
retried = await mcp.call_tool(
"proposal_submit",
{"proposal_json": candidate.model_dump_json()},
)
assert not retried.is_error, retried.content
retry_outcome = json.loads(_text_payload(retried))
assert retry_outcome["receipt"] == outcome["receipt"]
assert repository.head() == accepted_head
accepted_status = await mcp.call_tool(
"proposal_status",
{"proposal_id": candidate.proposal_id, "revision": candidate.revision},
)
assert not accepted_status.is_error, accepted_status.content
accepted_status_payload = json.loads(_text_payload(accepted_status))
assert accepted_status_payload["lifecycle"] == "accepted"
assert accepted_status_payload["receipt"] == outcome["receipt"]
other_token = identities.provision(
credential_id="other-agent-key",
principal_id="agent-other",
roles=("agent",),
project_ids=("dome-performance",),
)
async with httpx2.AsyncClient(
transport=httpx2.ASGITransport(app=app),
base_url="http://localhost",
headers={"Authorization": f"Bearer {other_token}"},
) as other_http:
async with Client(
streamable_http_client("http://localhost/mcp", http_client=other_http)
) as other_mcp:
private_status = await other_mcp.call_tool(
"proposal_status",
{
"proposal_id": candidate.proposal_id,
"revision": candidate.revision,
},
)
assert private_status.is_error
impersonated_evaluation = await other_mcp.call_tool(
"proposal_evaluate",
{"proposal_json": candidate.model_dump_json()},
)
assert impersonated_evaluation.is_error
unauthorized_withdrawal = await other_mcp.call_tool(
"proposal_withdraw",
{
"proposal_id": candidate.proposal_id,
"revision": candidate.revision,
},
)
assert unauthorized_withdrawal.is_error
read = await mcp.call_tool(
"get_node",
{"project_id": node.project_id, "node_id": node.node_id},
)
assert not read.is_error
envelope = json.loads(_text_payload(read))
assert envelope["canonical_revision"] == outcome["receipt"]["commit_id"]
assert envelope["citations"][0]["quote"] == BLOCK_TEXT
selected_path = tmp_path / "operator-selected.md"
selected_bytes = b"A separately selected source for the stdio bridge.\n"
selected_path.write_bytes(selected_bytes)
@asynccontextmanager
async def remote_client() -> AsyncIterator[Client]:
async with Client(
streamable_http_client("http://localhost/mcp", http_client=http)
) as remote:
yield remote
connector = create_connector_server(
(SelectedFile("source-1", selected_path),), remote_client
)
async with Client(connector) as local:
connector_tools = await local.list_tools()
assert {tool.name for tool in connector_tools.tools} == {
"list_selected_files",
"upload_selected_file",
}
connector_result = await local.call_tool(
"upload_selected_file",
{
"file_id": "source-1",
"project_id": "dome-performance",
"source_revision_id": "dome:connector-probe:r1",
},
)
assert not connector_result.is_error
connector_text = _text_payload(connector_result)
connector_receipt = json.loads(connector_text)
assert (
connector_receipt["sha256"]
== hashlib.sha256(selected_bytes).hexdigest()
)
assert selected_bytes.decode() not in connector_text
asyncio.run(exercise())
+139
View File
@@ -0,0 +1,139 @@
import asyncio
import hashlib
import json
from pathlib import Path
import httpx2
import yaml
from mcp import Client
from mcp.client.streamable_http import streamable_http_client
from mcp.types import TextContent
from gewu.adapters.git_repository import GitCanonicalRepository
from gewu.adapters.sqlite_identity import SQLiteIdentityStore
from gewu.application.reading import CanonicalNodeReader
from gewu.bootstrap.app import create_app
from tests.domain.proposal_fixtures import NODE, SOURCE_REVISION
def test_mcp_exposes_only_authenticated_canonical_read(tmp_path: Path) -> None:
async def exercise() -> None:
repository = GitCanonicalRepository(tmp_path / "knowledge.git")
project_key = hashlib.sha256(NODE.project_id.encode()).hexdigest()
node_key = hashlib.sha256(NODE.node_id.encode()).hexdigest()
source_key = hashlib.sha256(SOURCE_REVISION.source_id.encode()).hexdigest()
source_revision_key = hashlib.sha256(SOURCE_REVISION.revision_id.encode()).hexdigest()
repository.publish_files(
expected_head=repository.head(),
files={
f"projects/{project_key}/nodes/{node_key}.yaml": yaml.safe_dump(
NODE.model_dump(mode="json")
).encode(),
f"sources/{source_key}/{source_revision_key}.yaml": yaml.safe_dump(
SOURCE_REVISION.model_dump(mode="json")
).encode(),
},
message="Publish test node",
index_directory=tmp_path / "publishing",
)
identities = SQLiteIdentityStore(tmp_path / "identity.sqlite3")
token = identities.provision(
credential_id="reader-key",
principal_id="research-agent",
roles=("agent",),
project_ids=(NODE.project_id,),
)
app = create_app(
identity_provider=identities,
node_reader=CanonicalNodeReader(repository),
)
async with app.router.lifespan_context(app):
async with httpx2.AsyncClient(
transport=httpx2.ASGITransport(app=app),
base_url="http://localhost",
headers={"Authorization": f"Bearer {token}"},
) as http:
async with Client(
streamable_http_client("http://localhost/mcp", http_client=http)
) as mcp:
tools = await mcp.list_tools()
assert {tool.name for tool in tools.tools} == {
"diagnostics",
"get_node",
"node_history",
"get_source",
}
diagnostic_result = await mcp.call_tool(
"diagnostics", {"projects": [NODE.project_id]}
)
assert not diagnostic_result.is_error
diagnostic_text = diagnostic_result.content[0]
assert isinstance(diagnostic_text, TextContent)
diagnostics = json.loads(diagnostic_text.text)
assert diagnostics["transport"] == "mcp"
assert diagnostics["selected_scope"]["projects"] == [NODE.project_id]
assert diagnostics["index_state"] == "not_configured"
assert "get_node" in diagnostics["authorized_operations"]
assert "proposal_submit" not in diagnostics["authorized_operations"]
assert token not in diagnostic_text.text
denied_diagnostics = await mcp.call_tool(
"diagnostics", {"projects": ["interactive-video"]}
)
assert denied_diagnostics.is_error
result = await mcp.call_tool(
"get_node",
{"project_id": NODE.project_id, "node_id": NODE.node_id},
)
assert not result.is_error
content = result.content[0]
assert content.type == "text"
envelope = json.loads(content.text)
assert envelope["canonical_revision"] == repository.head()
assert envelope["data"][0]["node_id"] == NODE.node_id
assert envelope["citations"][0]["quote"] == NODE.claims[0].evidence[0].quote
history = await mcp.call_tool(
"node_history",
{"project_id": NODE.project_id, "node_id": NODE.node_id},
)
assert not history.is_error
assert isinstance(history.content[0], TextContent)
assert len(json.loads(history.content[0].text)["data"]) == 1
source = await mcp.call_tool(
"get_source",
{
"project_id": NODE.project_id,
"revision_id": SOURCE_REVISION.revision_id,
},
)
assert not source.is_error
assert isinstance(source.content[0], TextContent)
assert json.loads(source.content[0].text)["data"][0]["revision_id"] == (
SOURCE_REVISION.revision_id
)
assert identities.revoke("reader-key")
revoked = await mcp.call_tool(
"get_node",
{"project_id": NODE.project_id, "node_id": NODE.node_id},
)
assert revoked.is_error
revoked_message = revoked.content[0]
assert isinstance(revoked_message, TextContent)
assert NODE.node_id not in revoked_message.text
async with httpx2.AsyncClient(
transport=httpx2.ASGITransport(app=app),
base_url="http://localhost",
) as http:
async with Client(
streamable_http_client("http://localhost/mcp", http_client=http)
) as mcp:
unauthenticated = await mcp.call_tool(
"get_node",
{"project_id": NODE.project_id, "node_id": NODE.node_id},
)
assert unauthenticated.is_error
error_message = unauthenticated.content[0]
assert isinstance(error_message, TextContent)
assert NODE.node_id not in error_message.text
asyncio.run(exercise())
+30
View File
@@ -0,0 +1,30 @@
from pathlib import Path
from typing import Any
from gewu.bootstrap.app import create_local_app
from gewu.domain.operations import OPERATIONS, Transport, operations_for
def test_http_openapi_routes_match_the_explicit_operation_allowlist(
tmp_path: Path, monkeypatch: Any
) -> None:
monkeypatch.setenv("GEWU_DATA_ROOT", str(tmp_path / "gewu-data"))
app = create_local_app()
schema = app.openapi()
exposed = {
operation["operationId"]: operation
for methods in schema["paths"].values()
for method, operation in methods.items()
if method in {"get", "post", "put", "delete", "patch"}
}
http_allowlist = {item.name for item in operations_for(Transport.HTTP)}
operation_index = {item.name: item for item in OPERATIONS}
assert set(exposed) == http_allowlist
assert "owner_decide" not in exposed
assert "proposal_submit" not in exposed
assert all(
not operation_index[name].mutates_knowledge
for name, route in exposed.items()
if "knowledge" in route.get("tags", [])
)
+227
View File
@@ -0,0 +1,227 @@
import asyncio
import hashlib
import json
import time
import httpx2
import pyotp
from cryptography.fernet import Fernet
from mcp import Client
from mcp.client.streamable_http import streamable_http_client
from gewu.adapters.git_repository import GitCanonicalRepository
from gewu.adapters.sqlite_identity import SQLiteIdentityStore
from gewu.application.reading import CanonicalNodeReader
from gewu.bootstrap.app import create_app
def test_owner_login_returns_a_short_lived_non_agent_token(tmp_path) -> None:
async def exercise() -> None:
repository = GitCanonicalRepository(tmp_path / "knowledge.git")
identities = SQLiteIdentityStore(
tmp_path / "identity.sqlite3", owner_encryption_key=Fernet.generate_key()
)
secret = pyotp.random_base32()
identities.provision_owner(
principal_id="owner-one",
password="correct horse battery staple",
totp_secret=secret,
project_ids=("dome-performance",),
)
now = int(time.time())
identities.issue_owner_challenge(
challenge_id="challenge-one",
owner_principal_id="owner-one",
proposal_id="proposal-one",
proposal_revision=1,
payload_digest="a" * 64,
diff_digest="b" * 64,
domains=("dome-performance",),
expires_at=now + 300,
now=now,
)
agent_token = identities.provision(
credential_id="agent-key",
principal_id="agent-one",
roles=("agent",),
project_ids=("dome-performance",),
)
app = create_app(
identity_provider=identities,
owner_identity_provider=identities,
owner_service_audience="gewu-local-test",
node_reader=CanonicalNodeReader(repository),
)
async with httpx2.AsyncClient(
transport=httpx2.ASGITransport(app=app), base_url="http://127.0.0.1"
) as client:
response = await client.post(
"/auth/owner-session",
json={
"principal_id": "owner-one",
"password": "correct horse battery staple",
"totp_code": pyotp.TOTP(secret).now(),
"challenge_id": "challenge-one",
},
)
assert response.status_code == 200, response.text
assert response.headers["cache-control"] == "no-store"
token = response.json()["session_token"]
assert token.startswith("gwo_")
assert identities.authenticate(token) is None
assert identities.authenticate(agent_token) is not None
assert (
identities.authenticate_owner_session(
token,
challenge_id="challenge-one",
audience="gewu-local-test",
)
is not None
)
wrong_origin = await client.post(
"/auth/owner-session",
headers={"Origin": "https://attacker.invalid"},
json={
"principal_id": "owner-one",
"password": "correct horse battery staple",
"totp_code": pyotp.TOTP(secret).now(),
"challenge_id": "challenge-two",
},
)
assert wrong_origin.status_code == 403
oversized = await client.post(
"/auth/owner-session",
content='{"padding":"' + ("x" * 5000) + '"}',
headers={"Content-Type": "application/json"},
)
assert oversized.status_code == 413
async with httpx2.AsyncClient(
transport=httpx2.ASGITransport(app=app), base_url="http://gewu.example"
) as remote_http:
insecure = await remote_http.post(
"/auth/owner-session",
json={
"principal_id": "owner-one",
"password": "correct horse battery staple",
"totp_code": "000000",
"challenge_id": "challenge-three",
},
)
assert insecure.status_code == 403
asyncio.run(exercise())
def test_owner_login_is_fail_closed_without_encryption_key_or_audience(tmp_path) -> None:
async def exercise() -> None:
repository = GitCanonicalRepository(tmp_path / "knowledge.git")
identities = SQLiteIdentityStore(tmp_path / "identity.sqlite3")
app = create_app(
identity_provider=identities,
owner_identity_provider=identities,
node_reader=CanonicalNodeReader(repository),
)
async with httpx2.AsyncClient(
transport=httpx2.ASGITransport(app=app), base_url="http://127.0.0.1"
) as client:
response = await client.post(
"/auth/owner-session",
json={
"principal_id": "owner-one",
"password": "correct horse battery staple",
"totp_code": "000000",
"challenge_id": "challenge-one",
},
)
assert response.status_code == 503
asyncio.run(exercise())
def test_dedicated_owner_mcp_reads_diff_and_records_decision(tmp_path) -> None:
async def exercise() -> None:
repository = GitCanonicalRepository(tmp_path / "knowledge.git")
identities = SQLiteIdentityStore(
tmp_path / "identity.sqlite3", owner_encryption_key=Fernet.generate_key()
)
secret = pyotp.random_base32()
identities.provision_owner(
principal_id="owner-one",
password="correct horse battery staple",
totp_secret=secret,
project_ids=("dome-performance",),
)
diff_json = '{"files":[{"path":"projects/dome/nodes/a.yaml"}]}'
diff_digest = hashlib.sha256(diff_json.encode()).hexdigest()
identities.issue_owner_challenge(
challenge_id="challenge-review",
owner_principal_id="owner-one",
proposal_id="proposal-one",
proposal_revision=1,
payload_digest="a" * 64,
diff_digest=diff_digest,
domains=("dome-performance",),
expires_at=int(time.time()) + 300,
diff_json=diff_json,
)
app = create_app(
identity_provider=identities,
owner_identity_provider=identities,
owner_service_audience="gewu-owner-test",
node_reader=CanonicalNodeReader(repository),
)
async with app.router.lifespan_context(app):
async with httpx2.AsyncClient(
transport=httpx2.ASGITransport(app=app), base_url="http://127.0.0.1"
) as login_http:
login = await login_http.post(
"/auth/owner-session",
json={
"principal_id": "owner-one",
"password": "correct horse battery staple",
"totp_code": pyotp.TOTP(secret).now(),
"challenge_id": "challenge-review",
},
)
assert login.status_code == 200, login.text
token = login.json()["session_token"]
async with httpx2.AsyncClient(
transport=httpx2.ASGITransport(app=app),
base_url="http://localhost",
headers={"Authorization": f"Bearer {token}"},
) as owner_http:
async with Client(
streamable_http_client("http://localhost/owner-mcp", http_client=owner_http)
) as owner:
names = {tool.name for tool in (await owner.list_tools()).tools}
assert names == {
"owner_challenge_get",
"owner_diff_get",
"owner_decide",
}
challenge = await owner.call_tool(
"owner_challenge_get", {"challenge_id": "challenge-review"}
)
assert not challenge.is_error
diff = await owner.call_tool(
"owner_diff_get", {"challenge_id": "challenge-review"}
)
assert not diff.is_error
decision = await owner.call_tool(
"owner_decide",
{
"challenge_id": "challenge-review",
"expected_proposal_revision": 1,
"expected_payload_digest": "a" * 64,
"expected_diff_digest": diff_digest,
"decision": "authorize",
"rationale": "Reviewed this exact change and approve it.",
},
)
assert not decision.is_error
assert json.loads(decision.content[0].text)["status"] == "authorized"
asyncio.run(exercise())
+153
View File
@@ -0,0 +1,153 @@
import asyncio
from pathlib import Path
import httpx2
from gewu.adapters.git_repository import GitCanonicalRepository
from gewu.adapters.sqlite_identity import SQLiteIdentityStore
from gewu.application.reading import CanonicalNodeReader
from gewu.application.registry import LocalProjectRegistry
from gewu.bootstrap.app import create_app
from gewu.domain.contracts import AutoAcceptPolicy, ProjectRecord, ProjectStatus
def test_project_registry_lifecycle_is_admin_only_and_durable(tmp_path: Path) -> None:
async def exercise() -> None:
identities = SQLiteIdentityStore(tmp_path / "identity.sqlite3")
admin_token = identities.provision(
credential_id="project-admin",
principal_id="owner-operator",
roles=("admin",),
)
viewer_token = identities.provision(
credential_id="project-viewer",
principal_id="research-reader",
roles=("viewer",),
)
mixed_agent_admin_token = identities.provision(
credential_id="mixed-agent-admin",
principal_id="agent-with-admin-role",
roles=("admin", "agent"),
)
repository = GitCanonicalRepository(tmp_path / "knowledge.git")
registry = LocalProjectRegistry(
tmp_path / "control" / "registry.json",
repository=repository,
index_directory=tmp_path / "publishing",
authority_lock_path=tmp_path / "workflow" / "locks" / "canonical-writer.lock",
)
app = create_app(
identity_provider=identities,
node_reader=CanonicalNodeReader(repository),
project_registry=registry,
)
project = ProjectRecord(
project_id="new-research",
title="New research",
status=ProjectStatus.ACTIVE,
description="An administrator-owned research lens.",
owners=("owner-operator",),
north_star="Investigate a new research area.",
key_questions=("What is known?",),
autonomy_policy_id="new-policy",
)
policy = AutoAcceptPolicy(
policy_id="new-policy",
policy_version=1,
project_id="new-research",
require_supported_assessment=True,
)
body = {
"project": project.model_dump(mode="json"),
"policy": policy.model_dump(mode="json"),
}
async with httpx2.AsyncClient(
transport=httpx2.ASGITransport(app=app), base_url="http://test"
) as http:
assert (await http.get("/v1/admin/projects")).status_code == 401
denied = await http.post(
"/v1/admin/projects",
json=body,
headers={"Authorization": f"Bearer {viewer_token}"},
)
assert denied.status_code == 403
mixed_agent_denied = await http.post(
"/v1/admin/projects",
json=body,
headers={"Authorization": f"Bearer {mixed_agent_admin_token}"},
)
assert mixed_agent_denied.status_code == 403
oversized = await http.post(
"/v1/admin/projects",
content=b" " * (64 * 1024 + 1),
headers={
"Authorization": f"Bearer {admin_token}",
"Content-Type": "application/json",
},
)
assert oversized.status_code == 413
headers = {"Authorization": f"Bearer {admin_token}"}
diagnostics = await http.get("/v1/diagnostics", headers=headers)
assert diagnostics.status_code == 200
assert {
"admin_list_projects",
"project_create",
"project_archive",
}.issubset(diagnostics.json()["authorized_operations"])
created = await http.post("/v1/admin/projects", json=body, headers=headers)
assert created.status_code == 201, created.text
assert created.json()["project_id"] == "new-research"
listing = await http.get("/v1/admin/projects", headers=headers)
assert [item["project_id"] for item in listing.json()] == ["new-research"]
duplicate = await http.post("/v1/admin/projects", json=body, headers=headers)
assert duplicate.status_code == 409
archived = await http.post("/v1/admin/projects/new-research/archive", headers=headers)
assert archived.status_code == 200
assert archived.json()["status"] == "archived"
assert registry.records()[0].status == ProjectStatus.ARCHIVED
session = identities.create_browser_session(admin_token)
browser_project = project.model_copy(
update={
"project_id": "browser-created",
"title": "Browser created",
"autonomy_policy_id": "browser-policy",
}
)
browser_policy = policy.model_copy(
update={"policy_id": "browser-policy", "project_id": "browser-created"}
)
browser_body = {
"project": browser_project.model_dump(mode="json"),
"policy": browser_policy.model_dump(mode="json"),
}
cookie = {"Cookie": f"gewu_session={session.session_token}"}
without_csrf = await http.post(
"/v1/admin/projects",
json=browser_body,
headers={**cookie, "Origin": "http://test"},
)
assert without_csrf.status_code == 403
wrong_origin = await http.post(
"/v1/admin/projects",
json=browser_body,
headers={
**cookie,
"Origin": "https://attacker.example",
"X-GEWU-CSRF": session.csrf_token,
},
)
assert wrong_origin.status_code == 403
browser_created = await http.post(
"/v1/admin/projects",
json=browser_body,
headers={
**cookie,
"Origin": "http://test",
"X-GEWU-CSRF": session.csrf_token,
},
)
assert browser_created.status_code == 201, browser_created.text
asyncio.run(exercise())
+439
View File
@@ -0,0 +1,439 @@
import asyncio
import hashlib
import json
from pathlib import Path
import httpx2
import yaml
from mcp import Client
from mcp.client.streamable_http import streamable_http_client
from mcp.types import TextContent
from gewu.adapters.git_repository import GitCanonicalRepository
from gewu.adapters.sqlite_identity import SQLiteIdentityStore
from gewu.adapters.sqlite_search import SQLiteSearchIndex
from gewu.application.reading import CanonicalNodeReader
from gewu.application.search import CanonicalSearchService
from gewu.bootstrap.app import create_app
from gewu.domain.contracts import EdgeEndpoint, Scope, SearchRequest, TypedEdge
from gewu.domain.validation import evidence_digest
from tests.domain.proposal_fixtures import CLAIM, EVIDENCE, NODE, SOURCE_REVISION
def _canonical_path(prefix: str, stable_id: str) -> str:
key = hashlib.sha256(stable_id.encode()).hexdigest()
return f"{prefix}/{key}.yaml"
def test_project_include_resolution_across_http_mcp_and_direct_reads(tmp_path: Path) -> None:
async def exercise() -> None:
repository = GitCanonicalRepository(tmp_path / "knowledge.git")
dome_node = NODE.model_copy(
update={
"node_id": "dome:scope-default-node",
"summary": "scopeprobe dome project record",
}
)
video_source = SOURCE_REVISION.model_copy(
update={
"source_id": "video:scope-source",
"revision_id": "video:scope-source:r1",
"title": "Video-only scope source",
"ingested_for": ("interactive-video",),
"artifact_sha256": None,
"media_type": "application/octet-stream",
"byte_length": 0,
"original_url": "https://video.example/scope-source",
"archive_status": "reference-only",
}
)
video_evidence = EVIDENCE.model_copy(
update={"source_revision_id": video_source.revision_id}
)
video_assessment = CLAIM.assessment.model_copy(
update={"evidence_digests": (evidence_digest(video_evidence),)}
)
video_claim = CLAIM.model_copy(
update={"evidence": (video_evidence,), "assessment": video_assessment}
)
video_node = NODE.model_copy(
update={
"project_id": "interactive-video",
"node_id": "video:scope-default-node",
"title": "Video scope fixture",
"summary": "scopeprobe interactive video project record",
"claims": (video_claim,),
}
)
edge = TypedEdge(
edge_id="scope:cross-project-edge",
edge_type="compared_with",
source=EdgeEndpoint(kind="node", id=dome_node.node_id),
target=EdgeEndpoint(kind="node", id=video_node.node_id),
)
files = {
f"projects/{hashlib.sha256(dome_node.project_id.encode()).hexdigest()}/nodes/"
f"{hashlib.sha256(dome_node.node_id.encode()).hexdigest()}.yaml": yaml.safe_dump(
dome_node.model_dump(mode="json")
).encode(),
f"projects/{hashlib.sha256(video_node.project_id.encode()).hexdigest()}/nodes/"
f"{hashlib.sha256(video_node.node_id.encode()).hexdigest()}.yaml": yaml.safe_dump(
video_node.model_dump(mode="json")
).encode(),
(
f"sources/{hashlib.sha256(video_source.source_id.encode()).hexdigest()}/"
f"{hashlib.sha256(video_source.revision_id.encode()).hexdigest()}.yaml"
): yaml.safe_dump(video_source.model_dump(mode="json")).encode(),
_canonical_path("edges", edge.edge_id): yaml.safe_dump(
edge.model_dump(mode="json")
).encode(),
}
repository.publish_files(
expected_head=repository.head(),
files=files,
message="Add project-scope fixtures",
index_directory=tmp_path / "publishing",
)
identities = SQLiteIdentityStore(tmp_path / "identity.sqlite3")
all_scope_token = identities.provision(
credential_id="all-scope-reader",
principal_id="all-scope-reader",
roles=("viewer",),
project_ids=("dome-performance", "interactive-video", "shared"),
)
dome_only_token = identities.provision(
credential_id="dome-only-reader",
principal_id="dome-only-reader",
roles=("viewer",),
project_ids=("dome-performance", "shared"),
)
search_index = SQLiteSearchIndex(tmp_path / "search.sqlite3", repository)
indexed_nodes = search_index.canonical_nodes()[1]
assert {node.node_id for node in indexed_nodes} == {dome_node.node_id, video_node.node_id}
lexical_scope_results = search_index.search(
query="scopeprobe",
project_ids=("dome-performance", "interactive-video"),
include_shared=False,
limit=10,
)
assert len(lexical_scope_results.nodes) == 2, [
item.node.summary for item in lexical_scope_results.nodes
]
search = CanonicalSearchService(
index=search_index,
project_ids_provider=lambda: ("dome-performance", "interactive-video"),
canonical_head=repository.head,
default_includes_provider=lambda project_id: (
("interactive-video", "shared") if project_id == "dome-performance" else ()
),
)
app = create_app(
identity_provider=identities,
node_reader=CanonicalNodeReader(repository),
search_service=search,
)
direct_service_result = search.search(
SearchRequest(
query="scopeprobe",
scope=Scope(projects=("dome-performance",), include_shared=False),
limit=10,
budget_tokens=50000,
),
principal=identities.authenticate(all_scope_token),
)
assert len(direct_service_result.data) == 2
async with app.router.lifespan_context(app):
async with httpx2.AsyncClient(
transport=httpx2.ASGITransport(app=app), base_url="http://localhost"
) as http:
default = await http.post(
"/v1/search",
json={
"query": "scopeprobe",
"scope": {"projects": ["dome-performance"], "include_shared": False},
"limit": 10,
"budget_tokens": 50000,
},
headers={"Authorization": f"Bearer {all_scope_token}"},
)
assert default.status_code == 200, default.text
assert {item["node"]["project_id"] for item in default.json()["data"]} == {
"dome-performance",
"interactive-video",
}, default.json()
assert video_source.revision_id in {
item["source_revision_id"] for item in default.json()["citations"]
}
explicit_empty = await http.post(
"/v1/search",
json={
"query": "scopeprobe",
"scope": {
"projects": ["dome-performance"],
"include_projects": [],
"include_shared": False,
},
"limit": 10,
"budget_tokens": 50000,
},
headers={"Authorization": f"Bearer {all_scope_token}"},
)
assert explicit_empty.status_code == 200, explicit_empty.text
assert {item["node"]["project_id"] for item in explicit_empty.json()["data"]} == {
"dome-performance"
}
assert video_source.revision_id not in {
item["source_revision_id"] for item in explicit_empty.json()["citations"]
}
explicit_include = await http.post(
"/v1/search",
json={
"query": "scopeprobe",
"scope": {
"projects": ["dome-performance"],
"include_projects": ["interactive-video"],
"include_shared": False,
},
"limit": 10,
"budget_tokens": 50000,
},
headers={"Authorization": f"Bearer {all_scope_token}"},
)
assert explicit_include.status_code == 200, explicit_include.text
assert len(explicit_include.json()["data"]) == 2
denied_default = await http.post(
"/v1/search",
json={
"query": "scopeprobe",
"scope": {"projects": ["dome-performance"], "include_shared": False},
"limit": 10,
"budget_tokens": 50000,
},
headers={"Authorization": f"Bearer {dome_only_token}"},
)
assert denied_default.status_code == 403
denied_explicit_include = await http.post(
"/v1/search",
json={
"query": "scopeprobe",
"scope": {
"projects": ["dome-performance"],
"include_projects": ["interactive-video"],
"include_shared": False,
},
"limit": 10,
"budget_tokens": 50000,
},
headers={"Authorization": f"Bearer {dome_only_token}"},
)
assert denied_explicit_include.status_code == 403
allowed_empty = await http.post(
"/v1/search",
json={
"query": "scopeprobe",
"scope": {
"projects": ["dome-performance"],
"include_projects": [],
"include_shared": False,
},
"limit": 10,
"budget_tokens": 50000,
},
headers={"Authorization": f"Bearer {dome_only_token}"},
)
assert allowed_empty.status_code == 200
default_neighbors = await http.post(
"/v1/neighbors",
json={
"node_id": dome_node.node_id,
"scope": {"projects": ["dome-performance"], "include_shared": False},
"limit": 10,
"budget_tokens": 50000,
},
headers={"Authorization": f"Bearer {all_scope_token}"},
)
assert default_neighbors.status_code == 200, default_neighbors.text
assert default_neighbors.json()["data"][0]["neighbor"]["id"] == video_node.node_id
restricted_neighbors = await http.post(
"/v1/neighbors",
json={
"node_id": dome_node.node_id,
"scope": {
"projects": ["dome-performance"],
"include_projects": [],
"include_shared": False,
},
"limit": 10,
"budget_tokens": 50000,
},
headers={"Authorization": f"Bearer {all_scope_token}"},
)
assert restricted_neighbors.status_code == 200
assert restricted_neighbors.json()["data"] == []
default_sources = await http.post(
"/v1/sources",
json={
"scope": {"projects": ["dome-performance"], "include_shared": False},
"limit": 10,
"budget_tokens": 50000,
},
headers={"Authorization": f"Bearer {all_scope_token}"},
)
assert default_sources.status_code == 200, default_sources.text
assert video_source.revision_id in {
item["source"]["revision_id"] for item in default_sources.json()["data"]
}
explicit_empty_sources = await http.post(
"/v1/sources",
json={
"scope": {
"projects": ["dome-performance"],
"include_projects": [],
"include_shared": False,
},
"limit": 10,
"budget_tokens": 50000,
},
headers={"Authorization": f"Bearer {all_scope_token}"},
)
assert explicit_empty_sources.status_code == 200
assert video_source.revision_id not in {
item["source"]["revision_id"] for item in explicit_empty_sources.json()["data"]
}
direct_node_denied = await http.get(
f"/v1/projects/interactive-video/nodes/{video_node.node_id}",
headers={"Authorization": f"Bearer {dome_only_token}"},
)
assert direct_node_denied.status_code == 403
direct_source_denied = await http.get(
f"/v1/projects/interactive-video/sources/{video_source.revision_id}",
headers={"Authorization": f"Bearer {dome_only_token}"},
)
assert direct_source_denied.status_code == 403
direct_node_allowed = await http.get(
f"/v1/projects/interactive-video/nodes/{video_node.node_id}",
headers={"Authorization": f"Bearer {all_scope_token}"},
)
assert direct_node_allowed.status_code == 200
direct_source_allowed = await http.get(
f"/v1/projects/interactive-video/sources/{video_source.revision_id}",
headers={"Authorization": f"Bearer {all_scope_token}"},
)
assert direct_source_allowed.status_code == 200
http.headers["Authorization"] = f"Bearer {all_scope_token}"
async with Client(
streamable_http_client("http://localhost/mcp", http_client=http)
) as mcp:
mcp_default = await mcp.call_tool(
"search",
{
"query": "scopeprobe",
"projects": ["dome-performance"],
"include_shared": False,
"all_projects": False,
"limit": 10,
"budget_tokens": 50000,
},
)
assert not mcp_default.is_error
assert isinstance(mcp_default.content[0], TextContent)
default_payload = json.loads(mcp_default.content[0].text)
assert len(default_payload["data"]) == 2
mcp_default_sources = await mcp.call_tool(
"list_sources",
{
"projects": ["dome-performance"],
"include_shared": False,
"all_projects": False,
"limit": 10,
"budget_tokens": 50000,
},
)
assert not mcp_default_sources.is_error
assert isinstance(mcp_default_sources.content[0], TextContent)
assert video_source.revision_id in {
row["source"]["revision_id"]
for row in json.loads(mcp_default_sources.content[0].text)["data"]
}
mcp_default_neighbors = await mcp.call_tool(
"neighbors",
{
"node_id": dome_node.node_id,
"projects": ["dome-performance"],
"include_shared": False,
"all_projects": False,
"edge_types": [],
"limit": 10,
"budget_tokens": 50000,
},
)
assert not mcp_default_neighbors.is_error
assert isinstance(mcp_default_neighbors.content[0], TextContent)
assert (
json.loads(mcp_default_neighbors.content[0].text)["data"][0]["neighbor"][
"id"
]
== video_node.node_id
)
mcp_explicit_empty = await mcp.call_tool(
"search",
{
"query": "scopeprobe",
"projects": ["dome-performance"],
"include_projects": [],
"include_shared": False,
"all_projects": False,
"limit": 10,
"budget_tokens": 50000,
},
)
assert not mcp_explicit_empty.is_error
assert isinstance(mcp_explicit_empty.content[0], TextContent)
assert len(json.loads(mcp_explicit_empty.content[0].text)["data"]) == 1
mcp_empty_sources = await mcp.call_tool(
"list_sources",
{
"projects": ["dome-performance"],
"include_projects": [],
"include_shared": False,
"all_projects": False,
"limit": 10,
"budget_tokens": 50000,
},
)
assert not mcp_empty_sources.is_error
assert isinstance(mcp_empty_sources.content[0], TextContent)
assert video_source.revision_id not in {
row["source"]["revision_id"]
for row in json.loads(mcp_empty_sources.content[0].text)["data"]
}
mcp_empty_neighbors = await mcp.call_tool(
"neighbors",
{
"node_id": dome_node.node_id,
"projects": ["dome-performance"],
"include_projects": [],
"include_shared": False,
"all_projects": False,
"edge_types": [],
"limit": 10,
"budget_tokens": 50000,
},
)
assert not mcp_empty_neighbors.is_error
assert isinstance(mcp_empty_neighbors.content[0], TextContent)
assert json.loads(mcp_empty_neighbors.content[0].text)["data"] == []
asyncio.run(exercise())
+616
View File
@@ -0,0 +1,616 @@
import asyncio
import hashlib
import json
from datetime import UTC, datetime
from pathlib import Path
import httpx2
import yaml
from mcp import Client
from mcp.client.streamable_http import streamable_http_client
from mcp.types import TextContent
from gewu.adapters.git_repository import GitCanonicalRepository
from gewu.adapters.sqlite_identity import SQLiteIdentityStore
from gewu.adapters.sqlite_search import SQLiteSearchIndex
from gewu.application.reading import CanonicalNodeReader
from gewu.application.search import CanonicalSearchService
from gewu.bootstrap.app import create_app
from gewu.domain.contracts import EdgeEndpoint, NodeType, TypedEdge
from tests.domain.proposal_fixtures import NODE, PROJECTS, SOURCE_REVISION
def test_authenticated_search_scope_through_http_and_mcp(tmp_path: Path) -> None:
async def exercise() -> None:
repository = GitCanonicalRepository(tmp_path / "knowledge.git")
project_key = hashlib.sha256(NODE.project_id.encode()).hexdigest()
node_key = hashlib.sha256(NODE.node_id.encode()).hexdigest()
source_key = hashlib.sha256(SOURCE_REVISION.revision_id.encode()).hexdigest()
stale_node = NODE.model_copy(
update={
"as_of": datetime(2024, 1, 1, tzinfo=UTC),
"stale_after": datetime(2025, 1, 1, tzinfo=UTC),
}
)
problem_node = NODE.model_copy(
update={
"node_id": "dome:problem-browse-fixture",
"node_type": NodeType.PROBLEM,
"title": "Problem browse fixture",
"value": None,
"unit": None,
}
)
problem_key = hashlib.sha256(problem_node.node_id.encode()).hexdigest()
older_source = SOURCE_REVISION.model_copy(
update={
"source_id": "dome:older-pilot-report",
"revision_id": "dome:older-pilot-report:r1",
"title": "Older pilot installation report",
"captured_at": datetime(2025, 1, 1, tzinfo=UTC),
}
)
related = NODE.model_copy(update={"node_id": "z:dome-related"})
related_key = hashlib.sha256(related.node_id.encode()).hexdigest()
related_two = NODE.model_copy(update={"node_id": "zz:dome-related-two"})
related_two_key = hashlib.sha256(related_two.node_id.encode()).hexdigest()
edge = TypedEdge(
edge_id="dome:related-edge",
edge_type="compared_with",
source=EdgeEndpoint(kind="node", id=NODE.node_id),
target=EdgeEndpoint(kind="node", id=related.node_id),
)
edge_two = TypedEdge(
edge_id="dome:related-edge-two",
edge_type="compared_with",
source=EdgeEndpoint(kind="node", id=NODE.node_id),
target=EdgeEndpoint(kind="node", id=related_two.node_id),
)
repository.publish_files(
expected_head=repository.head(),
files={
f"projects/{project_key}/nodes/{node_key}.yaml": yaml.safe_dump(
stale_node.model_dump(mode="json")
).encode(),
f"projects/{project_key}/nodes/{problem_key}.yaml": yaml.safe_dump(
problem_node.model_dump(mode="json")
).encode(),
f"projects/{project_key}/nodes/{related_key}.yaml": yaml.safe_dump(
related.model_dump(mode="json")
).encode(),
f"projects/{project_key}/nodes/{related_two_key}.yaml": yaml.safe_dump(
related_two.model_dump(mode="json")
).encode(),
(
f"sources/{hashlib.sha256(SOURCE_REVISION.source_id.encode()).hexdigest()}/"
f"{source_key}.yaml"
): yaml.safe_dump(SOURCE_REVISION.model_dump(mode="json")).encode(),
(
f"sources/{hashlib.sha256(older_source.source_id.encode()).hexdigest()}/"
f"{hashlib.sha256(older_source.revision_id.encode()).hexdigest()}.yaml"
): yaml.safe_dump(older_source.model_dump(mode="json")).encode(),
f"edges/{hashlib.sha256(edge.edge_id.encode()).hexdigest()}.yaml": yaml.safe_dump(
edge.model_dump(mode="json")
).encode(),
(
f"edges/{hashlib.sha256(edge_two.edge_id.encode()).hexdigest()}.yaml"
): yaml.safe_dump(edge_two.model_dump(mode="json")).encode(),
},
message="Add searchable node",
index_directory=tmp_path / "publishing",
)
identities = SQLiteIdentityStore(tmp_path / "identity.sqlite3")
token = identities.provision(
credential_id="search-key",
principal_id="search-reader",
roles=("viewer",),
project_ids=("dome-performance",),
)
search = CanonicalSearchService(
index=SQLiteSearchIndex(tmp_path / "search.sqlite3", repository),
project_ids_provider=lambda: PROJECTS.keys(),
canonical_head=repository.head,
)
app = create_app(
identity_provider=identities,
node_reader=CanonicalNodeReader(repository),
search_service=search,
)
async with app.router.lifespan_context(app):
async with httpx2.AsyncClient(
transport=httpx2.ASGITransport(app=app),
base_url="http://localhost",
headers={"Authorization": f"Bearer {token}"},
) as http:
diagnostic_unbuilt = await http.get("/v1/diagnostics?projects=dome-performance")
assert diagnostic_unbuilt.status_code == 200
unbuilt_state = diagnostic_unbuilt.json()
assert unbuilt_state["index_state"] == "not_built"
assert unbuilt_state["index_revision"] is None
assert unbuilt_state["index_schema_version"] == 4
result = await http.post(
"/v1/search",
json={
"query": "400 visitors",
"scope": {
"projects": ["dome-performance"],
"include_shared": False,
},
"limit": 10,
"budget_tokens": 5000,
},
)
assert result.status_code == 200, result.text
envelope = result.json()
assert envelope["retrieval_mode"] == "keyword"
assert envelope["index_revision"] == repository.head()
assert envelope["data"][0]["node"]["node_id"] == NODE.node_id
assert envelope["citations"][0]["quote"] == NODE.claims[0].evidence[0].quote
listed_sources = await http.post(
"/v1/sources",
json={
"scope": {"projects": ["dome-performance"], "include_shared": False},
"limit": 10,
"budget_tokens": 5000,
},
)
assert listed_sources.status_code == 200, listed_sources.text
assert listed_sources.json()["data"][0]["source"]["revision_id"] == (
SOURCE_REVISION.revision_id
)
source_page_one = await http.post(
"/v1/sources",
json={
"scope": {"projects": ["dome-performance"], "include_shared": False},
"limit": 1,
"budget_tokens": 5000,
},
)
source_cursor = source_page_one.json()["next_cursor"]
assert source_cursor
source_page_two = await http.post(
"/v1/sources",
json={
"scope": {"projects": ["dome-performance"], "include_shared": False},
"limit": 1,
"cursor": source_cursor,
"budget_tokens": 5000,
},
)
assert source_page_two.status_code == 200
assert source_page_two.json()["data"][0]["source"]["revision_id"] == (
older_source.revision_id
)
denied_sources = await http.post(
"/v1/sources",
json={
"scope": {"projects": ["interactive-video"], "include_shared": False},
"limit": 10,
"budget_tokens": 5000,
},
)
assert denied_sources.status_code == 403
problem_filter = await http.post(
"/v1/search",
json={
"query": "unmatched phrase",
"scope": {"projects": ["dome-performance"], "include_shared": False},
"node_types": ["problem"],
"budget_tokens": 5000,
},
)
assert problem_filter.status_code == 200, problem_filter.text
assert problem_filter.json()["data"] == []
problem_browse = await http.post(
"/v1/search",
json={
"query": "",
"scope": {"projects": ["dome-performance"], "include_shared": False},
"node_types": ["problem"],
"budget_tokens": 5000,
},
)
assert problem_browse.status_code == 200, problem_browse.text
assert [row["node"]["node_id"] for row in problem_browse.json()["data"]] == [
problem_node.node_id
]
unbounded_browse = await http.post(
"/v1/search",
json={
"query": "",
"scope": {"projects": ["dome-performance"], "include_shared": False},
"budget_tokens": 5000,
},
)
assert unbounded_browse.status_code == 422
stale_filter = await http.post(
"/v1/search",
json={
"query": "400 visitors",
"scope": {"projects": ["dome-performance"], "include_shared": False},
"stale_only": True,
"budget_tokens": 5000,
},
)
assert stale_filter.status_code == 200, stale_filter.text
assert stale_filter.json()["data"][0]["node"]["node_id"] == NODE.node_id
facet_filter = await http.post(
"/v1/search",
json={
"query": "400 visitors",
"scope": {"projects": ["dome-performance"], "include_shared": False},
"tags": ["attendance"],
"audience": ["product"],
"use_for": ["comparison"],
"domain": ["production"],
"budget_tokens": 5000,
},
)
assert facet_filter.status_code == 200, facet_filter.text
assert facet_filter.json()["data"][0]["node"]["node_id"] == NODE.node_id
unmatched_facet = await http.post(
"/v1/search",
json={
"query": "400 visitors",
"scope": {"projects": ["dome-performance"], "include_shared": False},
"domain": ["unrelated-domain"],
"budget_tokens": 5000,
},
)
assert unmatched_facet.status_code == 200, unmatched_facet.text
assert unmatched_facet.json()["data"] == []
diagnostic_current = await http.get("/v1/diagnostics?projects=dome-performance")
assert diagnostic_current.status_code == 200
current_state = diagnostic_current.json()
assert current_state["index_state"] == "current"
assert current_state["index_revision"] == repository.head()
assert "proposal_submit" not in current_state["authorized_operations"]
projects = await http.get("/v1/projects")
assert projects.status_code == 200
assert projects.json() == ["dome-performance"]
page_one = await http.post(
"/v1/search",
json={
"query": "visitors",
"scope": {"projects": ["dome-performance"], "include_shared": False},
"limit": 1,
"budget_tokens": 5000,
},
)
assert page_one.status_code == 200
cursor = page_one.json()["next_cursor"]
assert cursor
page_two = await http.post(
"/v1/search",
json={
"query": "visitors",
"scope": {"projects": ["dome-performance"], "include_shared": False},
"limit": 1,
"cursor": cursor,
"budget_tokens": 5000,
},
)
assert page_two.status_code == 200
assert (
page_two.json()["data"][0]["node"]["node_id"]
!= page_one.json()["data"][0]["node"]["node_id"]
)
reused_cursor = await http.post(
"/v1/search",
json={
"query": "400 visitors",
"scope": {"projects": ["dome-performance"], "include_shared": False},
"limit": 1,
"cursor": cursor,
"budget_tokens": 5000,
},
)
assert reused_cursor.status_code == 422
changed_facet_cursor = await http.post(
"/v1/search",
json={
"query": "visitors",
"scope": {"projects": ["dome-performance"], "include_shared": False},
"node_types": ["problem"],
"limit": 1,
"cursor": cursor,
"budget_tokens": 5000,
},
)
assert changed_facet_cursor.status_code == 422
changed_tag_cursor = await http.post(
"/v1/search",
json={
"query": "visitors",
"scope": {"projects": ["dome-performance"], "include_shared": False},
"tags": ["attendance"],
"limit": 1,
"cursor": cursor,
"budget_tokens": 5000,
},
)
assert changed_tag_cursor.status_code == 422
adjacent = await http.post(
"/v1/neighbors",
json={
"node_id": NODE.node_id,
"scope": {"projects": ["dome-performance"], "include_shared": False},
"limit": 1,
"budget_tokens": 5000,
},
)
assert adjacent.status_code == 200, adjacent.text
assert adjacent.json()["data"][0]["neighbor"]["id"] == related.node_id
neighbor_cursor = adjacent.json()["next_cursor"]
assert neighbor_cursor
adjacent_two = await http.post(
"/v1/neighbors",
json={
"node_id": NODE.node_id,
"scope": {"projects": ["dome-performance"], "include_shared": False},
"limit": 1,
"cursor": neighbor_cursor,
"budget_tokens": 5000,
},
)
assert adjacent_two.status_code == 200
assert adjacent_two.json()["data"][0]["neighbor"]["id"] == related_two.node_id
assert adjacent_two.json()["next_cursor"] is None
assert {citation["quote"] for citation in adjacent.json()["citations"]} == {
NODE.claims[0].evidence[0].quote
}
newer = NODE.model_copy(update={"node_id": "new:unrelated-revision-node"})
newer_key = hashlib.sha256(newer.node_id.encode()).hexdigest()
repository.publish_files(
expected_head=repository.head(),
files={
f"projects/{project_key}/nodes/{newer_key}.yaml": yaml.safe_dump(
newer.model_dump(mode="json")
).encode()
},
message="Advance canonical index for cursor-expiry check",
index_directory=tmp_path / "publishing",
)
diagnostic_lag = await http.get("/v1/diagnostics?projects=dome-performance")
assert diagnostic_lag.status_code == 200
lag_state = diagnostic_lag.json()
assert lag_state["index_state"] == "lagging"
assert lag_state["index_revision"] != repository.head()
assert lag_state["canonical_revision"] == repository.head()
expired_search = await http.post(
"/v1/search",
json={
"query": "visitors",
"scope": {"projects": ["dome-performance"], "include_shared": False},
"limit": 1,
"cursor": cursor,
"budget_tokens": 5000,
},
)
assert expired_search.status_code == 409
expired_neighbors = await http.post(
"/v1/neighbors",
json={
"node_id": NODE.node_id,
"scope": {"projects": ["dome-performance"], "include_shared": False},
"limit": 1,
"cursor": neighbor_cursor,
"budget_tokens": 5000,
},
)
assert expired_neighbors.status_code == 409
too_small = await http.post(
"/v1/search",
json={
"query": "visitors",
"scope": {
"projects": ["dome-performance"],
"include_shared": False,
},
"budget_tokens": 256,
},
)
assert too_small.status_code == 413
dated_confidence_search = await http.post(
"/v1/search",
json={
"query": "visitors",
"scope": {
"projects": ["dome-performance"],
"include_shared": False,
},
"confidences": ["medium"],
"as_of_from": "2024-01-01",
"as_of_to": "2024-12-31",
"budget_tokens": 5000,
},
)
assert dated_confidence_search.status_code == 200, dated_confidence_search.text
assert [
row["node"]["node_id"] for row in dated_confidence_search.json()["data"]
] == [NODE.node_id]
filtered_page = await http.post(
"/v1/search",
json={
"query": "visitors",
"scope": {
"projects": ["dome-performance"],
"include_shared": False,
},
"confidences": ["medium"],
"limit": 1,
"budget_tokens": 5000,
},
)
assert filtered_page.status_code == 200
filter_cursor = filtered_page.json()["next_cursor"]
assert filter_cursor
changed_filter_page = await http.post(
"/v1/search",
json={
"query": "visitors",
"scope": {
"projects": ["dome-performance"],
"include_shared": False,
},
"confidences": ["low"],
"limit": 1,
"cursor": filter_cursor,
"budget_tokens": 5000,
},
)
assert changed_filter_page.status_code == 422
forbidden = await http.post(
"/v1/search",
json={
"query": "visitors",
"scope": {
"projects": ["interactive-video"],
"include_shared": False,
},
"budget_tokens": 5000,
},
)
assert forbidden.status_code == 403
async with Client(
streamable_http_client("http://localhost/mcp", http_client=http)
) as mcp:
tools = await mcp.list_tools()
assert {tool.name for tool in tools.tools} == {
"diagnostics",
"get_node",
"node_history",
"get_source",
"list_projects",
"search",
"neighbors",
"list_sources",
}
listed = await mcp.call_tool("list_projects", {})
assert not listed.is_error
assert isinstance(listed.content[0], TextContent)
assert json.loads(listed.content[0].text) == ["dome-performance"]
searched = await mcp.call_tool(
"search",
{
"query": "400 visitors",
"projects": ["dome-performance"],
"include_shared": False,
"all_projects": False,
"limit": 10,
"budget_tokens": 5000,
},
)
assert not searched.is_error
assert isinstance(searched.content[0], TextContent)
assert (
json.loads(searched.content[0].text)["data"][0]["node"]["node_id"]
== NODE.node_id
)
dated_confidence_tool = await mcp.call_tool(
"search",
{
"query": "visitors",
"projects": ["dome-performance"],
"include_shared": False,
"all_projects": False,
"limit": 10,
"budget_tokens": 5000,
"confidences": ["medium"],
"as_of_from": "2024-01-01",
"as_of_to": "2024-12-31",
},
)
assert not dated_confidence_tool.is_error
assert [
row["node"]["node_id"]
for row in json.loads(dated_confidence_tool.content[0].text)["data"]
] == [NODE.node_id]
filtered_search = await mcp.call_tool(
"search",
{
"query": "unmatched phrase",
"projects": ["dome-performance"],
"include_shared": False,
"all_projects": False,
"limit": 10,
"budget_tokens": 5000,
"node_types": ["problem"],
},
)
assert not filtered_search.is_error
assert json.loads(filtered_search.content[0].text)["data"] == []
browsed_problems = await mcp.call_tool(
"search",
{
"query": "",
"projects": ["dome-performance"],
"include_shared": False,
"all_projects": False,
"limit": 10,
"budget_tokens": 5000,
"node_types": ["problem"],
},
)
assert not browsed_problems.is_error
assert [
row["node"]["node_id"]
for row in json.loads(browsed_problems.content[0].text)["data"]
] == [problem_node.node_id]
facet_search = await mcp.call_tool(
"search",
{
"query": "400 visitors",
"projects": ["dome-performance"],
"include_shared": False,
"all_projects": False,
"limit": 10,
"budget_tokens": 5000,
"tags": ["attendance"],
"audience": ["product"],
"use_for": ["comparison"],
"domain": ["production"],
},
)
assert not facet_search.is_error
assert (
json.loads(facet_search.content[0].text)["data"][0]["node"]["node_id"]
== NODE.node_id
)
listed_sources_tool = await mcp.call_tool(
"list_sources",
{
"projects": ["dome-performance"],
"include_shared": False,
"all_projects": False,
"limit": 10,
"budget_tokens": 5000,
},
)
assert not listed_sources_tool.is_error
assert (
json.loads(listed_sources_tool.content[0].text)["data"][0]["source"][
"revision_id"
]
== SOURCE_REVISION.revision_id
)
adjacent_tool = await mcp.call_tool(
"neighbors",
{
"node_id": NODE.node_id,
"projects": ["dome-performance"],
"include_shared": False,
"all_projects": False,
"edge_types": [],
"limit": 10,
"budget_tokens": 5000,
},
)
assert not adjacent_tool.is_error
assert isinstance(adjacent_tool.content[0], TextContent)
assert (
json.loads(adjacent_tool.content[0].text)["data"][0]["neighbor"]["id"]
== related.node_id
)
asyncio.run(exercise())
+374
View File
@@ -0,0 +1,374 @@
import asyncio
import base64
import hashlib
import json
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from pathlib import Path
import pytest
from mcp import Client
from mcp.server import MCPServer
from mcp.types import CallToolResult, TextContent
from gewu.domain.contracts import SourceRevisionRecord
from gewu.transports.stdio_connector import (
SelectedFile,
UploadStateStore,
_local_source_id,
_local_source_revision_id,
_read_selected_file,
_remote_factory,
_selected_media_type,
create_connector_server,
upload_selected_file,
)
def test_connector_uploads_allowlisted_bytes_without_exposing_them_to_local_tools(
tmp_path: Path,
) -> None:
async def exercise() -> None:
source = tmp_path / "selected.md"
payload = b"# Private source\n" + b"Exact source bytes stay in the connector.\n" * 20_000
source.write_bytes(payload)
backend = MCPServer("test GEWU backend")
chunks: list[bytes] = []
interrupted = False
extraction_interrupted = False
expected_digest = hashlib.sha256(payload).hexdigest()
@backend.tool(name="source_upload_begin", structured_output=False)
def begin(
project_id: str,
filename: str,
media_type: str,
byte_length: int,
expected_sha256: str,
) -> str:
assert (project_id, filename, media_type, byte_length, expected_sha256) == (
"dome-performance",
"selected.md",
"text/markdown",
len(payload),
expected_digest,
)
return json.dumps({"upload_id": "opaque-upload-id"})
@backend.tool(name="source_upload_chunk", structured_output=False)
def chunk(upload_id: str, offset: int, content_base64: str, chunk_sha256: str) -> str:
nonlocal interrupted
decoded = base64.b64decode(content_base64, validate=True)
assert upload_id == "opaque-upload-id"
assert hashlib.sha256(decoded).hexdigest() == chunk_sha256
assert offset == sum(map(len, chunks))
if offset > 0 and not interrupted:
interrupted = True
raise ValueError("simulated interrupted transfer")
chunks.append(decoded)
return json.dumps({"upload_id": upload_id})
@backend.tool(name="source_upload_status", structured_output=False)
def status(upload_id: str) -> str:
assert upload_id == "opaque-upload-id"
return json.dumps({"verified_offsets": [0] if chunks else []})
@backend.tool(name="source_upload_finalize", structured_output=False)
def finalize(upload_id: str) -> str:
assert upload_id == "opaque-upload-id"
return json.dumps(
{"sha256": hashlib.sha256(b"".join(chunks)).hexdigest(), "status": "staged"}
)
@backend.tool(name="source_extract", structured_output=False)
def extract(project_id: str, artifact_sha256: str, source_revision_id: str) -> str:
nonlocal extraction_interrupted
assert project_id == "dome-performance"
assert artifact_sha256 == expected_digest
assert source_revision_id == "source-r1"
if not extraction_interrupted:
extraction_interrupted = True
raise ValueError("simulated extraction interruption")
return json.dumps({"extraction_id": "e" * 64, "blocks": []})
@asynccontextmanager
async def remote_factory() -> AsyncIterator[Client]:
async with Client(backend) as remote:
yield remote
state_file = tmp_path / "connector-state.json"
selected_files = (SelectedFile("research-note", source),)
async def connector_call() -> CallToolResult:
connector_after_restart = create_connector_server(
selected_files, remote_factory, UploadStateStore(state_file)
)
async with Client(connector_after_restart) as local_after_restart:
return await local_after_restart.call_tool(
"upload_selected_file",
{
"file_id": "research-note",
"project_id": "dome-performance",
"source_revision_id": "source-r1",
},
)
connector = create_connector_server(
selected_files, remote_factory, UploadStateStore(state_file)
)
async with Client(connector) as local:
tools = await local.list_tools()
assert {tool.name for tool in tools.tools} == {
"list_selected_files",
"upload_selected_file",
}
listed = await local.call_tool("list_selected_files", {})
assert not listed.is_error
assert isinstance(listed.content[0], TextContent)
assert "research-note" in listed.content[0].text
assert str(source) not in listed.content[0].text
denied = await local.call_tool(
"upload_selected_file",
{
"file_id": "arbitrary-path",
"project_id": "dome-performance",
"source_revision_id": "source-r1",
},
)
assert denied.is_error
interrupted_result = await connector_call()
assert interrupted_result.is_error
assert state_file.exists()
finalize_interrupted_result = await connector_call()
assert finalize_interrupted_result.is_error
assert json.loads(state_file.read_text())
restarted_result = await connector_call()
assert not restarted_result.is_error
assert isinstance(restarted_result.content[0], TextContent)
receipt = json.loads(restarted_result.content[0].text)
assert receipt["sha256"] == expected_digest
assert receipt["byte_length"] == len(payload)
assert payload[:80].decode() not in restarted_result.content[0].text
source_candidate = SourceRevisionRecord.model_validate_json(
json.dumps(receipt["source_candidate"]), strict=True
)
assert source_candidate.archive_status == "archived"
assert source_candidate.capture_method == "local-file"
assert source_candidate.source_id.startswith("local:")
assert source_candidate.revision_id == "source-r1"
assert source_candidate.artifact_sha256 == expected_digest
assert str(source) not in restarted_result.content[0].text
assert b"".join(chunks) == payload
state = json.loads(state_file.read_text())
assert len(state) == 1
assert "upload_id" not in next(iter(state.values()))
duplicate_result = await connector_call()
assert not duplicate_result.is_error
assert json.loads(duplicate_result.content[0].text) == receipt
asyncio.run(exercise())
def test_connector_rejects_symlinked_selected_paths(tmp_path: Path) -> None:
outside = tmp_path / "outside.md"
outside.write_text("private")
link = tmp_path / "selected.md"
link.symlink_to(outside)
with pytest.raises(ValueError, match="symbolic links"):
_read_selected_file(SelectedFile("selected", link))
def test_local_file_identity_is_path_free_and_revisions_follow_content() -> None:
from datetime import UTC, datetime
captured_at = datetime(2026, 10, 13, tzinfo=UTC)
source_id = _local_source_id("research-note")
first = _local_source_revision_id(source_id, hashlib.sha256(b"first").hexdigest(), captured_at)
second = _local_source_revision_id(
source_id, hashlib.sha256(b"second").hexdigest(), captured_at
)
assert source_id.startswith("local:")
assert "/" not in source_id
assert first != second
assert first.startswith(f"{source_id}:r:")
assert len(first) <= 128
def test_selected_file_media_types_are_explicit() -> None:
assert _selected_media_type(Path("source.pdf")) == "application/pdf"
assert _selected_media_type(Path("source.md")) == "text/markdown"
assert _selected_media_type(Path("source.html")) == "text/html"
assert _selected_media_type(Path("source.txt")) == "text/plain"
assert _selected_media_type(Path("source.vtt")) == "text/vtt"
assert _selected_media_type(Path("source.srt")) == "application/x-subrip"
with pytest.raises(ValueError, match="not supported"):
_selected_media_type(Path("source.exe"))
def test_vtt_connector_candidate_is_marked_as_transcript(tmp_path: Path) -> None:
async def exercise() -> None:
source = tmp_path / "interview.vtt"
content = b"WEBVTT\n\n00:00:01.000 --> 00:00:02.000\nExact words.\n"
source.write_bytes(content)
digest = hashlib.sha256(content).hexdigest()
class Remote:
async def call_tool(self, name: str, arguments: dict[str, object]) -> object:
response: dict[str, object]
if name == "source_upload_begin":
response = {"upload_id": "transcript-upload"}
elif name == "source_upload_chunk":
response = {"upload_id": "transcript-upload"}
elif name == "source_upload_finalize":
response = {"sha256": digest}
elif name == "source_extract":
response = {"status": "complete", "extraction_id": "e" * 64, "blocks": []}
else:
raise AssertionError(f"Unexpected remote tool: {name}")
return type(
"ToolResult",
(),
{
"is_error": False,
"content": [TextContent(type="text", text=json.dumps(response))],
},
)()
receipt = await upload_selected_file(
Remote(), SelectedFile("interview", source), project_id="dome-performance"
)
candidate = SourceRevisionRecord.model_validate_json(
json.dumps(receipt["source_candidate"]), strict=True
)
assert candidate.capture_method == "transcript"
asyncio.run(exercise())
def test_local_connector_returns_archived_candidate_when_pdf_extraction_is_unavailable(
tmp_path: Path,
) -> None:
async def exercise() -> None:
source = tmp_path / "scanned.pdf"
content = b"%PDF-1.4 fixture"
source.write_bytes(content)
digest = hashlib.sha256(content).hexdigest()
calls: list[str] = []
class Remote:
async def call_tool(self, name: str, arguments: dict[str, object]) -> object:
calls.append(name)
response: dict[str, object]
if name == "source_upload_begin":
response = {"upload_id": "active-upload"}
elif name == "source_upload_chunk":
assert arguments["chunk_sha256"] == digest
response = {"upload_id": "active-upload"}
elif name == "source_upload_finalize":
response = {"sha256": digest}
elif name == "source_extract":
response = {
"status": "unavailable",
"error_code": "PDF_NO_TEXT",
"blocks": [],
}
else:
raise AssertionError(f"Unexpected remote tool: {name}")
return type(
"ToolResult",
(),
{
"is_error": False,
"content": [TextContent(type="text", text=json.dumps(response))],
},
)()
state = UploadStateStore(tmp_path / "state.json")
first = await upload_selected_file(
Remote(),
SelectedFile("scanned-paper", source),
project_id="dome-performance",
state_store=state,
)
second = await upload_selected_file(
Remote(),
SelectedFile("scanned-paper", source),
project_id="dome-performance",
state_store=state,
)
candidate = SourceRevisionRecord.model_validate_json(
json.dumps(first["source_candidate"]), strict=True
)
assert candidate.archive_status == "archived"
assert candidate.artifact_sha256 == digest
assert len(candidate.revision_id) <= 128
assert first["extraction_status"] == "unavailable"
assert first["extraction_error_code"] == "PDF_NO_TEXT"
assert second["source_candidate"] == first["source_candidate"]
assert calls.count("source_upload_begin") == 1
assert calls.count("source_extract") == 2
state_record = next(iter(json.loads((tmp_path / "state.json").read_text()).values()))
assert "completed_receipt" not in state_record
assert str(source) not in json.dumps(first)
asyncio.run(exercise())
def test_connector_rejects_cleartext_remote_service_urls() -> None:
with pytest.raises(ValueError, match="HTTPS"):
_remote_factory("http://gewu.example/mcp", "gwu_test")
_remote_factory("https://gewu.example/mcp", "gwu_test")
_remote_factory("http://localhost:8000/mcp", "gwu_test")
def test_remote_factory_verifies_service_certificate_before_authentication(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from contextlib import asynccontextmanager
import gewu.transports.stdio_connector as connector_module
observed: dict[str, object] = {}
class HttpClient:
def __init__(self, **kwargs: object) -> None:
observed.update(kwargs)
async def __aenter__(self) -> object:
return self
async def __aexit__(self, *args: object) -> None:
return None
class RemoteClient:
def __init__(self, transport: object) -> None:
self.transport = transport
async def __aenter__(self) -> object:
await self.transport.__aenter__()
return self
async def __aexit__(self, *args: object) -> None:
await self.transport.__aexit__(*args)
return None
@asynccontextmanager
async def transport_factory(url: str, *, http_client: object):
observed["url"] = url
observed["http_client"] = http_client
yield object()
monkeypatch.setattr(connector_module.httpx2, "AsyncClient", HttpClient)
monkeypatch.setattr(connector_module, "Client", RemoteClient)
monkeypatch.setattr(connector_module, "streamable_http_client", transport_factory)
async def exercise() -> None:
async with _remote_factory("https://gewu.example/mcp", "secret-token")() as remote:
assert isinstance(remote, RemoteClient)
asyncio.run(exercise())
assert observed["verify"] is True
assert observed["headers"] == {"Authorization": "Bearer secret-token"}
assert observed["url"] == "https://gewu.example/mcp"
assert "secret-token" not in str(observed["url"])
+1
View File
@@ -0,0 +1 @@
"""Application integration tests."""
@@ -0,0 +1,29 @@
"""Terminate acceptance at a durable publication boundary for recovery tests."""
from __future__ import annotations
import os
import sys
from pathlib import Path
from gewu.domain.contracts import ProposalRevision
from tests.application.test_acceptance import ACTOR, _service
def main() -> None:
data_root = Path(sys.argv[1])
proposal_path = Path(sys.argv[2])
crash_stage = sys.argv[3]
proposal = ProposalRevision.model_validate_json(proposal_path.read_bytes(), strict=True)
def terminate(stage: str) -> None:
if stage == crash_stage:
os._exit(73)
service, *_ = _service(data_root, hook=terminate)
service.submit(proposal, actor=ACTOR)
raise RuntimeError(f"Acceptance completed without reaching crash stage {crash_stage}")
if __name__ == "__main__":
main()
+850
View File
@@ -0,0 +1,850 @@
import hashlib
import json
import subprocess
import sys
import threading
from collections.abc import Callable
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
import pyotp
import pytest
from cryptography.fernet import Fernet
from gewu.adapters.git_repository import GitCanonicalRepository
from gewu.adapters.sqlite_identity import SQLiteIdentityStore
from gewu.adapters.sqlite_search import SQLiteSearchIndex
from gewu.adapters.text_extractor import TextMarkdownExtractor
from gewu.application.acceptance import (
AcceptanceError,
AcceptanceService,
ActorContext,
)
from gewu.application.context import ValidationContext
from gewu.application.identity import Principal
from gewu.application.reading import CanonicalNodeReader
from gewu.application.search import CanonicalSearchService
from gewu.application.serialization import safe_yaml_record
from gewu.application.uploads import LocalArtifactStore
from gewu.domain.contracts import (
AcceptanceManifest,
EdgeEndpoint,
ExtractionRecord,
KnowledgeNode,
ProposalOperation,
ProposalRevision,
ReliabilityGrade,
Scope,
SearchRequest,
SourceRevisionRecord,
TypedEdge,
)
from gewu.domain.validation import assertion_digest, evidence_digest, proposal_payload_digest
from tests.domain.proposal_fixtures import (
EXTRACTION,
NODE,
POLICIES,
PROJECTS,
SOURCE_REVISION,
proposal,
)
ACTOR = ActorContext(principal_id="agent-pilot", credential_id="agent-key-1")
SOURCE_BYTES = b"The installation hosted 400 visitors during the 2026 season."
class SimulatedProcessCrash(BaseException):
pass
def _service(
data_root: Path,
*,
hook: Callable[[str], None] = lambda _stage: None,
authorized: bool = True,
) -> tuple[
AcceptanceService,
GitCanonicalRepository,
LocalArtifactStore,
SourceRevisionRecord,
ExtractionRecord,
]:
artifact_store = LocalArtifactStore(data_root / "private-data")
upload = artifact_store.begin(
filename="pilot.md",
media_type="text/markdown",
byte_length=len(SOURCE_BYTES),
expected_sha256=hashlib.sha256(SOURCE_BYTES).hexdigest(),
now=100,
)
artifact_store.put_chunk(
upload.upload_id,
offset=0,
content=SOURCE_BYTES,
chunk_sha256=hashlib.sha256(SOURCE_BYTES).hexdigest(),
now=101,
)
artifact_store.finalize(upload.upload_id, now=102)
source = SOURCE_REVISION.model_copy(
update={
"artifact_sha256": hashlib.sha256(SOURCE_BYTES).hexdigest(),
"byte_length": len(SOURCE_BYTES),
}
)
extraction = EXTRACTION
repository = GitCanonicalRepository(data_root / "private-data" / "knowledge.git")
context = ValidationContext(
projects=PROJECTS,
policies=POLICIES,
sources={},
extractions={extraction.extraction_id: extraction},
existing_nodes={},
)
service = AcceptanceService(
repository=repository,
artifact_store=artifact_store,
workflow_root=data_root / "private-data" / "staging",
context_provider=lambda: context,
actor_is_current=lambda _actor: authorized,
failure_hook=hook,
)
return service, repository, artifact_store, source, extraction
def _proposal(repository: GitCanonicalRepository, source: SourceRevisionRecord) -> ProposalRevision:
candidate = proposal(source=source, base_commit=repository.head())
return candidate.model_copy(update={"payload_digest": proposal_payload_digest(candidate)})
def test_acceptance_commits_records_and_retry_returns_original_receipt(tmp_path: Path) -> None:
service, repository, _artifact_store, source, _extraction = _service(tmp_path)
candidate = _proposal(repository, source)
result = service.submit(candidate, actor=ACTOR)
assert result.validation.eligible_for_auto_accept
assert result.receipt is not None
assert result.receipt.state == "accepted"
assert result.receipt.commit_id == repository.head()
assert result.receipt.accepted_node_ids == (NODE.node_id,)
before_retry = repository.head()
retry = service.submit(candidate, actor=ACTOR)
assert retry.receipt == result.receipt
assert repository.head() == before_retry
status = service.proposal_status(candidate.proposal_id, candidate.revision, actor=ACTOR)
assert status.lifecycle == "accepted"
assert status.canonical_revision == result.receipt.commit_id
assert status.receipt == result.receipt
with pytest.raises(AcceptanceError) as accepted_withdrawal:
service.withdraw_proposal(candidate.proposal_id, candidate.revision, actor=ACTOR)
assert accepted_withdrawal.value.code == "PROPOSAL_ALREADY_ACCEPTED"
with pytest.raises(AcceptanceError) as unauthorized:
service.proposal_status(
candidate.proposal_id,
candidate.revision,
actor=ActorContext(principal_id="agent-other", credential_id="other-key"),
)
assert unauthorized.value.code == "PROPOSAL_NOT_FOUND"
changed_node = NODE.model_copy(update={"title": "Changed after acceptance"})
changed = proposal(node=changed_node, source=source, base_commit=candidate.base_commit)
with pytest.raises(AcceptanceError) as error:
service.submit(changed, actor=ACTOR)
assert error.value.code == "IDEMPOTENCY_CONFLICT"
def test_policy_exception_requires_exact_owner_decision_in_manifest(tmp_path: Path) -> None:
service, repository, _artifact_store, source, extraction = _service(tmp_path)
identities = SQLiteIdentityStore(
tmp_path / "owner-identity.sqlite3", owner_encryption_key=Fernet.generate_key()
)
secret = pyotp.random_base32()
identities.provision_owner(
principal_id="project-owner",
password="correct horse battery staple",
totp_secret=secret,
project_ids=("dome-performance", "shared"),
)
projects = {
**PROJECTS,
"dome-performance": PROJECTS["dome-performance"].model_copy(
update={"owners": ("project-owner",)}
),
"shared": PROJECTS["dome-performance"].model_copy(
update={
"project_id": "shared",
"title": "Shared knowledge",
"owners": ("project-owner",),
"autonomy_policy_id": "shared",
}
),
}
policies = {
**POLICIES,
"dome-policy": POLICIES["dome-policy"].model_copy(
update={"automatic_node_types": ("finding",)}
),
}
context = ValidationContext(
projects=projects,
policies=policies,
sources={},
extractions={extraction.extraction_id: extraction},
existing_nodes={},
)
service.context_provider = lambda: context
service.owner_challenge_issuer = identities.issue_owner_challenge
service.owner_decision_lookup = identities.authorized_owner_challenges
candidate = _proposal(repository, source)
held = service.submit(candidate, actor=ACTOR)
assert held.validation.status == "needs-review"
assert held.receipt is None
assert len(held.owner_challenge_ids) == 1
challenge = identities.get_owner_challenge(
held.owner_challenge_ids[0], principal_id="project-owner"
)
assert challenge is not None
assert challenge["diff_json"]
owner_session = identities.authenticate_owner(
principal_id="project-owner",
password="correct horse battery staple",
totp_code=pyotp.TOTP(secret).now(),
challenge_id=held.owner_challenge_ids[0],
audience="gewu-acceptance-test",
)
assert owner_session is not None
owner_principal = identities.authenticate_owner_session(
owner_session.session_token,
challenge_id=held.owner_challenge_ids[0],
audience="gewu-acceptance-test",
)
assert owner_principal is not None
decision = identities.decide_owner_challenge(
challenge_id=held.owner_challenge_ids[0],
principal_id="project-owner",
proposal_revision=candidate.revision,
payload_digest=candidate.payload_digest,
diff_digest=challenge["diff_digest"],
decision="authorize",
rationale="Reviewed the exact file diff and approve this policy exception.",
owner_session_id=owner_principal.credential_id.rpartition(":")[2],
audience="gewu-acceptance-test",
)
assert decision["status"] == "authorized"
original_context = context
changed_policies = {
**policies,
"dome-policy": policies["dome-policy"].model_copy(update={"policy_version": 2}),
}
context = ValidationContext(
projects=projects,
policies=changed_policies,
sources={},
extractions={extraction.extraction_id: extraction},
existing_nodes={},
)
stale_approval = service.submit(candidate, actor=ACTOR)
assert stale_approval.receipt is None
assert stale_approval.validation.status == "needs-review"
assert repository.head() == candidate.base_commit
context = original_context
accepted = service.submit(candidate, actor=ACTOR)
assert accepted.receipt is not None
assert accepted.validation.status == "needs-review"
raw_manifest = repository.read_file(
accepted.receipt.commit_id,
service._manifest_path(candidate.proposal_id, candidate.revision),
)
assert raw_manifest is not None
manifest = safe_yaml_record(raw_manifest, AcceptanceManifest)
assert len(manifest.owner_decisions) == 1
assert manifest.owner_decisions[0].challenge_id == held.owner_challenge_ids[0]
def test_contradiction_bundle_publishes_both_claims_and_their_typed_edge(
tmp_path: Path,
) -> None:
content = (
b"Field report: The seasonal audience count was 400.\n\n"
b"Vendor report: The seasonal audience count was 450.\n"
)
digest = hashlib.sha256(content).hexdigest()
artifact_store = LocalArtifactStore(tmp_path / "private-data")
upload = artifact_store.begin(
filename="conflicting-reports.md",
media_type="text/markdown",
byte_length=len(content),
expected_sha256=digest,
now=100,
)
artifact_store.put_chunk(
upload.upload_id,
offset=0,
content=content,
chunk_sha256=digest,
now=101,
)
artifact_store.finalize(upload.upload_id, now=102)
source = SOURCE_REVISION.model_copy(
update={"artifact_sha256": digest, "byte_length": len(content)}
)
extraction = TextMarkdownExtractor().extract(source.revision_id, content, "text/markdown")
def supported_node(
node_id: str, claim_id: str, quote: str, title: str, value: int
) -> KnowledgeNode:
block = next(item for item in extraction.blocks if quote in item.normalized_text)
start = block.normalized_text.index(quote)
evidence = (
NODE.claims[0]
.evidence[0]
.model_copy(
update={
"source_revision_id": source.revision_id,
"extraction_id": extraction.extraction_id,
"assertion_digest": assertion_digest(quote),
"quote": quote,
"locator": NODE.claims[0]
.evidence[0]
.locator.model_copy(
update={
"block_id": block.block_id,
"start": start,
"end": start + len(quote),
}
),
}
)
)
assessment = NODE.claims[0].assessment.model_copy(
update={
"assertion_digest": assertion_digest(quote),
"evidence_digests": (evidence_digest(evidence),),
}
)
claim = NODE.claims[0].model_copy(
update={
"claim_id": claim_id,
"text": quote,
"evidence": (evidence,),
"assessment": assessment,
}
)
return NODE.model_copy(
update={
"node_id": node_id,
"title": title,
"summary": quote,
"claims": (claim,),
"value": value,
}
)
first = supported_node(
"dome:count-field-report",
"dome:claim-count-400",
"The seasonal audience count was 400.",
"Field count",
400,
)
second = supported_node(
"dome:count-vendor-report",
"dome:claim-count-450",
"The seasonal audience count was 450.",
"Vendor count",
450,
)
edge = TypedEdge(
edge_id="dome:counts-conflict",
edge_type="contradicts",
source=EdgeEndpoint(kind="node", id=first.node_id),
target=EdgeEndpoint(kind="node", id=second.node_id),
conditions="Both reports refer to the same seasonal audience count.",
interpretation_rationale="The source-backed counts differ for the same reporting period.",
)
policies = {
**POLICIES,
"dome-policy": POLICIES["dome-policy"].model_copy(
update={"automatic_edge_types": ("contradicts",)}
),
}
repository = GitCanonicalRepository(tmp_path / "private-data" / "knowledge.git")
context = ValidationContext(
projects=PROJECTS,
policies=policies,
sources={},
extractions={extraction.extraction_id: extraction},
existing_nodes={},
)
service = AcceptanceService(
repository=repository,
artifact_store=artifact_store,
workflow_root=tmp_path / "private-data" / "staging",
context_provider=lambda: context,
actor_is_current=lambda _actor: True,
)
base = proposal(node=None, source=source, base_commit=repository.head())
candidate = base.model_copy(
update={
"operations": (
*base.operations,
ProposalOperation(operation="upsert_node", node=first),
ProposalOperation(operation="upsert_node", node=second),
ProposalOperation(operation="add_edge", edge=edge),
),
"payload_digest": "0" * 64,
}
)
candidate = candidate.model_copy(update={"payload_digest": proposal_payload_digest(candidate)})
result = service.submit(candidate, actor=ACTOR)
assert result.validation.status == "eligible", result.validation.findings
assert result.receipt is not None
assert result.receipt.accepted_node_ids == (first.node_id, second.node_id)
for node in (first, second):
stored = repository.read_file(
result.receipt.commit_id,
f"projects/{hashlib.sha256(node.project_id.encode()).hexdigest()}/nodes/"
f"{hashlib.sha256(node.node_id.encode()).hexdigest()}.yaml",
)
assert stored is not None
assert safe_yaml_record(stored, KnowledgeNode).claims[0].text == node.claims[0].text
stored_edge = repository.read_file(
result.receipt.commit_id,
f"edges/{hashlib.sha256(edge.edge_id.encode()).hexdigest()}.yaml",
)
assert stored_edge is not None
assert safe_yaml_record(stored_edge, TypedEdge) == edge
def test_independent_acceptance_services_exclude_a_concurrent_writer(tmp_path: Path) -> None:
entered_commit = threading.Event()
release_commit = threading.Event()
def hold_before_commit(stage: str) -> None:
if stage == "before_commit":
entered_commit.set()
assert release_commit.wait(timeout=5)
service, repository, artifact_store, source, _extraction = _service(
tmp_path, hook=hold_before_commit
)
second_context = ValidationContext(
projects=PROJECTS,
policies=POLICIES,
sources={},
extractions={EXTRACTION.extraction_id: EXTRACTION},
existing_nodes={},
)
second_service = AcceptanceService(
repository=repository,
artifact_store=artifact_store,
workflow_root=tmp_path / "private-data" / "staging",
context_provider=lambda: second_context,
actor_is_current=lambda _actor: True,
)
first = _proposal(repository, source).model_copy(update={"proposal_id": "proposal-writer-a"})
second = _proposal(repository, source).model_copy(update={"proposal_id": "proposal-writer-b"})
with ThreadPoolExecutor(max_workers=1) as workers:
pending = workers.submit(service.submit, first, actor=ACTOR)
try:
assert entered_commit.wait(timeout=5)
with pytest.raises(AcceptanceError) as busy:
second_service.submit(second, actor=ACTOR)
assert busy.value.code == "BUSY"
finally:
release_commit.set()
accepted = pending.result(timeout=5)
assert accepted.receipt is not None
assert repository.head() == accepted.receipt.commit_id
assert second_service.receipt(second.proposal_id, second.revision) is None
def test_acceptance_publishes_and_reads_evidence_backed_assumptions(tmp_path: Path) -> None:
service, repository, _artifact_store, source, extraction = _service(tmp_path)
original_claim = NODE.claims[0]
statement = "The installation's seasonal count is suitable for comparison."
statement_digest = assertion_digest(statement)
evidence = original_claim.evidence[0].model_copy(
update={
"assertion_path": "assumptions[0].text",
"assertion_digest": statement_digest,
}
)
claim = original_claim.model_copy(
update={
"claim_id": "dome:assumption-seasonal-comparison",
"text": statement,
"evidence": (evidence,),
"assessment": original_claim.assessment.model_copy(
update={
"assertion_digest": statement_digest,
"evidence_digests": (evidence_digest(evidence),),
}
),
}
)
node = NODE.model_copy(update={"assumptions": (claim,)})
candidate = proposal(node=node, source=source, base_commit=repository.head())
candidate = candidate.model_copy(update={"payload_digest": proposal_payload_digest(candidate)})
result = service.submit(
candidate,
actor=ACTOR,
staged_extractions={extraction.extraction_id: extraction},
)
assert result.receipt is not None
assert (
repository.read_file(
result.receipt.commit_id, f"extractions/{extraction.extraction_id}.json"
)
is not None
)
reader = CanonicalNodeReader(repository)
principal = Principal(
"reader", "reader-key", frozenset({"viewer"}), frozenset({"dome-performance"})
)
read = reader.get_node(node.project_id, node.node_id, principal=principal)
stored = read.data[0]
assert stored["assumptions"][0]["text"] == statement
assert any(item.assertion_digest == statement_digest for item in read.citations)
def test_reference_only_source_registers_without_an_artifact(tmp_path: Path) -> None:
service, repository, _artifacts, _source, _extraction = _service(tmp_path)
reference = SourceRevisionRecord(
source_id="dome:missing-paper",
revision_id="dome:missing-paper:r1",
media_type="application/octet-stream",
byte_length=0,
captured_at=SOURCE_REVISION.captured_at,
original_url="https://research.example/missing-paper",
title="Paper whose archive is unavailable",
license="unspecified",
source_type="paper",
reliability_grade=ReliabilityGrade.MEDIUM,
ingested_for=("dome-performance",),
archive_status="capture-failed",
)
candidate = ProposalRevision(
proposal_id="proposal-reference-only",
revision=1,
initiating_project_id="dome-performance",
scope=proposal().scope,
base_commit=repository.head(),
payload_digest="0" * 64,
proposer_id=ACTOR.principal_id,
run_id="run-reference-only",
operations=(ProposalOperation(operation="register_source", source=reference),),
)
candidate = candidate.model_copy(update={"payload_digest": proposal_payload_digest(candidate)})
result = service.submit(candidate, actor=ACTOR)
assert result.receipt is not None
assert result.receipt.state == "accepted"
paths = repository.list_files(repository.head(), "sources")
stored = next(repository.read_file(repository.head(), path) for path in paths)
assert stored is not None
assert b"capture-failed" in stored
assert b"artifact_sha256: null" in stored
@pytest.mark.parametrize("crash_stage", ("after_ref_update", "before_receipt"))
def test_ref_update_before_receipt_is_reconciled_without_republishing(
tmp_path: Path, crash_stage: str
) -> None:
def fail_after_ref_update(stage: str) -> None:
if stage == crash_stage:
raise SimulatedProcessCrash
service, repository, _store, source, _extraction = _service(
tmp_path, hook=fail_after_ref_update
)
candidate = _proposal(repository, source)
with pytest.raises(SimulatedProcessCrash):
service.submit(candidate, actor=ACTOR)
accepted_head = repository.head()
recovered, _same_repo, _same_store, _same_source, _same_extraction = (
_service_without_new_artifact(service, repository, tmp_path, source)
)
recovered.recover()
receipt = recovered.receipt(candidate.proposal_id, candidate.revision)
assert receipt is not None
assert receipt.commit_id == accepted_head
assert repository.head() == accepted_head
status = recovered.proposal_status(candidate.proposal_id, candidate.revision, actor=ACTOR)
assert status.lifecycle == "accepted"
assert status.receipt == receipt
retry = recovered.submit(candidate, actor=ACTOR)
assert retry.receipt == receipt
@pytest.mark.parametrize("crash_stage", ("after_ref_update", "before_receipt"))
def test_os_process_termination_recovers_publication_after_restart(
tmp_path: Path, crash_stage: str
) -> None:
service, _repository, _store, source, _extraction = _service(tmp_path)
candidate = _proposal(service.repository, source)
proposal_path = tmp_path / "candidate.json"
proposal_path.write_bytes(candidate.model_dump_json().encode("utf-8"))
crashed = subprocess.run(
[
sys.executable,
"-m",
"tests.application.crash_acceptance_worker",
str(tmp_path),
str(proposal_path),
crash_stage,
],
cwd=Path(__file__).parents[2],
capture_output=True,
check=False,
timeout=60,
)
assert crashed.returncode == 73, crashed.stderr.decode("utf-8", errors="replace")
# Construct fresh adapters as a new service process would, then recover from durable state.
recovered, repository, _store, _source, _extraction = _service(tmp_path)
committed_head = repository.head()
recovered.recover()
receipt = recovered.receipt(candidate.proposal_id, candidate.revision)
assert receipt is not None
assert receipt.commit_id == committed_head
status = recovered.proposal_status(candidate.proposal_id, candidate.revision, actor=ACTOR)
assert status.lifecycle == "accepted"
assert status.receipt == receipt
# The derived index was absent at the crash. Its first read after recovery must
# rebuild from the committed revision and retain the accepted claim's citation.
index = SQLiteSearchIndex(tmp_path / "private-data" / "search.sqlite3", repository)
assert index.diagnostic_status()[0] is None
reader = CanonicalSearchService(
index=index,
project_ids_provider=lambda: PROJECTS.keys(),
canonical_head=repository.head,
)
search_result = reader.search(
SearchRequest(
query="visitors",
scope=Scope(projects=("dome-performance",), include_shared=False),
budget_tokens=5000,
),
principal=Principal(
"recovery-reader",
"recovery-reader-key",
frozenset({"viewer"}),
frozenset({"dome-performance"}),
),
)
assert search_result.data
indexed_node = search_result.data[0]["node"]
assert isinstance(indexed_node, dict)
assert indexed_node["node_id"] == NODE.node_id
assert search_result.citations
retry = recovered.submit(candidate, actor=ACTOR)
assert retry.receipt == receipt
assert repository.head() == committed_head
def test_prepublication_crash_leaves_proposal_staged_and_retryable(tmp_path: Path) -> None:
def fail_before_commit(stage: str) -> None:
if stage == "before_commit":
raise SimulatedProcessCrash
service, repository, _store, source, _extraction = _service(tmp_path, hook=fail_before_commit)
candidate = _proposal(repository, source)
base = repository.head()
with pytest.raises(SimulatedProcessCrash):
service.submit(candidate, actor=ACTOR)
assert repository.head() == base
recovered, *_ = _service_without_new_artifact(service, repository, tmp_path, source)
recovered.recover()
status = recovered.proposal_status(candidate.proposal_id, candidate.revision, actor=ACTOR)
assert status.lifecycle == "staged"
retry = recovered.submit(candidate, actor=ACTOR)
assert retry.receipt is not None
assert repository.head() == retry.receipt.commit_id
def test_unrelated_submission_waits_until_pending_publication_is_reconciled(
tmp_path: Path,
) -> None:
crash_pending_writer = True
def fail_after_ref_update(stage: str) -> None:
nonlocal crash_pending_writer
if stage == "after_ref_update" and crash_pending_writer:
crash_pending_writer = False
raise SimulatedProcessCrash
service, repository, _store, source, _extraction = _service(
tmp_path, hook=fail_after_ref_update
)
pending = _proposal(repository, source)
with pytest.raises(SimulatedProcessCrash):
service.submit(pending, actor=ACTOR)
committed_head = repository.head()
journal_path = sorted((tmp_path / "private-data/staging/journal").glob("*/*.json"))[-1]
journal_entry = json.loads(journal_path.read_text(encoding="utf-8"))
expected_manifest_hash = journal_entry["manifest_sha256"]
journal_entry["manifest_sha256"] = "0" * 64
journal_path.write_text(json.dumps(journal_entry), encoding="utf-8")
followup_node = NODE.model_copy(
update={"node_id": "dome:follow-up-finding", "title": "Follow-up visitor finding"}
)
followup = proposal(
node=followup_node,
source=source,
base_commit=committed_head,
).model_copy(update={"proposal_id": "proposal-pilot-2"})
followup = followup.model_copy(update={"payload_digest": proposal_payload_digest(followup)})
with pytest.raises(AcceptanceError) as blocked:
service.submit(followup, actor=ACTOR)
assert blocked.value.code == "RECOVERY_REQUIRED"
assert repository.head() == committed_head
assert not service._proposal_path(followup.proposal_id, followup.revision).exists()
journal_entry["manifest_sha256"] = expected_manifest_hash
journal_path.write_text(json.dumps(journal_entry), encoding="utf-8")
service.recover()
pending_receipt = service.receipt(pending.proposal_id, pending.revision)
assert pending_receipt is not None
assert pending_receipt.commit_id == committed_head
completed = service.submit(followup, actor=ACTOR)
assert completed.receipt is not None
assert completed.receipt.commit_id == repository.head()
assert repository.head() != committed_head
def test_proposal_withdrawal_is_idempotent_and_prevents_later_submission(tmp_path: Path) -> None:
service, repository, _store, source, _extraction = _service(tmp_path)
initial_head = repository.head()
candidate = _proposal(repository, source).model_copy(update={"base_commit": "f" * 40})
candidate = candidate.model_copy(update={"payload_digest": "0" * 64})
candidate = candidate.model_copy(update={"payload_digest": proposal_payload_digest(candidate)})
evaluated = service.submit(candidate, actor=ACTOR)
assert evaluated.validation.status == "conflict"
assert evaluated.receipt is None
withdrawn = service.withdraw_proposal(candidate.proposal_id, candidate.revision, actor=ACTOR)
assert withdrawn.lifecycle == "withdrawn"
assert (
service.withdraw_proposal(candidate.proposal_id, candidate.revision, actor=ACTOR)
== withdrawn
)
assert (
service.proposal_status(candidate.proposal_id, candidate.revision, actor=ACTOR) == withdrawn
)
with pytest.raises(AcceptanceError) as error:
service.submit(candidate, actor=ACTOR)
assert error.value.code == "PROPOSAL_WITHDRAWN"
assert repository.head() == initial_head
def _service_without_new_artifact(
old_service: AcceptanceService,
repository: GitCanonicalRepository,
data_root: Path,
source: SourceRevisionRecord,
) -> tuple[
AcceptanceService,
GitCanonicalRepository,
LocalArtifactStore,
SourceRevisionRecord,
ExtractionRecord,
]:
from tests.domain.proposal_fixtures import EXTRACTION
store = old_service.artifact_store
context = ValidationContext(
projects=PROJECTS,
policies=POLICIES,
sources={},
extractions={EXTRACTION.extraction_id: EXTRACTION},
existing_nodes={},
)
service = AcceptanceService(
repository=repository,
artifact_store=store,
workflow_root=data_root / "private-data" / "staging",
context_provider=lambda: context,
actor_is_current=lambda _actor: True,
)
return service, repository, store, source, EXTRACTION
def test_changed_authority_blocks_publication(tmp_path: Path) -> None:
service, repository, _store, source, _extraction = _service(tmp_path, authorized=False)
candidate = _proposal(repository, source)
with pytest.raises(AcceptanceError) as error:
service.submit(candidate, actor=ACTOR)
assert error.value.code == "UNAUTHORIZED"
assert repository.head() == candidate.base_commit
def test_revoked_key_during_validation_is_rechecked_before_publication(tmp_path: Path) -> None:
service, repository, _store, source, _extraction = _service(tmp_path)
identities = SQLiteIdentityStore(tmp_path / "identity.sqlite3")
token = identities.provision(
credential_id="agent-key-1",
principal_id="agent-pilot",
roles=("agent",),
project_ids=("dome-performance", "shared"),
)
principal = identities.authenticate(token)
assert principal is not None
actor = ActorContext(
principal_id=principal.principal_id,
credential_id=principal.credential_id,
roles=principal.roles,
project_ids=principal.project_ids,
)
context = ValidationContext(
projects=PROJECTS,
policies=POLICIES,
sources={},
extractions={_extraction.extraction_id: _extraction},
existing_nodes={},
)
context_reads = 0
def context_provider() -> ValidationContext:
nonlocal context_reads
context_reads += 1
if context_reads == 2:
assert identities.revoke("agent-key-1")
return context
service.context_provider = context_provider
service.actor_is_current = lambda candidate: identities.is_current(
candidate.principal_id,
candidate.credential_id,
roles=candidate.roles,
project_ids=candidate.project_ids,
)
candidate = _proposal(repository, source)
with pytest.raises(AcceptanceError) as error:
service.submit(candidate, actor=actor)
assert error.value.code == "POLICY_CHANGED"
assert repository.head() == candidate.base_commit
status = service.proposal_status(candidate.proposal_id, candidate.revision, actor=actor)
assert status.lifecycle == "evaluated"
assert status.receipt is None
+305
View File
@@ -0,0 +1,305 @@
from __future__ import annotations
import hashlib
import sqlite3
from pathlib import Path
import pytest
import yaml
from gewu.adapters.git_repository import GitCanonicalRepository
from gewu.adapters.sqlite_search import SearchIndexError, SQLiteSearchIndex
from gewu.adapters.sqlite_vectors import SQLiteVectorIndex
from gewu.application.identity import Principal
from gewu.application.search import CanonicalSearchService, HybridSearchIndex, SearchError
from gewu.domain.contracts import (
Confidence,
EdgeEndpoint,
NodeType,
Scope,
SearchRequest,
TypedEdge,
)
from tests.domain.proposal_fixtures import NODE
class FixtureEmbeddings:
dimensions = 2
model_id = "fixture-e5@v1"
artifact_sha256 = "b" * 64
def embed_queries(self, texts: tuple[str, ...]) -> tuple[tuple[float, ...], ...]:
return tuple((1.0, 0.0) for _ in texts)
def embed_passages(self, texts: tuple[str, ...]) -> tuple[tuple[float, ...], ...]:
return tuple(
(1.0, 0.0) if text.startswith("Semantic match") else (0.0, 1.0) for text in texts
)
def _record_path(node_id: str, project_id: str) -> str:
project_key = hashlib.sha256(project_id.encode()).hexdigest()
node_key = hashlib.sha256(node_id.encode()).hexdigest()
return f"projects/{project_key}/nodes/{node_key}.yaml"
def _repository(tmp_path: Path) -> GitCanonicalRepository:
repository = GitCanonicalRepository(tmp_path / "knowledge.git")
nodes = [
NODE.model_copy(
update={
"node_id": f"dome:alpha-{number:02}",
"title": f"Alpha candidate {number:02}",
"summary": f"Alpha lexical candidate {number:02}.",
"node_type": NodeType.FINDING,
"confidence": Confidence.MEDIUM,
}
)
for number in range(12)
]
nodes.extend(
(
NODE.model_copy(
update={
"node_id": "dome:semantic-match",
"title": "Semantic match for the intent",
"summary": "A close meaning with no shared query word.",
"node_type": NodeType.TECHNIQUE,
"confidence": Confidence.HIGH,
}
),
NODE.model_copy(
update={
"node_id": "dome:graph-related",
"title": "Connected nonlexical technique",
"summary": "A related result found through one typed edge.",
"node_type": NodeType.TECHNIQUE,
"confidence": Confidence.MEDIUM,
}
),
NODE.model_copy(
update={
"node_id": "video:semantic-match",
"project_id": "interactive-video",
"title": "Semantic match in another project",
"summary": "Must remain outside the requested project scope.",
"node_type": NodeType.TECHNIQUE,
"confidence": Confidence.HIGH,
}
),
)
)
edges = (
TypedEdge(
edge_id="dome:search-expansion-edge",
edge_type="compared_with",
source=EdgeEndpoint(kind="node", id="dome:alpha-00"),
target=EdgeEndpoint(kind="node", id="dome:graph-related"),
interpretation_rationale="Both notes inform the same audience-attendance question.",
),
TypedEdge(
edge_id="dome:search-cross-project-edge",
edge_type="compared_with",
source=EdgeEndpoint(kind="node", id="dome:alpha-00"),
target=EdgeEndpoint(kind="node", id="video:semantic-match"),
interpretation_rationale="Cross-project edge must not widen search scope.",
),
)
files = {
_record_path(node.node_id, node.project_id): yaml.safe_dump(
node.model_dump(mode="json")
).encode()
for node in nodes
}
files.update(
{
f"edges/{hashlib.sha256(edge.edge_id.encode()).hexdigest()}.yaml": yaml.safe_dump(
edge.model_dump(mode="json")
).encode()
for edge in edges
}
)
repository.publish_files(
expected_head=repository.head(),
files=files,
message="Add hybrid search fixtures",
index_directory=tmp_path / "publishing",
)
return repository
def test_hybrid_search_fuses_ranks_and_preserves_scope_and_filters(
tmp_path: Path, monkeypatch
) -> None:
from gewu.application import search as search_module
monkeypatch.setattr(search_module, "_VECTOR_CANDIDATE_LIMIT", 1)
repository = _repository(tmp_path)
search_path = tmp_path / "search.sqlite3"
lexical = SQLiteSearchIndex(search_path, repository)
vectors = SQLiteVectorIndex(search_path)
hybrid = HybridSearchIndex(
lexical=lexical,
vectors=vectors,
embeddings=FixtureEmbeddings(),
)
lexical.configure_vector_generation(
extension_loader=vectors.load_extension,
builder=hybrid.build_vector_generation,
is_current=hybrid.vector_generation_is_current,
)
service = CanonicalSearchService(
index=hybrid,
project_ids_provider=lambda: ("dome-performance", "interactive-video"),
canonical_head=repository.head,
)
principal = Principal(
"reader", "reader-key", frozenset({"viewer"}), frozenset({"dome-performance"})
)
request = SearchRequest(
query="alpha",
scope=Scope(projects=("dome-performance",), include_shared=False),
limit=20,
budget_tokens=50_000,
)
response = service.search(request, principal=principal)
first_generation = vectors.active_generation()
assert first_generation is not None
assert lexical.path == vectors.path
assert first_generation.revision == response.index_revision
assert first_generation.node_count == 15
result_ids = [item["node"]["node_id"] for item in response.data]
assert response.retrieval_mode == "hybrid"
assert result_ids.index("dome:semantic-match") < result_ids.index("dome:alpha-11")
graph_hit = next(
item for item in response.data if item["node"]["node_id"] == "dome:graph-related"
)
assert graph_hit["retrieval_path"] == "graph_expansion"
assert graph_hit["expanded_from"] == "dome:alpha-00"
assert graph_hit["expansion_edge"]["edge_type"] == "compared_with"
assert not any(node_id.startswith("video:") for node_id in result_ids)
assert all("retrieval_score" in item for item in response.data)
first_page = service.search(request.model_copy(update={"limit": 5}), principal=principal)
assert first_page.next_cursor is not None
second_page = service.search(
request.model_copy(update={"limit": 5, "cursor": first_page.next_cursor}),
principal=principal,
)
first_ids = {item["node"]["node_id"] for item in first_page.data}
second_ids = {item["node"]["node_id"] for item in second_page.data}
assert len(first_ids) == len(second_ids) == 5
assert first_ids.isdisjoint(second_ids)
class ChangedModelEmbeddings(FixtureEmbeddings):
model_id = "fixture-e5@v2"
changed_hybrid = HybridSearchIndex(
lexical=lexical,
vectors=vectors,
embeddings=ChangedModelEmbeddings(),
)
changed_profile_service = CanonicalSearchService(
index=changed_hybrid,
project_ids_provider=lambda: ("dome-performance", "interactive-video"),
canonical_head=repository.head,
)
with pytest.raises(SearchError, match="cursor is invalid"):
changed_profile_service.search(
request.model_copy(update={"limit": 5, "cursor": first_page.next_cursor}),
principal=principal,
)
filtered = service.search(
request.model_copy(
update={"node_types": (NodeType.TECHNIQUE,), "confidences": (Confidence.HIGH,)}
),
principal=principal,
)
assert [item["node"]["node_id"] for item in filtered.data] == ["dome:semantic-match"]
assert filtered.retrieval_mode == "hybrid"
lexical.configure_vector_generation(
extension_loader=vectors.load_extension,
builder=changed_hybrid.build_vector_generation,
is_current=changed_hybrid.vector_generation_is_current,
)
changed_response = changed_profile_service.search(
request.model_copy(update={"limit": 5}), principal=principal
)
changed_generation = vectors.active_generation()
assert changed_response.retrieval_mode == "hybrid"
assert changed_generation is not None and changed_generation.model_id == "fixture-e5@v2"
assert vectors.generation(first_generation.generation_id) is not None
class FailingEmbeddings(FixtureEmbeddings):
model_id = "fixture-e5@failed-build"
def embed_passages(self, texts: tuple[str, ...]) -> tuple[tuple[float, ...], ...]:
raise RuntimeError("fixture model failure")
failing_hybrid = HybridSearchIndex(
lexical=lexical,
vectors=vectors,
embeddings=FailingEmbeddings(),
)
lexical.configure_vector_generation(
extension_loader=vectors.load_extension,
builder=failing_hybrid.build_vector_generation,
is_current=failing_hybrid.vector_generation_is_current,
)
degraded = CanonicalSearchService(
index=failing_hybrid,
project_ids_provider=lambda: ("dome-performance", "interactive-video"),
canonical_head=repository.head,
).search(request.model_copy(update={"limit": 5}), principal=principal)
assert degraded.retrieval_mode == "keyword"
assert any(item.code == "HYBRID_RETRIEVAL_DEGRADED" for item in degraded.warnings)
assert degraded.next_cursor is None
def test_fts_and_vector_publication_roll_back_as_one_transaction(tmp_path: Path) -> None:
repository = _repository(tmp_path)
search_path = tmp_path / "search.sqlite3"
lexical = SQLiteSearchIndex(search_path, repository)
vectors = SQLiteVectorIndex(search_path)
hybrid = HybridSearchIndex(
lexical=lexical,
vectors=vectors,
embeddings=FixtureEmbeddings(),
)
def fail_after_vector_publish(connection, revision, nodes) -> None:
hybrid.build_vector_generation(connection, revision, nodes)
raise sqlite3.OperationalError("simulated termination before index commit")
lexical.configure_vector_generation(
extension_loader=vectors.load_extension,
builder=fail_after_vector_publish,
is_current=hybrid.vector_generation_is_current,
)
with pytest.raises(SearchIndexError):
lexical.search(
query="alpha",
project_ids=("dome-performance",),
include_shared=False,
limit=5,
)
assert lexical.diagnostic_status()[0] is None
assert vectors.active_generation() is None
lexical.configure_vector_generation(
extension_loader=vectors.load_extension,
builder=hybrid.build_vector_generation,
is_current=hybrid.vector_generation_is_current,
)
recovered = hybrid.search(
query="alpha",
project_ids=("dome-performance",),
include_shared=False,
limit=5,
)
active = vectors.active_generation()
assert recovered.retrieval_mode == "hybrid"
assert active is not None and active.revision == recovered.index_revision
+165
View File
@@ -0,0 +1,165 @@
import hashlib
import json
import subprocess
import sys
from pathlib import Path
import portalocker
import pytest
import yaml
from gewu.adapters.git_repository import GitCanonicalRepository
from gewu.application.registry import LocalContextProvider, LocalProjectRegistry
from gewu.domain.contracts import AutoAcceptPolicy, ProjectRecord, ProjectStatus
from gewu.domain.validation import extraction_digest
from tests.domain.proposal_fixtures import EXTRACTION, NODE, POLICIES, PROJECTS
def test_project_registry_create_and_archive_are_atomic_and_idempotent(tmp_path: Path) -> None:
registry_file = tmp_path / "control" / "registry.json"
registry = LocalProjectRegistry(
registry_file,
repository=GitCanonicalRepository(tmp_path / "knowledge.git"),
index_directory=tmp_path / "publishing",
authority_lock_path=tmp_path / "workflow" / "locks" / "canonical-writer.lock",
)
project = ProjectRecord(
project_id="new-research",
title="New research",
status=ProjectStatus.ACTIVE,
owners=("owner-one",),
north_star="Investigate a new research area.",
key_questions=("What is known?",),
autonomy_policy_id="new-policy",
default_includes=("shared",),
)
policy = AutoAcceptPolicy(
policy_id="new-policy",
policy_version=1,
project_id="new-research",
require_supported_assessment=True,
)
assert registry.create(project, policy) == project
assert registry.repository is not None
revision_after_create = registry.repository.head()
assert (
registry.repository.read_file(revision_after_create, "projects/new-research/project.md")
is not None
)
assert registry.repository.list_files(revision_after_create, "policies")
canonical_context = LocalContextProvider(registry.repository, registry_file)()
assert canonical_context.projects["new-research"] == project
assert canonical_context.policies["new-policy"] == policy
assert registry.records() == (project,)
assert "new-research" in registry()
lock_path = tmp_path / "workflow" / "locks" / "canonical-writer.lock"
with portalocker.Lock(lock_path, mode="a", timeout=0):
with pytest.raises(portalocker.exceptions.LockException):
registry.archive("new-research")
assert registry.archive("new-research").status == ProjectStatus.ARCHIVED
assert registry.repository.head() != revision_after_create
assert registry.archive("new-research").status == ProjectStatus.ARCHIVED
assert registry.records()[0].status == ProjectStatus.ARCHIVED
assert (
LocalContextProvider(registry.repository, registry_file)().projects["new-research"].status
== ProjectStatus.ARCHIVED
)
def test_archive_migrates_legacy_project_to_canonical_registry(tmp_path: Path) -> None:
registry_file = tmp_path / "control" / "registry.json"
registry_file.parent.mkdir(parents=True)
registry_file.write_text(
json.dumps(
{
"schema_version": 1,
"projects": [project.model_dump(mode="json") for project in PROJECTS.values()],
"policies": [policy.model_dump(mode="json") for policy in POLICIES.values()],
}
),
encoding="utf-8",
)
repository = GitCanonicalRepository(tmp_path / "knowledge.git")
registry = LocalProjectRegistry(
registry_file,
repository=repository,
index_directory=tmp_path / "publishing",
authority_lock_path=tmp_path / "workflow" / "locks" / "canonical-writer.lock",
)
archived = registry.archive("dome-performance")
context = LocalContextProvider(repository, registry_file)()
assert archived.status == ProjectStatus.ARCHIVED
assert context.projects["dome-performance"].status == ProjectStatus.ARCHIVED
assert context.projects["interactive-video"] == PROJECTS["interactive-video"]
assert context.policies == POLICIES
def test_canonical_registry_retains_prior_extractions_after_parser_replacement(
tmp_path: Path,
) -> None:
repository = GitCanonicalRepository(tmp_path / "knowledge.git")
prior = EXTRACTION
replacement_candidate = prior.model_copy(
update={"extraction_id": "0" * 64, "parser_version": "replacement-parser-1"}
)
replacement = replacement_candidate.model_copy(
update={"extraction_id": extraction_digest(replacement_candidate)}
)
node_path = (
f"projects/{hashlib.sha256(NODE.project_id.encode()).hexdigest()}/nodes/"
f"{hashlib.sha256(NODE.node_id.encode()).hexdigest()}.yaml"
)
first_revision = repository.publish_files(
expected_head=repository.head(),
files={
f"extractions/{prior.extraction_id}.json": prior.model_dump_json().encode(),
node_path: yaml.safe_dump(NODE.model_dump(mode="json")).encode(),
},
message="Publish a cited node using the original parser extraction",
index_directory=tmp_path / "publishing",
)
replacement_revision = repository.publish_files(
expected_head=repository.head(),
files={
f"extractions/{replacement.extraction_id}.json": replacement.model_dump_json().encode()
},
message="Add replacement parser extraction without deleting history",
index_directory=tmp_path / "publishing",
)
context = LocalContextProvider(repository, tmp_path / "control/registry.json")()
assert context.extractions[prior.extraction_id] == prior
assert context.extractions[replacement.extraction_id] == replacement
assert context.existing_nodes[NODE.node_id] == NODE
assert (
context.existing_nodes[NODE.node_id].claims[0].evidence[0].extraction_id
== prior.extraction_id
)
assert (
repository.read_file(first_revision, f"extractions/{prior.extraction_id}.json") is not None
)
assert (
repository.read_file(replacement_revision, f"extractions/{prior.extraction_id}.json")
is not None
)
def test_disposable_registry_seed_loads_with_versioned_contracts(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setenv("GEWU_DATA_ROOT", str(tmp_path))
result = subprocess.run(
[sys.executable, "scripts/seed_test_registry.py"],
check=False,
capture_output=True,
text=True,
)
assert result.returncode == 0, result.stderr
repository = GitCanonicalRepository(tmp_path / "knowledge.git")
context = LocalContextProvider(repository, tmp_path / "control/registry.json")()
assert set(context.projects) == {"dome-performance", "interactive-video"}
assert set(context.policies) == {"dome-pilot-policy", "video-pilot-policy", "shared"}
+106
View File
@@ -0,0 +1,106 @@
from pathlib import Path
import pytest
from pydantic import ValidationError
from gewu.application.acceptance import ActorContext
from gewu.application.runs import LocalRunService, RunError
from gewu.domain.contracts import (
RunCheckpointRequest,
RunCreateRequest,
RunModulePin,
RunState,
Scope,
)
def _create(run_id: str = "run-pilot") -> RunCreateRequest:
return RunCreateRequest(
run_id=run_id,
objective="Compare audience response",
scope=Scope(projects=("dome-performance",), include_shared=False),
lens="Audience behavior",
pack_id="gewu",
pack_version="0.3.0",
pack_snapshot_digest="a" * 64,
module_pins=(
RunModulePin(
module_id="profile",
implementation_version="1.0.0",
sha256="b" * 64,
),
),
)
def _checkpoint(run_id: str = "run-pilot", **overrides: object) -> RunCheckpointRequest:
values: dict[str, object] = {
"run_id": run_id,
"expected_revision": 0,
"stage": "source-review",
"state": RunState.ACTIVE,
"budgets": {"source_count": 3},
}
values.update(overrides)
return RunCheckpointRequest.model_validate(values)
def test_run_create_and_checkpoint_are_idempotent_and_revision_checked(tmp_path: Path) -> None:
current = True
actor = ActorContext("agent-pilot", "pilot-key")
service = LocalRunService(
workflow_root=tmp_path / "workflow",
actor_is_current=lambda _: current,
)
created = service.create(_create(), actor=actor)
assert created.revision == 0
assert service.create(_create(), actor=actor) == created
with pytest.raises(RunError, match="different input"):
service.create(_create().model_copy(update={"objective": "Different"}), actor=actor)
checkpoint = _checkpoint()
updated = service.checkpoint(checkpoint, actor=actor)
assert updated.revision == 1
assert service.checkpoint(checkpoint, actor=actor) == updated
with pytest.raises(RunError, match="changed content"):
service.checkpoint(_checkpoint(stage="different-stage"), actor=actor)
service.checkpoint(_checkpoint(expected_revision=1, stage="next-stage"), actor=actor)
with pytest.raises(RunError, match="stale"):
service.checkpoint(_checkpoint(expected_revision=0, stage="different-stage"), actor=actor)
assert service.status("run-pilot", actor=actor).revision == 2
def test_run_owner_revocation_and_terminal_state_are_enforced(tmp_path: Path) -> None:
active = True
owner = ActorContext("agent-pilot", "pilot-key")
other = ActorContext("agent-other", "other-key")
service = LocalRunService(
workflow_root=tmp_path / "workflow",
actor_is_current=lambda actor: active and actor.credential_id in {"pilot-key", "other-key"},
)
service.create(_create(), actor=owner)
with pytest.raises(RunError, match="unavailable"):
service.status("run-pilot", actor=other)
completed = service.checkpoint(_checkpoint(state=RunState.COMPLETED), actor=owner)
assert completed.state is RunState.COMPLETED
with pytest.raises(RunError, match="cannot be updated"):
service.checkpoint(_checkpoint(expected_revision=1), actor=owner)
active = False
with pytest.raises(RunError, match="no longer active"):
service.checkpoint(_checkpoint(expected_revision=1), actor=owner)
with pytest.raises(RunError, match="no longer active"):
service.status("run-pilot", actor=owner)
def test_run_contract_rejects_global_scope_and_negative_budgets() -> None:
with pytest.raises(ValidationError):
RunCreateRequest.model_validate(
_create().model_dump() | {"scope": Scope(projects=("all",), all_projects=True)}
)
with pytest.raises(ValidationError):
RunCheckpointRequest.model_validate(
{"run_id": "run-pilot", "expected_revision": 0, "stage": "x", "budgets": {"tokens": -1}}
)
+84
View File
@@ -0,0 +1,84 @@
import json
from pathlib import Path
from typing import cast
import pytest
from gewu.application.identity import Principal
from gewu.application.registry import LocalProjectRegistry
from gewu.application.search import CanonicalSearchService, SearchError, SearchIndex
from gewu.domain.contracts import ProjectRecord, ProjectStatus, Scope
def _service() -> CanonicalSearchService:
return CanonicalSearchService(
index=cast(SearchIndex, object()),
project_ids_provider=lambda: ("dome-performance", "interactive-video"),
canonical_head=lambda: "a" * 40,
default_includes_provider=lambda project_id: (
("interactive-video", "shared") if project_id == "dome-performance" else ()
),
)
def _principal(*projects: str) -> Principal:
return Principal("reader", "reader-key", frozenset({"viewer"}), frozenset(projects))
def test_single_project_scope_resolves_defaults_and_shared() -> None:
resolved = _service()._resolve_scope(
Scope(projects=("dome-performance",)),
_principal("dome-performance", "interactive-video", "shared"),
)
assert resolved == ("dome-performance", "interactive-video")
def test_explicit_empty_includes_suppresses_project_defaults() -> None:
resolved = _service()._resolve_scope(
Scope(projects=("dome-performance",), include_projects=(), include_shared=False),
_principal("dome-performance", "interactive-video", "shared"),
)
assert resolved == ("dome-performance",)
def test_default_includes_must_be_authorized() -> None:
with pytest.raises(SearchError, match="cannot read every requested project") as error:
_service()._resolve_scope(
Scope(projects=("dome-performance",)), _principal("dome-performance", "shared")
)
assert error.value.status_code == 403
def test_explicit_include_projects_override_defaults() -> None:
resolved = _service()._resolve_scope(
Scope(projects=("dome-performance",), include_projects=("interactive-video",)),
_principal("dome-performance", "interactive-video", "shared"),
)
assert resolved == ("dome-performance", "interactive-video")
def test_local_project_registry_exposes_validated_default_includes(tmp_path: Path) -> None:
project = ProjectRecord(
project_id="dome-performance",
title="Dome",
status=ProjectStatus.ACTIVE,
north_star="Build useful dome experiences.",
key_questions=("What works?",),
default_includes=("interactive-video", "shared"),
autonomy_policy_id="dome-policy",
)
registry_file = tmp_path / "registry.json"
registry_file.write_text(
json.dumps(
{"schema_version": 1, "projects": [project.model_dump(mode="json")], "policies": []}
),
encoding="utf-8",
)
registry = LocalProjectRegistry(registry_file)
assert registry.default_includes("dome-performance") == ("interactive-video",)
+395
View File
@@ -0,0 +1,395 @@
import hashlib
from pathlib import Path
import pytest
from gewu.application.uploads import CHUNK_BYTES, LocalArtifactStore, UploadError
from tests.domain.proposal_fixtures import EXTRACTION
def _begin(store: LocalArtifactStore, content: bytes, *, now: int = 1_000) -> str:
session = store.begin(
filename="selected.md",
media_type="text/markdown",
byte_length=len(content),
expected_sha256=hashlib.sha256(content).hexdigest(),
now=now,
)
return session.upload_id
def test_matching_chunk_retry_and_restart_are_idempotent(tmp_path: Path) -> None:
content = b"# Evidence\nA source block.\n"
first_store = LocalArtifactStore(tmp_path)
upload_id = _begin(first_store, content)
digest = hashlib.sha256(content).hexdigest()
assert first_store.put_chunk(
upload_id, offset=0, content=content, chunk_sha256=digest, now=1_001
) == (0,)
restarted = LocalArtifactStore(tmp_path)
assert restarted.verified_offsets(upload_id, now=1_002) == (0,)
assert restarted.put_chunk(
upload_id, offset=0, content=content, chunk_sha256=digest, now=1_002
) == (0,)
receipt = restarted.finalize(upload_id, now=1_003)
assert receipt.sha256 == digest
assert receipt.status == "staged"
with restarted.open_artifact(receipt.sha256) as artifact:
assert artifact.read() == content
def test_pasted_capture_receipt_survives_restart_and_conflicts_on_changed_input(
tmp_path: Path,
) -> None:
first = LocalArtifactStore(tmp_path)
result = {"sha256": "a" * 64, "source_candidate": {"revision_id": "paste:r1"}}
with first.capture_receipt_guard():
first.save_capture_receipt(
"capture-1",
owner_id="agent-a",
project_id="project-a",
request_sha256="b" * 64,
result=result,
)
restarted = LocalArtifactStore(tmp_path)
with restarted.capture_receipt_guard():
assert (
restarted.get_capture_receipt(
"capture-1",
owner_id="agent-a",
project_id="project-a",
request_sha256="b" * 64,
)
== result
)
with pytest.raises(UploadError) as error:
restarted.get_capture_receipt(
"capture-1",
owner_id="agent-a",
project_id="project-a",
request_sha256="c" * 64,
)
assert error.value.code == "CAPTURE_CONFLICT"
with pytest.raises(UploadError) as error:
restarted.get_capture_receipt(
"capture-1",
owner_id="agent-a",
project_id="project-b",
request_sha256="b" * 64,
)
assert error.value.code == "CAPTURE_CONFLICT"
assert (
restarted.get_capture_receipt(
"capture-1",
owner_id="agent-b",
project_id="project-a",
request_sha256="b" * 64,
)
is None
)
def test_upload_session_is_private_to_its_principal(tmp_path: Path) -> None:
store = LocalArtifactStore(tmp_path)
content = b"# Scoped upload\n"
upload_id = store.begin(
filename="scoped.md",
media_type="text/markdown",
byte_length=len(content),
expected_sha256=hashlib.sha256(content).hexdigest(),
owner_id="agent-a",
).upload_id
with pytest.raises(UploadError) as error:
store.verified_offsets(upload_id, owner_id="agent-b")
assert error.value.code == "UPLOAD_NOT_FOUND"
with pytest.raises(UploadError) as error:
store.put_chunk(
upload_id,
offset=0,
content=content,
chunk_sha256=hashlib.sha256(content).hexdigest(),
owner_id="agent-b",
)
assert error.value.code == "UPLOAD_NOT_FOUND"
def test_staged_extraction_is_immutable_and_principal_scoped(tmp_path: Path) -> None:
store = LocalArtifactStore(tmp_path)
store.save_extraction(EXTRACTION, owner_id="agent-a")
assert store.get_extraction(EXTRACTION.extraction_id, owner_id="agent-a") == EXTRACTION
assert store.get_extraction(EXTRACTION.extraction_id, owner_id="agent-b") is None
with pytest.raises(UploadError) as error:
store.save_extraction(EXTRACTION, owner_id="agent-b")
assert error.value.code == "EXTRACTION_STORE_CONFLICT"
def test_artifact_access_is_scoped_to_principal_and_project(tmp_path: Path) -> None:
store = LocalArtifactStore(tmp_path)
content = b"private project source"
digest = hashlib.sha256(content).hexdigest()
upload_id = store.begin(
filename="source.md",
media_type="text/markdown",
byte_length=len(content),
expected_sha256=digest,
owner_id="agent-a",
project_id="project-a",
).upload_id
store.put_chunk(
upload_id,
offset=0,
content=content,
chunk_sha256=digest,
owner_id="agent-a",
)
store.finalize(upload_id, owner_id="agent-a")
assert (
store.artifact_media_type(digest, owner_id="agent-a", project_id="project-a")
== "text/markdown"
)
with store.open_artifact(digest, owner_id="agent-a", project_id="project-a") as stream:
assert stream.read() == content
with pytest.raises(UploadError):
store.open_artifact(digest, owner_id="agent-b", project_id="project-a")
with pytest.raises(UploadError):
store.open_artifact(digest, owner_id="agent-a", project_id="project-b")
def test_changed_retry_at_same_offset_is_rejected(tmp_path: Path) -> None:
store = LocalArtifactStore(tmp_path)
upload_id = _begin(store, b"abc")
store.put_chunk(
upload_id,
offset=0,
content=b"abc",
chunk_sha256=hashlib.sha256(b"abc").hexdigest(),
now=1_001,
)
with pytest.raises(UploadError, match="different bytes") as error:
store.put_chunk(
upload_id,
offset=0,
content=b"xyz",
chunk_sha256=hashlib.sha256(b"xyz").hexdigest(),
now=1_002,
)
assert error.value.code == "IDEMPOTENCY_CONFLICT"
def test_chunk_and_complete_artifact_digests_are_verified(tmp_path: Path) -> None:
store = LocalArtifactStore(tmp_path)
content = b"verified bytes"
digest = hashlib.sha256(content).hexdigest()
upload_id = store.begin(
filename="verified.md",
media_type="text/markdown",
byte_length=len(content),
expected_sha256="f" * 64,
).upload_id
with pytest.raises(UploadError) as error:
store.put_chunk(
upload_id,
offset=0,
content=content,
chunk_sha256="0" * 64,
)
assert error.value.code == "CHUNK_DIGEST_MISMATCH"
store.put_chunk(upload_id, offset=0, content=content, chunk_sha256=digest)
with pytest.raises(UploadError) as error:
store.finalize(upload_id)
assert error.value.code == "ARTIFACT_DIGEST_MISMATCH"
with pytest.raises(UploadError) as error:
store.open_artifact("f" * 64)
assert error.value.code == "ARTIFACT_NOT_FOUND"
def test_interrupted_upload_does_not_publish_an_artifact(tmp_path: Path) -> None:
store = LocalArtifactStore(tmp_path)
content = b"a" * (CHUNK_BYTES + 1)
digest = hashlib.sha256(content).hexdigest()
upload_id = store.begin(
filename="partial.md",
media_type="text/markdown",
byte_length=len(content),
expected_sha256=digest,
owner_id="agent-a",
project_id="project-a",
).upload_id
first_chunk = content[:CHUNK_BYTES]
store.put_chunk(
upload_id,
offset=0,
content=first_chunk,
chunk_sha256=hashlib.sha256(first_chunk).hexdigest(),
owner_id="agent-a",
)
with pytest.raises(UploadError) as error:
store.finalize(upload_id, owner_id="agent-a")
assert error.value.code == "UPLOAD_INCOMPLETE"
with pytest.raises(UploadError) as error:
store.open_artifact(digest, owner_id="agent-a", project_id="project-a")
assert error.value.code == "ARTIFACT_NOT_FOUND"
def test_symlinked_upload_paths_are_not_followed(tmp_path: Path) -> None:
store = LocalArtifactStore(tmp_path)
outside = tmp_path / "outside"
outside.mkdir()
sensitive = outside / "chunk"
sensitive.write_bytes(b"data")
forged_id = "A" * 24
try:
(store.upload_root / forged_id).symlink_to(outside, target_is_directory=True)
except OSError as error:
pytest.skip(f"directory symlinks are unavailable: {error}")
with pytest.raises(UploadError) as error:
store.verified_offsets(forged_id)
assert error.value.code == "UPLOAD_NOT_FOUND"
upload_id = _begin(store, b"data")
chunk_link = store.upload_root / upload_id / f"{0:020d}.chunk"
chunk_link.symlink_to(sensitive)
assert store.verified_offsets(upload_id, now=1_001) == ()
with pytest.raises(UploadError) as error:
store.finalize(upload_id, now=1_001)
assert error.value.code == "UPLOAD_INCOMPLETE"
assert sensitive.read_bytes() == b"data"
def test_upload_requires_order_complete_digest_and_utf8(tmp_path: Path) -> None:
store = LocalArtifactStore(tmp_path)
content = b"a" * (CHUNK_BYTES + 1)
upload_id = _begin(store, content)
final_chunk = content[CHUNK_BYTES:]
with pytest.raises(UploadError, match="in order"):
store.put_chunk(
upload_id,
offset=CHUNK_BYTES,
content=final_chunk,
chunk_sha256=hashlib.sha256(final_chunk).hexdigest(),
now=1_001,
)
with pytest.raises(UploadError) as error:
store.finalize(upload_id, now=1_002)
assert error.value.code == "UPLOAD_INCOMPLETE"
invalid_bytes = b"\xff"
invalid_id = _begin(store, invalid_bytes)
store.put_chunk(
invalid_id,
offset=0,
content=invalid_bytes,
chunk_sha256=hashlib.sha256(invalid_bytes).hexdigest(),
now=1_001,
)
with pytest.raises(UploadError, match="UTF-8") as error:
store.finalize(invalid_id, now=1_002)
assert error.value.code == "TEXT_ENCODING_INVALID"
def test_rejects_unsupported_types_expiry_and_malicious_ids(tmp_path: Path) -> None:
store = LocalArtifactStore(tmp_path)
with pytest.raises(UploadError) as error:
store.begin(
filename="payload.exe",
media_type="application/octet-stream",
byte_length=4,
expected_sha256=hashlib.sha256(b"data").hexdigest(),
now=1_000,
)
assert error.value.code == "UNSUPPORTED_MEDIA_TYPE"
upload_id = _begin(store, b"data")
with pytest.raises(UploadError) as error:
store.verified_offsets(upload_id, now=1_601)
assert error.value.code == "UPLOAD_EXPIRED"
with pytest.raises(UploadError) as error:
store.verified_offsets("../../outside", now=1_001)
assert error.value.code == "UPLOAD_NOT_FOUND"
def test_pdf_upload_requires_matching_suffix_and_pdf_signature(tmp_path: Path) -> None:
store = LocalArtifactStore(tmp_path)
payload = b"%PDF-fixture"
digest = hashlib.sha256(payload).hexdigest()
with pytest.raises(UploadError) as error:
store.begin(
filename="source.txt",
media_type="application/pdf",
byte_length=len(payload),
expected_sha256=digest,
)
assert error.value.code == "UNSUPPORTED_MEDIA_TYPE"
upload_id = store.begin(
filename="source.pdf",
media_type="application/pdf",
byte_length=len(payload),
expected_sha256=digest,
).upload_id
store.put_chunk(upload_id, offset=0, content=payload, chunk_sha256=digest)
assert store.finalize(upload_id).media_type == "application/pdf"
invalid = b"not pdf"
invalid_digest = hashlib.sha256(invalid).hexdigest()
invalid_id = store.begin(
filename="invalid.pdf",
media_type="application/pdf",
byte_length=len(invalid),
expected_sha256=invalid_digest,
).upload_id
store.put_chunk(invalid_id, offset=0, content=invalid, chunk_sha256=invalid_digest)
with pytest.raises(UploadError) as error:
store.finalize(invalid_id)
assert error.value.code == "PDF_INVALID"
def test_identical_artifact_bytes_dedupe_without_collapsing_upload_receipts(
tmp_path: Path,
) -> None:
store = LocalArtifactStore(tmp_path)
content = b"Shared source content."
digest = hashlib.sha256(content).hexdigest()
first_id = store.begin(
filename="shared.md",
media_type="text/markdown",
byte_length=len(content),
expected_sha256=digest,
owner_id="agent-shared",
project_id="project-a",
).upload_id
second_session = store.begin(
filename="second-copy.txt",
media_type="text/plain",
byte_length=len(content),
expected_sha256=digest,
owner_id="agent-shared",
project_id="project-b",
now=1_000,
)
for upload_id in (first_id, second_session.upload_id):
store.put_chunk(
upload_id,
offset=0,
content=content,
chunk_sha256=digest,
owner_id="agent-shared",
now=1_001,
)
first = store.finalize(first_id, owner_id="agent-shared", now=1_002)
second = store.finalize(second_session.upload_id, owner_id="agent-shared", now=1_002)
assert first.sha256 == second.sha256
assert first.byte_length == second.byte_length == len(content)
for project_id in ("project-a", "project-b"):
with store.open_artifact(
digest, owner_id="agent-shared", project_id=project_id
) as artifact:
assert artifact.read() == content
with pytest.raises(UploadError) as error:
store.open_artifact(digest, owner_id="agent-shared", project_id="project-c")
assert error.value.code == "ARTIFACT_NOT_FOUND"
+1
View File
@@ -0,0 +1 @@
"""Reusable contract checks for replaceable implementation adapters."""
@@ -0,0 +1,188 @@
"""Shared canonical repository contract and a substitutable consumer test."""
from __future__ import annotations
import hashlib
import json
from collections.abc import Mapping
from pathlib import Path
import pytest
import yaml
from gewu.adapters.git_repository import GitCanonicalRepository
from gewu.application.identity import Principal
from gewu.application.ports import (
CanonicalConflictError,
CanonicalRepository,
CanonicalRepositoryError,
validate_canonical_path,
)
from gewu.application.reading import CanonicalNodeReader
from gewu.domain.contracts import KnowledgeNode
from tests.domain.proposal_fixtures import NODE
class InMemoryCanonicalRepository:
"""Independent port implementation used to prove consumer substitution."""
def __init__(self) -> None:
initial = hashlib.sha256(b"memory-repository-initial").hexdigest()[:40]
self._head = initial
self._snapshots = {initial: {}}
self._parents: dict[str, str | None] = {initial: None}
self._changed: dict[str, frozenset[str]] = {initial: frozenset()}
def head(self) -> str:
return self._head
def read_file(self, revision: str, path: str) -> bytes | None:
validate_canonical_path(path)
try:
return self._snapshots[revision].get(path)
except KeyError as error:
raise CanonicalRepositoryError("Unknown canonical revision") from error
def list_files(self, revision: str, prefix: str) -> tuple[str, ...]:
prefix = prefix.rstrip("/")
validate_canonical_path(prefix)
try:
files = self._snapshots[revision]
except KeyError as error:
raise CanonicalRepositoryError("Unknown canonical revision") from error
return tuple(sorted(path for path in files if path.startswith(prefix)))
def publish_files(
self,
*,
expected_head: str,
files: Mapping[str, bytes],
message: str,
index_directory: Path,
) -> str:
del message, index_directory
if expected_head != self._head:
raise CanonicalConflictError("Canonical branch changed before publication")
if not files:
raise CanonicalRepositoryError("Canonical publication requires at least one file")
for path in files:
validate_canonical_path(path)
parent_files = self._snapshots[self._head]
changed = frozenset(path for path, data in files.items() if parent_files.get(path) != data)
digest = hashlib.sha256()
digest.update(self._head.encode())
for path, data in sorted(files.items()):
digest.update(path.encode())
digest.update(data)
revision = digest.hexdigest()[:40]
snapshot = {**parent_files, **files}
self._snapshots[revision] = snapshot
self._parents[revision] = self._head
self._changed[revision] = changed
self._head = revision
return revision
def introducing_commit(self, revision: str, path: str) -> str | None:
revisions = self.history(revision, path, 50)
return revisions[-1] if revisions else None
def history(self, revision: str, path: str, limit: int) -> tuple[str, ...]:
validate_canonical_path(path)
if not 1 <= limit <= 50:
raise CanonicalRepositoryError("Canonical history limit is invalid")
if revision not in self._snapshots:
raise CanonicalRepositoryError("Unknown canonical revision")
revisions: list[str] = []
cursor: str | None = revision
while cursor is not None and len(revisions) < limit:
if path in self._changed[cursor]:
revisions.append(cursor)
cursor = self._parents[cursor]
return tuple(revisions)
@pytest.fixture(params=("git", "memory"), ids=("git-adapter", "in-memory-substitute"))
def canonical_repository(request: pytest.FixtureRequest, tmp_path: Path) -> CanonicalRepository:
if request.param == "git":
return GitCanonicalRepository(tmp_path / "knowledge.git")
return InMemoryCanonicalRepository()
def test_canonical_repository_contract(
canonical_repository: CanonicalRepository,
tmp_path: Path,
) -> None:
base = canonical_repository.head()
record = "projects/project-a/nodes/node-a.yaml"
source = "sources/source-a/revision-a.yaml"
first = canonical_repository.publish_files(
expected_head=base,
files={record: b"title: first\n", source: b"title: source\n"},
message="Initial records",
index_directory=tmp_path / "publishing",
)
assert canonical_repository.head() == first
assert canonical_repository.read_file(first, record) == b"title: first\n"
assert canonical_repository.list_files(first, "projects/") == (record,)
assert canonical_repository.read_file(first, "unrelated/user-file.txt") is None
assert canonical_repository.introducing_commit(first, record) == first
assert canonical_repository.history(first, record, 20) == (first,)
assert canonical_repository.introducing_commit(first, "projects/absent.yaml") is None
second = canonical_repository.publish_files(
expected_head=first,
files={record: b"title: second\n"},
message="Update record",
index_directory=tmp_path / "publishing",
)
assert canonical_repository.read_file(first, record) == b"title: first\n"
assert canonical_repository.read_file(second, record) == b"title: second\n"
assert canonical_repository.history(second, record, 20) == (second, first)
assert canonical_repository.introducing_commit(second, record) == first
with pytest.raises(CanonicalConflictError):
canonical_repository.publish_files(
expected_head=base,
files={record: b"stale\n"},
message="Stale update",
index_directory=tmp_path / "publishing",
)
assert canonical_repository.head() == second
with pytest.raises(CanonicalRepositoryError):
canonical_repository.read_file(second, "../outside.yaml")
with pytest.raises(CanonicalRepositoryError):
canonical_repository.publish_files(
expected_head=second,
files={"../outside.yaml": b"unsafe\n"},
message="Unsafe update",
index_directory=tmp_path / "publishing",
)
def test_node_reader_works_unchanged_with_substitute_repository(tmp_path: Path) -> None:
repository = InMemoryCanonicalRepository()
project_hash = hashlib.sha256(NODE.project_id.encode()).hexdigest()
node_hash = hashlib.sha256(NODE.node_id.encode()).hexdigest()
path = f"projects/{project_hash}/nodes/{node_hash}.yaml"
raw = yaml.safe_dump(NODE.model_dump(mode="json"), allow_unicode=True).encode()
revision = repository.publish_files(
expected_head=repository.head(),
files={path: raw},
message="Publish reader fixture",
index_directory=tmp_path / "publishing",
)
reader = CanonicalNodeReader(repository)
response = reader.get_node(
NODE.project_id,
NODE.node_id,
principal=Principal(
"reader",
"reader-key",
frozenset({"viewer"}),
frozenset({NODE.project_id}),
),
)
loaded = KnowledgeNode.model_validate_json(json.dumps(response.data[0]), strict=True)
assert loaded == NODE
assert response.canonical_revision == revision
assert response.citations == NODE.claims[0].evidence
+1
View File
@@ -0,0 +1 @@
"""Domain contract tests and fixtures."""
+212
View File
@@ -0,0 +1,212 @@
"""Golden pilot proposal fixtures for deterministic evidence and policy checks."""
from datetime import UTC, datetime
from gewu.domain.contracts import (
AutoAcceptPolicy,
Claim,
Confidence,
EvidenceReference,
ExtractionBlock,
ExtractionRecord,
KnowledgeNode,
LensFit,
Lifecycle,
Locator,
NodeType,
ProjectRecord,
ProjectStatus,
ProposalOperation,
ProposalRevision,
ReliabilityGrade,
ReviewState,
Scope,
SourceRevisionRecord,
SupportAssessment,
SupportStatus,
)
from gewu.domain.validation import (
assertion_digest,
evidence_digest,
extraction_digest,
proposal_payload_digest,
)
BASE_COMMIT = "1" * 40
NOW = datetime(2026, 10, 13, tzinfo=UTC)
SOURCE_REVISION = SourceRevisionRecord(
source_id="shared:pilot-report",
revision_id="shared:pilot-report:r1",
artifact_sha256="2" * 64,
media_type="text/markdown",
byte_length=100,
captured_at=NOW,
original_url=None,
title="Pilot dome installation report",
authors=("Pilot team",),
publisher="GEWU fixtures",
license="internal",
source_type="internal",
reliability_grade=ReliabilityGrade.MEDIUM,
ingested_for=("dome-performance",),
archive_status="archived",
)
BLOCK_TEXT = "The installation hosted 400 visitors during the 2026 season."
_EXTRACTION_UNHASHED = ExtractionRecord(
extraction_id="0" * 64,
source_revision_id=SOURCE_REVISION.revision_id,
parser_name="gewu-text",
parser_version="0.1.0",
parser_config_sha256="4" * 64,
normalization_profile="unicode-nfc-lf-v1",
status="complete",
blocks=(
ExtractionBlock(
block_id="block-1",
ordinal=0,
original_text=BLOCK_TEXT,
normalized_text=BLOCK_TEXT,
normalization_map=tuple((index, index + 1) for index in range(len(BLOCK_TEXT))),
page=None,
),
),
)
EXTRACTION = _EXTRACTION_UNHASHED.model_copy(
update={"extraction_id": extraction_digest(_EXTRACTION_UNHASHED)}
)
CLAIM_TEXT = "The installation hosted 400 visitors during the 2026 season."
CLAIM_DIGEST = assertion_digest(CLAIM_TEXT)
START = 0
END = len(BLOCK_TEXT)
EVIDENCE = EvidenceReference(
source_revision_id=SOURCE_REVISION.revision_id,
extraction_id=EXTRACTION.extraction_id,
assertion_path="claims[0].text",
assertion_digest=CLAIM_DIGEST,
quote=BLOCK_TEXT,
locator=Locator(kind="text_span", block_id="block-1", start=START, end=END),
normalization_profile="unicode-nfc-lf-v1",
)
CLAIM = Claim(
claim_id="dome:claim-visitors",
text=CLAIM_TEXT,
evidence_type="reported",
evidence=(EVIDENCE,),
assessment=SupportAssessment(
assessment_id="assessment-1",
assertion_digest=CLAIM_DIGEST,
evidence_digests=(evidence_digest(EVIDENCE),),
assessor_id="agent-pilot",
run_id="run-pilot-1",
method="paired source claim check",
status=SupportStatus.SUPPORTED,
rationale="The quoted sentence states the count and period.",
),
)
NODE = KnowledgeNode(
node_id="dome:season-visitors",
project_id="dome-performance",
node_type=NodeType.DATAPOINT,
title="Dome season visitors",
summary="The installation hosted 400 visitors in the 2026 season.",
created=NOW,
updated=NOW,
review_state=ReviewState.DRAFT,
lifecycle=Lifecycle.ACTIVE,
confidence=Confidence.MEDIUM,
claim_support="supported",
tags=("attendance",),
audience=("product",),
domain=("production",),
use_for=("comparison",),
lens_fit={
"dome-performance": LensFit(
score=0.9,
horizon="H1",
key_question="How do audiences engage?",
reason="Reports a representative audience measure.",
),
},
run_id="run-pilot-1",
agent_id="agent-pilot",
model_id="fixture-model",
claims=(CLAIM,),
as_of=NOW,
stale_after=datetime(2027, 1, 1, tzinfo=UTC),
value=400,
unit="visitors",
sample_size=None,
reporting_year=2026,
)
PROJECTS = {
"dome-performance": ProjectRecord(
project_id="dome-performance",
title="Dome interactive content",
status=ProjectStatus.ACTIVE,
north_star="Create compelling interactive experiences for dome audiences.",
key_questions=("How do audiences engage?",),
autonomy_policy_id="dome-policy",
),
"interactive-video": ProjectRecord(
project_id="interactive-video",
title="Interactive video",
status=ProjectStatus.ACTIVE,
north_star="Understand useful interactive video experiences.",
key_questions=("How do teams control branching?",),
autonomy_policy_id="video-policy",
),
}
POLICIES = {
"dome-policy": AutoAcceptPolicy(
policy_id="dome-policy",
policy_version=1,
project_id="dome-performance",
automatic_node_types=("datapoint", "problem", "technique", "case"),
automatic_source_registration=True,
require_supported_assessment=True,
),
"video-policy": AutoAcceptPolicy(
policy_id="video-policy",
policy_version=1,
project_id="interactive-video",
automatic_node_types=("datapoint", "problem", "technique", "case"),
automatic_source_registration=True,
require_supported_assessment=True,
),
"shared": AutoAcceptPolicy(
policy_id="shared",
policy_version=1,
project_id="shared",
automatic_source_registration=True,
require_supported_assessment=True,
),
}
def proposal(
*,
node: KnowledgeNode | None = NODE,
source: SourceRevisionRecord | None = SOURCE_REVISION,
scope: Scope | None = None,
base_commit: str = BASE_COMMIT,
) -> ProposalRevision:
operations = tuple(
operation
for operation in (
ProposalOperation(operation="register_source", source=source) if source else None,
ProposalOperation(operation="upsert_node", node=node) if node else None,
)
if operation is not None
)
candidate = ProposalRevision(
proposal_id="proposal-pilot-1",
revision=1,
initiating_project_id="dome-performance",
scope=scope or Scope(projects=("dome-performance",), include_shared=True),
base_commit=base_commit,
payload_digest="0" * 64,
proposer_id="agent-pilot",
run_id="run-pilot-1",
operations=operations,
)
return candidate.model_copy(update={"payload_digest": proposal_payload_digest(candidate)})
+14
View File
@@ -0,0 +1,14 @@
import subprocess
import sys
from scripts.export_agent_contract import PACK
def test_pack_manifest_and_nested_contract_hashes_verify() -> None:
result = subprocess.run(
[sys.executable, str(PACK / "scripts" / "manifest_tool.py"), "verify"],
capture_output=True,
check=False,
text=True,
)
assert result.returncode == 0, result.stderr
+40
View File
@@ -0,0 +1,40 @@
import json
from pathlib import Path
from gewu.domain.operations import OPERATIONS, Transport, catalog_bytes, operations_for
def test_read_http_catalog_contains_no_mutation_operations() -> None:
http_operations = operations_for(Transport.HTTP)
assert all(not operation.mutates_knowledge for operation in http_operations)
assert not {"proposal_submit", "owner_decide"} & {item.name for item in http_operations}
def test_owner_decision_is_separate_from_agent_mcp() -> None:
agent_operations = {item.name for item in operations_for(Transport.MCP)}
owner_operations = {item.name for item in operations_for(Transport.OWNER_MCP)}
assert "owner_decide" not in agent_operations
assert owner_operations == {"owner_challenge_get", "owner_diff_get", "owner_decide"}
def test_stdio_connector_has_only_selected_file_operations() -> None:
stdio_operations = {item.name for item in operations_for(Transport.STDIO)}
assert stdio_operations == {"list_selected_files", "upload_selected_file"}
def test_exported_catalog_matches_the_application_allowlist() -> None:
path = Path(__file__).resolve().parents[2] / "schema" / "v1" / "catalog.json"
catalog = json.loads(path.read_text(encoding="utf-8"))
assert path.read_bytes() == catalog_bytes()
names_by_transport = {
transport.value: {operation.name for operation in operations_for(transport)}
for transport in Transport
}
entries = {operation["name"]: operation for operation in catalog["operations"]}
assert set(entries) == {operation.name for operation in OPERATIONS}
for name, entry in entries.items():
assert set(entry["transports"]) == {
transport for transport, names in names_by_transport.items() if name in names
}
expected_mutates = next(item.mutates_knowledge for item in OPERATIONS if item.name == name)
assert entry["mutates_knowledge"] is expected_mutates
+231
View File
@@ -0,0 +1,231 @@
import json
from datetime import UTC, date, datetime
from pathlib import Path
import pytest
from pydantic import ValidationError
from gewu.domain.contracts import (
AutoAcceptPolicy,
Confidence,
Locator,
NodeType,
ProjectRecord,
ProjectStatus,
ProposalReceipt,
ProposalState,
ReliabilityGrade,
Scope,
SearchRequest,
SourceRevisionRecord,
)
def test_project_contract_rejects_unknown_fields() -> None:
with pytest.raises(ValidationError):
ProjectRecord.model_validate(
{
"project_id": "dome",
"title": "Dome",
"status": ProjectStatus.ACTIVE,
"north_star": "Understand interactive environments",
"key_questions": ("What works?",),
"autonomy_policy_id": "pilot",
"untrusted_extra": "ignored?",
}
)
def test_project_status_supports_pause_without_losing_registry_record() -> None:
project = ProjectRecord(
project_id="dome",
title="Dome",
status=ProjectStatus.PAUSED,
north_star="Understand interactive environments",
key_questions=("What works?",),
autonomy_policy_id="pilot",
)
assert (
ProjectRecord.model_validate_json(project.model_dump_json()).status == ProjectStatus.PAUSED
)
@pytest.mark.parametrize("project_id", ["Dome", "two_words", "project.slug"])
def test_project_ids_must_be_lowercase_slugs(project_id: str) -> None:
with pytest.raises(ValidationError):
ProjectRecord(
project_id=project_id,
title="Invalid project ID",
status=ProjectStatus.ACTIVE,
north_star="A stable project lens.",
key_questions=("What matters?",),
autonomy_policy_id="policy",
)
def test_scope_requires_explicit_all_projects_flag() -> None:
with pytest.raises(ValidationError):
Scope(projects=("all",))
assert Scope(projects=("all",), all_projects=True)
@pytest.mark.parametrize("unsafe_id", ["../private", "bad/id", " leading-space"])
def test_scope_rejects_unsafe_project_ids(unsafe_id: str) -> None:
with pytest.raises(ValidationError):
Scope(projects=(unsafe_id,))
def test_search_facets_are_typed_and_reject_duplicate_node_types() -> None:
request = SearchRequest(
query="audience",
scope=Scope(projects=("dome-performance",)),
node_types=(NodeType.PROBLEM, NodeType.TECHNIQUE),
tags=("attendance", "real-time"),
audience=("product",),
use_for=("comparison",),
domain=("production",),
confidences=(Confidence.MEDIUM,),
as_of_from=date(2024, 1, 1),
as_of_to=date(2024, 12, 31),
stale_only=True,
budget_tokens=1000,
)
assert request.stale_only
assert request.tags == ("attendance", "real-time")
assert request.domain == ("production",)
assert request.confidences == (Confidence.MEDIUM,)
assert request.as_of_from == date(2024, 1, 1)
with pytest.raises(ValidationError, match="must not be later"):
SearchRequest(
query="audience",
scope=Scope(projects=("dome-performance",)),
as_of_from=date(2025, 1, 1),
as_of_to=date(2024, 12, 31),
budget_tokens=1000,
)
with pytest.raises(ValidationError, match="confidences must be unique"):
SearchRequest(
query="audience",
scope=Scope(projects=("dome-performance",)),
confidences=(Confidence.MEDIUM, Confidence.MEDIUM),
budget_tokens=1000,
)
with pytest.raises(ValidationError):
SearchRequest(
query="audience",
scope=Scope(projects=("dome-performance",)),
node_types=(NodeType.PROBLEM, NodeType.PROBLEM),
budget_tokens=1000,
)
with pytest.raises(ValidationError):
SearchRequest(
query="audience",
scope=Scope(projects=("dome-performance",)),
tags=("attendance", "attendance"),
budget_tokens=1000,
)
with pytest.raises(ValidationError):
SearchRequest(
query="audience",
scope=Scope(projects=("dome-performance",)),
domain=(" production",),
budget_tokens=1000,
)
def test_search_allows_empty_query_only_for_a_typed_browse() -> None:
request = SearchRequest(
scope=Scope(projects=("dome-performance",)),
node_types=(NodeType.PROBLEM,),
budget_tokens=1000,
)
assert request.query == ""
with pytest.raises(ValidationError, match="search requires text"):
SearchRequest(
scope=Scope(projects=("dome-performance",)),
budget_tokens=1000,
)
with pytest.raises(ValidationError, match="search requires text"):
SearchRequest(
query=" ",
scope=Scope(projects=("dome-performance",)),
budget_tokens=1000,
)
def test_pdf_locator_uses_one_based_page_and_nonempty_span() -> None:
with pytest.raises(ValidationError):
Locator(kind="pdf_page_span", block_id="p1-b1", start=8, end=8, page=0)
def test_unarchived_source_requires_a_url_and_cannot_claim_artifact_bytes() -> None:
base = {
"source_id": "project:bookmark",
"revision_id": "project:bookmark:r1",
"media_type": "application/octet-stream",
"byte_length": 0,
"captured_at": datetime.now(UTC),
"original_url": "https://example.org/paper",
"title": "Referenced paper",
"license": "unspecified",
"source_type": "paper",
"reliability_grade": ReliabilityGrade.MEDIUM,
"ingested_for": ("dome-performance",),
"archive_status": "reference-only",
}
record = SourceRevisionRecord.model_validate(base)
assert record.artifact_sha256 is None
with pytest.raises(ValidationError):
SourceRevisionRecord.model_validate({**base, "byte_length": 10})
with pytest.raises(ValidationError):
SourceRevisionRecord.model_validate({**base, "original_url": None})
def test_receipt_is_immutable_and_versioned() -> None:
receipt = ProposalReceipt(
proposal_id="proposal-1",
revision=1,
payload_digest="a" * 64,
state=ProposalState.ACCEPTED,
outcome="accepted",
commit_id="b" * 64,
decided_at=datetime.now(UTC),
)
assert receipt.schema_version == 1
with pytest.raises(ValidationError):
receipt.revision = 2 # type: ignore[misc]
def test_pilot_registry_fixture_matches_project_and_policy_contracts() -> None:
fixture_path = (
Path(__file__).resolve().parents[2] / "schema" / "fixtures" / "pilot-projects.json"
)
def reject_duplicate_keys(pairs: list[tuple[str, object]]) -> dict[str, object]:
result: dict[str, object] = {}
for key, value in pairs:
if key in result:
raise ValueError(f"duplicate JSON key: {key}")
result[key] = value
return result
fixture = json.loads(
fixture_path.read_text(encoding="utf-8"), object_pairs_hook=reject_duplicate_keys
)
projects = tuple(
ProjectRecord.model_validate_json(json.dumps(record), strict=True)
for record in fixture["projects"]
)
policies = tuple(
AutoAcceptPolicy.model_validate_json(json.dumps(record), strict=True)
for record in fixture["test_policies"]
)
assert {project.project_id for project in projects} == {
"dome-performance",
"interactive-video",
}
assert {policy.project_id for policy in policies} == {
"dome-performance",
"interactive-video",
"shared",
}
+160
View File
@@ -0,0 +1,160 @@
import copy
import hashlib
import json
import stat
from pathlib import Path
import agents.gewu.scripts.run_checkpoint as checkpoint_module
import pytest
from agents.gewu.scripts.run_checkpoint import (
create_checkpoint,
load_checkpoint,
update_checkpoint,
)
def test_run_checkpoint_records_scope_coverage_receipt_and_pins(tmp_path: Path) -> None:
workspace = tmp_path / "research-workspace"
checkpoint = create_checkpoint(
workspace,
run_id="run-2026-01",
objective="Compare audience response",
projects=["dome-performance"],
include_shared=True,
lens="Audience behavior",
budgets={"sources": 8},
)
path = workspace / "Runs" / "run-2026-01.json"
assert stat.S_IMODE(path.stat().st_mode) == 0o600
assert checkpoint["pack"]["snapshot_digest"]
updated = update_checkpoint(
workspace,
"run-2026-01",
{
"stage": "report",
"sources": [
{
"source_revision_id": "source:r1",
"extraction_id": "a" * 64,
"status": "extracted",
}
],
"coverage": {
"selected": ["section-1", "section-2"],
"included": ["section-1"],
"excluded": [{"item": "section-2", "reason": "outside project lens"}],
"skipped": [],
},
"pending_operations": [
{
"operation_id": "proposal-op-1",
"tool_name": "proposal_submit",
"input_sha256": "b" * 64,
"status": "complete",
}
],
"proposals": [
{
"proposal_id": "proposal-1",
"revision": 1,
"status": "accepted",
"receipt": {
"proposal_id": "proposal-1",
"revision": 1,
"state": "accepted",
"commit_id": "c" * 40,
},
}
],
"outputs": [{"sha256": "d" * 64}],
},
)
resumed = load_checkpoint(workspace, "run-2026-01")
assert resumed == updated
assert resumed["coverage"]["excluded"][0]["reason"] == "outside project lens"
assert resumed["proposals"][0]["receipt"]["commit_id"] == "c" * 40
with pytest.raises(ValueError, match="does not match"):
update_checkpoint(
workspace,
"run-2026-01",
{
"proposals": [
{
"proposal_id": "proposal-1",
"revision": 1,
"status": "accepted",
"receipt": {
"proposal_id": "proposal-other",
"revision": 1,
"state": "accepted",
"commit_id": "c" * 40,
},
}
]
},
)
def test_run_checkpoint_rejects_secrets_bad_progress_and_snapshot_drift(tmp_path: Path) -> None:
workspace = tmp_path / "research-workspace"
create_checkpoint(
workspace,
run_id="run-drift-01",
objective="Test resume pinning",
projects=["dome-performance"],
include_shared=False,
lens="A bounded lens",
)
with pytest.raises(ValueError, match="credentials or source bodies"):
update_checkpoint(workspace, "run-drift-01", {"last_failure": {"api_key": "secret"}})
with pytest.raises(ValueError, match="SHA-256"):
update_checkpoint(
workspace,
"run-drift-01",
{
"outputs": [{"sha256": "not-a-digest"}],
"stage": "report",
},
)
path = workspace / "Runs" / "run-drift-01.json"
checkpoint = json.loads(path.read_text(encoding="utf-8"))
checkpoint["pack"]["snapshot_digest"] = "0" * 64
path.write_text(json.dumps(checkpoint), encoding="utf-8")
with pytest.raises(ValueError, match="pack changed"):
load_checkpoint(workspace, "run-drift-01")
def test_existing_run_cannot_resume_after_verified_pack_upgrade(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
workspace = tmp_path / "research-workspace"
create_checkpoint(
workspace,
run_id="run-pack-update-01",
objective="Resume with pinned modules",
projects=["dome-performance"],
include_shared=True,
lens="Keep the original extraction and evidence rules",
)
checkpoint_path = workspace / "Runs" / "run-pack-update-01.json"
original_checkpoint = checkpoint_path.read_bytes()
updated_manifest = copy.deepcopy(checkpoint_module.load_manifest())
updated_manifest["pack_version"] = "0.7.0"
updated_manifest["modules"][0]["implementation_version"] += ".updated"
modules_json = json.dumps(
updated_manifest["modules"], ensure_ascii=False, sort_keys=True, separators=(",", ":")
)
updated_manifest["snapshot"]["digest"] = hashlib.sha256(modules_json.encode()).hexdigest()
# This fixture stands in for a newer pack whose own manifest verification
# succeeded; the behavior under test is the active run's compatibility pin.
monkeypatch.setattr(checkpoint_module, "load_manifest", lambda: updated_manifest)
monkeypatch.setattr(checkpoint_module, "verify", lambda _manifest: None)
with pytest.raises(ValueError, match="pack changed since this run"):
load_checkpoint(workspace, "run-pack-update-01")
assert checkpoint_path.read_bytes() == original_checkpoint
+698
View File
@@ -0,0 +1,698 @@
from gewu.adapters.text_extractor import TextMarkdownExtractor
from gewu.domain.contracts import (
EdgeEndpoint,
KnowledgeNode,
Locator,
NodeType,
ProjectStatus,
ProposalOperation,
ProposalRevision,
Scope,
SupportStatus,
TypedEdge,
)
from gewu.domain.validation import (
ValidationReport,
assertion_digest,
edge_assertion_digest,
evidence_digest,
proposal_payload_digest,
validate_proposal,
)
from tests.domain.proposal_fixtures import (
BASE_COMMIT,
CLAIM,
EVIDENCE,
EXTRACTION,
NODE,
POLICIES,
PROJECTS,
SOURCE_REVISION,
proposal,
)
def _validate(candidate: ProposalRevision) -> ValidationReport:
return validate_proposal(
candidate,
projects=PROJECTS,
policies=POLICIES,
sources={SOURCE_REVISION.revision_id: SOURCE_REVISION},
extractions={EXTRACTION.extraction_id: EXTRACTION},
existing_nodes={},
current_base_commit=BASE_COMMIT,
)
def test_supported_exact_citation_and_both_bundle_policies_are_eligible() -> None:
report = _validate(proposal())
assert report.status == "eligible", report.findings
assert report.eligible_for_auto_accept
assert set(report.affected_policy_ids) == {"dome-policy", "shared"}
def test_transcript_timestamp_locator_must_match_the_cited_cue() -> None:
transcript_bytes = (
b"WEBVTT\n\n"
b"cue-1\n00:14:32.000 --> 00:14:36.500\n"
b"The installation hosted 400 visitors during the 2026 season.\n"
)
source = SOURCE_REVISION.model_copy(
update={
"media_type": "text/vtt",
"byte_length": len(transcript_bytes),
"artifact_sha256": __import__("hashlib").sha256(transcript_bytes).hexdigest(),
}
)
extraction = TextMarkdownExtractor().extract(source.revision_id, transcript_bytes, "text/vtt")
cue = next(block for block in extraction.blocks if block.time_start_ms is not None)
quote = CLAIM.text
start = cue.normalized_text.index(quote)
evidence = EVIDENCE.model_copy(
update={
"extraction_id": extraction.extraction_id,
"quote": quote,
"locator": Locator(
kind="transcript_timestamp_span",
block_id=cue.block_id,
start=start,
end=start + len(quote),
time_start_ms=cue.time_start_ms,
time_end_ms=cue.time_end_ms,
),
}
)
assessment = CLAIM.assessment.model_copy(
update={"evidence_digests": (evidence_digest(evidence),)}
)
claim = CLAIM.model_copy(update={"evidence": (evidence,), "assessment": assessment})
node = NODE.model_copy(update={"claims": (claim,)})
eligible = validate_proposal(
proposal(node=node, source=source),
projects=PROJECTS,
policies=POLICIES,
sources={},
extractions={extraction.extraction_id: extraction},
existing_nodes={},
current_base_commit=BASE_COMMIT,
)
assert eligible.status == "eligible", eligible.findings
wrong_time = evidence.model_copy(
update={
"locator": evidence.locator.model_copy(
update={"time_start_ms": evidence.locator.time_start_ms + 1}
)
}
)
wrong_assessment = assessment.model_copy(
update={"evidence_digests": (evidence_digest(wrong_time),)}
)
wrong_claim = claim.model_copy(
update={"evidence": (wrong_time,), "assessment": wrong_assessment}
)
rejected = validate_proposal(
proposal(node=NODE.model_copy(update={"claims": (wrong_claim,)}), source=None),
projects=PROJECTS,
policies=POLICIES,
sources={source.revision_id: source},
extractions={extraction.extraction_id: extraction},
existing_nodes={},
current_base_commit=BASE_COMMIT,
)
assert rejected.status == "invalid"
assert "EVIDENCE_LOCATION_MISMATCH" in {finding.code for finding in rejected.findings}
def test_paused_or_archived_projects_reject_new_acceptance() -> None:
for status in (ProjectStatus.PAUSED, ProjectStatus.ARCHIVED):
project = PROJECTS["dome-performance"].model_copy(update={"status": status})
report = validate_proposal(
proposal(),
projects={**PROJECTS, "dome-performance": project},
policies=POLICIES,
sources={SOURCE_REVISION.revision_id: SOURCE_REVISION},
extractions={EXTRACTION.extraction_id: EXTRACTION},
existing_nodes={},
current_base_commit=BASE_COMMIT,
)
assert not report.eligible_for_auto_accept
assert any(item.code == "PROJECT_NOT_ACTIVE" for item in report.findings)
def test_cross_project_source_bundle_requires_every_affected_policy() -> None:
shared_source = SOURCE_REVISION.model_copy(
update={"ingested_for": ("dome-performance", "interactive-video")}
)
candidate = proposal(
source=shared_source,
scope=Scope(projects=("dome-performance", "interactive-video"), include_shared=True),
)
eligible = validate_proposal(
candidate,
projects=PROJECTS,
policies=POLICIES,
sources={},
extractions={EXTRACTION.extraction_id: EXTRACTION},
existing_nodes={},
current_base_commit=BASE_COMMIT,
)
assert eligible.status == "eligible", eligible.findings
assert set(eligible.affected_policy_ids) == {
"dome-policy",
"video-policy",
"shared",
}
restrictive_video_policy = POLICIES["video-policy"].model_copy(
update={"automatic_source_registration": False}
)
blocked = validate_proposal(
candidate,
projects=PROJECTS,
policies={**POLICIES, "video-policy": restrictive_video_policy},
sources={},
extractions={EXTRACTION.extraction_id: EXTRACTION},
existing_nodes={},
current_base_commit=BASE_COMMIT,
)
assert blocked.status == "needs-review"
assert not blocked.eligible_for_auto_accept
assert "AUTO_ACCEPT_UNSPECIFIED" in {finding.code for finding in blocked.findings}
def test_matching_quote_at_wrong_location_is_rejected() -> None:
evidence = EVIDENCE.model_copy(
update={
"quote": "400 visitors",
"locator": EVIDENCE.locator.model_copy(update={"start": 0, "end": 13}),
}
)
changed_claim = CLAIM.model_copy(update={"evidence": (evidence,)})
changed_node = NODE.model_copy(update={"claims": (changed_claim,)})
report = _validate(proposal(node=changed_node, source=None))
assert report.status == "invalid"
assert "EVIDENCE_MISMATCH" in {finding.code for finding in report.findings}
def test_reference_only_source_cannot_support_accepted_evidence() -> None:
reference = SOURCE_REVISION.model_copy(
update={
"artifact_sha256": None,
"media_type": "application/octet-stream",
"byte_length": 0,
"original_url": "https://research.example/missing",
"archive_status": "reference-only",
}
)
report = validate_proposal(
proposal(source=None),
projects=PROJECTS,
policies=POLICIES,
sources={reference.revision_id: reference},
extractions={EXTRACTION.extraction_id: EXTRACTION},
existing_nodes={},
current_base_commit=BASE_COMMIT,
)
assert report.status == "invalid"
assert "SOURCE_NOT_ARCHIVED" in {finding.code for finding in report.findings}
def test_correct_quote_with_unsupported_assessment_is_rejected() -> None:
assessment = CLAIM.assessment.model_copy(update={"status": SupportStatus.UNSUPPORTED})
changed_claim = CLAIM.model_copy(update={"assessment": assessment})
changed_node = NODE.model_copy(update={"claims": (changed_claim,)})
report = _validate(proposal(node=changed_node, source=None))
assert report.status == "invalid"
assert "SUPPORT_UNAVAILABLE" in {finding.code for finding in report.findings}
def test_node_and_assessor_attribution_cannot_impersonate_another_agent() -> None:
other_assessment = CLAIM.assessment.model_copy(update={"assessor_id": "agent-other"})
assessment_claim = CLAIM.model_copy(update={"assessment": other_assessment})
assessment_node = NODE.model_copy(update={"claims": (assessment_claim,)})
assessment_report = _validate(proposal(node=assessment_node, source=None))
assert "ASSESSOR_ATTRIBUTION_MISMATCH" in {
finding.code for finding in assessment_report.findings
}
other_node = NODE.model_copy(update={"agent_id": "agent-other"})
node_report = _validate(proposal(node=other_node, source=None))
assert "AGENT_ATTRIBUTION_MISMATCH" in {finding.code for finding in node_report.findings}
def test_entity_name_collision_requires_review_without_creating_a_link() -> None:
entity = NODE.model_copy(
update={"node_type": NodeType.ENTITY, "title": "The Dome", "aliases": ("Planetarium",)}
)
existing = NODE.model_copy(
update={
"node_type": NodeType.ENTITY,
"node_id": "dome:existing-entity",
"title": " planetarium ",
}
)
report = validate_proposal(
proposal(
node=entity,
source=None,
scope=Scope(projects=("dome-performance",), include_shared=False),
),
projects=PROJECTS,
policies=POLICIES,
sources={SOURCE_REVISION.revision_id: SOURCE_REVISION},
extractions={EXTRACTION.extraction_id: EXTRACTION},
existing_nodes={existing.node_id: existing},
current_base_commit=BASE_COMMIT,
)
assert report.status == "needs-review"
assert not report.eligible_for_auto_accept
assert "POSSIBLE_DUPLICATE_ENTITY" in {finding.code for finding in report.findings}
def test_entity_duplicate_detection_respects_project_scope() -> None:
entity = NODE.model_copy(
update={"node_type": NodeType.ENTITY, "title": "The Dome", "aliases": ("Planetarium",)}
)
unrelated = NODE.model_copy(
update={
"node_type": NodeType.ENTITY,
"node_id": "video:existing-entity",
"project_id": "interactive-video",
"title": "Planetarium",
}
)
report = validate_proposal(
proposal(node=entity, source=None),
projects=PROJECTS,
policies=POLICIES,
sources={SOURCE_REVISION.revision_id: SOURCE_REVISION},
extractions={EXTRACTION.extraction_id: EXTRACTION},
existing_nodes={unrelated.node_id: unrelated},
current_base_commit=BASE_COMMIT,
)
assert "POSSIBLE_DUPLICATE_ENTITY" not in {finding.code for finding in report.findings}
def test_entity_duplicate_review_uses_only_readable_projects_in_explicit_scope() -> None:
entity = NODE.model_copy(
update={"node_type": NodeType.ENTITY, "title": "The Dome", "aliases": ("Planetarium",)}
)
other_entity = NODE.model_copy(
update={
"node_type": NodeType.ENTITY,
"node_id": "video:existing-entity",
"project_id": "interactive-video",
"title": "Planetarium",
}
)
existing = {other_entity.node_id: other_entity}
candidate = proposal(
node=entity,
source=None,
scope=Scope(projects=("dome-performance", "interactive-video")),
)
readable = validate_proposal(
candidate,
projects=PROJECTS,
policies=POLICIES,
sources={SOURCE_REVISION.revision_id: SOURCE_REVISION},
extractions={EXTRACTION.extraction_id: EXTRACTION},
existing_nodes=existing,
current_base_commit=BASE_COMMIT,
readable_project_ids=frozenset({"interactive-video"}),
)
unreadable = validate_proposal(
candidate,
projects=PROJECTS,
policies=POLICIES,
sources={SOURCE_REVISION.revision_id: SOURCE_REVISION},
extractions={EXTRACTION.extraction_id: EXTRACTION},
existing_nodes=existing,
current_base_commit=BASE_COMMIT,
)
assert "POSSIBLE_DUPLICATE_ENTITY" in {finding.code for finding in readable.findings}
assert "POSSIBLE_DUPLICATE_ENTITY" not in {finding.code for finding in unreadable.findings}
def test_entity_duplicate_review_does_not_expand_beyond_proposal_scope() -> None:
entity = NODE.model_copy(
update={"node_type": NodeType.ENTITY, "title": "The Dome", "aliases": ("Planetarium",)}
)
shared_entity = NODE.model_copy(
update={
"node_type": NodeType.ENTITY,
"node_id": "shared:planetarium-entity",
"project_id": "shared",
"title": "Planetarium",
}
)
report = validate_proposal(
proposal(
node=entity,
source=None,
scope=Scope(projects=("dome-performance",), include_shared=False),
),
projects=PROJECTS,
policies=POLICIES,
sources={SOURCE_REVISION.revision_id: SOURCE_REVISION},
extractions={EXTRACTION.extraction_id: EXTRACTION},
existing_nodes={shared_entity.node_id: shared_entity},
current_base_commit=BASE_COMMIT,
readable_project_ids=frozenset({"shared"}),
)
assert "POSSIBLE_DUPLICATE_ENTITY" not in {finding.code for finding in report.findings}
def test_requested_shared_scope_does_not_reveal_unreadable_shared_entity_names() -> None:
entity = NODE.model_copy(
update={"node_type": NodeType.ENTITY, "title": "The Dome", "aliases": ("Planetarium",)}
)
shared_entity = NODE.model_copy(
update={
"node_type": NodeType.ENTITY,
"node_id": "shared:planetarium-entity",
"project_id": "shared",
"title": "Planetarium",
}
)
report = validate_proposal(
proposal(node=entity, source=None),
projects=PROJECTS,
policies=POLICIES,
sources={SOURCE_REVISION.revision_id: SOURCE_REVISION},
extractions={EXTRACTION.extraction_id: EXTRACTION},
existing_nodes={shared_entity.node_id: shared_entity},
current_base_commit=BASE_COMMIT,
)
assert "POSSIBLE_DUPLICATE_ENTITY" not in {finding.code for finding in report.findings}
explicitly_readable = validate_proposal(
proposal(node=entity, source=None, scope=Scope(projects=("dome-performance", "shared"))),
projects=PROJECTS,
policies=POLICIES,
sources={SOURCE_REVISION.revision_id: SOURCE_REVISION},
extractions={EXTRACTION.extraction_id: EXTRACTION},
existing_nodes={shared_entity.node_id: shared_entity},
current_base_commit=BASE_COMMIT,
readable_project_ids=frozenset({"shared"}),
)
assert "POSSIBLE_DUPLICATE_ENTITY" in {finding.code for finding in explicitly_readable.findings}
def test_changed_assertion_and_old_assessment_are_rejected() -> None:
changed_text = "The installation hosted 401 visitors during the 2026 season."
stale_assessment = CLAIM.assessment
evidence = CLAIM.evidence[0].model_copy(
update={"assertion_digest": assertion_digest(changed_text)}
)
changed_claim = CLAIM.model_copy(
update={"text": changed_text, "evidence": (evidence,), "assessment": stale_assessment}
)
changed_node = NODE.model_copy(update={"claims": (changed_claim,)})
report = _validate(proposal(node=changed_node, source=None))
assert report.status == "invalid"
assert "ASSESSMENT_STALE" in {finding.code for finding in report.findings}
def test_assumption_and_limitation_claims_use_the_same_evidence_contract() -> None:
assumption_text = CLAIM.text
assumption_digest = assertion_digest(assumption_text)
assumption_evidence = EVIDENCE.model_copy(
update={
"assertion_path": "assumptions[0].text",
"assertion_digest": assumption_digest,
}
)
assumption = CLAIM.model_copy(
update={
"claim_id": "dome:assumption-seasonality",
"text": assumption_text,
"evidence": (assumption_evidence,),
"assessment": CLAIM.assessment.model_copy(
update={
"assertion_digest": assumption_digest,
"evidence_digests": (evidence_digest(assumption_evidence),),
}
),
}
)
limitation_evidence = EVIDENCE.model_copy(
update={
"assertion_path": "limitations[0].text",
"assertion_digest": assumption_digest,
}
)
limitation = assumption.model_copy(
update={
"claim_id": "dome:limitation-seasonality",
"evidence": (limitation_evidence,),
"assessment": assumption.assessment.model_copy(
update={"evidence_digests": (evidence_digest(limitation_evidence),)}
),
}
)
node = NODE.model_copy(update={"assumptions": (assumption,), "limitations": (limitation,)})
report = _validate(proposal(node=node, source=None))
assert report.eligible_for_auto_accept, report.findings
wrong_location = assumption_evidence.model_copy(
update={
"quote": "400 visitors",
"locator": assumption_evidence.locator.model_copy(update={"start": 0, "end": 13}),
}
)
invalid_assumption = assumption.model_copy(
update={
"evidence": (wrong_location,),
"assessment": assumption.assessment.model_copy(
update={"evidence_digests": (evidence_digest(wrong_location),)}
),
}
)
invalid = _validate(
proposal(node=NODE.model_copy(update={"assumptions": (invalid_assumption,)}), source=None)
)
assert "EVIDENCE_MISMATCH" in {finding.code for finding in invalid.findings}
assert any(
finding.path == "operations[0].node.assumptions[0].evidence[0]"
for finding in invalid.findings
)
def test_missing_policy_and_explicit_scope_escape_do_not_accept() -> None:
no_shared_policy = {key: value for key, value in POLICIES.items() if key != "shared"}
candidate = proposal()
report = validate_proposal(
candidate,
projects=PROJECTS,
policies=no_shared_policy,
sources={SOURCE_REVISION.revision_id: SOURCE_REVISION},
extractions={EXTRACTION.extraction_id: EXTRACTION},
existing_nodes={},
current_base_commit=BASE_COMMIT,
)
assert report.status == "needs-review"
assert "POLICY_MISSING" in {finding.code for finding in report.findings}
outside_scope = proposal(
node=NODE.model_copy(update={"project_id": "interactive-video"}),
source=None,
scope=Scope(projects=("dome-performance",)),
)
report = _validate(outside_scope)
assert report.status == "invalid"
assert "OUT_OF_SCOPE" in {finding.code for finding in report.findings}
def test_policy_cannot_waive_required_support_review() -> None:
weakened_policy = POLICIES["dome-policy"].model_copy(
update={"require_supported_assessment": False}
)
report = validate_proposal(
proposal(),
projects=PROJECTS,
policies={**POLICIES, "dome-policy": weakened_policy},
sources={SOURCE_REVISION.revision_id: SOURCE_REVISION},
extractions={EXTRACTION.extraction_id: EXTRACTION},
existing_nodes={},
current_base_commit=BASE_COMMIT,
)
assert report.status == "invalid"
assert not report.eligible_for_auto_accept
assert "POLICY_WEAKENING" in {finding.code for finding in report.findings}
def test_changed_base_is_a_conflict_and_edge_policy_is_explicit() -> None:
report = _validate(proposal(base_commit="f" * 64))
assert report.status == "conflict"
assert "BASE_CONFLICT" in {finding.code for finding in report.findings}
def test_relationship_evidence_is_bound_to_the_edge_and_exact_source_span() -> None:
policies = {
**POLICIES,
"dome-policy": POLICIES["dome-policy"].model_copy(
update={"automatic_edge_types": ("causes",)}
),
}
edge = TypedEdge(
edge_id="edge-pilot",
edge_type="causes",
source=EdgeEndpoint(kind="node", id=NODE.node_id),
target=EdgeEndpoint(kind="node", id="dome:other-node"),
evidence=(EVIDENCE,),
)
bound_evidence = EVIDENCE.model_copy(update={"assertion_digest": edge_assertion_digest(edge)})
edge = edge.model_copy(update={"evidence": (bound_evidence,)})
base = proposal(source=None)
candidate = base.model_copy(
update={
"operations": (*base.operations, ProposalOperation(operation="add_edge", edge=edge)),
"payload_digest": "0" * 64,
}
)
candidate = candidate.model_copy(update={"payload_digest": proposal_payload_digest(candidate)})
report = validate_proposal(
candidate,
projects=PROJECTS,
policies=policies,
sources={SOURCE_REVISION.revision_id: SOURCE_REVISION},
extractions={EXTRACTION.extraction_id: EXTRACTION},
existing_nodes={
"dome:other-node": NODE.model_copy(update={"node_id": "dome:other-node"}),
},
current_base_commit=BASE_COMMIT,
)
assert report.status == "eligible", report.findings
tampered = bound_evidence.model_copy(
update={"assertion_digest": assertion_digest("unrelated claim")}
)
invalid_edge = edge.model_copy(update={"evidence": (tampered,)})
invalid = candidate.model_copy(
update={
"operations": (
*base.operations,
ProposalOperation(operation="add_edge", edge=invalid_edge),
),
"payload_digest": "0" * 64,
}
)
invalid = invalid.model_copy(update={"payload_digest": proposal_payload_digest(invalid)})
invalid_report = validate_proposal(
invalid,
projects=PROJECTS,
policies=policies,
sources={SOURCE_REVISION.revision_id: SOURCE_REVISION},
extractions={EXTRACTION.extraction_id: EXTRACTION},
existing_nodes={},
current_base_commit=BASE_COMMIT,
)
assert invalid_report.status == "invalid"
assert "EVIDENCE_ASSERTION_MISMATCH" in {finding.code for finding in invalid_report.findings}
def test_contradiction_bundle_preserves_both_supported_claims() -> None:
import hashlib
from gewu.domain.contracts import EdgeEndpoint
first_quote = "The seasonal audience count was 400."
second_quote = "The seasonal audience count was 450."
content = f"Field report: {first_quote}\n\nVendor report: {second_quote}\n".encode()
source = SOURCE_REVISION.model_copy(
update={
"artifact_sha256": hashlib.sha256(content).hexdigest(),
"byte_length": len(content),
}
)
extraction = TextMarkdownExtractor().extract(source.revision_id, content, "text/markdown")
def node_with_claim(node_id: str, claim_id: str, quote: str, title: str) -> KnowledgeNode:
block = next(item for item in extraction.blocks if quote in item.normalized_text)
start = block.normalized_text.index(quote)
evidence = EVIDENCE.model_copy(
update={
"source_revision_id": source.revision_id,
"extraction_id": extraction.extraction_id,
"quote": quote,
"locator": Locator(
kind="text_span", block_id=block.block_id, start=start, end=start + len(quote)
),
"assertion_digest": assertion_digest(quote),
}
)
assessment = CLAIM.assessment.model_copy(
update={
"assertion_digest": assertion_digest(quote),
"evidence_digests": (evidence_digest(evidence),),
}
)
claim = CLAIM.model_copy(
update={
"claim_id": claim_id,
"text": quote,
"evidence": (evidence,),
"assessment": assessment,
}
)
return NODE.model_copy(
update={
"node_id": node_id,
"title": title,
"summary": quote,
"claims": (claim,),
"value": 400 if "400" in quote else 450,
}
)
first = node_with_claim(
"dome:count-field-report", "dome:claim-count-400", first_quote, "Field count"
)
second = node_with_claim(
"dome:count-vendor-report", "dome:claim-count-450", second_quote, "Vendor count"
)
contradiction = TypedEdge(
edge_id="dome:counts-conflict",
edge_type="contradicts",
source=EdgeEndpoint(kind="node", id=first.node_id),
target=EdgeEndpoint(kind="node", id=second.node_id),
conditions="Both reports refer to the same seasonal audience count.",
interpretation_rationale="The source-backed counts differ for the same reporting period.",
)
base = proposal(node=None, source=source)
candidate = base.model_copy(
update={
"operations": (
*base.operations,
ProposalOperation(operation="upsert_node", node=first),
ProposalOperation(operation="upsert_node", node=second),
ProposalOperation(operation="add_edge", edge=contradiction),
),
"payload_digest": "0" * 64,
}
)
candidate = candidate.model_copy(update={"payload_digest": proposal_payload_digest(candidate)})
report = validate_proposal(
candidate,
projects=PROJECTS,
policies={
**POLICIES,
"dome-policy": POLICIES["dome-policy"].model_copy(
update={"automatic_edge_types": ("contradicts",)}
),
},
sources={},
extractions={extraction.extraction_id: extraction},
existing_nodes={},
current_base_commit=BASE_COMMIT,
)
assert report.status == "eligible", report.findings
assert tuple(
operation.node.node_id for operation in candidate.operations if operation.node is not None
) == (first.node_id, second.node_id)
assert first.claims[0].text == first_quote
assert second.claims[0].text == second_quote
assert any(operation.edge == contradiction for operation in candidate.operations)
+200
View File
@@ -0,0 +1,200 @@
from __future__ import annotations
import importlib.util
import json
from pathlib import Path
import pytest
INSTALLER_PATH = Path(__file__).resolve().parents[1] / "agents/gewu/scripts/install.py"
SPEC = importlib.util.spec_from_file_location("gewu_pack_installer", INSTALLER_PATH)
assert SPEC is not None and SPEC.loader is not None
installer = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(installer)
def make_pack(tmp_path: Path) -> Path:
pack = tmp_path / "pack"
(pack / "scripts").mkdir(parents=True)
(pack / "manifest.json").write_text('{"pack":"fixture"}\n', encoding="utf-8")
(pack / "AGENTS.md").write_text("fixture profile\n", encoding="utf-8")
return pack
def test_install_and_uninstall_preserve_unrelated_workspace_and_mcp_config(tmp_path: Path) -> None:
workspace = tmp_path / "workspace"
workspace.mkdir()
(workspace / "AGENTS.md").write_bytes(b"Existing project guidance\nKeep it.\n")
(workspace / "CLAUDE.md").write_bytes(b"Existing Claude instructions without final newline")
(workspace / ".gemini").mkdir()
mcp_config = workspace / ".gemini/settings.json"
original_mcp = b'{"mcpServers":{"other":{"httpUrl":"https://example.test/mcp"}}}\n'
mcp_config.write_bytes(original_mcp)
(workspace / ".agents").mkdir()
(workspace / ".agents/skills").mkdir()
(workspace / ".agents/skills/custom.md").write_text("user skill", encoding="utf-8")
original_agents = (workspace / "AGENTS.md").read_bytes()
original_claude = (workspace / "CLAUDE.md").read_bytes()
installer.install(workspace, make_pack(tmp_path), "integrated")
assert (workspace / ".agents/gewu/AGENTS.md").is_file()
assert original_agents in (workspace / "AGENTS.md").read_bytes()
assert original_claude in (workspace / "CLAUDE.md").read_bytes()
assert (workspace / "GEMINI.md").is_file()
assert mcp_config.read_bytes() == original_mcp
assert (workspace / ".agents/skills/custom.md").read_text() == "user skill"
installer.uninstall(workspace)
assert (workspace / "AGENTS.md").read_bytes() == original_agents
assert (workspace / "CLAUDE.md").read_bytes() == original_claude
assert not (workspace / "GEMINI.md").exists()
assert mcp_config.read_bytes() == original_mcp
assert (workspace / ".agents/skills/custom.md").read_text() == "user skill"
assert not (workspace / ".agents/gewu").exists()
assert not (workspace / ".agents/gewu-install.json").exists()
def test_uninstall_refuses_modified_owned_entry_and_preserves_everything(tmp_path: Path) -> None:
workspace = tmp_path / "workspace"
workspace.mkdir()
(workspace / "AGENTS.md").write_text("Project rules\n", encoding="utf-8")
installer.install(workspace, make_pack(tmp_path))
(workspace / "AGENTS.md").write_text("User edit after installation\n", encoding="utf-8")
with pytest.raises(ValueError, match="edited routing block"):
installer.uninstall(workspace)
assert "User edit after installation" in (workspace / "AGENTS.md").read_text()
assert (workspace / ".agents/gewu/manifest.json").is_file()
assert (workspace / ".agents/gewu-install.json").is_file()
def test_uninstall_preserves_edits_outside_routing_block_and_user_pack_files(
tmp_path: Path,
) -> None:
workspace = tmp_path / "workspace"
workspace.mkdir()
root_instructions = workspace / "AGENTS.md"
root_instructions.write_text("Project rules\n", encoding="utf-8")
installer.install(workspace, make_pack(tmp_path))
root_instructions.write_text(
"Updated project rules\n" + root_instructions.read_text(encoding="utf-8"),
encoding="utf-8",
)
custom = workspace / ".agents/gewu/user-notes.txt"
custom.write_text("user-owned", encoding="utf-8")
installer.uninstall(workspace)
assert "Updated project rules" in root_instructions.read_text(encoding="utf-8")
assert custom.read_text(encoding="utf-8") == "user-owned"
assert (workspace / ".agents/gewu").is_dir()
assert not (workspace / ".agents/gewu/AGENTS.md").exists()
def test_install_refuses_occupied_gewu_destination_without_overwriting(tmp_path: Path) -> None:
workspace = tmp_path / "workspace"
(workspace / ".agents/gewu").mkdir(parents=True)
marker = workspace / ".agents/gewu/keep.txt"
marker.write_text("user data", encoding="utf-8")
with pytest.raises(FileExistsError):
installer.install(workspace, make_pack(tmp_path))
assert marker.read_text() == "user data"
def test_install_rollback_restores_original_routes_if_pack_copy_fails(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
workspace = tmp_path / "workspace"
workspace.mkdir()
original = b"Existing instructions\n"
(workspace / "AGENTS.md").write_bytes(original)
pack = make_pack(tmp_path)
real_atomic_write = installer._atomic_write
def failing_write(path: Path, content: bytes) -> None:
if path.name == "CLAUDE.md":
raise OSError("fixture failure")
real_atomic_write(path, content)
monkeypatch.setattr(installer, "_atomic_write", failing_write)
with pytest.raises(OSError, match="fixture failure"):
installer.install(workspace, pack)
assert (workspace / "AGENTS.md").read_bytes() == original
assert not (workspace / "CLAUDE.md").exists()
assert not (workspace / "GEMINI.md").exists()
assert not (workspace / ".agents/gewu").exists()
assert not (workspace / ".agents/gewu-install.json").exists()
def test_racing_installer_does_not_delete_the_other_installation(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
workspace = tmp_path / "workspace"
workspace.mkdir()
pack = make_pack(tmp_path)
original_mkdir = Path.mkdir
def competing_create(
path: Path,
mode: int = 0o777,
parents: bool = False,
exist_ok: bool = False,
) -> None:
if path == workspace / ".agents/gewu":
original_mkdir(path, mode=mode, parents=parents, exist_ok=True)
(path / "other-installation.txt").write_text("keep", encoding="utf-8")
raise FileExistsError("another installer won the race")
original_mkdir(path, mode=mode, parents=parents, exist_ok=exist_ok)
monkeypatch.setattr(Path, "mkdir", competing_create)
with pytest.raises(FileExistsError, match="another installer"):
installer.install(workspace, pack)
assert (workspace / ".agents/gewu/other-installation.txt").read_text() == "keep"
assert not (workspace / "AGENTS.md").exists()
def test_uninstall_rejects_traversal_in_edited_state_before_deleting_files(
tmp_path: Path,
) -> None:
workspace = tmp_path / "workspace"
workspace.mkdir()
outside = workspace.parent / "keep.txt"
outside.write_text("user data", encoding="utf-8")
installer.install(workspace, make_pack(tmp_path))
state_path = workspace / ".agents/gewu-install.json"
state = json.loads(state_path.read_text(encoding="utf-8"))
state["files"]["../../keep.txt"] = installer.digest(b"user data")
state_path.write_text(json.dumps(state), encoding="utf-8")
with pytest.raises(ValueError, match="file list is invalid"):
installer.uninstall(workspace)
assert outside.read_text(encoding="utf-8") == "user data"
assert (workspace / ".agents/gewu/manifest.json").is_file()
def test_uninstall_rejects_unrecognized_route_names_in_edited_state(tmp_path: Path) -> None:
workspace = tmp_path / "workspace"
workspace.mkdir()
installer.install(workspace, make_pack(tmp_path))
state_path = workspace / ".agents/gewu-install.json"
state = json.loads(state_path.read_text(encoding="utf-8"))
state["routes"]["../../keep.txt"] = state["routes"].pop("AGENTS.md")
state_path.write_text(json.dumps(state), encoding="utf-8")
with pytest.raises(ValueError, match="routes are invalid"):
installer.uninstall(workspace)
assert (workspace / "AGENTS.md").is_file()
def test_uninstall_rejects_agents_symlink_before_reading_state(tmp_path: Path) -> None:
workspace = tmp_path / "workspace"
external = tmp_path / "external"
workspace.mkdir()
external.mkdir()
(external / "gewu-install.json").write_text("not a state file", encoding="utf-8")
(workspace / ".agents").symlink_to(external, target_is_directory=True)
with pytest.raises(ValueError, match=".agents must not be a symlink"):
installer.uninstall(workspace)
assert (external / "gewu-install.json").read_text(encoding="utf-8") == "not a state file"