fix(serenity): resolve languageCode by Semrush BCP-47 code, not English name - #3221
Conversation
…sh name resolveLanguageId matched a languageCode's Intl.DisplayNames English name against Semrush's catalog name, which silently broke on catalog renames (e.g. "Chinese Simplified" vs "Simplified Chinese") or aliases (Filipino/ Tagalog) — the root cause behind LLMO-7309. Semrush's language catalog now carries a stable BCP-47 code (LLMO-7420); resolve directly against it instead, and drop the English-name matching entirely. listLanguageCatalog and the SerenityLanguage OpenAPI schema now surface that code so clients can build language pickers from it. Supersedes #3183 without merging it. Depends on a future @adobe/spacecat-shared-project-engine-client release carrying the catalog's code field (see the spacecat-shared LLMO-7420 branch) — the dependency version is not bumped here since that package hasn't published yet.
|
This PR will trigger a patch release when merged. |
…code field
listSerenityOrgLanguages' AJV contract test fixture returned
{ id, name } without code, failing schema validation now that
SerenityLanguage requires it. Caught by re-running the full suite
directly (npm test), not the piped/truncated run used earlier.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
| handlerName: 'listLanguageCatalog', | ||
| handlerResult: { | ||
| items: [{ id: 'lang-en', name: 'English' }], | ||
| items: [{ id: 'lang-en', name: 'English', code: 'en' }], |
There was a problem hiding this comment.
Must Fix: This fixture's handlerResult returned { id, name } without code. Since SerenityLanguage now requires code, the AJV contract check for listSerenityOrgLanguages failed: must have required property 'code'. Only surfaced when npm test was run unpiped (an earlier local check piped through tail, which reports tail's exit code, not npm test's — masked the failure).
| handlerName: 'listLanguageCatalog', | ||
| handlerResult: { | ||
| items: [{ id: 'lang-en', name: 'English' }], | ||
| items: [{ id: 'lang-en', name: 'English', code: 'en' }], |
There was a problem hiding this comment.
Must Fix: This fixture's handlerResult returned { id, name } without code. Since SerenityLanguage now requires code, the AJV contract check for listSerenityOrgLanguages failed: must have required property code. Only surfaced when npm test was run unpiped (an earlier local check piped through tail, which reports tail exit code, not npm test's - masked the failure).
There was a problem hiding this comment.
Fixed in 6f8c3b6 - fixture now returns code: 'en'. Verified: npx mocha --timeout 20000 test/openapi-contract/serenity-api.test.js -> 37 passing, 0 failing.
| * when Semrush adds or renames a language. | ||
| * @param {SerenityTransport} transport | ||
| */ | ||
| export async function resolveLanguageId(transport, languageCode, log) { |
There was a problem hiding this comment.
Should Fix (informational, not blocking): ci/it-postgres will fail on this PR until adobe/spacecat-shared#1919 merges and publishes and the @adobe/spacecat-shared-project-engine-client dependency here is bumped. The IT suite pulls a version-pinned, already-published GHCR mock image that predates the code field, so resolveLanguageId's catalog cache ends up empty against it and every market-creation IT test 400s unknownLanguage. Confirmed locally with Docker and reproduced on this PR's own CI run. This is the sequencing already called out in the PR description, not a defect in this diff - flagging inline so it is not missed as a real regression during review.
There was a problem hiding this comment.
Acknowledged - not fixable within this PR; it resolves itself once adobe/spacecat-shared#1919 merges+publishes and the dependency here is bumped in a follow-up commit (see section 9, Deployment & merge order, in the PR description). Not addressed by a code change.
…U logic resolveLanguageId no longer converts languageCode to an English name via Intl.DisplayNames — the comment and test title still described that deleted code path. Update both to describe the actual behavior: an unresolved code simply isn't a key in the byCode map.
| // tags. The handler guards against that and returns null from | ||
| // isoToEnglishName, which surfaces as 400 unknownLanguage. | ||
| it('400s when the language tag is not a real language (ICU returns it unchanged)', async () => { | ||
| // Branch coverage: a syntactically valid but unresolvable code (not a key in |
There was a problem hiding this comment.
Nit: the comment above this test (and its title) still referenced isoToEnglishName/ICU-DisplayNames-echo behavior, which was deleted in this PR. The test itself still passed (input 'xx' just isn't a key in the new byCode map), but the comment would mislead the next reader into thinking ICU is still involved in this resolution path.
There was a problem hiding this comment.
Fixed in 58fdef0 - comment and test title now describe the actual byCode-lookup behavior. Verified: npx mocha --timeout 20000 test/support/serenity/handlers/markets.test.js -> 105 passing, 0 failing.
MysticatBot
left a comment
There was a problem hiding this comment.
Mysticat could not complete a verified review of this pull request.
This is a service-side gap, not a finding about your code: no binding APPROVE or REQUEST_CHANGES review could be produced and verified for the current commit.
- Current head:
58fdef0bcf8ad818766e0e27ade6ad5b7bf5a98a - Last reviewed head:
6f8c3b60e03988fd192dff458ea4b5fab13ae5bf - Failure category:
head_drift - Request ID:
b8afd7c0-aacf-11f1-810f-aac20616eec9 - Cost: $9.26
- Duration: 4m 35s
Please re-request a review (e.g. push a new commit or re-request from the configured reviewer) to try again.
There was a problem hiding this comment.
Hey @aliciadriani,
Verdict: Request changes - two addressable gaps before this ships.
Complexity: MEDIUM - medium diff; API surface change.
Changes: Replaces brittle English-name-based Semrush language resolution with direct BCP-47 code matching, adds code to the SerenityLanguage schema and catalog endpoint (6 files).
Note: CI checks are currently pending - confirm they pass before merge.
Must fix before merge
- [Important] No regression test for the root-cause scenario (zh-Hans/zh-Hant script-subtag disambiguation) -
test/support/serenity/handlers/markets.test.js(details inline) - [Important]
listLanguageCatalogsilently drops catalog entries missingcodewith no observability -src/support/serenity/handlers/markets.js:~1035(details inline)
Non-blocking (3): minor issues and suggestions
- suggestion: Add a brief code comment at the
byCode.setcall explaining the.toLowerCase()normalization (BCP-47 tags are case-insensitive per RFC 5646) so a future reader does not "fix" it to preserve casing -src/support/serenity/handlers/markets.js:100 - suggestion: Hoist the
if (!hasText(languageCode)) return nullguard above the cache-refresh block to avoid a wastedtransport.listLanguages()call on null/empty input when the cache is cold -src/support/serenity/handlers/markets.js:114 - suggestion: The OpenAPI
codefield description showszh-Hanswith original casing, but the resolution path lowercases; consider noting the case-insensitive matching so API consumers know they should store as-is and send as-is -docs/openapi/schemas.yaml:~12639
Skill: pr-review | Model: us.anthropic.claude-opus-4-6-v1[1m] | Duration: 3m 57s | Cost: $5.69 | Commit: 946d22df863a8427b12a1aea901d2436b06a3201
If this code review was useful, please react with 👍. Otherwise, react with 👎.
| @@ -338,7 +338,7 @@ describe('handlers/markets.js — handleCreateMarket', () => { | |||
| dataAccess.BrandSemrushProject.create.resolves(); | |||
There was a problem hiding this comment.
issue (blocking): No regression test for the root-cause scenario. Every listLanguages stub uses simple 2-letter codes (en, de). The motivating bug - Chinese zh-Hans/zh-Hant collapsing to the same resolution under the old English-name approach (LLMO-7309) - is never exercised.
The case-folding path (String(item.code).toLowerCase() in cache vs. the already-lowercased input from normalizeLanguageCode) is load-bearing for CJK languages and has no test coverage.
How to fix: Add a test where the transport returns at least two entries with different script subtags:
listLanguages: sinon.stub().resolves({ items: [
{ id: 'lang-zhcn', name: 'Chinese Simplified', code: 'zh-Hans' },
{ id: 'lang-zhtw', name: 'Chinese Traditional', code: 'zh-Hant' },
] })Then assert that languageCode: 'zh-hans' resolves to lang-zhcn and languageCode: 'zh-hant' resolves to lang-zhtw. This is the single most valuable test this PR can carry - it guards against re-regression of the exact bug it fixes.
There was a problem hiding this comment.
Fixed in c1f5c6c - added a dedicated resolveLanguageId describe block with a test resolving zh-Hans and zh-Hant to distinct catalog ids (the exact LLMO-7309 root-cause scenario), plus a sibling test confirming an unknown code returns null with no fallback matching.
| languageCache.byTag.set(String(item.name).toLowerCase(), String(item.id)); | ||
| if (hasText(item?.code) && hasText(item?.id)) { | ||
| languageCache.byCode.set(String(item.code).toLowerCase(), String(item.id)); | ||
| } |
There was a problem hiding this comment.
issue (blocking): listLanguageCatalog silently drops entries missing code with no log trail. The filter at ~line 1035 changed from hasText(l.name) to hasText(l.name) && hasText(l.code). If the upstream catalog returns entries with a name but no code (partial rollout, Semrush adding a new language before populating code, or companion PR not yet deployed), those entries vanish from the catalog response. The UI language picker would show fewer languages than Semrush supports, with no observability signal.
resolveLanguageId already has a log.warn for when the catalog returns no usable entries - listLanguageCatalog has no equivalent.
How to fix: Add a warn-level log when entries are dropped for missing code. This requires threading log into listLanguageCatalog, which is a minor signature change:
const dropped = rawItems.filter(l => l && typeof l === 'object' && hasText(l.name) && !hasText(l.code));
if (dropped.length > 0) {
log?.warn?.('listLanguageCatalog: dropped entries missing code', { count: dropped.length });
}There was a problem hiding this comment.
Fixed in c1f5c6c - listLanguageCatalog now takes an optional log param and warns (with a dropped count) when entries are dropped for missing code, threaded through from the listOrgLanguages controller. Added two tests covering the warn and no-warn paths.
There was a problem hiding this comment.
Hey @aliciadriani,
⚠ Degraded review - no spec document was found for this change (searched the PR links, the touched repos' docs, the architecture/guidelines docs, and linked Jira). This review covers code-level quality but could not validate the change against an agreed design, so confidence is reduced. Add a spec link (PR template section 4) and re-request review for a full-confidence pass.
Verdict: Request changes - two gaps to close before merge.
Complexity: HIGH - medium diff; OpenAPI schema is a risk surface.
Changes: Replaces English-name matching with exact BCP-47 code-based resolution for Semrush language IDs, adding a required code field to the SerenityLanguage schema and updating all test fixtures (6 files).
Note: Recommend a human read before merge - this change modifies a shared contract (OpenAPI schema). The bot review is a complement to, not a replacement for, a human read here.
Note: CI checks are still pending - verify before merge.
Must fix before merge
- [Important] Stale JSDoc on
listLanguages()insrc/support/serenity/rest-transport.jsstill describes the old resolution mechanism ("resolve the language_id UUID from an ISO 639-1 code"). The resolution key is now the BCP-47codefield from the catalog, not an ISO 639-1 to English-name lookup. Update the JSDoc to reflect the BCP-47 code-based resolution (LLMO-7420). (File not in diff; cannot anchor inline.) - [Important] No tests exercise the motivating regression scenarios (zh-Hans, zh-Hant, fil) -
test/support/serenity/handlers/markets.test.js:614(details inline)
Non-blocking (3): minor issues and suggestions
- nit:
resolveLanguageIdJSDoc could clarify the function is intentionally case-insensitive regardless of caller normalization -src/support/serenity/handlers/markets.js:85 - suggestion:
listLanguageCatalogshows id-less languages thatresolveLanguageIdcannot resolve (pre-existing asymmetry); consider aligning filters or adding a comment documenting the intended gap -src/support/serenity/handlers/markets.js:1035 - suggestion:
|| nullatsrc/support/serenity/handlers/markets.js:118could be?? nullto express intent more precisely (functionally equivalent given thehasTextguard on cache population)
Skill: pr-review | Model: us.anthropic.claude-opus-4-6-v1[1m] | Duration: 7m 35s | Cost: $5.86 | Commit: 946d22df863a8427b12a1aea901d2436b06a3201
If this code review was useful, please react with 👍. Otherwise, react with 👎.
| // Branch coverage: a syntactically valid but unresolvable code (not a key in | ||
| // the catalog's byCode map) surfaces as 400 unknownLanguage — no fallback | ||
| // heuristic catches it (LLMO-7420: exact code match only). | ||
| it('400s when the language code is not in the Semrush catalog', async () => { |
There was a problem hiding this comment.
issue (blocking): No tests exercise the motivating regression scenarios. Every test stub uses simple 2-letter codes (en, de, fr, xx), but the entire rationale for this PR (LLMO-7420) is that Chinese (zh-Hans, zh-Hant) and Filipino (fil) broke under the old English-name matching.
What's wrong: The fix this PR ships - exact BCP-47 code match with case-insensitive lowering - is not exercised by any runnable assertion with script subtags or 3-letter primary subtags. A regression (e.g., someone reintroducing .split('-')[0]) would pass the full test suite.
Why it matters: The lowercased round-trip (zh-Hans -> zh-hans in cache, matching zh-hans from normalizeLanguageCode) and the ability to distinguish zh-Hans from zh-Hant as separate IDs are the core invariants this PR establishes. Without tests, they are documented in JSDoc but not proven.
How to fix: Add a test that stubs the catalog with entries like [{ id: 'lang-zh-s', name: 'Chinese (Simplified)', code: 'zh-Hans' }, { id: 'lang-zh-t', name: 'Chinese (Traditional)', code: 'zh-Hant' }, { id: 'lang-fil', name: 'Filipino', code: 'fil' }] and asserts:
languageCode: 'zh-hans'resolves tolang-zh-slanguageCode: 'zh-hant'resolves tolang-zh-tlanguageCode: 'fil'resolves tolang-fillanguageCode: 'zh'(bare primary subtag) does NOT resolve when only script-qualified entries exist
There was a problem hiding this comment.
Strengthened in 97d9c84 - the regression test now covers all three motivating LLMO-7309 scenarios in one fixture (zh-Hans, zh-Hant, fil) plus a negative assertion that the bare primary subtag 'zh' does NOT resolve when only script-qualified entries exist. (Note: the core zh-Hans/zh-Hant distinction test already existed from an earlier round of this same review loop, in c1f5c6c - this commit adds fil and the negative case this specific comment asked for.)
…t + observability - Add a direct resolveLanguageId unit test resolving zh-Hans/zh-Hant to distinct catalog ids — the exact LLMO-7309 root-cause scenario (script subtags collapsing under English-name matching) had no regression coverage. - listLanguageCatalog now warns when catalog entries are dropped for missing code, threading log through from the controller — previously silent, with no observability signal if the catalog partially regresses. - Hoist resolveLanguageId's empty-languageCode guard above the cache refresh to avoid a wasted upstream call. - Document BCP-47 case-insensitive matching at the byCode.set call site and in the OpenAPI code field description.
| if (hasText(item?.name) && hasText(item?.id)) { | ||
| languageCache.byTag.set(String(item.name).toLowerCase(), String(item.id)); | ||
| if (hasText(item?.code) && hasText(item?.id)) { | ||
| // BCP-47 tags are case-insensitive (RFC 5646) — lowercase both the catalog's `code` |
There was a problem hiding this comment.
MysticatBot suggestion: add a comment at byCode.set explaining the .toLowerCase() normalization so a future reader doesn't 'fix' it to preserve casing.
There was a problem hiding this comment.
Fixed in c1f5c6c - added the RFC 5646 case-insensitivity comment at the byCode.set call site.
| * @param {SerenityTransport} transport | ||
| */ | ||
| export async function resolveLanguageId(transport, languageCode, log) { | ||
| if (!hasText(languageCode)) { |
There was a problem hiding this comment.
MysticatBot suggestion: hoist the empty/null languageCode guard above the cache-refresh block to avoid a wasted transport.listLanguages() call when the cache is cold and input is invalid.
There was a problem hiding this comment.
Fixed in c1f5c6c - the guard now runs before the cache-refresh block.
| description: English language name as Semrush spells it. Metadata/fallback text only — not a resolution key (LLMO-7420). | ||
| code: | ||
| type: string | ||
| description: BCP-47 language code (e.g. zh-Hans, zh-Hant, fil) — the stable identifier a client persists and later sends back as languageCode (LLMO-7420). Matching is case-insensitive (RFC 5646); store and send it as returned here, exact casing is not significant. |
There was a problem hiding this comment.
MysticatBot suggestion: the code field description shows zh-Hans with original casing while resolution is case-insensitive - note that for API consumers.
There was a problem hiding this comment.
Fixed in c1f5c6c - the description now notes matching is case-insensitive (RFC 5646) and consumers should store/send the code as returned.
…solver - Document that a null-id catalog entry is never resolvable, in both the OpenAPI schema description and (already present) the code comment. - Add a test pinning resolveLanguageId's early-return guard: a null/empty languageCode returns null without ever calling the transport. - Clarify in the JSDoc that the hasText guard is defense-in-depth, not redundant with caller-side normalization.
| id: | ||
| type: [string, 'null'] | ||
| description: Semrush language catalog id (null when the catalog omits it). | ||
| description: Semrush language catalog id (null when the catalog omits it). A null id is never resolvable — createProject will 400 unknownLanguage for this entry's code regardless of what the catalog otherwise reports. |
There was a problem hiding this comment.
MysticatBot suggestion: listLanguageCatalog can return entries with code but id: null that can never resolve at create time - document this for API consumers.
There was a problem hiding this comment.
Fixed in 4709942 - added a note to the id field description that a null id is never resolvable.
| expect(await resolveLanguageId(transport, 'xx')).to.equal(null); | ||
| }); | ||
|
|
||
| it('returns null for a null/empty languageCode without ever calling the transport (early-return guard)', async () => { |
There was a problem hiding this comment.
MysticatBot suggestion: add a one-line test pinning the early-return hasText guard, confirming the transport is never called for null/empty languageCode.
There was a problem hiding this comment.
Fixed in 4709942 - added a test asserting resolveLanguageId(transport, null) and (transport, '') both return null and transport.listLanguages is never called.
| * is intentionally case-insensitive regardless of caller normalization | ||
| * (BCP-47 tags are case-insensitive per RFC 5646) — this function does not | ||
| * rely on the caller having lowercased its input. An unresolved code returns | ||
| * `null` (400 `unknownLanguage` at the call sites) — |
There was a problem hiding this comment.
MysticatBot nit: clarify the hasText guard is defense-in-depth, not redundant with caller-side normalization, so a future reader doesn't remove it.
There was a problem hiding this comment.
Fixed in 4709942 - JSDoc now explicitly says the guard is defense-in-depth, called directly in tests without assuming prior normalization.
| { receivedKeys: Object.keys(items[0] || {}) }, | ||
| ); | ||
| /* c8 ignore stop */ | ||
| } |
There was a problem hiding this comment.
MysticatBot nit: ?? null vs || null is cosmetically correct but doesn't alter behavior here, since Map.get never returns 0/'' for a missing key.
There was a problem hiding this comment.
Acknowledged - agreed this is cosmetic-only, no behavior change intended or introduced. No further action needed.
There was a problem hiding this comment.
Hey @aliciadriani,
⚠ Degraded review - no spec document was found for this change (searched the PR links, the touched repos' docs, the architecture/guidelines docs, and linked Jira). This review covers code-level quality but could not validate the change against an agreed design, so confidence is reduced. Add a spec link (PR template section 4) and re-request review for a full-confidence pass.
Verdict: Approve - prior findings addressed, no new blocking issues.
Complexity: HIGH - medium diff; API surface change (OpenAPI schema).
Changes: Adds JSDoc for the hasText defense-in-depth guard, expands the OpenAPI id-field description to document the null-id-is-never-resolvable behavior, and pins the null/empty languageCode early-return with a direct unit test (3 files).
Note: Recommend a human read before merge - this change modifies a shared contract (OpenAPI schema). The bot review is a complement to, not a replacement for, a human read here.
Note: CI checks are still pending - verify before merge.
Non-blocking (1): minor issues and suggestions
- suggestion:
listLanguageCatalogcan return entries withcodebutid: nullthat will always 400 at create time (thebyCodecache only admits entries where both are present). The comment at the filter documents this asymmetry. Consider filtering outid: nullentries from the catalog response in a follow-up so the UI picker never offers unresolvable options, or adding aresolvableflag -src/support/serenity/handlers/markets.js:1048
Previously flagged, now resolved
- Direct unit test for null/empty
languageCodeearly-return guard now pins behavior and confirms transport is never called resolveLanguageIdJSDoc expanded to explain thehasTextguard is defense-in-depth, not redundant with caller normalization- OpenAPI
idfield description now documents that null id is never resolvable (400 unknownLanguage)
Skill: pr-review | Model: us.anthropic.claude-opus-4-6-v1[1m] | Duration: 2m 26s | Cost: $4.08 | Commit: 4709942435e3d7d9ece86bcdb5494daf1df4957f
If this code review was useful, please react with 👍. Otherwise, react with 👎.
…talog listLanguageCatalog kept entries with code but no usable id (id: null), which resolveLanguageId's byCode map can never admit — the UI picker could offer a language that then 400s unknownLanguage at create time. Filter those out at the source instead of just documenting the gap, so listLanguageCatalog's notion of "resolvable" matches resolveLanguageId's. id is now always a real string on every returned entry; the OpenAPI schema reflects that.
| .filter((l) => l && typeof l === 'object' && hasText(l.name)) | ||
| .map((l) => ({ id: hasText(l.id) ? String(l.id) : null, name: String(l.name) })) | ||
| const usable = rawItems.filter((l) => l && typeof l === 'object' && hasText(l.name)); | ||
| const resolvable = usable.filter((l) => hasText(l.code) && hasText(l.id)); |
There was a problem hiding this comment.
MysticatBot suggestion: listLanguageCatalog can return entries with code but id: null that will always 400 at create time (the byCode cache only admits entries where both are present). The comment at the filter documents this asymmetry. Consider filtering out id: null entries from the catalog response in a follow-up so the UI picker never offers unresolvable options, or adding a resolvable flag.
There was a problem hiding this comment.
Fixed in d16e1de - went with the stronger option (filtering, not just documenting). listLanguageCatalog now drops any entry missing a usable id, not just entries missing code, so every returned item is guaranteed resolvable via resolveLanguageId's byCode map. id is now a required, non-nullable string in the OpenAPI schema and the handler's output type. Also updated the earlier 'document only' fix (comment 3956660401) - superseded by this stronger fix.
There was a problem hiding this comment.
Hey @aliciadriani,
Verdict: Approve - prior suggestion addressed, no blocking issues.
Complexity: HIGH - medium diff; API surface change (OpenAPI schema).
Changes: Tightens listLanguageCatalog to drop entries missing a usable id (not just code), making SerenityLanguage.id required and non-nullable so every catalog entry is guaranteed resolvable (8 files).
Note: Recommend a human read before merge - this change modifies a shared contract (OpenAPI schema). The bot review is a complement to, not a replacement for, a human read here.
Note: CI checks are still pending - verify before merge.
Non-blocking (2): minor issues and suggestions
- suggestion: The endpoint description in
docs/openapi/serenity-api.yaml(line ~1171) still saysid (nullable), but the schema now declaresidas required and non-nullable. Drop the parenthetical or change to match -docs/openapi/serenity-api.yaml:1171 - nit: Stale comment above the
listLanguageCatalog drops entries with a missing or blank idtest still references the old ternary (id: hasText(l.id)?String(l.id):null) and old line number. The test title was updated but the comment block was not -test/support/serenity/handlers/markets.test.js:2336
Previously flagged, now resolved
- listLanguageCatalog now drops entries missing a usable id (not just code), closing the picker-to-resolver asymmetry flagged in the prior review
- OpenAPI id field updated from nullable to required, matching the runtime invariant
Skill: pr-review | Model: us.anthropic.claude-opus-4-6-v1[1m] | Duration: 2m 15s | Cost: $4.23 | Commit: d16e1dece095aebedcbe04c4389039c818c77551
If this code review was useful, please react with 👍. Otherwise, react with 👎.
- serenity-api.yaml: drop the stale "(nullable)" on the languages endpoint description now that SerenityLanguage.id is required. - markets.test.js: replace the stale comment referencing the old id:null ternary and line number with one matching the current drop-on-missing-id filter behavior. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
| and the English `name` used as the resolution key. Tolerates a 404/405 | ||
| from the upstream by returning an empty list. Org access is enforced; IMS | ||
| authentication is required. | ||
| rejected at create time). Each item carries the catalog `id`, |
There was a problem hiding this comment.
Fixed in 00cb4b1: dropped the stale (nullable) parenthetical from the endpoint description — SerenityLanguage.id is required and non-nullable in the schema, so the description no longer contradicts it.
| // when a language item has a blank or missing `id` field. listLanguageCatalog | ||
| // keeps such rows (they have a valid name) and maps id to null. | ||
| it('listLanguageCatalog maps a language item with missing id to null', async () => { | ||
| // listLanguageCatalog drops any entry with a blank or missing `id` entirely — |
There was a problem hiding this comment.
Fixed in 00cb4b1: replaced the stale comment that referenced the old id: hasText(l.id)?String(l.id):null ternary and its old line number with one describing the current behavior — listLanguageCatalog drops (not nulls) any entry with a blank or missing id.
adobe/spacecat-shared#1919 (which adds the BCP-47 code field this PR consumes) merged and published as 1.23.0. The it-postgres suite was failing because the pinned 1.22.0 mock image predates that field, so listLanguageCatalog's resolvable-entry filter dropped every catalog row (GET /serenity/languages returned an empty list) and every downstream /serenity/markets and /serenity/tags IT case 400/404'd. Bumping the dependency also bumps the lockstep mock image tag (SERENITY_PE_MOCK_TAG) to 1.23.0, which carries the code field. Verified locally: npm test (full suite), npx mocha against test/it/postgres/serenity.test.js (81 passing), npm run type-check, npm run docs:lint. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
## [1.799.3](v1.799.2...v1.799.3) (2026-09-09) ### Bug Fixes * **serenity:** resolve languageCode by Semrush BCP-47 code, not English name ([#3221](#3221)) ([23aef8c](23aef8c)), closes [#3183](#3183) [Hi#level](https://github.com/Hi/issues/level) [#1919](#1919) [#1919](#1919) [#1919](#1919) [#1919](#1919)
|
🎉 This PR is included in version 1.799.3 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
1. Abstract
Resolves Brand Markets
languageCodeto a Semrush language id by exact BCP-47codematch instead of English-name matching, and surfacescodefrom the org-level language catalog endpoint.2. Reasoning
resolveLanguageIdconverted a market's BCP-47languageCodeto an English display name viaIntl.DisplayNamesand matched it against Semrush's catalogname— brittle by construction: Semrush spells "Chinese Simplified" whereIntl.DisplayNamessays "Simplified Chinese" (word-order collision), aliases like Filipino/Tagalog exist, and any future catalog rename silently drops a language. This is the root cause behind LLMO-7309 (Chinese language selection broke, tracked in the still-open, not-yet-merged #3183). Semrush's language catalog now returns a stable BCP-47codeper entry (LLMO-7420, confirmed live 2026-09-07); this PR makes thatcodethe resolution key end-to-end and deletes the English-name matching entirely.3. High-level overview of the changes
resolveLanguageId(src/support/serenity/handlers/markets.js) no longer convertslanguageCodeto an English name viaIntl.DisplayNamesand matches by name. It now caches the language catalog keyed by lowercasedcodeand looks up the (already-normalized)languageCodedirectly. An unresolved code still 400sunknownLanguage— no fallback heuristics, so a new or renamed Semrush language needs no Adobe-side mapping change.listLanguageCatalognow includescodein each returned item (previously onlyid/name) and drops rows missing eithernameorcode.SerenityLanguageOpenAPI schema (docs/openapi/schemas.yaml) gained a requiredcodefield; theGET /serenity/languagesroute description (docs/openapi/serenity-api.yaml) no longer describesnameas "the resolution key" —codeis.languageCodeinput validation (normalizeLanguageCode) or to the slice-key shape.4. Required information
codefield this PR consumes), adobe/project-elmo-uiLLMO-7420-semrush-bcp47-language-source-of-truth(builds the language picker fromcode)6. Affected / used mysticat-workspace projects
@adobe/spacecat-shared-project-engine-client'slistLanguages()must returncodeper entry for this resolver to work — depends on PR fix(deps): update adobe fixes #1919 publishing before this can be deployed against live Semrush (verified locally vianpm linkto the unpublished branch; the dependency version inpackage.jsonis intentionally not bumped yet).codefromGET /v2/orgs/{orgId}/serenity/languagesto build the Brand Markets language picker.8. Test plan
(a) Ran the full unit suite locally, including the rewritten Chinese-script resolution cases (
zh-Hans/zh-Hant/filresolving by exactcode, no name-matching fallback) and thelistLanguageCatalogcatalog-shape tests, plusnpm run type-checkandnpm run docs:lintagainst the updated OpenAPI schema.(b) Dev/stage: after bumping
@adobe/spacecat-shared-project-engine-clientto the published version carryingcode, exercisePOST /serenity/marketsfor a Hong Kong brand withlanguageCode: zh-Hansand confirm it resolves to the Chinese Simplified project (previously broken per LLMO-7309); confirmGET /v2/orgs/{orgId}/serenity/languagesreturnscodeper entry.9. Deployment & merge order
LLMO-7420-semrush-bcp47-language-source-of-truth(consumes this PR'scodefield from the languages endpoint).🤖 Generated with review-kit
Change Management