Skip to content

feat(user-manager-client): stateful Counterfact mock with seed/reset + E2E (LLMO-5616)#1685

Open
byteclimber wants to merge 7 commits into
mainfrom
feat/llmo-5616-user-manager-mock
Open

feat(user-manager-client): stateful Counterfact mock with seed/reset + E2E (LLMO-5616)#1685
byteclimber wants to merge 7 commits into
mainfrom
feat/llmo-5616-user-manager-mock

Conversation

@byteclimber

Copy link
Copy Markdown
Contributor

Stacked on #1680 (foundation). Implements LLMO-5616 — a stateful mock of the Semrush User Manager API for E2E tests + local dev.

What works (17 tests, 100% coverage on src/)

  • Stateful core chain (POST reflected in later GET): workspaces create-child/get/update/delete/list, members add/list/update/delete, profile get/update, resources (+ deprecated /limits alias), service-units balance.
  • Seed + reset: starts in a known fixture state; POST /__reset returns to it; E2E suite resets in beforeEach.
  • Run: npm run mock:4010 under /enterprise/users/api, permissive auth (--no-validate-request).
  • Generated from the spec, no stub hand-editing: regeneration preserves the hand-edited handlers.

Reviewer focus — the ~13 hand-authored files (rest of .counterfact/ is generated, marked linguist-generated)

  • .counterfact/routes/_.context.ts — the store (state + seed/reset)
  • .counterfact/routes/__reset.ts
  • .counterfact/routes/v1/profile.ts, v1/workspaces.ts, v1/workspaces/{id}.ts, v1/workspaces/{id}/child.ts, v1/workspaces/{id}/members.ts, v1/workspaces/{id}/resources.ts, v1/workspaces/resources.ts, v1/workspaces/{id}/limits.ts, v1/workspaces/limits.ts, v1/workspaces/{id}/service-units/balance.ts, v2/workspaces/{id}.ts
  • src/config.js, test/mock.test.js, README "Mock" section; package.json/.gitignore/clean/root eslint ignore.

Key architecture decision (validated, differs from the original plan)

Counterfact only compiles its own routes/ tree, so mock state lives on the Context class ($.context), not src/ — an imported external store 500s. The logic is therefore validated behaviorally by the E2E suite (the 100% gate covers src/ only). The .counterfact/ tree is committed except .cache/.

Deferred (LLMO-5615)

spacecat-api-service calls no User Manager endpoints today, so wiring it to the mock is deferred to when the client lands. The base-URL env var SEMRUSH_USERS_BASE_URL is recorded in src/config.js; re-open trigger documented in the README (incl. the http-localhost-vs-https caveat).

Known caveat

Counterfact warns of ambiguous sibling wildcards ({id} vs {workspace_id}) on some non-core paths; the core chain uses {id} and routes correctly.

🤖 Generated with Claude Code

byteclimber and others added 6 commits June 15, 2026 19:24
…ion pipeline

Foundation slice for @adobe/spacecat-shared-user-manager-client, mirroring the
Project Engine foundation (LLMO-5461) applied to the larger User Manager API
(LLMO-5558). Private, types-only package: vendors the Swagger 2.0 spec
(/enterprise/users/api, 234 paths / 284 operations, 187 Auth-Data-Jwt routes)
and wires the swagger2openapi --patch -> openapi-typescript / datamodel-codegen
pipeline behind `npm run generate`. The openapi-fetch client wrapper with dual
auth (IMS bearer + API-Key) and the counterfact mock store land in follow-up PRs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Generated by `npm run generate` from the vendored spec. Committed in its own
commit and marked linguist-generated (see .gitattributes) so it collapses in
review and is excluded from diff counts.

- src/generated/types.ts: openapi-typescript 7.13.0 paths/components (all ops)
- python/serenity_user_manager/: datamodel-codegen pydantic_v2 package

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds @adobe/spacecat-shared-user-manager-client and its generation-pipeline
devDeps (openapi-typescript, swagger2openapi, counterfact) to the root lockfile
so `npm ci` stays in sync in CI.

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

Addresses mysticat-bot review: `/child` and `/status` were too generic
and would pass against any large OpenAPI types file. Replace with the
full, unambiguous path templates that only exist in this spec.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Auth-Data-Jwt appears on ~187 operations in the vendored spec but is a
vendor spec artifact — the live Adobe gateway authenticates on
Authorization: Bearer only (same finding as rainer-friederich CR2 on
Project Engine PR #1661, 2026-06-15). Sending Auth-Data-Jwt returns 401.

Update README and test label to make clear this is a spec artifact, not
the live auth contract. A generation-time overlay to strip it from
generated types follows in a later PR.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…+ E2E (LLMO-5616)

Adds a stateful mock of the Semrush User Manager API for E2E tests and local dev.

- Stateful core chain on Counterfact's Context singleton (`.counterfact/routes/_.context.ts`):
  workspaces create-child/get/update/delete/list, members, profile, resources (+ deprecated
  `/limits` alias), service-units balance. POST is reflected in a later GET.
- Seed-on-start + `POST /__reset` to return to a known fixture state between tests.
- `npm run mock` serves on :4010 under `/enterprise/users/api` with `--no-validate-request`
  (permissive auth; the spec's `Auth-Data-Jwt` is a vendor artifact).
- E2E suite (`test/mock.test.js`) boots the mock in a child process: statefulness, reset,
  negative path, committed-handler + env-var assertions. 17 tests, 100% coverage on `src/`.

Architecture note: Counterfact only compiles its own `routes/` tree, so mock state lives on
the Context class, not `src/` (verified: an imported external store 500s). The `.counterfact/`
tree is committed except `.cache/`; regeneration preserves hand-edited routes. `.gitignore`,
`clean`, and the root eslint ignore are updated accordingly.

Defers api-service wiring to LLMO-5615 (api-service calls no User Manager endpoints today);
the future client's base-URL env var `SEMRUSH_USERS_BASE_URL` is recorded in `src/config.js`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@byteclimber byteclimber requested review from MysticatBot and rainer-friederich and removed request for MysticatBot and rainer-friederich June 17, 2026 11:43
…ount, dedup nf, more coverage

- deleteMembers now returns the count of members actually removed (null only
  when the workspace is missing), mirroring the spec's "number of user units
  freed" semantics where 0 is a valid success. Previously returned {deleted:true}
  even for absent members; the DELETE handler now returns 200 + count.
- Extract the duplicated `nf()` 404-or-500 responder into _.helpers.ts (an
  underscore-prefixed, compiled-not-routed sibling of _.context.ts).
- Add E2E coverage for the { members: [...] } and bare-array body shapes,
  the missing-workspace add path, delete-member count (1 and 0), and the
  update-missing-member error path.
- Clarifying comments: clone() JSON-safety constraint, nextId/reset coupling,
  and why the test port (4099) differs from the documented default (4010).

22 passing, 100% coverage on src/.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

This PR will trigger a minor release when merged.

@rainer-friederich

Copy link
Copy Markdown
Contributor

Heads-up now that this mock stacks on the User Manager foundation branch (feat/user-manager-client), which now carries a generation-time spec-correction overlay (spec/overlays/corrections.yaml) — the User Manager analog of the Project Engine overlay. Same class of coupling I flagged on the Project Engine mock:

#1665 (comment)

What this PR already gets right (the Project Engine mock had to be fixed for both): the mock script already passes --prefix /enterprise/users/api — so the route-prefix problem doesn't bite here (Counterfact honors a Swagger-2.0 basePath but ignores an OAS3 servers[0].url; the explicit --prefix covers it under either spec) — and --no-validate-request, which makes CR1 (the Auth-Data-Jwt removal) a no-op for the mock. Nothing needed there.

The one coupling left — CR2 (workspace-status shape). After rebasing onto the updated base to pick up the overlay (spec/overlays/corrections.yaml, scripts/apply-overlay.mjs, the spec:overlay step in npm run generate, and the js-yaml devDep — this branch predates all of them), the mock's GET /v1/workspaces/{id}/status returns the wrong shape. The committed handler is:

export const GET: workspacesStatusGet = async ($) => {
  return $.response[200].random();
};

$.response[200].random() is Counterfact's spec-derived 200. Because the mock is fed the pristine spec/usermanager_swagger.yaml, that 200 is typed as an array of WorkspaceCheckResponse — so the mock returns an array, while the corrected client and the live API return a single object { status: "created" } (CR2; the transport polls status.status === 'created'). The mock and the real contract disagree, and an E2E that polls workspace status will see the mismatch.

Two ways to fix — pick one:

  • Surgical (recommended for a hand-authored stateful mock): keep feeding the swagger, but have status.ts return a single object explicitly (e.g. return { status: "created" };) rather than $.response[200].random(). CR2 is the only response-shape correction, and this route is already a committed handler, so this fully covers it without re-scaffolding.
  • Spec-consistent: feed Counterfact the overlaid build/openapi3.json (run npm run generate, which now includes spec:overlay, before boot — build/ is gitignored) and regenerate the Counterfact types so .random() yields the corrected object surface. Keep --prefix /enterprise/users/api (still required — Counterfact ignores OAS3 servers). This is the broader, future-proof option but regenerates the whole scaffold, so mind the committed stateful handlers.

Context — the overlay + corrections that introduce CR2:

#1680

Base automatically changed from feat/user-manager-client to main June 23, 2026 07:37
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