Skip to content

fix: wire schema drift errors and constrain notification triggers - #35

Merged
2233admin merged 3 commits into
mainfrom
fix/wiring-schema-drift-notify
Jul 19, 2026
Merged

fix: wire schema drift errors and constrain notification triggers#35
2233admin merged 3 commits into
mainfrom
fix/wiring-schema-drift-notify

Conversation

@2233admin

Copy link
Copy Markdown
Owner

What changed

  • propagate structured error_type values from RSS bozo failures and CLI JSON decode failures
  • map SAXParseException to SCHEMA_DRIFT
  • restrict notification rule create/update payloads to the supported on_new_record trigger
  • split the affected channel test suites by concern so each module stays below the repository's 200-line limit

Why

The control recorder can only emit schema-drift events when a structured error type reaches it. Several parse failure paths discarded that signal. Notification dispatch currently produces only on_new_record, so accepting other new trigger strings creates inert rules.

Scope

This PR prevents new unsupported trigger_event values through the create/update schemas. It does not migrate or reject already-persisted invalid rows at read time.

HTTP 200 responses containing HTML or an empty body can still produce bozo=False with zero entries in feedparser; that separate detection gap is tracked as W1b rather than being folded into this wiring fix.

Validation

  • full pre-push suite: 1823 passed, 11 skipped (90.27% coverage)
  • focused W1/W2 suite after the test split: 73 passed
  • focused suite also passes with RuntimeWarning promoted to an error
  • Ruff passed for all affected test modules
  • frontend lint, typecheck, and build passed in the pre-push hook
  • git diff --check passed

2233admin and others added 2 commits July 19, 2026 04:19
… constrain trigger_event [W1,W2]

W1: the control layer's SCHEMA_DRIFT chain (error_kinds -> evaluator ->
policies -> actuator) runs every 60s but control/recorder.py only records
an error kind when error_type is not None. Of 4 parse-failure sites, only
crawl4ai_channel.py passed error_type correctly. Wire the other 3:

- rss_channel.py: both collect() and fetch()'s feedparser bozo-flag branch
  now pass error_type via a new _bozo_error_type() helper, using feedparser's
  real bozo_exception class name (verified empirically: SAXParseException
  across malformed markup, truncated declarations, and encoding mismatches),
  falling back to "ParseError" when none is attached.
- cli_channel.py: the json.JSONDecodeError branch now passes
  error_type=type(exc).__name__ ("JSONDecodeError", already mapped).
- error_kinds.py: added "SAXParseException" -> ErrorKind.SCHEMA_DRIFT since
  it wasn't in the map yet (minimal extension of the existing taxonomy).

W2: NotificationRule.trigger_event was a free-text str but
dispatch_notifications() only ever queries/fires "on_new_record" (its
default and only caller-supplied value) -- a rule saved with any other
value became permanently, silently inert with no producer. Constrained
trigger_event to Literal["on_new_record"] in NotificationRuleCreate/Update
(backend/schemas/notification.py) so the API rejects an unsupported value
loudly instead of persisting a dead rule. DB column stays String (no
migration) -- this is a validation-layer guard only.
@repowise-bot

repowise-bot Bot commented Jul 18, 2026

Copy link
Copy Markdown

✅ Health: 9.4

📋 At a glance
2 hotspots touched · 8 new findings introduced. Scoped to tests.

🚨 Change risk: 9.5/10 (high)
This change's risk is driven by:

  • more lines added than baseline
  • more scattered than baseline
🔎 More signals (1)

🔥 Hotspots touched (2)

  • .../channels/test_rss_channel.py — 2 commits/90d, 0 dependents · primary owner: xujinghua (100%)
  • .../channels/test_cli_channel.py — 2 commits/90d, 0 dependents · primary owner: xujinghua (100%)

👀 Suggested reviewers @xujinghua


📊 Full report · ⭐ Star Repowise · 📥 Install bot · Last updated 2026-07-19 03:22 UTC
Silence on a single PR with [skip repowise] in the title · Per-repo toggle on repowise.dev/settings?tab=bot

@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@2233admin, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 32 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9e3c6c99-485f-453a-b527-595e94101c93

📥 Commits

Reviewing files that changed from the base of the PR and between 9f1c5ce and b9177c1.

📒 Files selected for processing (2)
  • backend/channels/cli_channel.py
  • tests/unit/channels/test_cli_channel_execution.py
📝 Walkthrough

Walkthrough

CLI and RSS channel failures now preserve structured error types for schema-drift classification. Channel tests were reorganized and expanded across execution, allowlists, parsing, HTTP statuses, and entry mapping. Notification schemas now restrict trigger events to the supported value.

Changes

Channel error classification

Layer / File(s) Summary
Structured parse error propagation
backend/channels/cli_channel.py, backend/channels/rss_channel.py, backend/control/error_kinds.py, tests/unit/channels/test_cli_channel_execution.py, tests/unit/channels/test_rss_channel_schema_drift.py, tests/unit/channels/test_rss_fetch.py, tests/unit/control/test_error_kinds.py
CLI JSON decode failures and RSS bozo parse failures now expose error types mapped to schema drift, including SAX parse exceptions and fallback parse errors.
CLI execution and allowlist coverage
tests/unit/channels/test_cli_channel.py, tests/unit/channels/test_cli_channel_allowlist.py, tests/unit/channels/test_cli_channel_execution.py
CLI tests are split into focused modules covering allowlist enforcement, subprocess prevention, output parsing, failures, timeouts, and retry classification.
RSS parsing and fetch coverage
backend/channels/rss_channel.py, tests/unit/channels/test_rss_channel.py, tests/unit/channels/test_rss_channel_entries.py, tests/unit/channels/test_rss_channel_errors.py, tests/unit/channels/test_rss_fetch.py, tests/unit/channels/test_rss_fetch_statuses.py
RSS tests add entry mapping, request failure, HTTP status, feed parsing, cursor, metadata, thread, and runner coverage while simplifying HTTP mocks.

Notification schema validation

Layer / File(s) Summary
Notification trigger event contract
backend/schemas/notification.py, tests/unit/test_schemas_notification.py
Create and update schemas now accept only on_new_record, while update requests may omit the field.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related issues

  • Issue 36 — Both changes concern RSS parse/error handling, although this PR adds error classification while the issue addresses empty or HTML response detection.

Poem

I’m a small rabbit, thumping with glee,
Structured errors now hop clearly.
RSS knows when XML goes astray,
CLI tells schema drift the right way.
Triggers accept one tune to sing—
on_new_record, the approved spring!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 39.24% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the two main changes: schema-drift error wiring and notification trigger restriction.
Description check ✅ Passed The description is clearly related to the code changes and matches the PR's error propagation, schema, and test-splitting work.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request improves error classification and schema validation across channels and notifications. Specifically, it ensures that JSON decode errors in the CLI channel and parse exceptions (like SAXParseException) in the RSS channel propagate their specific error types to trigger the SCHEMA_DRIFT pipeline instead of being silently dropped. It also constrains the notification rule trigger_event to Literal["on_new_record"] to prevent inert rules. The unit tests have been thoroughly reorganized and expanded to cover these changes. Review feedback suggests ensuring bozo_exception is an instance of Exception before checking its type, and using an 'or' fallback when retrieving the exception message to avoid displaying None if the attribute is explicitly set to None.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +39 to +41
bozo_exception = getattr(parsed, "bozo_exception", None)
if bozo_exception is not None:
return type(bozo_exception).__name__

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

To ensure robust error classification and prevent unexpected type names (e.g., if bozo_exception is mocked as a string or set to a non-exception type), it is safer to explicitly verify that bozo_exception is an instance of Exception before accessing its class name.

Suggested change
bozo_exception = getattr(parsed, "bozo_exception", None)
if bozo_exception is not None:
return type(bozo_exception).__name__
bozo_exception = getattr(parsed, "bozo_exception", None)
if isinstance(bozo_exception, Exception):
return type(bozo_exception).__name__

if parsed.bozo and not parsed.entries:
return ChannelResult.fail(
f"Failed to parse feed: {getattr(parsed, 'bozo_exception', 'unknown error')}"
f"Failed to parse feed: {getattr(parsed, 'bozo_exception', 'unknown error')}",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

If parsed.bozo_exception is explicitly set to None, getattr(parsed, 'bozo_exception', 'unknown error') will return None rather than the default 'unknown error', resulting in a confusing error message like Failed to parse feed: None. Using or ensures we fall back to 'unknown error' in this case.

Suggested change
f"Failed to parse feed: {getattr(parsed, 'bozo_exception', 'unknown error')}",
f"Failed to parse feed: {getattr(parsed, 'bozo_exception', None) or 'unknown error'}",

if parsed.bozo and not parsed.entries:
raise ChannelFetchError(
f"Failed to parse feed: {getattr(parsed, 'bozo_exception', 'unknown error')}"
f"Failed to parse feed: {getattr(parsed, 'bozo_exception', 'unknown error')}",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

If parsed.bozo_exception is explicitly set to None, getattr(parsed, 'bozo_exception', 'unknown error') will return None rather than the default 'unknown error', resulting in a confusing error message like Failed to parse feed: None. Using or ensures we fall back to 'unknown error' in this case.

Suggested change
f"Failed to parse feed: {getattr(parsed, 'bozo_exception', 'unknown error')}",
f"Failed to parse feed: {getattr(parsed, 'bozo_exception', None) or 'unknown error'}",

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/unit/channels/test_cli_channel_allowlist.py`:
- Around line 91-108: Update
test_allowlist_rejection_permanent_through_fetch_seam to assert that
effective_error_type(excinfo.value) equals the expected permanent allowlist
rejection type, rather than only checking is_retryable(...) is False. Retain the
existing fetch invocation and use the established error-taxonomy symbol for the
exact type.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 274bb22e-1705-4eed-bade-1ca57c45cedf

📥 Commits

Reviewing files that changed from the base of the PR and between 36b9b7e and 9f1c5ce.

📒 Files selected for processing (15)
  • backend/channels/cli_channel.py
  • backend/channels/rss_channel.py
  • backend/control/error_kinds.py
  • backend/schemas/notification.py
  • tests/unit/channels/test_cli_channel.py
  • tests/unit/channels/test_cli_channel_allowlist.py
  • tests/unit/channels/test_cli_channel_execution.py
  • tests/unit/channels/test_rss_channel.py
  • tests/unit/channels/test_rss_channel_entries.py
  • tests/unit/channels/test_rss_channel_errors.py
  • tests/unit/channels/test_rss_channel_schema_drift.py
  • tests/unit/channels/test_rss_fetch.py
  • tests/unit/channels/test_rss_fetch_statuses.py
  • tests/unit/control/test_error_kinds.py
  • tests/unit/test_schemas_notification.py

Comment on lines +91 to +108
@pytest.mark.asyncio
async def test_allowlist_rejection_permanent_through_fetch_seam(channel):
"""The fetch seam preserves the permanent allowlist rejection."""
from backend.channels.base import ChannelFetchError, FetchContext
from backend.pipeline.error_taxonomy import effective_error_type, is_retryable

with _allow():
with pytest.raises(ChannelFetchError) as excinfo:
await channel.fetch(
FetchContext(
config={
"binary": sys.executable,
"command": ["-c", "print(1)"],
},
params={},
)
)
assert is_retryable(effective_error_type(excinfo.value)) is False

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Assert that the fetch seam preserves the exact error type.

is_retryable(...) is False also passes for None or unknown types, so this test would miss the regression it claims to cover.

Proposed fix
-    assert is_retryable(effective_error_type(excinfo.value)) is False
+    error_type = effective_error_type(excinfo.value)
+    assert error_type == "BinaryNotAllowedError"
+    assert is_retryable(error_type) is False
📝 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
@pytest.mark.asyncio
async def test_allowlist_rejection_permanent_through_fetch_seam(channel):
"""The fetch seam preserves the permanent allowlist rejection."""
from backend.channels.base import ChannelFetchError, FetchContext
from backend.pipeline.error_taxonomy import effective_error_type, is_retryable
with _allow():
with pytest.raises(ChannelFetchError) as excinfo:
await channel.fetch(
FetchContext(
config={
"binary": sys.executable,
"command": ["-c", "print(1)"],
},
params={},
)
)
assert is_retryable(effective_error_type(excinfo.value)) is False
`@pytest.mark.asyncio`
async def test_allowlist_rejection_permanent_through_fetch_seam(channel):
"""The fetch seam preserves the permanent allowlist rejection."""
from backend.channels.base import ChannelFetchError, FetchContext
from backend.pipeline.error_taxonomy import effective_error_type, is_retryable
with _allow():
with pytest.raises(ChannelFetchError) as excinfo:
await channel.fetch(
FetchContext(
config={
"binary": sys.executable,
"command": ["-c", "print(1)"],
},
params={},
)
)
error_type = effective_error_type(excinfo.value)
assert error_type == "BinaryNotAllowedError"
assert is_retryable(error_type) is False
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/channels/test_cli_channel_allowlist.py` around lines 91 - 108,
Update test_allowlist_rejection_permanent_through_fetch_seam to assert that
effective_error_type(excinfo.value) equals the expected permanent allowlist
rejection type, rather than only checking is_retryable(...) is False. Retain the
existing fetch invocation and use the established error-taxonomy symbol for the
exact type.

…ft-notify

# Conflicts:
#	tests/unit/channels/test_cli_channel.py
@2233admin
2233admin merged commit 583e9e5 into main Jul 19, 2026
5 checks passed
@2233admin
2233admin deleted the fix/wiring-schema-drift-notify branch July 19, 2026 03:25
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