Skip to content

feat(serenity): expose semrush provisioning status on brand reads (LLMO-7352/LLMO-7418) - #3250

Open
IrisAlexandrescu wants to merge 2 commits into
feat/LLMO-7418-phase4-skip-mode-asyncfrom
feat/LLMO-7418-expose-provisioning-status
Open

IrisAlexandrescu wants to merge 2 commits into
feat/LLMO-7418-phase4-skip-mode-asyncfrom
feat/LLMO-7418-expose-provisioning-status

Conversation

@IrisAlexandrescu

@IrisAlexandrescu IrisAlexandrescu commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Summary

Fifth and last in the stack (base: #3249). Small, focused prerequisite for the frontend UI-states work (LLMO-7419, adobe/project-elmo-ui#3128): surfaces brands.semrush_provisioning_status / semrush_provisioning_error as read-only semrushProvisioningStatus / semrushProvisioningError on the V2 Brand DTO.

Why this PR exists: those two columns previously existed only for the async provisioning worker's own internal CAS bookkeeping (getBrandProvisioningState's narrow PROVISIONING_SELECT) — no GET response exposed them. Without this the frontend cannot show a persistent "Setting up / Ready / Setup failed" indicator on the brand list or detail view; it would have to rely on the one-shot jobId an async: true response carries, which is useless on any later page load.

Changes

  • mapDbBrandToV2 (src/support/brands-storage.js) — the single mapping point for all 3 GET paths (listBrandsForOrg / getBrandForOrg / getBrandForOrgSite) and the create/update write-response paths (all route through withSerenityState) — now copies the two columns onto the DTO. No SELECT changes needed: PostgREST's '*' already returns every brands column.
  • V2Brand in docs/openapi/schemas.yaml — both fields added as readOnly, nullable, not in required.

Test plan

  • npm run lint, npm run type-check, npm run docs:lint — clean
  • 1827 passing across serenity, brands, brands-storage and the handlers
  • Unit test asserting both the failed-with-error case and the null/never-provisioned case
  • Rebased onto current main

Related Issues

  • LLMO-7352 / LLMO-7418
  • Consumer: adobe/project-elmo-ui#3128 (LLMO-7419)

🤖 Generated with Claude Code

@codecov

codecov Bot commented Sep 9, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@IrisAlexandrescu
IrisAlexandrescu force-pushed the feat/LLMO-7418-expose-provisioning-status branch from a048d64 to c7c0239 Compare September 9, 2026 14:47
@IrisAlexandrescu

Copy link
Copy Markdown
Contributor Author

Rebased onto Phase 4's (#3249) fix commit — no changes needed in this PR itself, the fixed activate-brand-workspace-job.js behavior now carries through automatically.

@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown

This PR will trigger a minor release when merged.

@IrisAlexandrescu
IrisAlexandrescu force-pushed the feat/LLMO-7418-phase4-skip-mode-async branch from 4d3123e to a964882 Compare September 9, 2026 16:20
@IrisAlexandrescu
IrisAlexandrescu force-pushed the feat/LLMO-7418-expose-provisioning-status branch from c7c0239 to 2be9230 Compare September 9, 2026 16:36
@IrisAlexandrescu
IrisAlexandrescu force-pushed the feat/LLMO-7418-phase4-skip-mode-async branch 2 times, most recently from 135eec6 to e888c2a Compare September 9, 2026 17:29
@IrisAlexandrescu
IrisAlexandrescu force-pushed the feat/LLMO-7418-expose-provisioning-status branch from 2be9230 to 581d363 Compare September 9, 2026 17:33
@IrisAlexandrescu
IrisAlexandrescu force-pushed the feat/LLMO-7418-phase4-skip-mode-async branch from e888c2a to f822111 Compare September 9, 2026 18:42
@IrisAlexandrescu
IrisAlexandrescu force-pushed the feat/LLMO-7418-expose-provisioning-status branch from 581d363 to feb52c7 Compare September 9, 2026 18:47
@IrisAlexandrescu
IrisAlexandrescu force-pushed the feat/LLMO-7418-phase4-skip-mode-async branch from f822111 to a587be3 Compare September 10, 2026 08:17
@IrisAlexandrescu
IrisAlexandrescu force-pushed the feat/LLMO-7418-expose-provisioning-status branch from feb52c7 to 60e70ad Compare September 10, 2026 08:21
@IrisAlexandrescu
IrisAlexandrescu force-pushed the feat/LLMO-7418-expose-provisioning-status branch from 60e70ad to 24911fc Compare September 10, 2026 08:46
@IrisAlexandrescu
IrisAlexandrescu force-pushed the feat/LLMO-7418-expose-provisioning-status branch from 24911fc to d8befb8 Compare September 10, 2026 09:10
@IrisAlexandrescu
IrisAlexandrescu force-pushed the feat/LLMO-7418-phase4-skip-mode-async branch from b0a89f6 to 90986fd Compare September 10, 2026 10:23
@IrisAlexandrescu
IrisAlexandrescu force-pushed the feat/LLMO-7418-expose-provisioning-status branch from d8befb8 to d4706fd Compare September 10, 2026 10:23
@IrisAlexandrescu
IrisAlexandrescu force-pushed the feat/LLMO-7418-phase4-skip-mode-async branch from 90986fd to f77926a Compare September 10, 2026 11:17
@IrisAlexandrescu
IrisAlexandrescu force-pushed the feat/LLMO-7418-expose-provisioning-status branch from d4706fd to fdc0a01 Compare September 10, 2026 11:17
@IrisAlexandrescu
IrisAlexandrescu force-pushed the feat/LLMO-7418-expose-provisioning-status branch from fdc0a01 to 8473538 Compare September 10, 2026 16:08
@IrisAlexandrescu
IrisAlexandrescu force-pushed the feat/LLMO-7418-phase4-skip-mode-async branch from af1cbe8 to fe80d7d Compare September 10, 2026 17:54
@IrisAlexandrescu
IrisAlexandrescu force-pushed the feat/LLMO-7418-expose-provisioning-status branch from 8473538 to 3925628 Compare September 10, 2026 17:54
@IrisAlexandrescu
IrisAlexandrescu force-pushed the feat/LLMO-7418-expose-provisioning-status branch from 3925628 to 7fc9a1f Compare September 10, 2026 18:35
@IrisAlexandrescu
IrisAlexandrescu force-pushed the feat/LLMO-7418-expose-provisioning-status branch from 7fc9a1f to ba32f7d Compare September 14, 2026 11:26
@IrisAlexandrescu
IrisAlexandrescu force-pushed the feat/LLMO-7418-phase4-skip-mode-async branch from 6d2220d to e797823 Compare September 14, 2026 13:43
@IrisAlexandrescu
IrisAlexandrescu force-pushed the feat/LLMO-7418-expose-provisioning-status branch from ba32f7d to 3b99f06 Compare September 14, 2026 13:43
@IrisAlexandrescu
IrisAlexandrescu force-pushed the feat/LLMO-7418-phase4-skip-mode-async branch from e797823 to 78019c4 Compare September 14, 2026 13:58
@IrisAlexandrescu
IrisAlexandrescu force-pushed the feat/LLMO-7418-expose-provisioning-status branch from 3b99f06 to ab359b2 Compare September 14, 2026 14:04
@IrisAlexandrescu
IrisAlexandrescu force-pushed the feat/LLMO-7418-phase4-skip-mode-async branch from 78019c4 to b93aa8b Compare September 14, 2026 14:38
@IrisAlexandrescu
IrisAlexandrescu force-pushed the feat/LLMO-7418-expose-provisioning-status branch from ab359b2 to 555eacc Compare September 14, 2026 14:49

@luis6156 luis6156 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Clean, minimal, correct. The single mapping point (mapDbBrandToV2) covers all three GET paths simultaneously, no SELECT changes needed since PostgREST's '*' already returns every column, and the "Null unless semrushProvisioningStatus is failed" comment is a DB-level guarantee (confirmed: beginProvisioningAttempt explicitly writes semrush_provisioning_error: null in its UPDATE, so a successful retry atomically clears any prior error). Two small items:


Finding 1 — Test coverage missing pending and ready states

The test exercises failed-with-error and null (never provisioned), but the two states the frontend UI actually renders differently are pending (show "Setting up..." spinner) and ready (silent success). Adding cases for both would ensure a future rename of an enum value or a mapping typo doesn't go undetected:

it('maps semrush_provisioning_status: pending and ready', async () => {
  for (const status of ['pending', 'ready']) {
    const row = makeBrandRow({ semrush_provisioning_status: status });
    const query = createChainableQuery({ data: row, error: null });
    const result = await getBrandById(ORG_ID, BRAND_ID, { from: sinon.stub().returns(query) });
    expect(result.semrushProvisioningStatus).to.equal(status);
    expect(result.semrushProvisioningError).to.equal(null); // error only set on failed
  }
});

Finding 2 — OpenAPI description doesn't explain the null / ready distinction for clients

The semrushProvisioningStatus description says "show a persistent Setting-up/Ready/Setup-failed indicator without relying on … jobId" but doesn't tell a reader what each value means from the client side. The null vs ready split is particularly subtle: both mean "workspace is usable", but null indicates a brand that went through the sync path (or predates async provisioning) while ready indicates an async-provisioned brand. A frontend developer implementing LLMO-7419 should not need to read the provisioning worker source to know this.

Suggested description:

semrushProvisioningStatus:
  description: >-
    Read-only: the brand's async Semrush provisioning state.
    `pending` — a provisioning attempt is in flight; the client should
    show a "Setting up" indicator and may poll the job status via `jobId`.
    `ready` — async provisioning completed successfully.
    `failed` — the last attempt failed; see `semrushProvisioningError` for detail.
    `null` — the brand was created via the sync path or before async
    provisioning was introduced; no active provisioning issue.
    For display purposes, treat both `null` and `ready` as "workspace available".

The four-state breakdown also confirms the OpenAPI enum [pending, ready, failed, null] is complete.

@IrisAlexandrescu
IrisAlexandrescu force-pushed the feat/LLMO-7418-phase4-skip-mode-async branch from b93aa8b to a688e78 Compare September 16, 2026 09:37
IrisAlexandrescu pushed a commit that referenced this pull request Sep 16, 2026
…e for clients

Luis's review on #3250, both findings.

The DTO mapping test covered only `failed`-with-error and absent. The two states
the frontend actually branches on — `pending`, which drives the "Setting up"
indicator, and `ready`, the silent-success case — were untested, so a renamed
enum value or a mapping typo in either would have shipped undetected.

The OpenAPI description said what the field is FOR but not what each value means
to a client. The `null` vs `ready` split is the subtle one: both mean the
workspace is available, but `null` is a brand created through the synchronous
path or predating async provisioning, while `ready` is an async-provisioned one.
A frontend developer should not have to read the provisioning worker to learn
that. Now documents all four values and states plainly that `null` and `ready`
are equivalent for display.

eslint, type-check and docs:lint clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@IrisAlexandrescu
IrisAlexandrescu force-pushed the feat/LLMO-7418-expose-provisioning-status branch from 555eacc to 397b59b Compare September 16, 2026 09:37
@IrisAlexandrescu
IrisAlexandrescu force-pushed the feat/LLMO-7418-phase4-skip-mode-async branch from a688e78 to 59dbd25 Compare September 16, 2026 12:00
IrisAlexandrescu pushed a commit that referenced this pull request Sep 16, 2026
…e for clients

Luis's review on #3250, both findings.

The DTO mapping test covered only `failed`-with-error and absent. The two states
the frontend actually branches on — `pending`, which drives the "Setting up"
indicator, and `ready`, the silent-success case — were untested, so a renamed
enum value or a mapping typo in either would have shipped undetected.

The OpenAPI description said what the field is FOR but not what each value means
to a client. The `null` vs `ready` split is the subtle one: both mean the
workspace is available, but `null` is a brand created through the synchronous
path or predating async provisioning, while `ready` is an async-provisioned one.
A frontend developer should not have to read the provisioning worker to learn
that. Now documents all four values and states plainly that `null` and `ready`
are equivalent for display.

eslint, type-check and docs:lint clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@IrisAlexandrescu
IrisAlexandrescu force-pushed the feat/LLMO-7418-expose-provisioning-status branch from 397b59b to 574d9eb Compare September 16, 2026 12:07

@luis6156 luis6156 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review — feat(serenity): expose semrush provisioning status on brand reads

Clean and focused. The single-mapping-point strategy (mapDbBrandToV2 covers all three GET paths and the write-response path via withSerenityState) is correct, and no SELECT/query changes are needed since PostgREST's '*' already returns every column.


null vs ready semantics

The OpenAPI description correctly documents the distinction: null = synchronous create or predates async provisioning; ready = async-provisioned workspace confirmed created. Worth confirming with the frontend that they treat both as "workspace available" — the description now states this plainly ("clients should treat null and ready as equivalent for display"), but the UI rendering code is the authoritative check.


Non-Semrush / flat-mode brands

semrushProvisioningStatus will always be null for flat-mode organizations since the provisioning columns are never written for them. The OpenAPI schema marks the field nullable and not required, which is correct. One thing worth noting for frontend consumers: null can mean either "synchronous Semrush brand" or "non-Semrush brand" — a flat-mode client should treat null as "not applicable" rather than "provisioning status unknown." The current description says null and ready are equivalent for display, which is accurate for the Semrush case but could mislead a flat-mode consumer into polling for a status that will never change. A clarifying note ("for non-Semrush organizations this field is always null") in the schema description would close that ambiguity.

Non-blocking. Approving.

@IrisAlexandrescu
IrisAlexandrescu force-pushed the feat/LLMO-7418-phase4-skip-mode-async branch from 59dbd25 to 79e847f Compare September 16, 2026 13:24
IrisAlexandrescu pushed a commit that referenced this pull request Sep 16, 2026
…e for clients

Luis's review on #3250, both findings.

The DTO mapping test covered only `failed`-with-error and absent. The two states
the frontend actually branches on — `pending`, which drives the "Setting up"
indicator, and `ready`, the silent-success case — were untested, so a renamed
enum value or a mapping typo in either would have shipped undetected.

The OpenAPI description said what the field is FOR but not what each value means
to a client. The `null` vs `ready` split is the subtle one: both mean the
workspace is available, but `null` is a brand created through the synchronous
path or predating async provisioning, while `ready` is an async-provisioned one.
A frontend developer should not have to read the provisioning worker to learn
that. Now documents all four values and states plainly that `null` and `ready`
are equivalent for display.

eslint, type-check and docs:lint clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@IrisAlexandrescu
IrisAlexandrescu force-pushed the feat/LLMO-7418-expose-provisioning-status branch from 574d9eb to 8cf602c Compare September 16, 2026 13:31
@IrisAlexandrescu
IrisAlexandrescu force-pushed the feat/LLMO-7418-phase4-skip-mode-async branch from 79e847f to f7227b1 Compare September 16, 2026 18:08
Iris Alexandrescu and others added 2 commits September 16, 2026 21:08
…MO-7352/LLMO-7418)

Surfaces brands.semrush_provisioning_status/semrush_provisioning_error as
read-only fields (semrushProvisioningStatus/semrushProvisioningError) on the
V2 Brand DTO. These columns previously existed only for the async
provisioning worker's own internal CAS bookkeeping (getBrandProvisioningState's
narrow PROVISIONING_SELECT) -- no GET response exposed them, so a client had
no way to show a persistent provisioning-status indicator without relying on
an `async: true` create/activate response's one-shot jobId.

mapDbBrandToV2 is the single mapping point for all 3 GET paths
(listBrandsForOrg/getBrandForOrg/getBrandForOrgSite) and the create/update
write-response paths (all route through withSerenityState, which just spreads
whatever the mapper produced) -- fixing it there propagates everywhere for
free, no SELECT/query changes needed since PostgREST's `'*'` already returns
every brands column.

Prerequisite for the frontend UI-states work (LLMO-7419): a persistent
Setting-up/Ready/Setup-failed indicator on the brand list/detail views can't
exist without a backend field to read it from.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…e for clients

Luis's review on #3250, both findings.

The DTO mapping test covered only `failed`-with-error and absent. The two states
the frontend actually branches on — `pending`, which drives the "Setting up"
indicator, and `ready`, the silent-success case — were untested, so a renamed
enum value or a mapping typo in either would have shipped undetected.

The OpenAPI description said what the field is FOR but not what each value means
to a client. The `null` vs `ready` split is the subtle one: both mean the
workspace is available, but `null` is a brand created through the synchronous
path or predating async provisioning, while `ready` is an async-provisioned one.
A frontend developer should not have to read the provisioning worker to learn
that. Now documents all four values and states plainly that `null` and `ready`
are equivalent for display.

eslint, type-check and docs:lint clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@IrisAlexandrescu
IrisAlexandrescu force-pushed the feat/LLMO-7418-expose-provisioning-status branch from 8cf602c to bcb52cf Compare September 16, 2026 18:15
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.

2 participants