Bound adapter assemblies and extraction caches
This commit is contained in:
parent
9cd7c4e424
commit
f00aa65a73
4 changed files with 232 additions and 8 deletions
|
|
@ -39,6 +39,7 @@ from docforge.mcp_server import (
|
|||
)
|
||||
from docforge.models import (
|
||||
Edge,
|
||||
Limits,
|
||||
LogicEdge,
|
||||
LogicNode,
|
||||
LogicProjection,
|
||||
|
|
@ -341,6 +342,23 @@ class AdapterContractTests(unittest.TestCase):
|
|||
self.assertEqual("Workflow", index.get_node("guide.workflow")["node"]["title"])
|
||||
self.assertEqual(projection.identity(), projection.identity())
|
||||
|
||||
def test_adapter_project_enforces_graph_bounds_before_publication(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory).resolve()
|
||||
project = AdapterProject(
|
||||
Loader(self.projection(root)),
|
||||
cache_root=root / ".cache" / "bounded",
|
||||
settings=AdapterProjectSettings(limits=Limits(max_nodes=1)),
|
||||
)
|
||||
|
||||
with self.assertRaises(DocForgeError) as captured:
|
||||
project.load()
|
||||
|
||||
self.assertEqual("adapter_limit", captured.exception.code)
|
||||
self.assertEqual("nodes", captured.exception.details["kind"])
|
||||
self.assertEqual(1, captured.exception.details["maximum"])
|
||||
self.assertEqual(2, captured.exception.details["actual"])
|
||||
|
||||
def test_projection_rejects_unsorted_metadata_graph_and_identity_changes(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory).resolve()
|
||||
|
|
|
|||
102
tests/test_incremental_cache.py
Normal file
102
tests/test_incremental_cache.py
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from docforge.errors import DocForgeError
|
||||
from docforge.incremental import (
|
||||
CachedSource,
|
||||
ExtractionCache,
|
||||
load_extraction_cache,
|
||||
write_extraction_cache,
|
||||
)
|
||||
|
||||
|
||||
class ExtractionCacheBoundsTests(unittest.TestCase):
|
||||
@staticmethod
|
||||
def cache(*, payload: str = "ok", count: int = 1) -> ExtractionCache:
|
||||
sources = tuple(
|
||||
CachedSource(
|
||||
source_id=f"source-{index}",
|
||||
source_path=f"src/{index}.txt",
|
||||
fingerprint=hashlib.sha256(str(index).encode()).hexdigest(),
|
||||
extractor_version="fixture@1",
|
||||
dependencies=(),
|
||||
payload={"content": payload},
|
||||
)
|
||||
for index in range(count)
|
||||
)
|
||||
return ExtractionCache("fixture", "fixture-adapter", "1", sources)
|
||||
|
||||
def test_bounded_round_trip_and_oversized_read_is_a_cache_miss(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
path = Path(directory) / "extractions.json"
|
||||
write_extraction_cache(path, self.cache(), max_bytes=1_000, max_sources=2)
|
||||
loaded = load_extraction_cache(
|
||||
path,
|
||||
project_id="fixture",
|
||||
adapter_id="fixture-adapter",
|
||||
adapter_version="1",
|
||||
max_bytes=1_000,
|
||||
max_sources=2,
|
||||
)
|
||||
self.assertEqual(self.cache(), loaded)
|
||||
|
||||
with path.open("ab") as handle:
|
||||
handle.write(b" " * 1_000)
|
||||
self.assertIsNone(
|
||||
load_extraction_cache(
|
||||
path,
|
||||
project_id="fixture",
|
||||
adapter_id="fixture-adapter",
|
||||
adapter_version="1",
|
||||
max_bytes=1_000,
|
||||
max_sources=2,
|
||||
)
|
||||
)
|
||||
|
||||
def test_write_rejects_byte_and_source_limits_without_replacing_target(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
path = Path(directory) / "extractions.json"
|
||||
path.write_bytes(b"preserved\n")
|
||||
for cache, maximum_bytes, maximum_sources in (
|
||||
(self.cache(payload="x" * 500), 100, 2),
|
||||
(self.cache(count=2), 1_000, 1),
|
||||
):
|
||||
with self.assertRaises(DocForgeError) as captured:
|
||||
write_extraction_cache(
|
||||
path,
|
||||
cache,
|
||||
max_bytes=maximum_bytes,
|
||||
max_sources=maximum_sources,
|
||||
)
|
||||
self.assertEqual("cache_limit", captured.exception.code)
|
||||
self.assertEqual(b"preserved\n", path.read_bytes())
|
||||
|
||||
def test_symlink_and_non_regular_targets_are_cache_misses(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
real = root / "real.json"
|
||||
write_extraction_cache(real, self.cache(), max_bytes=1_000, max_sources=2)
|
||||
linked = root / "linked.json"
|
||||
linked.symlink_to(real)
|
||||
fifo = root / "fifo"
|
||||
os.mkfifo(fifo)
|
||||
for path in (linked, fifo):
|
||||
self.assertIsNone(
|
||||
load_extraction_cache(
|
||||
path,
|
||||
project_id="fixture",
|
||||
adapter_id="fixture-adapter",
|
||||
adapter_version="1",
|
||||
max_bytes=1_000,
|
||||
max_sources=2,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue