test: add provider postgres conformance

This commit is contained in:
2026-06-16 07:33:34 +02:00
parent 1f2ac6666f
commit a1692c62e3
7 changed files with 347 additions and 16 deletions

View File

@@ -262,3 +262,8 @@ Postgres implementation. It accepts a provider-owned DB-API or psycopg-like
connection, applies the bootstrap SQL in `migrate`, and persists generic
records, audit records, and pending outbox events without depending on a
specific driver package.
`user_engine.testing.postgres_provider` provides env-gated live conformance
helpers for provider repositories. They require a dedicated test DSN plus
`USER_ENGINE_POSTGRES_TEST_RESET=1` before deleting rows from bootstrap-owned
tables.

View File

@@ -37,6 +37,19 @@ The command runs:
PYTHONPATH=src python3 -m unittest discover -s tests -p 'test_*.py'
```
Live Postgres conformance tests are skipped by default. To run them against a
dedicated disposable database, install `psycopg` or `psycopg2` in the active
environment and set:
```bash
USER_ENGINE_POSTGRES_TEST_DSN='postgresql://...' \
USER_ENGINE_POSTGRES_TEST_RESET=1 \
make test
```
The reset flag is required because those tests delete rows from the
bootstrap-owned `user_engine_*` tables.
## Implementation Rule
Add new behavior in this order:

View File

@@ -308,10 +308,15 @@ dedicated bootstrap tables, applies the bootstrap SQL through `migrate`, and
uses the shared conformance harness with a fake Postgres connection for local
unit coverage.
USER-WP-0019 adds optional provider-backed conformance tests. They are skipped
by default and run only when a dedicated test database is supplied through
`USER_ENGINE_POSTGRES_TEST_DSN` and destructive cleanup is acknowledged with
`USER_ENGINE_POSTGRES_TEST_RESET=1`. The helper supports either `psycopg` or
`psycopg2` when a provider repository installs one of them. Cleanup touches
only the bootstrap-owned `user_engine_*` tables.
Likely future follow-up work should be:
- Add provider-backed conformance tests for locking, uniqueness races,
migration readiness, outbox claiming, redacted diagnostics, and restore
validation.
- Add conformance tests that run against both in-memory and Postgres stores.
- Add provider-backed conformance tests for locking, uniqueness races, outbox
claiming, redacted diagnostics, and restore validation.
- Integrate the adapter with the future NetKingdom Postgres provider repo.

View File

@@ -90,9 +90,9 @@ class PostgresUserEngineStore:
return self.schema_version == LATEST_SCHEMA_VERSION
def migrate(self) -> None:
sql = _load_bootstrap_sql()
with self._cursor() as cursor:
cursor.execute(sql)
for statement in _bootstrap_sql_statements():
cursor.execute(statement)
self.connection.commit()
@contextmanager
@@ -549,6 +549,7 @@ class PostgresUserEngineStore:
)
def _has_latest_schema(self) -> bool:
try:
with self._cursor() as cursor:
cursor.execute(
"""
@@ -559,6 +560,9 @@ class PostgresUserEngineStore:
(LATEST_SCHEMA_VERSION,),
)
return cursor.fetchone() is not None
except Exception:
self.connection.rollback()
return False
@contextmanager
def _cursor(self) -> Iterator[PostgresCursor]:
@@ -624,3 +628,11 @@ def _load_bootstrap_sql() -> str:
return (repo_root / "migrations/postgres/0001_user_engine_store.sql").read_text(
encoding="utf-8"
)
def _bootstrap_sql_statements() -> tuple[str, ...]:
return tuple(
f"{statement.strip()};"
for statement in _load_bootstrap_sql().split(";")
if statement.strip()
)

View File

@@ -0,0 +1,82 @@
"""Opt-in live Postgres conformance helpers for provider repositories."""
from __future__ import annotations
import os
from dataclasses import dataclass
from typing import Any, Mapping
from user_engine.adapters.postgres import PostgresConnection, PostgresUserEngineStore
POSTGRES_TEST_DSN_ENV = "USER_ENGINE_POSTGRES_TEST_DSN"
POSTGRES_TEST_RESET_ENV = "USER_ENGINE_POSTGRES_TEST_RESET"
_TRUTHY = {"1", "true", "yes", "on"}
_TABLES = (
"user_engine_outbox_events",
"user_engine_audit_records",
"user_engine_records",
"user_engine_schema_versions",
)
@dataclass(frozen=True)
class PostgresProviderTestConfig:
"""Configuration for destructive provider-backed Postgres tests."""
dsn: str
def postgres_provider_test_config(
environ: Mapping[str, str] | None = None,
) -> tuple[PostgresProviderTestConfig | None, str | None]:
"""Return live test config or a skip reason."""
env = environ or os.environ
dsn = env.get(POSTGRES_TEST_DSN_ENV, "").strip()
if not dsn:
return None, f"{POSTGRES_TEST_DSN_ENV} is not set"
reset_value = env.get(POSTGRES_TEST_RESET_ENV, "").strip().lower()
if reset_value not in _TRUTHY:
return (
None,
f"{POSTGRES_TEST_RESET_ENV}=1 is required because tests reset "
"user_engine_* tables",
)
return PostgresProviderTestConfig(dsn=dsn), None
def connect_postgres_provider(dsn: str) -> PostgresConnection:
"""Connect with psycopg3 or psycopg2 when a provider installs either one."""
try:
import psycopg # type: ignore[import-not-found]
return psycopg.connect(dsn) # type: ignore[no-any-return]
except ImportError:
pass
try:
import psycopg2 # type: ignore[import-not-found]
return psycopg2.connect(dsn) # type: ignore[no-any-return]
except ImportError as exc:
raise RuntimeError("install psycopg or psycopg2 to run live tests") from exc
def reset_user_engine_postgres_tables(connection: PostgresConnection) -> None:
"""Create then empty user-engine tables in a dedicated provider test DB."""
PostgresUserEngineStore(connection).migrate()
cursor = connection.cursor()
try:
for table in _TABLES:
cursor.execute(f"DELETE FROM {table}")
finally:
close = getattr(cursor, "close", None)
if callable(close):
close()
connection.commit()
def close_postgres_provider_connection(connection: Any) -> None:
"""Close provider connections that expose a close method."""
close = getattr(connection, "close", None)
if callable(close):
close()

View File

@@ -0,0 +1,107 @@
import unittest
from user_engine.adapters.postgres import PostgresUserEngineStore
from user_engine.domain import User
from user_engine.testing.postgres_provider import (
POSTGRES_TEST_DSN_ENV,
POSTGRES_TEST_RESET_ENV,
close_postgres_provider_connection,
connect_postgres_provider,
postgres_provider_test_config,
reset_user_engine_postgres_tables,
)
from user_engine.testing.store_conformance import (
assert_user_engine_store_conformance,
)
class ProviderPostgresConfigTests(unittest.TestCase):
def test_config_skips_without_dsn(self):
config, reason = postgres_provider_test_config({})
self.assertIsNone(config)
self.assertIn(POSTGRES_TEST_DSN_ENV, reason or "")
def test_config_requires_reset_acknowledgement(self):
config, reason = postgres_provider_test_config(
{POSTGRES_TEST_DSN_ENV: "postgresql://example.test/db"}
)
self.assertIsNone(config)
self.assertIn(POSTGRES_TEST_RESET_ENV, reason or "")
def test_config_accepts_dsn_and_reset_acknowledgement(self):
config, reason = postgres_provider_test_config(
{
POSTGRES_TEST_DSN_ENV: "postgresql://example.test/db",
POSTGRES_TEST_RESET_ENV: "1",
}
)
self.assertIsNotNone(config)
self.assertIsNone(reason)
class ProviderPostgresConformanceTests(unittest.TestCase):
def setUp(self):
self.config, reason = postgres_provider_test_config()
if reason:
self.skipTest(reason)
self.connections = []
def tearDown(self):
for connection in self.connections:
close_postgres_provider_connection(connection)
if self.config is not None:
cleanup = connect_postgres_provider(self.config.dsn)
try:
reset_user_engine_postgres_tables(cleanup)
finally:
close_postgres_provider_connection(cleanup)
def test_live_postgres_store_satisfies_store_conformance(self):
assert_user_engine_store_conformance(self, self._store_factory)
def test_live_postgres_migration_readiness(self):
store = self._store_factory()
self.assertFalse(store.ready)
store.migrate()
self.assertTrue(store.ready)
self.assertEqual(store.schema_version, "0001_initial")
def test_live_postgres_upsert_keeps_one_logical_record(self):
store = self._store_factory()
store.migrate()
user = User(user_id="usr_live_upsert", display_name="Original")
replacement = User(user_id="usr_live_upsert", display_name="Replacement")
store.save_user(user)
store.save_user(replacement)
self.assertEqual(store.user(user.user_id), replacement)
cursor = store.connection.cursor()
try:
cursor.execute(
"""
SELECT COUNT(*)
FROM user_engine_records
WHERE record_type = %s AND record_key = %s
""",
("users", user.user_id),
)
self.assertEqual(cursor.fetchone()[0], 1)
finally:
cursor.close()
def _store_factory(self) -> PostgresUserEngineStore:
assert self.config is not None
connection = connect_postgres_provider(self.config.dsn)
reset_user_engine_postgres_tables(connection)
self.connections.append(connection)
return PostgresUserEngineStore(connection)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,107 @@
---
id: USER-WP-0019
type: workplan
title: "Provider Backed Postgres Conformance"
domain: netkingdom
repo: user-engine
status: finished
owner: codex
topic_slug: netkingdom
planning_priority: medium
planning_order: 19
created: "2026-06-16"
updated: "2026-06-16"
depends_on:
- USER-WP-0018
---
# USER-WP-0019 - Provider Backed Postgres Conformance
## Goal
Add opt-in live Postgres conformance tests for `PostgresUserEngineStore` so
provider repositories can prove the adapter against a real database without
making ordinary user-engine tests require infrastructure.
## Scope Direction
The suite should be skipped unless an explicit test DSN and destructive reset
acknowledgement are supplied. It should cover migration readiness, the shared
store conformance harness, uniqueness/upsert behavior, rollback semantics, and
record-count diagnostics against a real provider connection.
## Non-Goals
- Do not add a mandatory Postgres driver dependency.
- Do not run live database tests by default.
- Do not implement outbox claim/ack/retry or restore validation yet.
## Tasks
```task
id: USER-WP-0019-T1
status: done
priority: high
```
Add env-gated live Postgres connection helpers that support `psycopg` or
`psycopg2` when installed.
```task
id: USER-WP-0019-T2
status: done
priority: high
```
Require an explicit destructive reset acknowledgement before cleaning
`user_engine_*` provider test tables.
```task
id: USER-WP-0019-T3
status: done
priority: high
```
Run the shared store conformance harness against a live provider connection
when configured.
```task
id: USER-WP-0019-T4
status: done
priority: medium
```
Add live checks for migration readiness and deterministic upsert uniqueness.
```task
id: USER-WP-0019-T5
status: done
priority: medium
```
Document provider setup, skip behavior, and remaining live conformance gaps.
## Acceptance Criteria
- Standard `make test` skips live Postgres tests unless env vars are present.
- Live tests fail closed if a DSN is supplied without reset acknowledgement.
- Provider cleanup only touches `user_engine_*` tables created by the bootstrap.
- The live suite can use either `psycopg` or `psycopg2` if available.
- Documentation names the required env vars and remaining follow-up work.
## Expected Outputs
- `user_engine.testing.postgres_provider` helper.
- Env-gated provider-backed tests.
- Durable-store documentation updates.
## Implementation Notes
Implemented on 2026-06-16:
- Added optional provider connection and reset helpers for live Postgres tests.
- Added env-gated tests for migration readiness, shared conformance, and
deterministic upsert uniqueness.
- Hardened adapter readiness before migration when provider tables do not yet
exist.
- Kept the default unit suite dependency-free and infrastructure-free.