Skip to content

project-engine-client mock: AIO tag ids embed %3A (name-derived) and don't round-trip through URLs — blocks nested children-drill / PATCH-by-id IT #1760

Description

@rainer-friederich

Summary

The @adobe/spacecat-shared-project-engine-client Counterfact mock derives an AIO tag's id from its name — tag-${encodeURIComponent(name)} (see mock/tag-id.js). For a category:<NAME> tag that id therefore embeds a literal %3A (the encoded colon), e.g. tag-category%3AFootwear.

That id round-trips faithfully through a JSON body, but not through a URL (query or path): the %3A is decoded back to : in transit, so the value a consumer sends no longer matches the stored id. As a result, the two id-in-URL nested-tag operations cannot be exercised against the mock:

  • GET /v2/workspaces/{id}/projects/{project_id}/aio/tags?parent_id=<childId's parent> — drilling a parent's children by id returns empty (the decoded parent_id query value ≠ the stored id).
  • PATCH /v2/workspaces/{id}/projects/{project_id}/aio/tags/{tag_id} — a re-parent/rename of an existing category:* tag 404s (the decoded tag_id path segment ≠ the stored id).

This is mock-only: real Semrush uses opaque UUID tag ids (no reserved characters), so both operations work end-to-end there. Verified with a live prod probe on 2026-07-01 (adobe-hackathon.semrush.com, workspace 0a496c87…): direct POST parent_id nests, GET ?parent_id=<uuid> returns the child with a path[] breadcrumb, and PATCH /aio/tags/{uuid} renames/re-parents → 200.

Impact

The nested-tag support added in #1758 is partially untestable against the mock. The spacecat-api-service consumer ( adobe/spacecat-api-service#2737 ) can only assert the JSON-body paths (nested create + derived children_count) against the mock; the children-drill-by-id and a successful PATCH-by-id are validated against live Semrush instead, and their IT assertions are omitted with a note.

Proposed fix

Make the mock's tag id opaque and URL-safe — a value with no reserved characters — so a stored id survives a URL round-trip:

  • e.g. a short hash of the name (tag-<hex-digest>), or a UUID, instead of tag-${encodeURIComponent(name)}.
  • Idempotency by name must be preserved (a repeated POST of the same name still reuses the id, as today), so a deterministic hash of the name is a natural fit.
  • Note the documented trade-off already in the tags.js header: because ids are name-derived, a rename leaves the id keyed to the old name. A hash keeps that same property; it only removes the reserved-character problem. (If desired, this is also an opportunity to model opaque ids that stay stable across rename — but that is a larger change and not required to close this issue.)

Acceptance

With URL-safe ids, a consumer IT can assert, entirely against the mock:

  • GET /aio/tags?parent_id=<parentId> returns the child, with children_count and a path[] breadcrumb.
  • PATCH /aio/tags/{tag_id} on an existing tag returns 200 with the updated name / parent_id.

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions