Skip to content

fix(OMN-1867): Address CodeRabbit review feedback for contract persistence - #233

Merged
jonahgabriel merged 5 commits into
mainfrom
fix/omn-1867-contract-persistence-followup
Feb 2, 2026
Merged

jonahgabriel merged 5 commits into
mainfrom
fix/omn-1867-contract-persistence-followup

Conversation

@jonahgabriel

@jonahgabriel jonahgabriel commented Feb 2, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Quick fixes from CodeRabbit review of PR #232 (NodeContractPersistenceEffect).

Changes

  • Add limit validation to search_contracts() in ProjectionReaderContract to prevent unbounded database queries (matches other list methods with limit <= 1000 bounds)
  • Remove stale docstring note in handlers/__init__.py that incorrectly stated handlers were "to be implemented" when they are fully implemented

Context

These are the "fix now" items from the CodeRabbit review analysis. The remaining items (X-Correlation-ID in error responses, tuple types in frozen models) are deferred to the main follow-up ticket OMN-1867.

Test plan

  • Existing unit tests pass (65 tests in test_projection_reader_contract.py)
  • Pre-commit hooks pass

Summary by CodeRabbit

  • Bug Fixes

    • Added validation to clamp search query limits to [1, 1000] range with debug logging for corrections.
    • Added X-Correlation-ID headers to error responses for improved tracking and debugging.
  • Improvements

    • Strengthened error handling infrastructure with unified error response mechanisms and enhanced error context propagation across backend operations.

…tence

Quick fixes from CodeRabbit review of PR #232:

- Add limit validation to search_contracts() in ProjectionReaderContract
  to prevent unbounded database queries (matches other list methods)
- Remove stale docstring note in handlers/__init__.py that incorrectly
  stated handlers were "to be implemented"
@coderabbitai

coderabbitai Bot commented Feb 2, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR introduces a centralized PostgreSQL error handling mechanism through a new MixinPostgresErrorResponse mixin and PostgresErrorContext dataclass. Six handler classes are refactored to inherit from this mixin, consolidating granular exception handling into a unified error response builder. Additional changes include input validation in the projection reader, type updates to model fields, HTTP header additions to error responses, expanded test coverage, and module documentation cleanup.

Changes

Cohort / File(s) Summary
New PostgreSQL Error Handling Infrastructure
src/omnibase_infra/mixins/__init__.py, src/omnibase_infra/mixins/mixin_postgres_error_response.py
Introduces MixinPostgresErrorResponse and PostgresErrorContext to provide centralized error handling, exception mapping, message sanitization, logging, and duration calculation for PostgreSQL operations; exports new public classes and dataclass.
Handler Migration to Error Handling Mixin
src/omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_*.py (cleanup_topics, contract_upsert, deactivate, heartbeat, mark_stale, topic_update)
Refactors all six handlers to inherit from MixinPostgresErrorResponse, replacing granular exception branches with unified error context construction and delegation to _build_error_response; adds explicit init to store asyncpg.Pool.
Model & API Updates
src/omnibase_infra/services/registry_api/models/model_contract_view.py, src/omnibase_infra/services/registry_api/routes.py
Changes topics_published and topics_subscribed fields from list to tuple types; adds X-Correlation-ID headers to HTTP error responses in get_contract, list_topics, and get_topic endpoints.
Input Validation & Type Support
src/omnibase_infra/projectors/projection_reader_contract.py
Adds limit clamping validation [1, 1000] to search_contracts with debug logging for corrections.
Test Coverage & Documentation
src/omnibase_infra/nodes/node_contract_persistence_effect/handlers/__init__.py, tests/unit/nodes/node_contract_persistence_effect/handlers/test_normalize_topic.py, tests/integration/mixins/test_mixin_postgres_error_response.py
Removes docstring note from handlers module; adds parameterized edge-case tests for topic normalization; introduces comprehensive integration test suite for error handling mixin covering exception mapping, sanitization, logging, duration, and context propagation.

Sequence Diagram(s)

sequenceDiagram
    participant Handler as Handler (e.g., ContractUpsert)
    participant MixinPostgres as MixinPostgresErrorResponse
    participant ErrorContext as PostgresErrorContext
    participant LogSystem as Logging & Response
    
    Handler->>Handler: Execute operation (SQL, etc.)
    Handler->>Handler: Exception occurs
    Handler->>ErrorContext: new PostgresErrorContext(exception, operation, ...)
    ErrorContext-->>Handler: context initialized with error metadata
    Handler->>MixinPostgres: _build_error_response(ctx)
    MixinPostgres->>MixinPostgres: Map exception to EnumPostgresErrorCode
    MixinPostgres->>MixinPostgres: Sanitize error message
    MixinPostgres->>LogSystem: Log with appropriate level & context
    MixinPostgres->>MixinPostgres: Calculate duration_ms
    MixinPostgres-->>Handler: Return ModelBackendResult (success=False)
    Handler-->>Handler: Return result to caller
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 A mixin hops in with centralized grace,
Error contexts dance in their structured place,
Six handlers align in harmonious glow,
Where exceptions map to responses below. 🌟

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately reflects the main objective: addressing CodeRabbit review feedback for contract persistence work, specifically the fixes that are ready to merge.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/omn-1867-contract-persistence-followup

Comment @coderabbitai help to get the list of available commands and usage tips.

…stence

- Add X-Correlation-ID header to all HTTPException error responses in
  contract/topic API endpoints (routes.py)
- Change topics_published/topics_subscribed from list[str] to tuple[str, ...]
  in ModelContractView for true immutability with frozen=True
- Create MixinPostgresErrorResponse with PostgresErrorContext dataclass
  for shared PostgreSQL exception handling pattern
- Add 19 edge case tests for normalize_topic_for_storage() covering:
  - Empty string input
  - Only-prefix inputs (dev., {env}., etc.)
  - Double-prefix scenarios
  - Prefix-in-middle (should not strip)
Refactored all PostgreSQL handlers in NodeContractPersistenceEffect to use
the new MixinPostgresErrorResponse mixin, reducing ~473 lines of duplicated
exception handling code:

- HandlerPostgresContractUpsert: -110 lines
- HandlerPostgresTopicUpdate: -108 lines
- HandlerPostgresMarkStale: -77 lines
- HandlerPostgresHeartbeat: -70 lines
- HandlerPostgresDeactivate: -54 lines
- HandlerPostgresCleanupTopics: -54 lines

Each handler now uses PostgresErrorContext with operation-specific:
- operation name for logging
- log_context with relevant fields (contract_id, topic_suffix, etc.)
- operation_error_code for precise error classification

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In
`@src/omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_deactivate.py`:
- Around line 191-200: Replace the PostgresErrorContext usage in the PostgreSQL
handlers (e.g., deactivate_contract handler in handler_postgres_deactivate.py
and other handlers: topic_update, heartbeat, mark_stale, contract_upsert,
cleanup_topics) with ModelInfraErrorContext.with_correlation(), passing the
original exception, correlation_id, and include transport_type="postgres" plus
operation="deactivate_contract" (or the appropriate operation name for each
handler) and start_time/log_context/operation_error_code as before; then return
self._build_error_response(...) using the new ModelInfraErrorContext result so
all node_* handlers follow the ModelInfraErrorContext.with_correlation() pattern
required by the coding guidelines.
🧹 Nitpick comments (1)
src/omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_topic_update.py (1)

286-298: Avoid swallowing task cancellations in async contexts.
Catching Exception can intercept asyncio.CancelledError on Python versions where it subclasses Exception, preventing cooperative cancellation. Consider re‑raising it explicitly.

♻️ Suggested guard for cancellation
+import asyncio
 ...
-        except Exception as e:
+        except asyncio.CancelledError:
+            raise
+        except Exception as e:
             ctx = PostgresErrorContext(

Comment on lines +191 to +200
except Exception as e:
ctx = PostgresErrorContext(
exception=e,
operation="deactivate_contract",
correlation_id=correlation_id,
start_time=start_time,
log_context={"contract_id": payload.contract_id},
operation_error_code=EnumPostgresErrorCode.DEACTIVATE_ERROR,
)
return self._build_error_response(ctx)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Check PostgresErrorContext usage in this node's handlers
rg -n "PostgresErrorContext" --type py -C 2 src/omnibase_infra/nodes/node_contract_persistence_effect/handlers/

Repository: OmniNode-ai/omnibase_infra

Length of output: 8328


🏁 Script executed:

# Check if ModelInfraErrorContext is used elsewhere in the nodes
rg -n "ModelInfraErrorContext" --type py src/omnibase_infra/nodes/

Repository: OmniNode-ai/omnibase_infra

Length of output: 8646


🏁 Script executed:

# Check the definition of PostgresErrorContext to understand its structure
rg -n "class PostgresErrorContext" --type py -A 10

Repository: OmniNode-ai/omnibase_infra

Length of output: 1228


🏁 Script executed:

# Check if _build_error_response method wraps or transforms the context
rg -n "_build_error_response" --type py -B 2 -A 8 src/omnibase_infra/nodes/node_contract_persistence_effect/handlers/

Repository: OmniNode-ai/omnibase_infra

Length of output: 5017


Use ModelInfraErrorContext.with_correlation() for exception handling instead of PostgresErrorContext.

The coding guidelines mandate using ModelInfraErrorContext.with_correlation() for all exception raises in **/nodes/**/*.py files with transport_type and operation parameters. The current implementation uses PostgresErrorContext, which is a specialized mixin-level context that does not comply with this requirement. This pattern is inconsistent with other nodes in the codebase (e.g., node_registration_orchestrator, node_ledger_write_effect) which correctly use ModelInfraErrorContext.with_correlation().

Additionally, the transport_type parameter required by the guideline is missing from the error context. Replace PostgresErrorContext with ModelInfraErrorContext.with_correlation() and include the appropriate transport_type.

Note: This issue affects all PostgreSQL handlers in this node (at least 6 handlers: deactivate, topic_update, heartbeat, mark_stale, contract_upsert, cleanup_topics).

🤖 Prompt for AI Agents
In
`@src/omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_deactivate.py`
around lines 191 - 200, Replace the PostgresErrorContext usage in the PostgreSQL
handlers (e.g., deactivate_contract handler in handler_postgres_deactivate.py
and other handlers: topic_update, heartbeat, mark_stale, contract_upsert,
cleanup_topics) with ModelInfraErrorContext.with_correlation(), passing the
original exception, correlation_id, and include transport_type="postgres" plus
operation="deactivate_contract" (or the appropriate operation name for each
handler) and start_time/log_context/operation_error_code as before; then return
self._build_error_response(...) using the new ModelInfraErrorContext result so
all node_* handlers follow the ModelInfraErrorContext.with_correlation() pattern
required by the coding guidelines.

- Add 46 integration tests covering all exception type mappings
- Test error sanitization, duration calculation, correlation ID propagation
- Test logging levels (WARNING for retriable, ERROR for non-retriable)
- Test operation-specific error codes for all 6 handler operations
- Add debug logging when search_contracts() limit is corrected

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@tests/integration/mixins/test_mixin_postgres_error_handler.py`:
- Around line 33-34: This module under tests/integration lacks an integration
marker; add a module-level pytest marker by defining pytestmark =
pytest.mark.integration (after the existing import pytest) so all tests in this
file are marked as integration tests; ensure you use the pytestmark symbol
rather than decorating individual functions.
- Around line 58-82: The three test helper factories (make_infra_timeout_error,
make_infra_auth_error, make_infra_connection_error) build error contexts without
a correlation_id; update each to ensure the context includes a correlation_id by
generating one when absent (use uuid.uuid4() -> string) and attach it to the
ModelTimeoutErrorContext / ModelInfraErrorContext before constructing the
Infra*Error so tests propagate a correlation_id for observability.

Comment on lines +33 to +34
import pytest

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add an integration pytest marker for this module.

This file lives under tests/integration/ but has no integration marker, which prevents reliable test selection. A module-level marker keeps the intent explicit.

✅ Suggested change
 import pytest
 
+pytestmark = pytest.mark.integration

As per coding guidelines: Use pytest markers: @pytest.mark.unit, @pytest.mark.integration, @pytest.mark.slow, @pytest.mark.chaos, @pytest.mark.serial for test classification.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import pytest
import pytest
pytestmark = pytest.mark.integration
🤖 Prompt for AI Agents
In `@tests/integration/mixins/test_mixin_postgres_error_handler.py` around lines
33 - 34, This module under tests/integration lacks an integration marker; add a
module-level pytest marker by defining pytestmark = pytest.mark.integration
(after the existing import pytest) so all tests in this file are marked as
integration tests; ensure you use the pytestmark symbol rather than decorating
individual functions.

Comment on lines +58 to +82
def make_infra_timeout_error(message: str) -> InfraTimeoutError:
"""Create an InfraTimeoutError with required context."""
ctx = ModelTimeoutErrorContext(
transport_type=EnumInfraTransportType.DATABASE,
operation="test_operation",
)
return InfraTimeoutError(message, context=ctx)


def make_infra_auth_error(message: str) -> InfraAuthenticationError:
"""Create an InfraAuthenticationError with optional context."""
ctx = ModelInfraErrorContext(
transport_type=EnumInfraTransportType.DATABASE,
operation="test_operation",
)
return InfraAuthenticationError(message, context=ctx)


def make_infra_connection_error(message: str) -> InfraConnectionError:
"""Create an InfraConnectionError with optional context."""
ctx = ModelInfraErrorContext(
transport_type=EnumInfraTransportType.DATABASE,
operation="test_operation",
)
return InfraConnectionError(message, context=ctx)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Include correlation_id in Infra error contexts created by helpers.

The helper factories build infra error contexts without a correlation_id, which violates the correlation propagation requirement and weakens observability in tests.

🛠️ Suggested change
-def make_infra_timeout_error(message: str) -> InfraTimeoutError:
+def make_infra_timeout_error(
+    message: str,
+    correlation_id: UUID | None = None,
+) -> InfraTimeoutError:
     """Create an InfraTimeoutError with required context."""
     ctx = ModelTimeoutErrorContext(
         transport_type=EnumInfraTransportType.DATABASE,
         operation="test_operation",
+        correlation_id=correlation_id or uuid4(),
     )
     return InfraTimeoutError(message, context=ctx)
 
-def make_infra_auth_error(message: str) -> InfraAuthenticationError:
+def make_infra_auth_error(
+    message: str,
+    correlation_id: UUID | None = None,
+) -> InfraAuthenticationError:
     """Create an InfraAuthenticationError with optional context."""
-    ctx = ModelInfraErrorContext(
+    ctx = ModelInfraErrorContext.with_correlation(
         transport_type=EnumInfraTransportType.DATABASE,
         operation="test_operation",
+        correlation_id=correlation_id,
     )
     return InfraAuthenticationError(message, context=ctx)
 
-def make_infra_connection_error(message: str) -> InfraConnectionError:
+def make_infra_connection_error(
+    message: str,
+    correlation_id: UUID | None = None,
+) -> InfraConnectionError:
     """Create an InfraConnectionError with optional context."""
-    ctx = ModelInfraErrorContext(
+    ctx = ModelInfraErrorContext.with_correlation(
         transport_type=EnumInfraTransportType.DATABASE,
         operation="test_operation",
+        correlation_id=correlation_id,
     )
     return InfraConnectionError(message, context=ctx)

As per coding guidelines: Propagate correlation_id from incoming requests; auto-generate with uuid4() if missing; include in all error context.

🤖 Prompt for AI Agents
In `@tests/integration/mixins/test_mixin_postgres_error_handler.py` around lines
58 - 82, The three test helper factories (make_infra_timeout_error,
make_infra_auth_error, make_infra_connection_error) build error contexts without
a correlation_id; update each to ensure the context includes a correlation_id by
generating one when absent (use uuid.uuid4() -> string) and attach it to the
ModelTimeoutErrorContext / ModelInfraErrorContext before constructing the
Infra*Error so tests propagate a correlation_id for observability.

@jonahgabriel jonahgabriel left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Issue Found

File: src/omnibase_infra/mixins/mixin_postgres_error_handler.py:106

CLAUDE.md naming violation: File is mixin_postgres_error_handler.py but class is MixinPostgresErrorResponse. Per CLAUDE.md, mixin naming requires exact correspondence: mixin_<name>.py → Mixin<Name>.

All 27 other mixins in the codebase follow this pattern exactly.

Fix: Rename file to mixin_postgres_error_response.py (preferred, since the mixin builds error responses via ModelBackendResult).

Fix: Rename src/omnibase_infra/mixins/mixin_postgres_error_handler.py to mixin_postgres_error_response.py to match class name MixinPostgresErrorResponse per CLAUDE.md naming convention

Checked for bugs and CLAUDE.md compliance. No other issues found.

Rename mixin_postgres_error_handler.py to mixin_postgres_error_response.py
to comply with CLAUDE.md naming convention requiring exact correspondence
between file name and class name (mixin_<name>.py → Mixin<Name>).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/omnibase_infra/mixins/mixin_postgres_error_response.py`:
- Around line 186-203: The code uses ctx.correlation_id directly when building
base_context which can be None; before computing duration_ms and constructing
base_context (in the error handling block where correlation_id =
ctx.correlation_id and duration_ms is computed) add a fallback to generate a new
UUID (uuid.uuid4()) when ctx.correlation_id is falsy and assign it back to
correlation_id so all logs/results use a non-null string; ensure you import uuid
at top of the module and convert the generated UUID to a string when placing it
into base_context (references: ctx.correlation_id, correlation_id, duration_ms,
base_context, ModelBackendResult/BackendResult).

Comment on lines +186 to +203
exception = ctx.exception
operation = ctx.operation
correlation_id = ctx.correlation_id
start_time = ctx.start_time
log_context = ctx.log_context
operation_error_code = ctx.operation_error_code
# Local import to avoid circular import at module load time
# (mixins/__init__.py loads before nodes/__init__.py in some paths)
from omnibase_infra.nodes.effects.models.model_backend_result import (
ModelBackendResult as BackendResult,
)

duration_ms = (time.perf_counter() - start_time) * 1000

# Build base log context
base_context: dict[str, object] = {
"correlation_id": str(correlation_id),
"duration_ms": duration_ms,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Auto-generate correlation_id when missing to keep error context traceable.

If ctx.correlation_id is None, logs and results will carry "None", breaking traceability. Add a uuid4() fallback before building the base context.

🛠️ Suggested fix
-from typing import TYPE_CHECKING
+from typing import TYPE_CHECKING
+from uuid import uuid4
@@
-        correlation_id = ctx.correlation_id
+        correlation_id = ctx.correlation_id or uuid4()

As per coding guidelines: Propagate correlation_id from incoming requests; auto-generate with uuid4() if missing; include in all error context.

🤖 Prompt for AI Agents
In `@src/omnibase_infra/mixins/mixin_postgres_error_response.py` around lines 186
- 203, The code uses ctx.correlation_id directly when building base_context
which can be None; before computing duration_ms and constructing base_context
(in the error handling block where correlation_id = ctx.correlation_id and
duration_ms is computed) add a fallback to generate a new UUID (uuid.uuid4())
when ctx.correlation_id is falsy and assign it back to correlation_id so all
logs/results use a non-null string; ensure you import uuid at top of the module
and convert the generated UUID to a string when placing it into base_context
(references: ctx.correlation_id, correlation_id, duration_ms, base_context,
ModelBackendResult/BackendResult).

@jonahgabriel
jonahgabriel merged commit 418fa6b into main Feb 2, 2026
4 of 13 checks passed
@jonahgabriel
jonahgabriel deleted the fix/omn-1867-contract-persistence-followup branch April 4, 2026 02:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant