The context sets, the packaged knowledge bases and the example bundles are replaced by one fictitious example set about IT operations in an invented organisation: three context sets (serverrom-2027, driftsavtale-2027 and the two-base drift-og-avtale-2027), two synthetic knowledge bases under src/portfolio_optimiser/data/kunnskapsbaser and two example bundles under src/portfolio_optimiser/data/bundles. Numbers, codes and structural values in tests and fixtures are kept; names, ids and wording change. Dated measurement documents that only recorded runs on the replaced material are deleted. Gate figures measured on the new set are not comparable with earlier ones. The exclusion gate from the previous commit is green: 0 tracked files hit outside the shared/ subtree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
95 lines
3.3 KiB
Python
95 lines
3.3 KiB
Python
"""Step 5 tests — local-folder retriever core (exact locators + EscapeRoute path-security).
|
|
|
|
Citations are exact by construction (locator slices the snippet byte-for-byte) and
|
|
deterministic; the path-security checks are behavioural (traversal / symlink-escape /
|
|
prefix-collision sibling all fail-closed). Pattern: tests/test_reference_domain.py +
|
|
tests/test_backends.py (raises).
|
|
"""
|
|
|
|
import os
|
|
|
|
import pytest
|
|
|
|
from portfolio_optimiser.retrieval import (
|
|
PathSecurityError,
|
|
is_within_dir,
|
|
retrieve,
|
|
safe_resolve,
|
|
)
|
|
|
|
|
|
@pytest.fixture()
|
|
def docs(tmp_path):
|
|
d = tmp_path / "docs"
|
|
d.mkdir()
|
|
(d / "licence.txt").write_text(
|
|
"Licence unit price renegotiation reduced the office-suite cost for the head office.",
|
|
encoding="utf-8",
|
|
)
|
|
(d / "cabling.txt").write_text(
|
|
"Cabling length was reduced after a survey on the low-use floor.",
|
|
encoding="utf-8",
|
|
)
|
|
return d
|
|
|
|
|
|
def test_locator_exactly_slices_snippet(docs) -> None:
|
|
hits = retrieve("licence office-suite cost", str(docs), top_k=5)
|
|
assert hits
|
|
for h in hits:
|
|
text = (docs / h.file).read_text(encoding="utf-8")
|
|
assert text[h.locator.start_index : h.locator.end_index] == h.snippet
|
|
|
|
|
|
def test_retrieve_is_deterministic(docs) -> None:
|
|
a = retrieve("cabling survey", str(docs), top_k=3)
|
|
b = retrieve("cabling survey", str(docs), top_k=3)
|
|
assert [(h.file, h.locator) for h in a] == [(h.file, h.locator) for h in b]
|
|
|
|
|
|
def test_top_k_respected(docs) -> None:
|
|
assert len(retrieve("the", str(docs), top_k=1)) <= 1
|
|
|
|
|
|
def test_non_positive_top_k_rejected(docs) -> None:
|
|
with pytest.raises(ValueError):
|
|
retrieve("x", str(docs), top_k=0)
|
|
|
|
|
|
def test_parent_traversal_rejected(docs) -> None:
|
|
# A `..` traversal escaping the docs folder is rejected fail-closed.
|
|
with pytest.raises(PathSecurityError):
|
|
safe_resolve(str(docs), "../outside.txt")
|
|
|
|
|
|
def test_symlink_escape_is_not_read(tmp_path, docs) -> None:
|
|
# A symlink INSIDE docs_dir pointing OUTSIDE must never be read (fail-closed).
|
|
secret = tmp_path / "secret.txt"
|
|
secret.write_text("TOPSECRET licence licence licence exfiltrated", encoding="utf-8")
|
|
os.symlink(str(secret), str(docs / "link.txt"))
|
|
# safe_resolve refuses the escaping symlink...
|
|
with pytest.raises(PathSecurityError):
|
|
safe_resolve(str(docs), "link.txt")
|
|
# ...and retrieve never surfaces the outside content even on a matching query.
|
|
hits = retrieve("licence", str(docs), top_k=10)
|
|
assert all("TOPSECRET" not in h.snippet for h in hits)
|
|
|
|
|
|
def test_prefix_collision_sibling_rejected(tmp_path) -> None:
|
|
# A sibling dir sharing a name prefix (docs vs docs-evil) is NOT inside docs.
|
|
root = tmp_path / "docs"
|
|
root.mkdir()
|
|
sibling = tmp_path / "docs-evil"
|
|
sibling.mkdir()
|
|
intruder = sibling / "secret.txt"
|
|
intruder.write_text("x", encoding="utf-8")
|
|
assert is_within_dir(str(intruder), str(root)) is False
|
|
|
|
|
|
def test_null_byte_path_rejected(docs) -> None:
|
|
# A path carrying an embedded null byte cannot be canonicalised (``os.path.realpath`` raises
|
|
# ``ValueError``); the seam MUST treat it as not-within / escaping, fail-closed — never leak the
|
|
# raw ``ValueError`` to callers.
|
|
assert is_within_dir("a\x00b.txt", str(docs)) is False
|
|
with pytest.raises(PathSecurityError):
|
|
safe_resolve(str(docs), "a\x00b.txt")
|