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
Summary
The
@adobe/spacecat-shared-project-engine-clientCounterfact mock derives an AIO tag's id from its name —tag-${encodeURIComponent(name)}(seemock/tag-id.js). For acategory:<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
%3Ais 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 decodedparent_idquery value ≠ the stored id).PATCH /v2/workspaces/{id}/projects/{project_id}/aio/tags/{tag_id}— a re-parent/rename of an existingcategory:*tag 404s (the decodedtag_idpath 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, workspace0a496c87…): directPOST parent_idnests,GET ?parent_id=<uuid>returns the child with apath[]breadcrumb, andPATCH /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:
tag-<hex-digest>), or a UUID, instead oftag-${encodeURIComponent(name)}.POSTof the same name still reuses the id, as today), so a deterministic hash of the name is a natural fit.tags.jsheader: 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, withchildren_countand apath[]breadcrumb.PATCH /aio/tags/{tag_id}on an existing tag returns 200 with the updatedname/parent_id.References