Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a06ed7-828d-7ca0-a8d4-0c3e5a0c4102
93 lines
4.6 KiB
Python
93 lines
4.6 KiB
Python
import asyncio
|
|
from datetime import datetime, timezone
|
|
from uuid import uuid4
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
|
|
|
|
from hub_core.models.agent_message import AgentMessage
|
|
from hub_core.runtime.app import create_app
|
|
from hub_core.runtime.config import RuntimeSettings
|
|
from hub_core.runtime.inbox_projection import SCHEMA, import_snapshot, snapshot_hash
|
|
from hub_core.runtime.postgres_store import PostgresPortStore
|
|
from hub_core.runtime.store import InMemoryPortStore
|
|
|
|
|
|
def message(**extra):
|
|
return dict(id=str(uuid4()), from_agent="sender", to_agent="state-hub", subject="Subject",
|
|
body="Preserved body", thread_id=None, read_at=None, archived_at=None,
|
|
created_at="2026-09-05T08:00:00Z", **extra)
|
|
|
|
|
|
def bundle(rows):
|
|
return dict(schema=SCHEMA, source="state-hub/primary", captured_at="2026-09-05T08:01:00Z",
|
|
messages=rows, count=len(rows), content_hash=snapshot_hash(rows))
|
|
|
|
|
|
def test_snapshot_import_and_read_contract(tmp_path):
|
|
engine = create_async_engine(f"sqlite+aiosqlite:///{tmp_path / 'inbox.db'}")
|
|
factory = async_sessionmaker(engine, expire_on_commit=False)
|
|
root = message()
|
|
read = {**message(), "read_at": "2026-09-05T08:00:01Z", "thread_id": root["id"]}
|
|
archived = {**message(), "archived_at": "2026-09-05T08:00:02Z"}
|
|
broadcast = {**message(), "to_agent": "broadcast"}
|
|
other = {**message(), "to_agent": "other"}
|
|
data = bundle([root, read, archived, broadcast, other])
|
|
|
|
async def prepare():
|
|
async with engine.begin() as conn:
|
|
await conn.run_sync(AgentMessage.__table__.create)
|
|
async with factory.begin() as session:
|
|
result = await import_snapshot(session, data, expected_hash=data['content_hash'], apply=True)
|
|
assert result['status'] == 'applied'
|
|
async with factory.begin() as session:
|
|
assert (await import_snapshot(session, data, expected_hash=data['content_hash'], apply=True))['status'] == 'noop'
|
|
async with factory.begin() as session:
|
|
changed = bundle([root])
|
|
with pytest.raises(ValueError, match='refusing overwrite'):
|
|
await import_snapshot(session, changed, expected_hash=changed['content_hash'], apply=True)
|
|
asyncio.run(prepare())
|
|
settings = RuntimeSettings(backend='postgresql', statehub_inbox_reads=True, api_token='test-reader')
|
|
client = TestClient(create_app(settings=settings, port_store=PostgresPortStore(engine)))
|
|
url='/ports/projections/statehub-inbox'
|
|
headers={'Authorization':'Bearer test-reader'}
|
|
assert client.get(url).status_code == 401
|
|
assert client.get(url,headers={'Authorization':'Bearer wrong'}).status_code == 401
|
|
response=client.get(url,headers=headers)
|
|
assert response.status_code == 200
|
|
assert {r['id'] for r in response.json()} == {root['id'],read['id'],broadcast['id']}
|
|
assert [r['id'] for r in response.json()] == sorted([root['id'],read['id'],broadcast['id']])
|
|
assert len(client.get(url,headers=headers,params={'unread_only':True}).json()) == 2
|
|
assert client.get(url,headers=headers,params={'from_agent':'missing'}).json() == []
|
|
assert len(client.get(url,headers=headers,params={'limit':1}).json()) == 1
|
|
assert client.get(url,headers=headers,params={'to_agent':'other'}).status_code == 422
|
|
assert client.post(url,headers=headers,json={}).status_code == 405
|
|
assert response.headers['x-hub-core-read-mode'] == 'snapshot-pilot'
|
|
asyncio.run(engine.dispose())
|
|
|
|
|
|
def test_disabled_and_unconfigured_reader():
|
|
c=TestClient(create_app(settings=RuntimeSettings(),port_store=InMemoryPortStore()))
|
|
assert c.get('/ports/projections/statehub-inbox').status_code == 404
|
|
with pytest.raises(ValueError,match='PostgreSQL and operator token'):
|
|
RuntimeSettings(statehub_inbox_reads=True)
|
|
|
|
|
|
@pytest.mark.parametrize('defect',['hash','duplicate','missing_root','cycle'])
|
|
def test_invalid_snapshot_fails_before_database_access(defect):
|
|
row=message()
|
|
data=bundle([row])
|
|
if defect=='hash': data['content_hash']='wrong'
|
|
if defect=='duplicate': data['messages'].append(row)
|
|
if defect=='missing_root':
|
|
row['thread_id']=str(uuid4()); data=bundle([row])
|
|
if defect=='cycle':
|
|
row['thread_id']=row['id']; data=bundle([row])
|
|
class EmptySession:
|
|
async def execute(self,*args):
|
|
class Result:
|
|
def scalars(self): return []
|
|
return Result()
|
|
with pytest.raises(ValueError):
|
|
asyncio.run(import_snapshot(EmptySession(),data,expected_hash=data['content_hash']))
|