feat(onboarding): T3 · Semrush workspace check + project fan-out + 200/207 (LLMO-5203/5204/5205)#2516
Open
andreeastroe96 wants to merge 4 commits into
Conversation
Implements step M5 of the Semrush onboarding orchestrator (epic LLMO-5007). When an org is in the SERENITY_SITE_ALLOWLIST cohort but has no Semrush workspace bound, onboarding now fails fast with a 404 and an operator- actionable message instead of proceeding into the provisioning path. Per the orchestration design (LLMO-5007 §M5), the check is hoisted to run immediately after createOrFindOrganization — before any other Adobe-side state (site, entitlement, audits, brand) is created — so a missing workspace leaves nothing behind and the operator can bind one via PATCH /organizations/:id and retry cleanly. The cohort gate (serenityEnabled) is evaluated once here and reused by the later M6-M8 block. - llmo-onboarding.js: early serenityEnabled gate + workspace presence check; throw a 404/preflight-tagged error; skip cleanup for pre-flight throws; reuse serenityEnabled at the M6-M8 block. - llmo.js: map a 404-typed onboarding error to notFound (matches the AIO Proxy convention); all other errors stay 400. - llmo-api.yaml: document the 404 response on POST /llmo/onboard. - tests: fail-fast unit test (404 before site creation), controller 404 mapping test, and bind a workspace on the existing allowlist test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Ai-Assisted-By: claude Ai-Assisted-By: claude-code
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
|
This PR will trigger a minor release when merged. |
…LLMO-5204)
Implements step M7 of the Semrush onboarding orchestrator (epic LLMO-5007).
After the M5 workspace check passes, performLlmoOnboarding now fans out one
Semrush project per (market, language) tuple by calling the already-shipped
AIO Proxy handler (handleCreateProject) in-process.
Best-effort per LLMO-5007 §M7 — no retry, no rollback:
- Sequential, one upstream create+publish per tuple.
- A per-tuple failure is collected, not thrown, and does not abort the
remaining tuples.
- A 409 (slice already exists) counts as success so re-invoking onboarding
with the same markets[] is idempotent (proxy 409-dedupes via findBySlice).
The brand UUID (from M4 upsertBrand) keys the fan-out; the org's
semrushWorkspaceId (M5-validated) is the workspace. The IMS bearer is
forwarded to Semrush; a missing/non-IMS token or unbuildable transport is
recorded as a per-tuple failure rather than failing onboarding.
performLlmoOnboarding returns the { requested, succeeded, failed } result as
`serenity` for the M8 read-back + 200/207 response shaping (LLMO-5205, T3c).
- llmo-onboarding.js: add performSerenityFanOut + extractImsBearer; capture
the brand from upsertBrand; call the fan-out in the M6-M8 block; thread the
result into the return object.
- tests: 9 unit tests for performSerenityFanOut (success, 409 idempotency,
partial failure, thrown transport error, no bearer, non-IMS auth, missing
brand, transport build failure, empty markets).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ai-Assisted-By: claude
Ai-Assisted-By: claude-code
20de771 to
8dcd974
Compare
…LLMO-5205)
Implements step M8 of the Semrush onboarding orchestrator (epic LLMO-5007).
After the M7 fan-out, read back the authoritative DB state and shape the final
200 or 207 response.
- reconcileSerenityProjects (M8): reads BrandSemrushProject.allByBrandId(brandId)
and asserts one row exists per requested (market, language) tuple — rather than
trusting the fan-out's own responses, which also catches the rare case where the
proxy returned 201 but the row insert failed silently. A tuple with a matching
row is `succeeded`; one without is `failed`, enriched with the fan-out's
status/error when available (otherwise `projectRowMissing`). Read-back failure
falls back to the fan-out result rather than masking a partial success.
- performLlmoOnboarding: runs M8 after M7 and returns the authoritative
{ requested, succeeded, failed } as `serenity`.
- llmo.js (controller): shape the response — all tuples present → 200 OK; some
failed → 207 Multi-Status. Both carry the standard onboarding fields plus the
per-tuple arrays. Successful tuples are never rolled back.
- llmo-api.yaml: clarify the 207 body mirrors the 200 plus the arrays.
- tests: 6 unit tests for reconcileSerenityProjects (empty/no-brand short-circuit,
all-present, missing tuple enriched from fan-out error, silently-missing row →
projectRowMissing, read-back throws → fallback) + 2 controller tests (207 on
partial failure, 200 when all succeed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ai-Assisted-By: claude
object-curly-newline (airbnb minProperties: 4) requires the failed-tuple object literals in the no-bearer and transport-build fan-out tests to be multi-line. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Ai-Assisted-By: claude
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements the full T3 chain of the Semrush onboarding orchestrator (epic LLMO-5007) inside
performLlmoOnboarding, gated by theSERENITY_SITE_ALLOWLISTcohort gate:(market, language)tupleM5 — fail fast on missing workspace (LLMO-5203)
Cohort org with no
semrushWorkspaceId→ 404 + operator-actionable message. Hoisted to right aftercreateOrFindOrganization(before any site/brand state) so a 404 leaves nothing behind and "retry onboarding" works — per the design doc §M5 rationale. 404 not 412; no rollback. Implemented via astatus:404/preflight:truethrow mapped tonotFoundin the controller; cleanupcatchskips rollback for pre-flight throws.M7 — fan out per market tuple (LLMO-5204)
performSerenityFanOutcalls the shipped AIO Proxy handlerhandleCreateProjectin-process, sequentially, one tuple at a time. Best-effort (§M7): per-tuple failures are collected, never abort the rest; 409 = idempotent success. Keyed by the brand UUID (captured fromupsertBrand) + the M5-validated workspace; the IMS bearer is forwarded to Semrush. Missing/non-IMS bearer, missing brand row, or unbuildable transport → recorded as per-tuple failures, not an onboarding failure.M8 — read back + shape 200/207 (LLMO-5205)
reconcileSerenityProjectsreadsBrandSemrushProject.allByBrandId(brandId)as the authoritative source — not the fan-out's own responses — so it also catches the "proxy returned 201 but the row insert failed silently" case. A requested tuple with a matching DB row issucceeded; one without isfailed(enriched with the fan-out's status/error, elseprojectRowMissing). The controller returns 200 when all tuples are present, 207 Multi-Status when any failed; both carry the standard onboarding fields plusrequested/succeeded/failed. Successful tuples are never rolled back; re-invoking with the samemarkets[]is safe (proxy 409-dedupes).Tests
preflight, noSite.create); controller 404 mapping; existing allowlist test binds a workspace.performSerenityFanOut(201 + body contract, 409 idempotency, partial failure, thrown transport error, no bearer, non-IMS auth, missing brand, transport-build failure, empty markets).reconcileSerenityProjects(empty/no-brand short-circuit, all-present, missing tuple enriched from fan-out error, silently-missing row →projectRowMissing, read-back throws → fallback) + 2 controller tests (207 on partial failure, 200 when all succeed).llmo-api.yaml: 404 documented; 207 clarified to mirror the 200 body + arrays.Heads-up for @luis6156
Hoists the
serenityEnabledcohort-gate evaluation to the M1/M2 position (matches T1/5201's "at the top" wording; PR #2513 placed it afterupsertBrand). The M6–M8 block reuses the same boolean — reconcile between branches.🤖 Generated with Claude Code