feat: hash-only stamp, verify, and hash_report MCP tools
Memory ledger for stdio tests; NatsLedger speaks locald subjects.
This commit is contained in:
parent
bab4593605
commit
9e3dfb6fb2
4 changed files with 1668 additions and 7 deletions
6
tests/conftest.py
Normal file
6
tests/conftest.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def anyio_backend():
|
||||
return "asyncio"
|
||||
54
tests/test_ledger.py
Normal file
54
tests/test_ledger.py
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
"""Memory ledger: hash-only stamp, verify, report. Same hash is idempotent."""
|
||||
|
||||
import pytest
|
||||
|
||||
from verae_mcp_timestamping_server.ledger import MemoryLedger, SHA256_EMPTY
|
||||
|
||||
pytestmark = pytest.mark.anyio
|
||||
|
||||
|
||||
async def test_stamp_returns_json_array_with_one_receipt():
|
||||
ledger = MemoryLedger()
|
||||
receipts = await ledger.stamp(SHA256_EMPTY, scope="local")
|
||||
assert isinstance(receipts, list)
|
||||
assert len(receipts) == 1
|
||||
rec = receipts[0]
|
||||
assert rec.hash == SHA256_EMPTY
|
||||
assert rec.scope == "local"
|
||||
assert rec.receipt_id
|
||||
assert rec.created_at
|
||||
assert rec.merkle
|
||||
|
||||
|
||||
async def test_same_hash_returns_same_receipt():
|
||||
ledger = MemoryLedger()
|
||||
first = await ledger.stamp(SHA256_EMPTY, scope="local")
|
||||
second = await ledger.stamp(SHA256_EMPTY, scope="local")
|
||||
assert first[0].receipt_id == second[0].receipt_id
|
||||
assert first[0].created_at == second[0].created_at
|
||||
|
||||
|
||||
async def test_rejects_non_sha256():
|
||||
ledger = MemoryLedger()
|
||||
with pytest.raises(ValueError, match="sha256"):
|
||||
await ledger.stamp("not-a-hash", scope="local")
|
||||
|
||||
|
||||
async def test_verify_receipt_and_scope_rank():
|
||||
ledger = MemoryLedger()
|
||||
[rec] = await ledger.stamp(SHA256_EMPTY, scope="local")
|
||||
ok = await ledger.verify(rec.receipt_id, want_scope="local")
|
||||
assert ok.valid is True
|
||||
assert ok.receipt is not None
|
||||
assert ok.receipt.receipt_id == rec.receipt_id
|
||||
|
||||
weak = await ledger.verify(rec.receipt_id, want_scope="global")
|
||||
assert weak.valid is False
|
||||
|
||||
|
||||
async def test_hash_report_is_json_array():
|
||||
ledger = MemoryLedger()
|
||||
await ledger.stamp(SHA256_EMPTY, scope="local")
|
||||
report = await ledger.hash_report(SHA256_EMPTY)
|
||||
assert isinstance(report, list)
|
||||
assert report[0].hash == SHA256_EMPTY
|
||||
64
tests/test_nats_ledger.py
Normal file
64
tests/test_nats_ledger.py
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
"""NatsLedger speaks locald subjects: verae.ts.request / verify / hash.report."""
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from verae_mcp_timestamping_server.ledger import SHA256_EMPTY, MemoryLedger, Receipt
|
||||
from verae_mcp_timestamping_server.nats_ledger import NatsLedger
|
||||
|
||||
pytestmark = pytest.mark.anyio
|
||||
|
||||
|
||||
class _Msg:
|
||||
def __init__(self, data: bytes):
|
||||
self.data = data
|
||||
|
||||
|
||||
class _FakeNc:
|
||||
"""Request/reply shaped like nats-py, backed by MemoryLedger (locald protocol)."""
|
||||
|
||||
def __init__(self, inner: MemoryLedger):
|
||||
self.inner = inner
|
||||
|
||||
async def request(self, subject: str, payload: bytes, timeout: float = 2.0):
|
||||
body = json.loads(payload)
|
||||
if subject == "verae.ts.request":
|
||||
receipts = await self.inner.stamp(
|
||||
body["hash"],
|
||||
scope=body.get("scope") or "local",
|
||||
cube_id=body.get("cube_id") or "",
|
||||
org_id=body.get("org_id") or "",
|
||||
)
|
||||
return _Msg(receipts[0].model_dump_json().encode())
|
||||
if subject == "verae.ts.verify":
|
||||
result = await self.inner.verify(
|
||||
body["receipt_id"], want_scope=body.get("want_scope") or ""
|
||||
)
|
||||
if not result.valid or result.receipt is None:
|
||||
return _Msg(json.dumps({"valid": False, "error": result.error}).encode())
|
||||
return _Msg(result.receipt.model_dump_json().encode())
|
||||
if subject == "verae.hash.report":
|
||||
receipts = await self.inner.hash_report(
|
||||
body["hash"], org_id=body.get("org_id") or ""
|
||||
)
|
||||
return _Msg(
|
||||
json.dumps({"receipts": [r.model_dump() for r in receipts]}).encode()
|
||||
)
|
||||
raise AssertionError(f"unexpected subject {subject}")
|
||||
|
||||
|
||||
async def test_nats_stamp_verify_report_roundtrip():
|
||||
nc = _FakeNc(MemoryLedger())
|
||||
ledger = NatsLedger(nc)
|
||||
receipts = await ledger.stamp(SHA256_EMPTY, scope="local", cube_id="cube-1")
|
||||
assert receipts[0].hash == SHA256_EMPTY
|
||||
assert receipts[0].cube_id == "cube-1"
|
||||
|
||||
ok = await ledger.verify(receipts[0].receipt_id, want_scope="local")
|
||||
assert ok.valid is True
|
||||
assert ok.receipt is not None
|
||||
|
||||
report = await ledger.hash_report(SHA256_EMPTY)
|
||||
assert [r.receipt_id for r in report] == [receipts[0].receipt_id]
|
||||
assert isinstance(report[0], Receipt)
|
||||
54
tests/test_server.py
Normal file
54
tests/test_server.py
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
"""In-memory MCP Client against hash-only tools."""
|
||||
|
||||
import pytest
|
||||
from mcp import Client
|
||||
|
||||
from verae_mcp_timestamping_server.ledger import SHA256_EMPTY, MemoryLedger
|
||||
from verae_mcp_timestamping_server.server import build_mcp
|
||||
|
||||
pytestmark = pytest.mark.anyio
|
||||
|
||||
|
||||
async def test_stamp_tool_returns_receipts():
|
||||
mcp = build_mcp(MemoryLedger())
|
||||
async with Client(mcp, raise_exceptions=True) as client:
|
||||
result = await client.call_tool(
|
||||
"stamp", {"sha256": SHA256_EMPTY, "scope": "local"}
|
||||
)
|
||||
assert result.is_error is not True
|
||||
data = result.structured_content
|
||||
assert data is not None
|
||||
receipts = data["receipts"]
|
||||
assert receipts[0]["hash"] == SHA256_EMPTY
|
||||
assert receipts[0]["scope"] == "local"
|
||||
|
||||
|
||||
async def test_stamp_does_not_accept_content_bytes():
|
||||
mcp = build_mcp(MemoryLedger())
|
||||
async with Client(mcp, raise_exceptions=True) as client:
|
||||
listed = await client.list_tools()
|
||||
stamp = next(t for t in listed.tools if t.name == "stamp")
|
||||
props = stamp.input_schema["properties"]
|
||||
assert "sha256" in props
|
||||
assert "content" not in props
|
||||
assert "bytes" not in props
|
||||
assert "file" not in props
|
||||
|
||||
|
||||
async def test_verify_and_hash_report_tools():
|
||||
mcp = build_mcp(MemoryLedger())
|
||||
async with Client(mcp, raise_exceptions=True) as client:
|
||||
stamped = await client.call_tool(
|
||||
"stamp", {"sha256": SHA256_EMPTY, "scope": "local"}
|
||||
)
|
||||
receipts = stamped.structured_content["receipts"]
|
||||
receipt_id = receipts[0]["receipt_id"]
|
||||
|
||||
verified = await client.call_tool(
|
||||
"verify", {"receipt_id": receipt_id, "want_scope": "local"}
|
||||
)
|
||||
assert verified.structured_content["valid"] is True
|
||||
|
||||
report = await client.call_tool("hash_report", {"sha256": SHA256_EMPTY})
|
||||
items = report.structured_content["receipts"]
|
||||
assert items[0]["receipt_id"] == receipt_id
|
||||
Loading…
Reference in a new issue