Skip to content

fix(backend): flip ai_analyzed via service role so idempotency holds#2331

Merged
WcaleNieWolny merged 1 commit into
mainfrom
worktree-fix-ai-analyzed-flag
May 22, 2026
Merged

fix(backend): flip ai_analyzed via service role so idempotency holds#2331
WcaleNieWolny merged 1 commit into
mainfrom
worktree-fix-ai-analyzed-flag

Conversation

@WcaleNieWolny
Copy link
Copy Markdown
Contributor

@WcaleNieWolny WcaleNieWolny commented May 22, 2026

Summary

  • POST /build/ai_analyze was running its post-success UPDATE build_requests SET ai_analyzed = true through the user-context apikey client (supabaseApikey). The build_requests table has only SELECT (org members) and ALL (service role) RLS policies — no UPDATE policy for non-service-role users — so the UPDATE was silently blocked. PostgREST returned { error: null }, the flag stayed false, and the if (row.ai_analyzed === true) idempotency guard never fired on subsequent requests for the same job.
  • Effect: a single failed build could be re-analyzed indefinitely, each call burning a fresh Workers AI invocation. The cost was real but invisible (Result events still showed success).
  • Fix: switch only the post-success UPDATE to supabaseAdmin(c) (service-role client). The SELECT stays on the apikey client — that's the authorization gate and still fails closed if the row isn't accessible to the caller. Also added .select('builder_job_id') so we can detect the zero-rows-matched case and log it loudly.

Test plan

  • Refactored mockBuildRequestRow to mock both clients independently (apikeyUpdate and adminUpdate jest mocks)
  • Success-path test now asserts:
    • adminUpdate called exactly once with ai_analyzed: true
    • apikeyUpdate never called (regression guard — if anyone moves the UPDATE back to the apikey client, this test fails)
  • New test for the zero-rows-matched case: analysis still returns to caller, success Result event still fires, but the bookkeeping miss is logged
  • 8/8 tests in build-ai-analyze.test.ts pass
  • 50/50 related suites pass (ai-analysis-telemetry, build-lifecycle-emit, build-timeout, build-tracking-helpers)
  • Manual: pick a failed-build jobId, hit the endpoint twice, confirm the second call returns 409 already_analyzed
  • DB check after a real run: select ai_analyzed from build_requests where builder_job_id = '<jobid>' returns true

Out of scope

  • The CLI side already handles 409 correctly (postAnalyzeRequest returns { kind: 'already_analyzed' }); no client change needed.
  • This does not change anything for the onboarding-AI PR (feat(cli): offer AI build debug in onboarding TUI #2328) — that PR's debug bypass still requires manual UPDATE … SET ai_analyzed=false between runs because it intentionally re-uses the same jobId. Once this fix lands, real (non-debug) runs will be idempotent automatically.

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • Bug Fixes
    • Strengthened AI analysis processing reliability with improved idempotency protection and error detection.
    • Enhanced handling of edge cases during analysis updates with better error logging.

Review Change Stack

…ly holds

`POST /build/ai_analyze` ran the post-success
`UPDATE build_requests SET ai_analyzed = true` through the user-context
apikey client (`supabaseApikey`). The `build_requests` table has only
two RLS policies — `Allow org members to select build_requests` (SELECT
for org members) and `Service role manages build requests` (ALL for the
service role) — so the user-context UPDATE was silently blocked by RLS:
PostgREST reported zero rows matched, returned `{ error: null }`, and
the flag stayed `false` for every successful analysis.

Consequence: the `if (row.ai_analyzed === true)` idempotency guard at
the top of the handler never fired for any subsequent request on the
same job. A single failed build could be re-analyzed indefinitely, each
call burning a fresh Workers AI invocation and racking up cost.

Fix:

- Switch the post-success `UPDATE` to `supabaseAdmin(c)` (service-role
  client), bypassing RLS for this targeted write. Authorization is
  unchanged — it's still enforced upstream by `checkPermission` and the
  user-context `SELECT` that fails closed if the row isn't accessible
  to the caller. The SELECT remains on the apikey client.

- Add `.select('builder_job_id')` to the UPDATE so we can detect the
  zero-rows-matched case (e.g. the row was deleted between our SELECT
  and our UPDATE). The analysis has already happened by this point, so
  we still don't throw — but we log loudly because idempotency is
  broken for that job and someone needs to look.

### Tests

- Refactored `mockBuildRequestRow` to mock the apikey client (SELECT
  only) and the admin client (UPDATE only) independently, returning
  both `apikeyUpdate` and `adminUpdate` jest mocks.
- The success test now asserts:
    * `adminUpdate` was called exactly once with `ai_analyzed: true`
    * `apikeyUpdate` was NEVER called (regression guard — if anyone
      moves the UPDATE back to the apikey client, this test fails)
- Added a new test: when the admin UPDATE matches zero rows, the
  handler still returns the analysis and still fires the success Result
  event (because the AI work succeeded), but the bookkeeping miss is
  logged.

All 8 tests in `build-ai-analyze.test.ts` pass; related suites
(`ai-analysis-telemetry`, `build-lifecycle-emit`, `build-timeout`,
`build-tracking-helpers`) — 50/50 still green.
@codspeed-hq
Copy link
Copy Markdown
Contributor

codspeed-hq Bot commented May 22, 2026

Merging this PR will not alter performance

✅ 43 untouched benchmarks
⏩ 2 skipped benchmarks1


Comparing worktree-fix-ai-analyzed-flag (4c8bb13) with main (db5951f)

Open in CodSpeed

Footnotes

  1. 2 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 22, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4867a303-cffd-4682-8d89-4013f87b5876

📥 Commits

Reviewing files that changed from the base of the PR and between db5951f and 4c8bb13.

📒 Files selected for processing (2)
  • supabase/functions/_backend/public/build/ai_analyze.ts
  • tests/build-ai-analyze.test.ts

📝 Walkthrough

Walkthrough

The AI build analysis handler switches the idempotency flag update from the user-context Supabase client to the service-role admin client, adds detection for zero-row UPDATE results to identify when the idempotency guard fails, and updates tests to verify both the admin-client usage and zero-row defensive behavior.

Changes

AI Analyze Idempotency Handling

Layer / File(s) Summary
Service-role client import and idempotency UPDATE
supabase/functions/_backend/public/build/ai_analyze.ts
The handler imports supabaseAdmin and replaces the ai_analyzed flag UPDATE to use the service-role client instead of the user-context client, adding .select('builder_job_id') to capture affected row counts.
Zero-row idempotency detection
supabase/functions/_backend/public/build/ai_analyze.ts
An else-if branch logs a dedicated "idempotency broken" error when the admin UPDATE matches zero rows, including job_id and app_id in the error message.
Test infrastructure and assertions
tests/build-ai-analyze.test.ts
A mockSupabaseAdmin mock is added alongside the existing apikey client mock; the shared mockBuildRequestRow helper is refactored to enforce SELECT-only behavior on the apikey chain and support configurable admin UPDATE row counts; existing tests are updated to assert the UPDATE goes through the admin client, and a new test covers the zero-row UPDATE scenario.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • Cap-go/capgo#2285: Introduces the ai_analyzed column in the database schema that this PR uses for the idempotency flag.
  • Cap-go/capgo#2284: Also modifies ai_analyzeBuild and related test infrastructure around the idempotency UPDATE behavior.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% 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 clearly and concisely describes the main fix: switching the ai_analyzed flag update to service-role access to restore idempotency behavior.
Description check ✅ Passed The description covers Summary (problem, effect, fix), comprehensive Test plan with specific assertions and passing test counts, and Out of scope section. All required sections present with substantive detail.
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.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


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

@sonarqubecloud
Copy link
Copy Markdown

@WcaleNieWolny WcaleNieWolny merged commit 7454f12 into main May 22, 2026
42 checks passed
@WcaleNieWolny WcaleNieWolny deleted the worktree-fix-ai-analyzed-flag branch May 22, 2026 15:16
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