Skip to content

feat(cloud): populate rule_patches from RULE_PATCHED events in /sync#43

Merged
Gradata merged 8 commits into
mainfrom
feat/self-healing-telemetry
Apr 15, 2026
Merged

feat(cloud): populate rule_patches from RULE_PATCHED events in /sync#43
Gradata merged 8 commits into
mainfrom
feat/self-healing-telemetry

Conversation

@Gradata

@Gradata Gradata commented Apr 14, 2026

Copy link
Copy Markdown
Owner

Summary

End-to-end self-healing telemetry was broken: `apply_patch` mutated lessons in memory, the SDK emitted `RULE_PATCHED` events, but nothing ever wrote to `rule_patches` — so the self-healing dashboard panel always showed empty state.

This PR makes /sync tee `RULE_PATCHED` events into `rule_patches` as they arrive. Dashboard surface lights up as soon as an SDK brain patches a rule.

Changes

  • `cloud/app/routes/sync.py`: partition RULE_PATCHED events after the events insert, resolve `lesson_id` by `(brain_id, category)` (prefer match on current description, fall back to first), skip silently if no lesson.
  • `cloud/tests/test_sync.py`: 2 new tests — happy path + no-lesson graceful skip.

Not using IN(...) filter

Main's `db.select` is eq-only; the IN extension is queued in PR #41. Per-category loop is cheap (patched-categories per sync is typically 1-3) and avoids the merge dependency.

Test plan

  • `python -m pytest cloud/tests/test_sync.py` — 7/7 (5 existing + 2 new)
  • `python -m ruff check cloud/app/routes/sync.py` clean (pre-existing warnings untouched)
  • Manual: deploy, run SDK with GRADATA_API_KEY, call `brain.patch_rule(...)`, verify row at `/brains/{id}/rule-patches` and in dashboard self-healing page

The self-healing dashboard panel reads from the rule_patches table, but
nothing was writing to it — apply_patch mutates lessons in memory, the SDK
emits a RULE_PATCHED event, and the event lands in the events table, but
rule_patches stayed orphan. Dashboard showed empty state even when
self-healing had run.

Fix: when /sync receives events with type='RULE_PATCHED', resolve the
lesson by (brain_id, category), then insert a matching rule_patches row
with old/new description + reason. Skip gracefully if no lesson matches
(edge: patch arrived without the corresponding lesson sync).

Tests: 2 new — populates rule_patches on match, skips silently on miss.
All 7 /sync tests green.

Co-Authored-By: Gradata <noreply@gradata.ai>

@greptile-apps greptile-apps 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.

Gradata has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@coderabbitai

coderabbitai Bot commented Apr 14, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@Gradata has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 0 minutes and 41 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 0 minutes and 41 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: f989491d-dddd-47db-beb0-cd4afa32718f

📥 Commits

Reviewing files that changed from the base of the PR and between 9849299 and 8c4fad6.

📒 Files selected for processing (2)
  • cloud/app/routes/sync.py
  • cloud/tests/test_sync.py
📝 Walkthrough

Walkthrough

The sync handler now extracts RULE_PATCHED events after inserting events, validates required data fields (with per-event warnings), groups by uppercased category, queries lessons per (brain_id, category), resolves a lesson_id by description match or fallback, and inserts corresponding rule_patches rows when any are produced.

Changes

Cohort / File(s) Summary
Rule patching sync handler
cloud/app/routes/sync.py
After inserting events, filter RULE_PATCHED events, validate required data string fields (emit per-event warnings and skip invalid), normalize category to uppercase, collect distinct categories, query lessons by (brain_id, category), resolve each event to a lesson_id by matching description to new_description or old_description (fallback to first candidate), build rule_patches rows (lesson_id, old_description, new_description, reason default "self_healing", include created_at when present), skip and warn when no matching lesson, and insert into rule_patches only if ≥1 row.
Sync endpoint tests
cloud/tests/test_sync.py
Added three tests for POST /api/v1/sync covering RULE_PATCHED: (1) event with matching lesson produces one rule_patches insert with resolved lesson_id, old_description, new_description, and "self-healing" in reason; (2) event with no matching lesson returns 200 and events_synced == 1 but no rule_patches insert; (3) malformed RULE_PATCHED payloads emit WARNING logs mentioning rule-patches and missing fields, are skipped, and produce no rule_patches inserts. Added logging import and caplog usage for log assertions.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Handler as Sync Handler
    participant EventsDB as Events DB
    participant LessonDB as Lessons DB
    participant PatchDB as Rule Patches DB

    Client->>Handler: POST /api/v1/sync (payload with events)
    Handler->>EventsDB: Insert body.events into `events` table
    Handler->>Handler: Filter `RULE_PATCHED` events and validate `data` fields
    Handler->>Handler: Normalize categories (uppercase) and collect distinct ones
    loop per category
        Handler->>LessonDB: Query `lessons` by (brain_id, category)
        LessonDB-->>Handler: Return candidate lessons
        Handler->>Handler: Resolve `lesson_id` (match new_description → old_description → first)
    end
    Handler->>Handler: Build `rule_patches` rows (include reason/created_at if present)
    alt any valid rows
        Handler->>PatchDB: Insert rows into `rule_patches`
        PatchDB-->>Handler: Insert complete
    end
    Handler-->>Client: 200 OK with sync counts
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(cloud): populate rule_patches from RULE_PATCHED events in /sync' accurately captures the main change: the /sync endpoint now writes RULE_PATCHED events to the rule_patches table, directly addressing the broken self-healing telemetry pipeline.
Description check ✅ Passed The description clearly explains the problem (self-healing dashboard was empty), the solution (tee RULE_PATCHED events into rule_patches), and implementation details including lesson_id resolution logic, error handling, and test coverage.
Docstring Coverage ✅ Passed Docstring coverage is 85.71% 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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/self-healing-telemetry

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

@coderabbitai coderabbitai Bot added the feature label Apr 14, 2026

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cloud/app/routes/sync.py`:
- Around line 125-150: The loop over patched events silently skips events
missing category/old_description/new_description; update the loop around patched
(where patch_rows is built, and variables ev, data, cat, old_desc, new_desc are
computed) to log a warning whenever an event is skipped for missing required
fields: include the brain_id, which fields are missing
(category/old_description/new_description), and useful event identifiers (e.g.,
ev.id or ev.created_at) to aid debugging before continuing; keep the existing
warning for lesson resolution (where _resolve_lesson_id is called) unchanged and
still continue to skip when no lesson_id is found before appending to patch_rows
and calling db.insert("rule_patches", patch_rows).

In `@cloud/tests/test_sync.py`:
- Around line 144-176: In
test_sync_rule_patched_event_without_matching_lesson_is_skipped, after asserting
resp.status_code == 200, add an assertion that the sync reported one event
inserted by checking resp.json()["events_synced"] == 1 so the test verifies the
event row was written even though no rule_patches row was created; locate the
assertion block using the test function name and the resp variable and insert
the new check immediately after the existing status code assertion.
🪄 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: ASSERTIVE

Plan: Pro

Run ID: 86668dd2-9875-49d2-8344-2af6f2c724ae

📥 Commits

Reviewing files that changed from the base of the PR and between 1c708b6 and bf73e95.

📒 Files selected for processing (2)
  • cloud/app/routes/sync.py
  • cloud/tests/test_sync.py
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: test (3.11)
  • GitHub Check: test (3.12)
  • GitHub Check: test (3.13)
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (5)
cloud/tests/test_sync.py (1)

92-141: LGTM!

The test comprehensively covers the happy path: seeding a matching lesson, submitting a RULE_PATCHED event, and verifying the rule_patches row is created with correct lesson_id, descriptions, and reason.

cloud/app/routes/sync.py (4)

94-100: LGTM!

The category extraction includes appropriate defensive checks: verifying e.data is a dict and filtering out empty/None categories before uppercasing.


101-112: LGTM!

The per-category query approach is reasonable given the noted db.select limitation. The inline comment provides good context for future maintainers.


114-123: LGTM!

The resolution strategy is sound: prefer an exact description match (either side of the patch), with a deterministic fallback to the first candidate. This handles the common case where the lesson's description has already been updated.


91-93: LGTM!

The comment clearly explains the purpose: RULE_PATCHED events populate the rule_patches table to enable the self-healing dashboard. The implementation correctly processes events after they're already inserted, maintaining the events table as the source of truth.

Comment thread cloud/app/routes/sync.py Outdated
- log a warning (with brain_id, missing fields, event timestamp) when a
  RULE_PATCHED event lacks category/old_description/new_description, so a
  silent-drop failure is debuggable.
- extend the no-matching-lesson test to also assert events_synced == 1,
  locking in the guarantee that the event row still lands even when
  rule_patches is skipped.

Co-Authored-By: Gradata <noreply@gradata.ai>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Apr 14, 2026

Copy link
Copy Markdown

Deploying gradata-dashboard with  Cloudflare Pages  Cloudflare Pages

Latest commit: 2ecc0ab
Status:⚡️  Build in progress...

View logs

@greptile-apps greptile-apps 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.

Gradata has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@Gradata

Gradata commented Apr 14, 2026

Copy link
Copy Markdown
Owner Author

Addressed both CR actionables in 87c5a5f: warning log for skipped RULE_PATCHED events + events_synced assertion. Tests 7/7 green. @coderabbitai full review

@coderabbitai

coderabbitai Bot commented Apr 14, 2026

Copy link
Copy Markdown

@Gradata: Thanks for the update! Kicking off a full review of the PR now, including the changes in commit 87c5a5f.

✅ Actions performed

Full review triggered.

@Gradata

Gradata commented Apr 14, 2026

Copy link
Copy Markdown
Owner Author

Addressed CR feedback (both items were resolved in commit 87c5a5f, already on the branch):

  • cloud/app/routes/sync.py: RULE_PATCHED events missing category/old_description/new_description now emit a _log.warning with brain_id, the list of missing fields, and created_at before being skipped — no more silent drops.
  • cloud/tests/test_sync.py: test_sync_rule_patched_event_without_matching_lesson_is_skipped now also asserts resp.json()["events_synced"] == 1 to confirm the event row is still written even when the lesson lookup fails.

Tests: pytest cloud/tests/test_sync.py -x -q → 7 passed.

@Gradata

Gradata commented Apr 14, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review — fixes pushed, please re-verify.

@coderabbitai

coderabbitai Bot commented Apr 14, 2026

Copy link
Copy Markdown

@Gradata: Got it! Kicking off a fresh review now to verify the fixes in the latest commits. I'll check both cloud/app/routes/sync.py (the warning log for malformed RULE_PATCHED events) and cloud/tests/test_sync.py (the events_synced == 1 assertion in the skip test).

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@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

♻️ Duplicate comments (1)
cloud/app/routes/sync.py (1)

101-162: ⚠️ Potential issue | 🟠 Major

Missing-field warnings are still bypassed when all patched events lack category.

Line 101 gates processing on if categories:, and the validation loop at Line 126 sits inside that block. If every RULE_PATCHED event is missing category, categories is empty and no warning is emitted before skipping.

💡 Proposed fix
         patched = [e for e in body.events if e.type == "RULE_PATCHED"]
         if patched:
             categories = sorted({
                 (e.data.get("category") or "").upper()
                 for e in patched
                 if isinstance(e.data, dict) and e.data.get("category")
             })
-            if categories:
-                # One query per category (our thin PostgREST wrapper only
-                # supports eq filters). Small N — one per patched category.
-                by_cat: dict[str, list[dict]] = {}
-                for cat in categories:
-                    rows = await db.select(
-                        "lessons",
-                        columns="id,category,description",
-                        filters={"brain_id": brain_id, "category": cat},
-                    )
-                    if rows:
-                        by_cat[cat] = rows
-
-                def _resolve_lesson_id(cat: str, old_desc: str, new_desc: str) -> str | None:
-                    candidates = by_cat.get(cat, [])
-                    if not candidates:
-                        return None
-                    # Prefer a lesson whose current description matches either
-                    # side of the patch (ordering-agnostic). Fall back to first.
-                    for lesson in candidates:
-                        if lesson.get("description") in (new_desc, old_desc):
-                            return lesson["id"]
-                    return candidates[0]["id"]
-
-                patch_rows: list[dict] = []
-                for ev in patched:
+            # One query per category (our thin PostgREST wrapper only
+            # supports eq filters). Small N — one per patched category.
+            by_cat: dict[str, list[dict]] = {}
+            for cat in categories:
+                rows = await db.select(
+                    "lessons",
+                    columns="id,category,description",
+                    filters={"brain_id": brain_id, "category": cat},
+                )
+                if rows:
+                    by_cat[cat] = rows
+
+            def _resolve_lesson_id(cat: str, old_desc: str, new_desc: str) -> str | None:
+                candidates = by_cat.get(cat, [])
+                if not candidates:
+                    return None
+                # Prefer a lesson whose current description matches either
+                # side of the patch (ordering-agnostic). Fall back to first.
+                for lesson in candidates:
+                    if lesson.get("description") in (new_desc, old_desc):
+                        return lesson["id"]
+                return candidates[0]["id"]
+
+            patch_rows: list[dict] = []
+            for ev in patched:
                     data = ev.data if isinstance(ev.data, dict) else {}
                     cat = (data.get("category") or "").upper()
                     old_desc = data.get("old_description") or ""
                     new_desc = data.get("new_description") or ""
                     if not cat or not old_desc or not new_desc:
@@
-                    patch_rows.append(row)
-                if patch_rows:
-                    await db.insert("rule_patches", patch_rows)
+                    patch_rows.append(row)
+            if patch_rows:
+                await db.insert("rule_patches", patch_rows)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cloud/app/routes/sync.py` around lines 101 - 162, The current code only
validates RULE_PATCHED events when categories is truthy, so if every patched
event lacks category no warnings are emitted; change the flow so validation of
each ev in patched (the checks that compute cat, old_desc, new_desc and call
_log.warning for missing fields) runs regardless of whether categories is
non-empty, and only perform the database lookups/creation of by_cat and calls to
_resolve_lesson_id / db.insert when categories exists and there are candidates;
in practice move the missing-field check (the block that builds missing and logs
via _log.warning) out of the if categories: branch so patched events without
category still produce warnings, while keeping lesson-id resolution, patch_rows
accumulation, and the final await db.insert("rule_patches", patch_rows) behavior
unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cloud/tests/test_sync.py`:
- Around line 144-177: Add a new test (similar to
test_sync_rule_patched_event_without_matching_lesson_is_skipped) that sends a
RULE_PATCHED event with malformed payloads missing required fields (e.g., no
"category", or missing "old_description"/"new_description") to POST /api/v1/sync
using the same fixtures (client, mock_supabase, auth_headers) and assert
status_code == 200 and events_synced == 1, then use caplog to assert a warning
was emitted and verify no rule_patch insert occurred by inspecting
mock_supabase._inserts for rows containing "old_description"/"new_description";
name the test clearly (e.g.,
test_sync_rule_patched_malformed_payload_is_warned_and_skipped) so it mirrors
existing tests and prevents telemetry regressions.

---

Duplicate comments:
In `@cloud/app/routes/sync.py`:
- Around line 101-162: The current code only validates RULE_PATCHED events when
categories is truthy, so if every patched event lacks category no warnings are
emitted; change the flow so validation of each ev in patched (the checks that
compute cat, old_desc, new_desc and call _log.warning for missing fields) runs
regardless of whether categories is non-empty, and only perform the database
lookups/creation of by_cat and calls to _resolve_lesson_id / db.insert when
categories exists and there are candidates; in practice move the missing-field
check (the block that builds missing and logs via _log.warning) out of the if
categories: branch so patched events without category still produce warnings,
while keeping lesson-id resolution, patch_rows accumulation, and the final await
db.insert("rule_patches", patch_rows) behavior unchanged.
🪄 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: ASSERTIVE

Plan: Pro

Run ID: 8fb989d9-79c1-453d-b2d1-d7c8884cbdb8

📥 Commits

Reviewing files that changed from the base of the PR and between bf73e95 and 87c5a5f.

📒 Files selected for processing (2)
  • cloud/app/routes/sync.py
  • cloud/tests/test_sync.py
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (1)
cloud/tests/test_sync.py (1)

92-142: Good coverage for the resolvable RULE_PATCHED path.

This test correctly verifies both events_synced and the inserted rule_patches payload shape, including lesson_id mapping and reason propagation.

Comment thread cloud/tests/test_sync.py
Round-3 CR on #43:
- sync.py: lift missing-field validation out of the `if categories:` guard
  so warnings still fire when every patched event lacks category.
- test_sync.py: add test_sync_rule_patched_malformed_payload_is_warned_and_skipped
  covering the all-malformed path with caplog.

@greptile-apps greptile-apps 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.

Gradata has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@Gradata

Gradata commented Apr 14, 2026

Copy link
Copy Markdown
Owner Author

Addressed round-3 CR:

  • sync.py: lifted missing-field validation out of the if categories: guard so warnings still fire when every RULE_PATCHED event lacks category (CR major nit on L101-162).
  • test_sync.py: added test_sync_rule_patched_malformed_payload_is_warned_and_skipped covering the all-malformed path with caplog (CR nitpick on L144-177).
  • All 118 cloud tests pass.

@Gradata

Gradata commented Apr 14, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Apr 14, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cloud/app/routes/sync.py`:
- Around line 102-104: The sync handler reads untrusted fields without type
checks (variables cat, old_desc, new_desc and similarly the 'reason' usage
later) and calls string methods like .upper(), which can raise AttributeError
for non-strings; update the code in cloud/app/routes/sync.py to validate types
before string operations by using isinstance(value, str) (e.g., cat =
data.get("category") if isinstance(data.get("category"), str) else "" and cat =
cat.upper()) or explicitly coerce with safe conversion (e.g., old_desc =
str(data.get("old_description")) if data.get("old_description") is not None else
""), and apply the same pattern for new_description and reason locations
(including the occurrence around line 160) so only strings are passed to
.upper() or other string methods.

In `@cloud/tests/test_sync.py`:
- Around line 203-225: Add a malformed-type RULE_PATCHED event to the events
list that sets "category": 123 (integer) with "old_description": "x" and
"new_description": "y" so the test covers type-safety; then after posting,
assert the HTTP response is 200, the events_synced counter increments, a warning
log is emitted, and no insert into the rule_patches table occurs. Apply the same
malformed-type case and identical assertions to the second events block
mentioned (the one around lines 231-244) so both paths are covered.
🪄 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: ASSERTIVE

Plan: Pro

Run ID: 2496bce4-d39d-4b31-9663-24612fb9a00e

📥 Commits

Reviewing files that changed from the base of the PR and between 87c5a5f and f54cbb9.

📒 Files selected for processing (2)
  • cloud/app/routes/sync.py
  • cloud/tests/test_sync.py
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: test (3.13)
  • GitHub Check: test (3.11)
  • GitHub Check: test (3.12)
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (2)
cloud/app/routes/sync.py (1)

96-119: Good defensive flow: validation-first pass with explicit warning logs.

The “validate then process” split avoids silent drops when all RULE_PATCHED payloads are malformed and preserves observability.

cloud/tests/test_sync.py (1)

94-143: Coverage is strong for the new RULE_PATCHED flows.

Happy path, unresolved lesson skip, and malformed warning/skip behavior are all covered with useful assertions.

Also applies to: 146-179, 182-252

Comment thread cloud/app/routes/sync.py Outdated
Comment thread cloud/tests/test_sync.py
CR round-4: untrusted SDK clients can send category=123 (or any
non-string truthy), which would have crashed the sync handler with
AttributeError on .upper() *after* events were already inserted.
Now we isinstance-check category/old_description/new_description/reason
and treat non-strings as missing — same warning + skip path.

Test: extend the malformed-payload test with a category=123 case;
asserts 200, events_synced==3, warning emitted, no rule_patches insert.

@greptile-apps greptile-apps 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.

Gradata has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@Gradata

Gradata commented Apr 15, 2026

Copy link
Copy Markdown
Owner Author

Round-4 CR addressed (commit 9849299):

  • sync.py: type-guard category/old_description/new_description/reason with isinstance(..., str) before any string ops. Non-string values (e.g. category=123) now fall through the existing missing-field warning path instead of raising AttributeError mid-handler after events were already inserted.
  • test_sync.py: extend test_sync_rule_patched_malformed_payload_is_warned_and_skipped with a category=123 (int) case; asserts 200, events_synced==3, warning emitted, no rule_patches insert.

All 118 cloud tests pass.

@Gradata

Gradata commented Apr 15, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Apr 15, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@Gradata

Gradata commented Apr 15, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review — fixes pushed since last review, please re-verify

@coderabbitai

coderabbitai Bot commented Apr 15, 2026

Copy link
Copy Markdown

@Gradata: Got it! Kicking off a fresh review now to verify the fixes pushed since the last review. I'll focus on:

  • cloud/app/routes/sync.py: The isinstance(..., str) type-guards for category, old_description, new_description, and reason to prevent AttributeError on non-string values.
  • cloud/tests/test_sync.py: The extended test_sync_rule_patched_malformed_payload_is_warned_and_skipped test covering the category=123 (int) case with events_synced == 3, warning assertion, and no rule_patches insert.
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@Gradata

Gradata commented Apr 15, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review — CR Pro active, please re-verify.

@coderabbitai

coderabbitai Bot commented Apr 15, 2026

Copy link
Copy Markdown

@Gradata: Got it! Kicking off a fresh review now. I'll verify the latest round-4 changes:

  • cloud/app/routes/sync.py: isinstance(..., str) type-guards for category, old_description, new_description, and reason ensuring non-string values fall into the missing-field warning path cleanly.
  • cloud/tests/test_sync.py: Extended test_sync_rule_patched_malformed_payload_is_warned_and_skipped with the category=123 (int) case asserting events_synced == 3, a warning emitted, and no rule_patches insert.
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@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: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cloud/app/routes/sync.py`:
- Around line 111-117: The malformed-event gate currently omits validating the
reason field, so non-string/blank reason values bypass the warning-and-skip
logic and later get coerced to "self_healing"; update the gate that checks
missing fields (where cat, old_desc, new_desc are validated) to include reason
in the missing list and treat empty/non-string reason as a missing/invalid field
so the flow logs a warning and skips the event instead of inserting a coerced
value; locate the block that builds missing = [...] (using variables cat,
old_desc, new_desc) and add ("reason", reason) to that tuple, and remove or
change the later logic that silently coerces non-string/blank reason to
"self_healing" so invalid reasons are rejected at the malformed-event gate.
- Around line 127-178: The rule_patches write path (blocks using by_cat,
_resolve_lesson_id, patch_rows and db.insert("rule_patches", ...)) must be
isolated so failures there do not cause /sync to fail after events were already
persisted; wrap the category lookups and the final insert in a try/except around
the loop that builds patch_rows and the db.insert call, catch and log any
exception (including the exception details) via _log.error with context
(brain_id, categories) and then continue without re-raising so the handler
returns success; ensure you still skip missing lessons as before and only
suppress unexpected DB/query errors while preserving existing behavior for
valid_events and lessons selection.

In `@cloud/tests/test_sync.py`:
- Around line 182-265: In
test_sync_rule_patched_malformed_payload_is_warned_and_skipped add additional
malformed RULE_PATCHED events that exercise invalid/missing reason values (e.g.,
one event with no reason key and one with reason as a non-string like 123), then
assert that the caplog warning collection (warning_msgs) contains entries
mentioning "reason" for those events and that rule_patches inserts remain empty
(same rule_patch_inserts check); update any existing assertions that check for
specific missing-field messages to also expect warnings about "reason" so the
test fails if reason validation regresses.
🪄 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: ASSERTIVE

Plan: Pro

Run ID: b78a46ac-303c-4065-bf22-3cb1ece9a932

📥 Commits

Reviewing files that changed from the base of the PR and between f54cbb9 and 9849299.

📒 Files selected for processing (2)
  • cloud/app/routes/sync.py
  • cloud/tests/test_sync.py
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (1)
cloud/tests/test_sync.py (1)

94-179: Nice coverage on the new RULE_PATCHED happy-path and no-lesson skip flows.

These tests clearly verify events_synced behavior and rule_patches side effects.

Comment thread cloud/app/routes/sync.py Outdated
Comment thread cloud/app/routes/sync.py Outdated
Comment thread cloud/tests/test_sync.py
Gradata added 2 commits April 14, 2026 21:54
CodeRabbit round-5 feedback on #43:
- Add `reason` to the RULE_PATCHED malformed-event gate so non-string
  or blank reason values are warned + skipped instead of silently
  coerced to "self_healing".
- Wrap the rule_patches lookup/insert path in try/except so a PostgREST
  outage there cannot fail /sync after events were already persisted
  (SDK retries would otherwise duplicate the event stream). Logs with
  exc_info for ops.
- Tests: extend malformed-payload coverage with missing-reason + non-
  string-reason events; add write-failure isolation test that forces
  the lessons lookup to raise and asserts /sync still returns 200 with
  events_synced > 0 and no rule_patches inserts.

@greptile-apps greptile-apps 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.

Gradata has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@Gradata

Gradata commented Apr 15, 2026

Copy link
Copy Markdown
Owner Author

Rebased on main + CR round-5 addressed:

  • reason now part of the malformed-event gate (cloud/app/routes/sync.py:108-129): non-string / blank reason values are warned + skipped instead of silently coerced to "self_healing". Added to the missing list with ("reason", reason) and removed the downstream coercion.
  • rule_patches write path isolated (cloud/app/routes/sync.py:131-178): the category lookups + final db.insert("rule_patches", ...) are wrapped in try/except. Failures log with exc_info and /sync still returns 200 so the SDK doesn't retry + duplicate the event stream.
  • Tests updated (cloud/tests/test_sync.py): malformed-payload test now covers missing-reason + non-string-reason events and asserts a warning mentioning reason is emitted. Added test_sync_rule_patches_write_failure_does_not_fail_sync which monkeypatches the lessons select to raise and asserts events_synced == 1 with zero patch inserts.

pytest cloud/tests/test_sync.py -x -q: 9 passed. Full cloud suite: 124 passed, 1 pre-existing unrelated failure (test_export_script_handles_empty_run_dir — the merged main references cloud/scripts/export_ab_proof.py which does not exist on disk).

@Gradata

Gradata commented Apr 15, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Apr 15, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@greptile-apps greptile-apps 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.

Gradata has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@Gradata

Gradata commented Apr 15, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review — rebased on latest main, all prior fixes applied

@coderabbitai

coderabbitai Bot commented Apr 15, 2026

Copy link
Copy Markdown

@Gradata: Got it! Kicking off a fresh full review now to verify all the round-5 fixes applied on top of the rebased branch:

  • cloud/app/routes/sync.py: reason field included in the malformed-event gate (non-string/blank reason warned + skipped, "self_healing" coercion removed), and the rule_patches write path wrapped in try/except so /sync still returns 200 on DB failures.
  • cloud/tests/test_sync.py: Extended malformed-payload test covering missing/non-string reason, and new test_sync_rule_patches_write_failure_does_not_fail_sync asserting resilience when the lessons select raises.
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@Gradata

Gradata commented Apr 15, 2026

Copy link
Copy Markdown
Owner Author

Rebased on latest main after batch merges

@greptile-apps greptile-apps 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.

Gradata has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@Gradata Gradata merged commit 6d3e37a into main Apr 15, 2026
8 of 9 checks passed
@Gradata Gradata deleted the feat/self-healing-telemetry branch April 15, 2026 07:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant