Skip to content
Menu

Documentation

sandloom.sdk.testing

The testing kit: every helper of the framework's test harness and the 1.0 names.

Public API 1.0 (spec §2.2, Appendix C).

from sandloom.sdk.testing import <name>. Signatures in a terminal: sandloom docs <name>. Beginner names are the ones generated code and the examples use; any other name is advanced (mark its import line # sandloom: advanced <reason> in generated code and examples).

Start with (beginner)

Name Kind Summary
as_user function (user_id, headers) with a freshly minted access token.
deliver async function Deliver one event to every consumer of its channel; returns how many.
fake_access_fact function Answer the access fact name with value inside the block.
fake_contract function Serve contract ref from impl for the block:
published function The events of event's channel published by the code under test:

Advanced

Name Kind Summary
apply_worker_env function Point environ (default os.environ) at this worker's backends.
assert_contract_conformance async function Assert contract_name behaves identically locally and over RPC.
assert_route_snapshot function Fail unless each module's routes equal its committed routes.snapshot.json.
bind_contract_session function Run contract calls in this block on session (TEST SEAM).
bind_test_uow function Bind session as the active UoW for database in this context.
block_mark_key function The cache key of user_id's subject-state record (the block lives there).
boot_module_alone async function create_app with only module loaded, then run its lifespan.
BootOutcome class What booting one module alone did.
capture_effects function Record publish_event / record_audit and typed Event.publish() calls made from modules.
Captured callable dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable)...
chain_coverage_problems function Tables / indexes a model declares that its owning chain does not create.
check_contract_conformance async function Run every case locally and over RPC; return the mismatches.
committing_uow function Run the block in a REAL Unit of Work that commits, like db_session().
ContractCase class One call to compare: method(*args, **kwargs) under tenant.
ContractConformanceError exception The local and the RPC path disagreed for at least one case.
email_of function The deterministic email claim minted for a test user.
FakeClock class The runtime's injectable clock (naive UTC).
flush_owned_redis async function FLUSHDB a test Redis database this run owns, keeping its marker.
get_db function Request-scoped DB session governed by a UnitOfWork (the canonical path).
Headers callable dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable)...
in_process_rpc_client function An HTTP client whose requests hit core's internal RPC routes in-process (authenticated and bound under settings, default the core's).
InMemoryCache class Dict-backed CacheProvider (incl. cas and an atomic set_max) for unit tests.
keyed_rpc_settings function A copy of cfg keyed as service_id calling ITSELF.
migrate_module function Move package's own chain in url to revision.
migration_chains function The core chain plus the chain of every loaded module that has one.
MigrationChain class One Alembic chain: the kernel's core chain or a module's own.
model_tables function {table: owning chain} for every ORM table of the core and the loaded modules — the owner is the package that DECLARES the model.
ModuleBootError exception The module started alone, but logged errors while doing so.
ModuleRegistrationDraft class A buffer of one module's registrations, applied atomically on commit.
open_saga_harness function The fixture body: point default at the test database, yield a harness.
Outcome class A normalized call outcome (comparable across the two paths).
override_instance function Configure capability/name with driver + options in the block.
probe function One readiness check: True once THIS spawn answers with its nonce.
record_revocation_checks function Record the sub of every access-token revocation check.
recording_uow function Bind an in-memory UoW that records audits/events (unit tests).
RecordingSession class Stand-in UoW session for recording_uow (only info).
register_event_schemas function Register schemas for channels (a list = permissive object schemas).
registration_surface function Run register against a recording ModuleAPI stand-in.
RegistrationSurface class What one register(core) call registered, in call order.
reload_test_env function load_dotenv(path, override=True) then re-apply the worker rewrite.
ReservedPort class A loopback port held by a listening socket owned by the test process.
revocation_mark_key function The cache key of user_id's subject-state record.
route_policy function The policy the framework applies to route (resolved live). An app asserts deny-by-default with it:
RoutePolicy class What the framework applies to one route (ADR-0009).
rpc_client_for function (client, base_url) of the RPC path for transport (see module doc).
rpc_env function rpc_settings as environment variables (maps JSON-encoded).
rpc_keys callable Fresh keys for service_id (generated once per process).
rpc_proxy_for function A generated RPC proxy for contract_name served by core via client.
rpc_request_headers function Authenticated headers of ONE raw internal-RPC request (tests).
rpc_response_headers function The binding headers a provider keyed with settings puts on its response to request_headers (tests faking a provider's answer).
rpc_settings function Settings field values keying service_id for internal RPC.
RpcKeys class One service's ephemeral internal-RPC keys (tests only).
run_on_commit async function Treat a test-owned UoW session as committed: run its before_commit checks (a failing one propagates and no on_commit callback runs —...
run_sql function Run sql in its own committed transaction; the rows it returned.
SagaHarness class The saga_harness fixture's object (see the module docstring).
SagaRun class What one run() / drive() observed.
served_rpc function Serve core's internal RPC routes (or app) on a loopback socket.
set_environment function Run the rest of the test with settings.environment == name.
SimulatedCrash exception The process "died": the transition's UoW rolls back (not an Exception, so the step classification never sees it).
spawn_served function Start app ("module:attr") in a child serving on port's socket.
SpawnedServer class A child serving on a ReservedPort, identified by its nonce.
SpawnedServerError exception The spawned server exited, or its port is answered by another process.
stop_revocation_listener async function Drop every per-process copy of a subject's state (test teardown).
tables_created_by function Tables a chain's revisions create (op.create_table(<name>, ...)).
temporary_database function Create an empty database on the test server; yield its sync URL.
TestResourceOwnershipError exception The test database / Redis index belongs to another run.
UNIQUE constant
upgrade_core_chain function Apply the core chain (outbox / audit / inbox / command log) to url.
use_security_cache function Resolve the kernel's revocation / block state to provider.
wait_revocation_listener_subscribed async function Return once no per-process copy of a subject's state can be stale.
worker_env_values function dotenv_values(path) (None dropped), the SANDLOOM_TEST_<VAR> overrides applied, rewritten for this worker. Missing file -> {} base.

Beginner names

as_user

def as_user(
    user_id: UUID | None = None,
    tenant_id: UUID | None = None,
    *,
    email: str | None = '',
    email_verified: bool | None = True,
    tenant: bool = True,
    **claims: Any,
) -> tuple[UUID, dict[str, str]]

(user_id, headers) with a freshly minted access token.

Today's make_user_headers: user_id / tenant_id default to random UUIDs; the token carries tenant_id and the signed tenant_ids allowlist; tenant=False mints a tenant-less token; any extra keyword becomes a claim (roles=["admin"]):

_, headers = as_user(tenant_id=uuid4())
await client.post("/api/v1/notes/", json={...}, headers=headers)

deliver

async def deliver(
    event: str | Event,
    payload: dict[str, Any] | None = None,
    *,
    times: int = 1,
    event_id: str | None = None,
    session: Any = None,
    core: Any = None,
) -> int

Deliver one event to every consumer of its channel; returns how many.

Today's deliver_event (same parameters and result) that also takes an Event instance. The envelope id is stable across times (a redelivery): a durable=True consumer applies it once:

await deliver(NoteCreated(note_id=n, tenant_id=t), times=2)

fake_access_fact

def fake_access_fact(
    name: str,
    value: bool | Callable[[Any], Any] = True,
    *,
    deny_category: str | None = None,
    core: Any = None,
) -> Iterator[None]

Answer the access fact name with value inside the block.

fake_contract

def fake_contract(
    ref: ContractRef[P] | str,
    impl: object,
) -> AbstractContextManager[None]

Serve contract ref from impl for the block:

with fake_contract(STATS_QUOTA_V1, AllowAll()):
    response = await client.post(...)

published

def published(event: type[E] | str) -> list[E]

The events of event's channel published by the code under test:

assert len(published(NoteCreated)) == 1

With a channel name, every recorded event of that channel.

Advanced names

apply_worker_env

def apply_worker_env(
    environ: MutableMapping[str, str] | None = None,
    *,
    create_database: bool = True,
) -> dict[str, str]

Point environ (default os.environ) at this worker's backends.

Idempotent: a value this function already wrote is left alone, a value put back by a .env.test reload is rewritten again. Creates the worker database on first use (create_database). Returns what changed.

assert_contract_conformance

async def assert_contract_conformance(
    core: Any,
    contract_name: str,
    cases: Sequence[ContractCase],
    *,
    transport: str = 'asgi',
) -> None

Assert contract_name behaves identically locally and over RPC.

assert_route_snapshot

def assert_route_snapshot(app: Any, *modules: str) -> None

Fail unless each module's routes equal its committed routes.snapshot.json.

The module's approval of its routes' resolved policy (SANDLOOM-T001): the snapshot lists method, path, auth and policy of every route the module mounts (kernel-mounted routes excluded). Regenerate with sandloom routes snapshot --write and review the diff. With no modules every loaded module is checked. A module test pins its own:

def test_routes_are_approved() -> None:
    from sandloom.main import app

    assert_route_snapshot(app, "sandloom.notes")

bind_contract_session

def bind_contract_session(session: Any) -> Iterator[Any]

Run contract calls in this block on session (TEST SEAM).

The contract runtime normally opens the provider's own Unit of Work. A test that owns an uncommitted transaction (the get_db override pattern) binds its session here so contracts read the test's data; the framework binds it automatically on the dependency-override path. Never use it in production code: it reintroduces the shared-transaction semantics the runtime exists to remove.

bind_test_uow

def bind_test_uow(
    session: Any,
    *,
    database: str = 'default',
    outbox: OutboxStore | None = None,
    audit: AuditStore | None = None,
) -> Iterator[Any]

Bind session as the active UoW for database in this context.

Uses the relational (SQLAlchemy) outbox/audit stores unless explicit ones are given. The caller owns the transaction (commit/rollback as the test needs); nothing is dispatched to the event bus.

block_mark_key

def block_mark_key(user_id: Any) -> str

The cache key of user_id's subject-state record (the block lives there).

Writing blocked_value to it blocks the subject on the very next authenticated request, exactly like a committed block_user (minus its audit / revocation / event); deleting it unblocks (unblock_user never deletes: it swaps the value for a revocation).

boot_module_alone

async def boot_module_alone(module: str, *, lifespan: bool = True) -> BootOutcome

create_app with only module loaded, then run its lifespan.

Returns started=True when the app built and (with lifespan) started and shut down cleanly; started=False with the missing contracts when the loader refused with ContractBindingError. Anything else raises (an error logged during the lifespan: ModuleBootError). Needs the test database / cache the harness configures (the db_engine fixture builds the schema).

BootOutcome

class BootOutcome(
    module: str,
    started: bool,
    missing_contracts: frozenset[str] = <factory>,
) -> None

What booting one module alone did.

capture_effects

def capture_effects(monkeypatch: Any, *modules: ModuleType) -> Captured

Record publish_event / record_audit and typed Event.publish() calls made from modules.

Patches the two names in each module (where the service imported them from sandloom.core.di), so the calls never reach a UoW: events are recorded as (channel, payload) and audits as (action, kwargs) — the exact keyword arguments, so kwargs["required"] can be asserted. Use it when the service runs on a session that is not bound as the UoW (committed sessions of a race test, a direct service call).

Captured

def Captured(*args, **kwargs)

dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)

chain_coverage_problems

def chain_coverage_problems(core: Any = None) -> list[str]

Tables / indexes a model declares that its owning chain does not create.

The harness builds tables from the models, production from the chains: a table or index on the model only passes the suite and is missing in production (SANDLOOM-T002). Each problem names the table, the model's owning module and that module's chain directory.

check_contract_conformance

async def check_contract_conformance(
    core: Any,
    contract_name: str,
    cases: Sequence[ContractCase],
    *,
    transport: str = 'asgi',
) -> list[tuple[ContractCase, Outcome, Outcome]]

Run every case locally and over RPC; return the mismatches.

committing_uow

def committing_uow(
    session_factory: Any,
    *,
    database: str = 'default',
) -> AsyncIterator[Any]

Run the block in a REAL Unit of Work that commits, like db_session().

session_factory is an async_sessionmaker on the test engine (e.g. async_sessionmaker(db_engine, expire_on_commit=False)). The session is bound as the UoW (outbox / audit rows join its transaction); on a clean exit it COMMITS and then runs the queued on_commit callbacks; an exception rolls back and skips them — the request / worker sequence, without the post-commit bus dispatch:

with pytest.raises(Boom):
    async with committing_uow(sessions) as db:
        await Service.remove(..., db)
        raise Boom          # rolled back: no post-commit effect ran

ContractCase

class ContractCase(
    method: str,
    args: tuple[Any, ...] = (),
    kwargs: Mapping[str, Any] = <factory>,
    tenant: UUID | None = None,
    label: str = '',
) -> None

One call to compare: method(*args, **kwargs) under tenant.

ContractCase.describe

def describe(self) -> str

ContractConformanceError

class ContractConformanceError(AssertionError)

The local and the RPC path disagreed for at least one case.

email_of

def email_of(user_id: uuid.UUID) -> str

The deterministic email claim minted for a test user.

FakeClock

class FakeClock(start: datetime | None = None) -> None

The runtime's injectable clock (naive UTC).

FakeClock.advance

def advance(self, delta: timedelta) -> datetime

FakeClock.advance_to

def advance_to(self, when: datetime) -> datetime

flush_owned_redis

async def flush_owned_redis(client: Any, *, owner: str | None = None) -> None

FLUSHDB a test Redis database this run owns, keeping its marker.

client is an asyncio Redis client (bytes or decoded responses). Raises TestResourceOwnershipError when another run marked it.

get_db

def get_db(request: Request) -> AsyncGenerator[AsyncSession, None]

Request-scoped DB session governed by a UnitOfWork (the canonical path).

The whole request runs in one transaction: commit when the request succeeds, rollback on any exception (including HTTPException). Services must use flush() — never commit() — so the UoW alone owns the transaction boundary. Events published via publish_event are written to the transactional outbox in the same transaction and dispatched to the bus only after a successful commit.

Test overrides registered for the raw provider (sandloom.core._internal.db_provider.get_db) bypass the UoW entirely: the test owns the session and its transaction.

Headers

def Headers(*args, **kwargs)

dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)

in_process_rpc_client

def in_process_rpc_client(core: Any, settings: Any = None) -> httpx.AsyncClient

An HTTP client whose requests hit core's internal RPC routes in-process (authenticated and bound under settings, default the core's).

InMemoryCache

class InMemoryCache(CacheProvider)(*, broken: bool = False) -> None

Dict-backed CacheProvider (incl. cas and an atomic set_max) for unit tests.

TTLs are accepted and ignored. broken=True makes every operation raise ConnectionError (a cache outage). Install it where the code under test resolves the cache, e.g. monkeypatch.setattr(cache_store, "cache", lambda name="default": mem) (sandloom.core.idempotency.cache_store) for the idempotency / contract-dedupe store.

InMemoryCache.features

def features(self) -> frozenset[str]

Optional capability features THIS instance actually supports.

A subset of the owning CapabilitySpec's declared features. A module may require a feature; the registry resolves against the driver's advertised set, so it can refuse a backend that cannot do what the module needs (e.g. a graph store without cypher). Default: none.

InMemoryCache.get

async def get(self, key: str) -> str | None

Return the value for key, or None if absent/expired.

InMemoryCache.set

async def set(self, key: str, value: str, *, ttl_seconds: int | None = None) -> None

Store value under key (overwriting), optionally with a TTL.

InMemoryCache.add

async def add(self, key: str, value: str, *, ttl_seconds: int | None = None) -> bool

Atomically store value ONLY if key is absent (SET NX).

Returns True when this call created the key, False when it already existed. The atomic primitive behind reserve-or-conflict guards (idempotency keys).

InMemoryCache.delete

async def delete(self, key: str) -> None

Remove key (no error if it does not exist).

InMemoryCache.incr

async def incr(self, key: str, *, ttl_seconds: int | None = None) -> int

Atomically increment the integer at key and return the new value.

When the key is first created (result 1) and ttl_seconds is given, the expiry is set in the same atomic step — the primitive behind fixed-window rate limiting (no counter can leak without a TTL).

InMemoryCache.compare_and_set

async def compare_and_set(
    self,
    key: str,
    expected: str,
    value: str,
    *,
    ttl_seconds: int | None = None,
) -> bool

Atomically replace key with value ONLY if it equals expected.

Returns True when swapped, False when the key is absent or holds something else. The primitive behind owner-checked state transitions (an idempotency reservation must only be finalised by its owner). Optional: drivers advertise it via the cas feature; the default raises NotImplementedError so callers can detect its absence.

InMemoryCache.compare_and_delete

async def compare_and_delete(self, key: str, expected: str) -> bool

Atomically delete key ONLY if it equals expected (cas).

InMemoryCache.set_max

async def set_max(self, key: str, value: str, *, ttl_seconds: int | None = None) -> str

Atomic monotonic write.

Reads through get first, so a subclass's fault injection (an outage simulated in get) applies, then decides and writes on the store with no await in between — atomic even when get yields.

InMemoryCache.health

async def health(self) -> HealthResult

Liveness probe for this instance. Default: always healthy.

keyed_rpc_settings

def keyed_rpc_settings(
    cfg: Any,
    contracts: Iterable[str],
    service_id: str = 'sandloom-conformance',
) -> Any

A copy of cfg keyed as service_id calling ITSELF.

The in-process RPC harness serves and calls with it: one service id on both sides, allowed exactly contracts.

migrate_module

def migrate_module(
    url: str,
    package: str,
    revision: str = 'head',
    *,
    downgrade: bool = False,
    chain: str | None = None,
) -> None

Move package's own chain in url to revision.

package is the module's import name ("sandloom_billing"); its chain lives in <package>/migrations with the version table alembic_version_<chain> (chain defaults to the package's last component, like ModuleDescriptor.migrations_chain). downgrade=True walks DOWN to revision instead of up.

migration_chains

def migration_chains(core: Any = None) -> list[MigrationChain]

The core chain plus the chain of every loaded module that has one.

Derived from the loaded descriptors (migrations_path, migration_chain_key), so a new module's chain is covered without editing any list. core defaults to the composition root's.

MigrationChain

class MigrationChain(
    key: str,
    module: str | None,
    script_location: Path,
    version_table: str,
) -> None

One Alembic chain: the kernel's core chain or a module's own.

MigrationChain.versions

versions  (property)

model_tables

def model_tables(core: Any = None) -> dict[str, MigrationChain]

{table: owning chain} for every ORM table of the core and the loaded modules — the owner is the package that DECLARES the model.

ModuleBootError

class ModuleBootError(AssertionError)

The module started alone, but logged errors while doing so.

ModuleRegistrationDraft

class ModuleRegistrationDraft() -> None

A buffer of one module's registrations, applied atomically on commit.

Module loading is transactional: instead of mutating the kernel as register() runs (then snapshotting/restoring private kernel state if it fails — fragile and tied to FastAPI/ServiceRegistry internals), every write a module makes is appended here. commit first preflights the whole draft against live kernel state (duplicate services / health checks, breaking or conflicting event schemas, malformed names) and only then applys it. If register() raises first, commit never runs; if preflight fails, NOTHING is applied. A half-registered module can no longer leave an orphan service or schema in the live inventory.

ModuleRegistrationDraft.commit

def commit(self, facade: CoreFacade) -> None

Validate the whole draft, then apply it (atomic: all or nothing).

ModuleRegistrationDraft.preflight

def preflight(self, facade: CoreFacade) -> None

Check the draft against live state; raise before applying anything.

ModuleRegistrationDraft.apply

def apply(self, facade: CoreFacade) -> None

Apply every buffered registration to facade (routers last).

Runs only after preflight passed, so the validated operations (services, schemas, health) should not fail partway. As a strict all-or-nothing backstop the whole apply is wrapped in a snapshot/restore: if ANY step still raises (e.g. include_router hits a FastAPI route conflict preflight cannot foresee), the kernel is rewound to exactly its pre-apply state so a half-registered module leaves no orphan service, route or schema behind.

open_saga_harness

def open_saga_harness(core: Any) -> AsyncIterator[SagaHarness]

The fixture body: point default at the test database, yield a harness.

Outcome

class Outcome(ok: bool, value: Any = None, error: tuple[Any, ...] | None = None) -> None

A normalized call outcome (comparable across the two paths).

override_instance

def override_instance(
    core: Any,
    capability: str,
    name: str = 'default',
    *,
    driver: str,
    **options: Any,
) -> AsyncIterator[Any]

Configure capability/name with driver + options in the block.

Yields the core's infrastructure registry. On exit the provider built for the override (if any) is closed and the previous instance configuration and provider are put back — so a lazily built, process-wide provider (e.g. the default engine) survives the test untouched:

async with override_instance(core, "cache", driver="redis",
                             url=os.environ["CACHE_URL"]):
    ...

probe

def probe(server: SpawnedServer, timeout: float = 1.0) -> bool

One readiness check: True once THIS spawn answers with its nonce.

False while it is not serving yet. Raises SpawnedServerError when the child has exited (e.g. a startup or bind failure) or when the port answers with anything but this spawn's nonce — a different process is serving there, and no request must be sent to it.

record_revocation_checks

def record_revocation_checks(monkeypatch: pytest.MonkeyPatch) -> list[str]

Record the sub of every access-token revocation check.

Wraps the kernel's single revocation lookup (the one every framework token guard reaches through the per-request memo): the returned list grows by one subject per actual cache lookup, so a request whose guards share the memo adds exactly one entry.

recording_uow

def recording_uow(*, database: str = 'default') -> Iterator[RecordingSession]

Bind an in-memory UoW that records audits/events (unit tests).

RecordingSession

class RecordingSession(
    info: dict[str, Any] = <factory>,
    audits: list[dict[str, Any]] = <factory>,
    events: list[tuple[str, dict[str, Any], str]] = <factory>,
) -> None

Stand-in UoW session for recording_uow (only info).

RecordingSession.audit_actions

def audit_actions(self) -> list[str]

register_event_schemas

def register_event_schemas(
    channels: Mapping[str, dict[str, Any] | None] | Iterable[str],
    *,
    target: Any = None,
) -> None

Register schemas for channels (a list = permissive object schemas).

target is where they go: a CoreFacade (register_event_schema), an event bus (its schema registry) or a schema registry; default: the active application's event bus. Re-registering an identical schema is a no-op.

registration_surface

def registration_surface(
    register: Callable[[Any], Any],
    *,
    api: Any | None = None,
) -> RegistrationSurface

Run register against a recording ModuleAPI stand-in.

By default nothing is registered anywhere (a pure stub: read methods such as get_service return None). Pass api=ModuleAPI(core) to also forward every call to a real core. Side effects of register itself (e.g. installing a process-wide client) still happen — restore them in the test if they matter.

RegistrationSurface

class RegistrationSurface(descriptor: Any, calls: list[Call] = <factory>) -> None

What one register(core) call registered, in call order.

RegistrationSurface.of

def of(self, method: str) -> list[Call]

The recorded calls of one ModuleAPI method, in order.

reload_test_env

def reload_test_env(path: str | os.PathLike[str]) -> None

load_dotenv(path, override=True) then re-apply the worker rewrite.

Use instead of a bare load_dotenv(..., override=True) in tests: the reload would otherwise put a parallel worker back on the shared base database / Redis.

ReservedPort

class ReservedPort(host: str = '127.0.0.1', backlog: int = 128) -> None

A loopback port held by a listening socket owned by the test process.

While open, no other process can bind the port; children inherit the very socket. A test that must make the port REFUSE connections for a while (a "service down" window) close it and reopen the same port afterwards — that re-bind fails loudly (OSError) if another process took the port meanwhile, it never silently shares it.

ReservedPort.reopen

def reopen(self) -> None

Re-reserve the SAME port after close (no-op while open).

ReservedPort.url

url  (property)

ReservedPort.fileno

def fileno(self) -> int

ReservedPort.closed

closed  (property)

ReservedPort.close

def close(self) -> None

Release the port (idempotent). Do it after the last child stopped.

revocation_mark_key

def revocation_mark_key(user_id: Any) -> str

The cache key of user_id's subject-state record.

A revocation is the value v1:after:<epoch seconds> (see revoked_after_value): the subject's access tokens issued at or before that instant are revoked. The same key holds a block (block_mark_key).

route_policy

def route_policy(route: Any) -> RoutePolicy

The policy the framework applies to route (resolved live). An app asserts deny-by-default with it:

for route in app.routes:
    policy = route_policy(route)
    assert policy.auth != "public" or policy.public_reason

RoutePolicy

class RoutePolicy(
    always_strict: bool,
    declares_budget: bool,
    staleness_budget: float,
    auth: str = 'user',
    public_reason: str | None = None,
    effect: str = 'read',
    risk: tuple[str, ...] = (),
) -> None

What the framework applies to one route (ADR-0009).

auth is "user", "service" or "public" (deny-by-default: a public route always carries public_reason). effect is "read" or "write" (by method) and risk the risks resolved from their sources (an admin gate, a money or security effect, sensitive()), sorted. Every route is strict: the subject-state record is read live on every authenticated request (always_strict is True, declares_budget False and staleness_budget 0.0 for every route).

rpc_client_for

def rpc_client_for(
    core: Any,
    transport: str,
    settings: Any = None,
) -> AsyncIterator[tuple[Any, str]]

(client, base_url) of the RPC path for transport (see module doc).

rpc_env

def rpc_env(
    service_id: str,
    *,
    peers: Iterable[str] = (),
    allowed_calls: Mapping[str, Any] | None = None,
) -> dict[str, str]

rpc_settings as environment variables (maps JSON-encoded).

rpc_keys

def rpc_keys(service_id: str) -> RpcKeys

Fresh keys for service_id (generated once per process).

rpc_proxy_for

def rpc_proxy_for(
    core: Any,
    contract_name: str,
    client: Any,
    base_url: str = 'http://sandloom-conformance',
    *,
    settings: Any = None,
) -> Any

A generated RPC proxy for contract_name served by core via client.

settings (default: the core's) key the call; the binding pins their service id — a core calling itself is its own provider.

rpc_request_headers

def rpc_request_headers(
    settings: Any,
    target: str,
    body: bytes = b'',
    *,
    http_method: str = 'POST',
    provider: str | None = None,
    sign: bool = False,
) -> dict[str, str]

Authenticated headers of ONE raw internal-RPC request (tests).

target is "<contract>/<method>" (a call) or "<contract>" (the describe GET); the request is MACed with settings' pair key towards provider (default: settings' own service id — a core calling itself) under a fresh nonce, and — with sign — also carries the command signature. The bound tenant (bind_tenant) travels in the context, as on a real call.

rpc_response_headers

def rpc_response_headers(
    settings: Any,
    request_headers: Mapping[str, str],
    status_code: int,
    body: bytes,
) -> dict[str, str]

The binding headers a provider keyed with settings puts on its response to request_headers (tests faking a provider's answer).

The caller is the one the request's pair-MAC header names; the response is keyed with the pair key towards it, exactly as a real provider does once that request verified.

rpc_settings

def rpc_settings(
    service_id: str,
    *,
    peers: Iterable[str] = (),
    allowed_calls: Mapping[str, Any] | None = None,
) -> dict[str, Any]

Settings field values keying service_id for internal RPC.

Its own Ed25519 and X25519 keys plus the public keys of service_id and every peer (rpc_keys), and the RPC_ALLOWED_CALLS map. Apply with monkeypatch.setattr(settings, name, value) per item, or settings.model_copy(update=...); rpc_env gives env names.

RpcKeys

class RpcKeys(
    service_id: str,
    kid: str,
    private_key: str,
    public_key: str,
    dh_private_key: str,
    dh_public_key: str,
) -> None

One service's ephemeral internal-RPC keys (tests only).

run_on_commit

async def run_on_commit(session: Any = None, *, database: str | None = None) -> None

Treat a test-owned UoW session as committed: run its before_commit checks (a failing one propagates and no on_commit callback runs — like a real UoW that refuses to commit), then its on_commit callbacks.

bind_test_uow / recording_uow never commit (the test owns the transaction), so post-commit side effects (e.g. revoke_user_tokens queued via on_commit) stay pending. Call this where the test treats the work as committed — e.g. after the yield of a get_db override, which is skipped when the endpoint raised (the request "rolled back"):

async def override_get_db():
    with bind_test_uow(test_db):
        yield test_db
    await run_on_commit(test_db)

Without session it takes the UoW currently bound for database (bind_test_uow / recording_uow / an ambient test UoW); with none bound there is nothing pending — on_commit outside a UoW runs at once.

run_sql

def run_sql(url: str, sql: str, params: Any = None) -> list[Any]

Run sql in its own committed transaction; the rows it returned.

SagaHarness

class SagaHarness(core: Any) -> None

The saga_harness fixture's object (see the module docstring).

SagaHarness.register

def register(
    self,
    definition: Saga[Any],
    *,
    module: str = 'saga_tests',
) -> RegisteredSaga

Register a definition for this test (checked like at startup).

SagaHarness.fake_contract

def fake_contract(
    self,
    name: str,
    *,
    idempotency_key: str = 'operation_key',
    **methods: Callable[..., Any],
) -> type

Register name as a command contract made of plain functions.

Each function takes the business arguments plus the idempotency key (operation_key unless renamed) and is wrapped in @contract_method(command=True) — its calls go through the durable command log like a real provider's. fake_calls["name.method"] records the executions (a replay does not execute the body).

SagaHarness.close

def close(self) -> None

SagaHarness.fail_step

def fail_step(
    self,
    name: str,
    *,
    times: int = 1,
    error: str = 'transient',
    direction: Direction = 'do',
    status: int | None = None,
    exception: BaseException | None = None,
) -> None

Fail the next times calls of name (direction).

error: transient (503 before the call), refused (422 before the call), timeout (the provider runs and commits, then the caller sees a timeout — ambiguous), misconfigured (401), conflict (409 ContractIdempotencyConflict) — or pass the exception to raise (before the call).

SagaHarness.crash_before

def crash_before(
    self,
    name: str,
    *,
    direction: Direction = 'do',
    times: int = 1,
) -> None

The process dies before the step call: nothing ran, the UoW rolls back.

SagaHarness.crash_after

def crash_after(self, name: str, *, direction: Direction = 'do', times: int = 1) -> None

The provider committed, then the process died before our commit.

SagaHarness.deliver_twice

def deliver_twice(self, enabled: bool = True) -> None

Deliver every nudge twice (the duplicate must be a no-op).

SagaHarness.start

async def start(
    self,
    definition: Saga[Any],
    *,
    saga_id: object,
    input: BaseModel | Mapping[str, Any],
    tenant_id: uuid.UUID | None = None,
) -> SagaHandle

start_saga in a committing UoW (the caller's business change).

SagaHarness.run

async def run(
    self,
    definition: Saga[Any],
    *,
    saga_id: object | None = None,
    input: BaseModel | Mapping[str, Any],
    tenant_id: uuid.UUID | None = None,
    max_transitions: int = 500,
    stop_when: Callable[[InstanceRow], bool] | None = None,
) -> SagaRun

Start definition and drive it until it settles (terminal/parked).

SagaHarness.drive

async def drive(
    self,
    definition: Saga[Any],
    saga_id: object,
    *,
    max_transitions: int = 500,
    stop_when: Callable[[InstanceRow], bool] | None = None,
) -> SagaRun

Drive an existing instance until it settles (or stop_when).

SagaHarness.deliver

async def deliver(self, reg: RegisteredSaga, row: InstanceRow, version: int) -> str

Deliver one nudge through the real advance path (its own UoW).

SagaHarness.instance

async def instance(
    self,
    reg: RegisteredSaga | Saga[Any],
    saga_id: object,
) -> InstanceRow

SagaHarness.assert_compensations_idempotent

async def assert_compensations_idempotent(
    self,
    definition: Saga[Any],
    *,
    input: BaseModel | Mapping[str, Any] | None = None,
    tenant_id: uuid.UUID | None = None,
) -> None

Every compensation runs once per applied step, survives a repeat and accepts "nothing to undo".

For each compensatable step k: the next step refuses, so steps 0..k apply and are compensated (compensated, one undo per applied step, in reverse). Then each compensation is called again with its key (a replay must succeed) and on a fresh saga id whose forward call never ran (nothing to undo — it must not fail).

SagaHarness.assert_resolves_before_compensating

async def assert_resolves_before_compensating(
    self,
    definition: Saga[Any],
    *,
    input: BaseModel | Mapping[str, Any] | None = None,
    tenant_id: uuid.UUID | None = None,
) -> None

An ambiguous step is re-issued (same key) before anything is undone.

For every command step up to the pivot: its first call commits at the provider but times out for the caller; the saga deadline then passes. The runtime must re-issue the forward call (<step>:do again, same key: the command log replays) and then compensate it as APPLIED — never compensate blindly, never skip it.

SagaRun

class SagaRun(
    definition: Saga[Any],
    instance: InstanceRow,
    calls: list[str],
    keys: list[str],
    steps: list[dict[str, Any]],
    transitions: list[str] = <factory>,
    crashes: int = 0,
) -> None

What one run() / drive() observed.

SagaRun.status

status  (property)

SagaRun.park_reason

park_reason  (property)

SagaRun.results

results  (property)

SagaRun.saga_id

saga_id  (property)

SagaRun.step_row

def step_row(self, step: str, direction: Direction = 'do') -> dict[str, Any]

SagaRun.assert_keys_derived

def assert_keys_derived(self) -> None

Every command call carried saga:<type>:<id>:<step>:do|undo.

served_rpc

def served_rpc(core: Any, app: Any = None, settings: Any = None) -> AsyncIterator[str]

Serve core's internal RPC routes (or app) on a loopback socket.

A uvicorn server (httptools, as deployed) runs in the CURRENT event loop and context; yields its base URL.

set_environment

def set_environment(
    monkeypatch: pytest.MonkeyPatch,
    name: Literal['dev', 'test', 'prod'],
) -> None

Run the rest of the test with settings.environment == name.

Behaviour gated on settings.is_prod / is_dev (startup guards, fail-closed defaults) follows; other settings are left as they are.

SimulatedCrash

class SimulatedCrash(BaseException)

The process "died": the transition's UoW rolls back (not an Exception, so the step classification never sees it).

spawn_served

def spawn_served(
    port: ReservedPort,
    app: str,
    *,
    env: dict[str, str],
    log_level: str = 'warning',
    label: str = '',
    **popen_kwargs: Any,
) -> SpawnedServer

Start app ("module:attr") in a child serving on port's socket.

SpawnedServer

class SpawnedServer(
    proc: subprocess.Popen[bytes],
    url: str,
    nonce: str,
    label: str = '',
) -> None

A child serving on a ReservedPort, identified by its nonce.

SpawnedServer.stop

def stop(self, timeout: float = 10.0) -> None

SpawnedServerError

class SpawnedServerError(AssertionError)

The spawned server exited, or its port is answered by another process.

stop_revocation_listener

async def stop_revocation_listener() -> None

Drop every per-process copy of a subject's state (test teardown).

There are none (ADR-0009: every check reads the record live), so this returns at once.

tables_created_by

def tables_created_by(chain: MigrationChain) -> set[str]

Tables a chain's revisions create (op.create_table(<name>, ...)).

Read from the revisions' syntax tree: the name is a string literal, the table_name= keyword, or a constant assigned a string literal in the revision (_TABLE = "x"; op.create_table(_TABLE, ...)).

temporary_database

def temporary_database(base_url: str | None = None) -> Iterator[str]

Create an empty database on the test server; yield its sync URL.

base_url (default: the test DATABASE_URL) names the server; the database is dropped (WITH (FORCE)) when the block exits.

TestResourceOwnershipError

class TestResourceOwnershipError(RuntimeError)

The test database / Redis index belongs to another run.

UNIQUE

UNIQUE: str = '__sandloom_conformance_unique__'

(no docstring)

upgrade_core_chain

def upgrade_core_chain(url: str) -> None

Apply the core chain (outbox / audit / inbox / command log) to url.

use_security_cache

def use_security_cache(
    monkeypatch: pytest.MonkeyPatch,
    provider: CacheProvider,
) -> CacheProvider

Resolve the kernel's revocation / block state to provider.

The revocation marks and the emergency block marks are read and written through sandloom.core.infra.access.cache at call time; for the rest of the test every such lookup (any instance name) returns provider.

wait_revocation_listener_subscribed

async def wait_revocation_listener_subscribed(timeout: float = 5.0) -> None

Return once no per-process copy of a subject's state can be stale.

Every authenticated request reads the subject-state record live (ADR-0009): there are no per-process copies and no invalidation listener, so this is always already true and returns at once.

worker_env_values

def worker_env_values(path: str | os.PathLike[str]) -> dict[str, str]

dotenv_values(path) (None dropped), the SANDLOOM_TEST_<VAR> overrides applied, rewritten for this worker. Missing file -> {} base.