Skip to content

fix(project-engine-client, user-manager-client): round-trip created+published market + live-fidelity sweep (#1745)#1746

Merged
rainer-friederich merged 5 commits into
mainfrom
feat/1745
Jun 29, 2026
Merged

fix(project-engine-client, user-manager-client): round-trip created+published market + live-fidelity sweep (#1745)#1746
rainer-friederich merged 5 commits into
mainfrom
feat/1745

Conversation

@rainer-friederich

@rainer-friederich rainer-friederich commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

1. Abstract

Raises the fidelity of the Project Engine and User Manager Counterfact mocks (spacecat-shared-project-engine-client, spacecat-shared-user-manager-client) so a created+published market round-trips, plus the complementary live-vs-mock divergences a second sweep surfaced. Mock + documentation only — no production-code change.

2. Reasoning

With the publish 406 fixed in 1.3.1 (#1742), POST markets / activate reach a published 201 against the PE mock, but a just-created market did not round-trip: it never appeared in GET markets, GET markets/{geo}/{lang} 404'd, and a prompt against that slice was skipped. Root cause: the mock's created-project read-view returned an empty settings.ai.language.name, so the consumer's langOf resolved null and listMarkets dropped the project; and publish never flipped the stored publish_status. A subsequent live-fidelity sweep against the real Semrush gateway (captured in issue 1745) surfaced a set of complementary mock-vs-live divergences worth closing while here. None is a production bug.

3. High-level overview of the changes

Project Engine mock:

  • The created read-view now resolves settings.ai.language.name to the ISO code the live API returns there (e.g. en) — not the empty string, and not the English display name — via a new shared mock/language-catalog.js (canonical id ↔ display-name ↔ iso catalog). The GET /v1/languages route now reads that catalog through the request context instead of carrying its own duplicate copy. country.name is resolved from the country code via Intl.DisplayNames.
  • publish now moves the stored project to the live state: publish_status flips draftlive and published_at is stamped, while is_draft is left unchanged (matching live, which keeps it true post-publish). A later list/get therefore reports the published status.
  • Net behaviour: create → publish → GET markets now lists the market with languageCode and a live status, GET markets/{geo}/{lang} resolves, and a prompt attaches to the slice.
  • Second-sweep fidelity (live-pinned 2026-06-29): the read-view echoes null (not []/'') for an omitted brand_names / location_name; prompt creation dedups by text into existing_count; benchmark creation returns a hard 409 on a duplicate brand name / alias / domain — including two conflicting entries within the same batch; deleting a non-existent project returns 404 (the consumer treats a delete-404 as success).

User Manager mock:

  • The pool-exhaustion 422 now returns the live string insufficient available units in subscription.
  • GET family on an unknown workspace now returns 403, mirroring GET status (previously 200 []).

Both packages' docs/mock-statefulness.md now record the intentional simplifications the sweep confirmed (no tenant/ownership model and presence-only bearer; the two-layer live error model and the unguarded 500s the mock does not reproduce; pagination asymmetry across catalogs; empty brand-topics prompts; the UM stuck-not ready / 422-on-delete zombie window and the spurious mock-only role field).

4. Required information

5. Affected / used mysticat-workspace projects

  • spacecat-api-service — consumed (contract). Its serenity API-level integration suite boots these two mock images; this PR makes the created-market read-back and prompt-lifecycle assertions there pass. No code change required in that repo for the mock behaviour, but it picks up the corrected images once these packages release.

6. Additional information outside the code

  • Drove the booted PE and UM mocks end-to-end through the serenity-relevant lifecycle (create → publish → list round-trip; prompt-text dedup; duplicate-benchmark 409; non-existent-project delete 404; UM over-allocation 422 string; UM family on an unknown workspace 403) and confirmed the read-back shapes match the live shapes captured in the issue. The live shapes themselves were captured against the real Semrush prod gateway in the sweeps recorded on issue 1745.

7. Test plan

  • (a) Local: ran each package's mock end-to-end via its e2e harness (which boots the Counterfact server) and exercised the round-trip, dedup, conflict, delete-404, and the UM string/403 paths described above, plus the existing lifecycle suites, all against the booted mock.
  • (b) Per-environment: not applicable — these packages ship only the mock under mock/ (used by local dev and the cross-repo e2e harness) and the typed client src/; there is no service deployment. Downstream verification happens in spacecat-api-service's serenity IT once the corrected mock images are consumed.

8. Deployment & merge order

🤖 Generated with Claude Code

rainer-friederich and others added 2 commits June 29, 2026 12:41
…e mock (#1745)

The Project Engine Counterfact mock dropped a just-created market: the create
read-view returned an empty settings.ai.language.name, so the consumer's langOf
mapped it to null and listMarkets dropped the project, and publish never flipped
the stored status. Resolve the create read-view's language.name to the ISO code
(via a new shared mock/language-catalog.js id<->name<->iso catalog) and country.name
via Intl.DisplayNames, and make publish flip the stored publish_status draft->live
and stamp published_at (leaving is_draft as-is, matching live).

Also fold in the second-angle live sweep (2026-06-29) fidelity items:
- read-view echoes null (not []/'') for an omitted brand_names / location_name
- prompt create dedups by text into existing_count; benchmark create 409s on a
  duplicate brand name / alias / domain
- DELETE of a non-existent project 404s (serenity treats 404 as success)
- document the no-ownership model, error layering, pagination asymmetry, and the
  empty brand-topics prompts as intentional mock simplifications

All mock-fidelity / doc items; no production-code change. Unit 100% coverage,
types + lint clean, e2e (38) green incl. the round-trip.

Introduced by: #1745

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…own ws (#1745)

Second-angle live sweep (2026-06-29) fidelity items for the User Manager mock,
complementary to the project-engine round-trip fixes:
- correct the pool-exhaustion 422 to the live string
  "insufficient available units in subscription" (child + transfer handlers)
- GET family on an unknown workspace now 403s, mirroring GET status (was 200 [])
- document the no-ownership / presence-only-bearer model, the resources-not-actually
  -required-on-create divergence, the stuck-not-ready / 422-on-delete zombie window,
  and the spurious mock-only role field as intentional simplifications

All mock-fidelity / doc items; no production-code change. Unit 100% coverage,
types + lint clean, e2e (14) green incl. the new family-403 case.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@MysticatBot MysticatBot 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.

@/tmp/mysticat-workspace-4_vd1e3t/review-body-1746.md

@MysticatBot MysticatBot 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.

Hey @rainer-friederich,

Verdict: Request changes - two mock fidelity gaps in the benchmark conflict detection path need attention.
Complexity: HIGH - large diff; API surface.
Changes: raises fidelity of the PE and UM Counterfact mocks so a created+published market round-trips, adds dedup/conflict detection, and documents live-vs-mock divergences (19 files).

Must fix before merge

  1. [Important] Benchmark intra-batch conflict not checked - conflicting entries within the same POST batch are not detected - packages/spacecat-shared-project-engine-client/mock/counterfact/routes/v2/workspaces/{id}/projects/{project_id}/ai_models/benchmarks.js:24 (details inline)
  2. [Important] Benchmark brand_aliases conflict path has zero test coverage - packages/spacecat-shared-project-engine-client/test/e2e/project-engine-mock.e2e.js:690 (details inline)
Non-blocking (5): minor issues and suggestions
  • nit: nullable() wrapper could be a typed constant instead of a function call - packages/spacecat-shared-project-engine-client/mock/factories.js:356
  • suggestion: identityTokens does not guard against non-array brand_aliases (spread of a string iterates characters) - packages/spacecat-shared-project-engine-client/mock/counterfact/routes/v2/workspaces/{id}/projects/{project_id}/ai_models/benchmarks.js:279
  • suggestion: publish e2e test should assert published_at is a valid ISO 8601 timestamp via round-trip parse - packages/spacecat-shared-project-engine-client/test/e2e/project-engine-mock.e2e.js:561
  • suggestion: language catalog test could verify iso uniqueness alongside the existing id uniqueness check - packages/spacecat-shared-project-engine-client/test/mock/language-catalog.test.js:749
  • suggestion: add a CATALOG_CAPTURED datestamp constant to language-catalog.js so future drift from the live Semrush catalog is discoverable - packages/spacecat-shared-project-engine-client/mock/language-catalog.js:1

- benchmark create now detects intra-batch conflicts (fold each entry's identity
  tokens into the seen-set while iterating), not just collisions with stored rows
- identityTokens guards against a non-array brand_aliases (spread of a string would
  iterate characters into spurious tokens)
- e2e: cover the brand_aliases cross-collision branch + an intra-batch duplicate;
  assert published_at is a valid ISO 8601 timestamp (round-trips through Date)
- nullable read-view helper is now a typed constant instead of a function call
- language-catalog: export a CATALOG_CAPTURED datestamp so live-catalog drift is
  discoverable; assert its shape in the catalog test

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

This PR will trigger a patch release when merged.

@rainer-friederich

Copy link
Copy Markdown
Contributor Author

Addressed the review in dcded4b4.

Must fix:

  • [Important] Benchmark intra-batch conflict — done. The conflict check now folds each incoming entry's identity tokens into the seen-set as it iterates, so two conflicting entries in the same POST batch collide too (not just against already-stored rows). Added an e2e for it.
  • [Important] brand_aliases conflict path untested — done. Added an e2e that registers a benchmark with brand_aliases: ['MyAlias'] then attempts a second whose brand_name is myalias (case-insensitive cross-collision) and asserts 409.

Non-blocking:

  • identityTokens non-array brand_aliases guard — done. Spreads brand_aliases only when it is an array (a raw string would otherwise iterate into character tokens).
  • nullable() → typed constant — done. Now a @type {any} NULLABLE constant instead of a function call.
  • publish e2e asserts published_at is valid ISO 8601 — done. Round-trips it through Date and compares.
  • CATALOG_CAPTURED datestamp — done. Exported from language-catalog.js (with a shape assertion) so live-catalog drift is greppable.
  • iso uniqueness assertion — declined. The reverse index is intentionally many-ids→one-iso (e.g. dialect rows could share a base code), so asserting iso uniqueness would encode an invariant the catalog does not guarantee; the meaningful uniqueness check is on id (the reverse-index key), which is kept.

@rainer-friederich rainer-friederich changed the title fix(project-engine-client): round-trip created+published market in the PE mock (#1745) fix(project-engine-client, user-manager-client): round-trip created+published market + live-fidelity sweep (#1745) Jun 29, 2026

@MysticatBot MysticatBot 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.

Hey @rainer-friederich,

⚠ 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 - fix commit cleanly resolves both prior blocking findings; no new issues.
Complexity: HIGH - large diff; API surface.
Changes: raises fidelity of the PE and UM Counterfact mocks so a created+published market round-trips, adds dedup/conflict detection with intra-batch support, and documents live-vs-mock divergences (19 files).

Previously flagged, now resolved

  • Benchmark intra-batch conflict not checked - fixed with fold-into-taken pattern in entries.some() loop
  • brand_aliases conflict path untested - e2e added for alias cross-collision plus Array.isArray guard on the spread
Non-blocking (1): minor issues and suggestions
  • nit: intra-batch e2e only covers brand_name collision, not domain-only collision within a single batch (the implementation is generic across all token types, but a domain-collision variant would pin that) - packages/spacecat-shared-project-engine-client/test/e2e/project-engine-mock.e2e.js:712

Skill: pr-review | Model: us.anthropic.claude-opus-4-6-v1[1m] | Duration: 2m 6s | Cost: $3.90 | Commit: dcded4b485639d26c4649af140b8ab3b89acb2b4
If this code review was useful, please react with 👍. Otherwise, react with 👎.

…atBot nit on #1745)

Add an e2e that POSTs two benchmarks with distinct brand names but a shared
domain in one batch and asserts 409 — pins that the intra-batch conflict check
is generic across token types (domain, not just brand_name), per MysticatBot's
non-blocking suggestion on the approved review.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@rainer-friederich

Copy link
Copy Markdown
Contributor Author

Addressed the non-blocking nit from the approved review in 8d582bc8.

MysticatBot:

  • nit: intra-batch e2e only covered a brand_name collision, not a domain-only collision within one batch — done. Added 409s an intra-batch domain collision (distinct names, same domain in one POST) to project-engine-mock.e2e.js, pinning that the fold-into-taken conflict check is generic across token types (domain is in identityTokens, not just brand_name).

Re-requesting so the approval lands on the new HEAD.

@MysticatBot MysticatBot 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.

Hey @rainer-friederich,

⚠ 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 - nit addressed cleanly; no new issues.
Complexity: HIGH - large diff; API surface.
Changes: raises fidelity of the PE and UM Counterfact mocks so a created+published market round-trips, adds dedup/conflict detection with intra-batch support, and documents live-vs-mock divergences (19 files).

Previously flagged, now resolved

  • Intra-batch e2e now covers domain-only collision within a single batch (distinct names, same domain in one POST)

Skill: pr-review | Model: us.anthropic.claude-opus-4-6-v1[1m] | Duration: 4m 55s | Cost: $1.39 | Commit: 8d582bc841bb8125d22ff6be77e2664a18890c3c
If this code review was useful, please react with 👍. Otherwise, react with 👎.

…t hidden, #1745)

The completeness sweep on #1745 flagged that serenity's updateProject omits the
required `type`, which 400s against live Semrush. The vendored spec already marks
ProjectUpdateRequest.type required and the mock enforces it at Counterfact's
request-validation seam, so the mock does NOT hide the bug — but there was no test
proving it. Add an e2e asserting a typeless PATCH 400s, so a future overlay/handler
change that dropped the requirement would be caught (and the serenity bug surfaces
in the IT rather than passing silently). No overlay/spec change needed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@rainer-friederich

Copy link
Copy Markdown
Contributor Author

Added one regression test in b3818f5a from the issue's completeness sweep (#1745 (comment)).

  • The sweep flagged that serenity's updateProject omits the required type, which 400s against live Semrush. Investigating: the vendored spec already marks ProjectUpdateRequest.type required and Counterfact enforces it, so the mock does NOT hide the bug — but nothing proved it. Added an e2e asserting a typeless PATCH 400s. No overlay/spec change was needed (the proposed "mark type required" correction is already present in the vendored swagger).
  • The real defect is serenity-side (omits type); filed separately: serenity updateProject omits required type — brand-alias PATCH 400s against live Semrush spacecat-api-service#2720

Re-requesting so the approval lands on the new HEAD.

@MysticatBot MysticatBot 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.

Hey @rainer-friederich,

⚠ 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 - regression test cleanly pins existing validation behavior; no new issues.
Complexity: HIGH - large diff; API surface.
Changes: raises fidelity of the PE and UM Counterfact mocks so a created+published market round-trips, adds dedup/conflict detection with intra-batch support, and documents live-vs-mock divergences (19 files).

Previously flagged, now resolved

  • Intra-batch e2e covers domain-only collision within a single batch (distinct names, same domain in one POST)
  • New commit pins that a typeless PATCH returns 400 (Counterfact enforces the vendored spec's required type field), confirming the mock surfaces the serenity omit-type bug rather than hiding it

Skill: pr-review | Model: us.anthropic.claude-opus-4-6-v1[1m] | Duration: 6m 3s | Cost: $1.67 | Commit: b3818f5ad36618fb73cf4411dc29fac8d6406c12
If this code review was useful, please react with 👍. Otherwise, react with 👎.

rainer-friederich added a commit to adobe/spacecat-api-service that referenced this pull request Jun 29, 2026
`model.ProjectUpdateRequest.type` is REQUIRED — live Semrush (and the
PE mock now that adobe/spacecat-shared#1746 enforces it) 400 a typeless
PATCH. The own-brand alias re-sync (syncBrandAliasesAcrossMarkets, on the
brand create/edit fan-out) omitted it, so the project brand_names re-sync
silently failed best-effort against live. Send `type: 'ai'` (serenity
projects are AI projects — the same literal createProject uses).

The transport's updateProject `body` param is loosely typed, which is why
tsc never flagged the missing required field; spacecat-shared#1746 commit
b3818f5a deliberately pins the mock-side 400 so the bug surfaces. Unit
tests updated to assert the type on the PATCH body.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@rainer-friederich
rainer-friederich merged commit d3d823c into main Jun 29, 2026
10 checks passed
@rainer-friederich
rainer-friederich deleted the feat/1745 branch June 29, 2026 12:32
solaris007 pushed a commit that referenced this pull request Jun 29, 2026
## [@adobe/spacecat-shared-project-engine-client-v1.3.2](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-project-engine-client-v1.3.1...@adobe/spacecat-shared-project-engine-client-v1.3.2) (2026-06-29)

### Bug Fixes

* **project-engine-client, user-manager-client:** round-trip created+published market + live-fidelity sweep ([#1745](#1745)) ([#1746](#1746)) ([d3d823c](d3d823c)), closes [#1742](#1742)
@solaris007

Copy link
Copy Markdown
Member

🎉 This PR is included in version @adobe/spacecat-shared-project-engine-client-v1.3.2 🎉

The release is available on:

Your semantic-release bot 📦🚀

solaris007 pushed a commit that referenced this pull request Jun 29, 2026
## [@adobe/spacecat-shared-user-manager-client-v1.3.1](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-user-manager-client-v1.3.0...@adobe/spacecat-shared-user-manager-client-v1.3.1) (2026-06-29)

### Bug Fixes

* **project-engine-client, user-manager-client:** round-trip created+published market + live-fidelity sweep ([#1745](#1745)) ([#1746](#1746)) ([d3d823c](d3d823c)), closes [#1742](#1742)
@solaris007

Copy link
Copy Markdown
Member

🎉 This PR is included in version @adobe/spacecat-shared-user-manager-client-v1.3.1 🎉

The release is available on:

Your semantic-release bot 📦🚀

rainer-friederich added a commit to adobe/spacecat-api-service that referenced this pull request Jun 29, 2026
…#2721)

<!-- mysticat-pr-skill -->

## 1. Abstract
Adds the required `type: 'ai'` field to the serenity brand-alias re-sync
`updateProject` PATCH body so the request validates against the Semrush
Project Engine gateway.

## 2. Reasoning
The brand-alias re-sync in `src/support/serenity/brand-aliases.js`
called `transport.updateProject(...)` with a body that omitted the
`type` field, which is `required` on `ProjectUpdateRequest`. A live
sweep (captured 2026-06-29) confirmed that a typeless `PATCH
/v1/workspaces/{id}/projects/{pid}` returns `400` (`Key:
'ProjectUpdateRequest.Type' Error:Field validation for 'Type' failed on
the 'required' tag`), while the same PATCH with `type: 'ai'` returns
`200`. As a result, any brand-alias change that actually drifts a
project's `brand_names` would fail in production and silently no-op the
project-level alias sync. The gap went uncaught because no IT exercised
the alias-resync PATCH path against the PE mock, even though the mock
already 400s a typeless PATCH.

## 3. High-level overview of the changes
- Before: the `brand_names` re-sync branch sent `{ brand_name_display?,
brand_names }` — no `type` — so a drifted update 400'd against the live
gateway and the alias sync silently failed.
- After: the PATCH body carries `type: 'ai'` (matching the literal used
at project create-time in `handlers/markets.js` and
`handlers/markets-subworkspace.js`; AIO projects are always type `ai`),
so the request validates and the drifted `brand_names` update succeeds.
- Behaviour is otherwise unchanged: the PATCH still fires only when the
live `brand_names` set has drifted from the desired set; the benchmark
`brand_aliases` PUT path is untouched.
- The three exact-body assertions in the unit test now pin `type: 'ai'`,
so a future regression that drops the field is caught locally.

## 4. Required information
- Jira / issue:
#2720
- Other: adobe/spacecat-shared#1746

## 5. Affected / used mysticat-workspace projects
- spacecat-shared (contract): the PE Counterfact mock enforces the
`type` requirement at the request-validation seam; spacecat-shared PR
#1746 pins the typeless-PATCH 400 as an e2e. This change makes serenity
conform to that contract.

## 7. Test plan
- (a) Local: ran the `brand-aliases` unit suite, which now asserts
`type: 'ai'` on all three `updateProject` body expectations; the
serenity `// @ts-check` type-check gate also covers the touched file. No
live e2e was run this session — the production defect was identified
from the 2026-06-29 live Semrush sweep referenced in the issue.
- (b) dev / stage / prod: after deploy, trigger a brand-alias change
that drifts a serenity-mode brand's `brand_names` (display name or alias
edit) and confirm the `PATCH /v1/workspaces/{id}/projects/{pid}` returns
`200` rather than `400`, and that the project's
`settings.ai.brand_names` reflects the new set.

## 8. Deployment & merge order
- adobe/spacecat-shared#1746 (related): pins the
mock-level typeless-PATCH 400 guard upstream. Independent of this PR —
no hard merge/deploy ordering required; the two together guard the
defect at both the mock layer and the serenity call site.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
rainer-friederich added a commit to adobe/spacecat-api-service that referenced this pull request Jun 29, 2026
…round-trip unconditionally

adobe/spacecat-shared#1746 merged and released the round-trip mock:
@adobe/spacecat-shared-project-engine-client 1.3.1 -> 1.3.2 and
@adobe/spacecat-shared-user-manager-client 1.3.0 -> 1.3.1. The harness
derives the mock image tag from the installed client version, so the
serenity IT now boots the published round-trip mocks.

The created+published market round-trip is now the pinned contract, so the
read-back / prompt-lifecycle block drops its capability probe and runs
unconditionally — a regression in the mock or transport fails loudly rather
than silently skipping. Full serenity IT: 26 passing (was 22 + 4 pending)
against the published 1.3.2 / 1.3.1 images.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-reviewed Reviewed by AI complexity:high High complexity PR released

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants