---
name: cqa-copy-test-case
description: Faithfully copy a ContextQA test case (with its full prerequisite tree, step groups, elements, test-data profiles, variables and environment bindings) from one environment to another, using only MCP primitives. This is also the completeness acceptance test for the authoring tool surface.
---

# ContextQA — Cross-Environment Test Case Copy

Copy a test case from a **source** environment to a **target** environment using only MCP
tools. There is **no `copy_test_case` server call** — you reconstruct the case from
primitives. If you can reconstruct it faithfully, the authoring surface is proven complete.
That is the real point of this skill: **copying is the test**, not the feature.

The proven server algorithm this mirrors is `DestinationSaveService.copyFromProdToDest` in
`ts-portal-server` (reference only — we never call it). This skill reproduces its recursion,
dedup and two-pass remap semantics, and **closes the gaps that reference has** (it does not
copy elements, global variables, or inline data tokens, has no cycle guard, and is not
idempotent).

## Connection model

Cross-env copy uses **two distinct MCP connections**, one per environment (qa / dev / prod).
Each connection is authenticated and pinned to its own env+tenant+workspace at login.

- **SOURCE connection** — you only ever READ from it (`export_test_case`, `list_*`, `get`).
- **TARGET connection** — you CREATE everything here.

Never assume a single connection can reach both. If you only have one connection, stop and
ask the user to connect the second environment.

## The function checklist (verify these exist and are CRUD-complete before you start)

Every tool this skill calls, with the actions it needs. `✅` = present, `⚠️` = partial,
`❌` = missing. If anything is `⚠️`/`❌`, the copy cannot be faithful — flag it.

**Extraction is a RUNBOOK over primary APIs (NOT the export manifest).**
`export_test_case` is LOSSY — its manifest reports `test_data_profiles` names as null,
derives `env_params` only from `*|token|` scans (so a case/step-level environment binding
is never seen), and its `test_case` block omits `environmentId`/`testDataId`. Use it ONLY
for a quick human overview. The authoritative per-case extraction is:

1. **Case metadata + bindings** — `get_test_case(id)`. Read `environment_id`,
   `test_data_id`, `test_data_start_index`, `test_data_end_index`, `is_data_driven`,
   `is_step_group`, `test_type`, `priority`, `tags`, `prerequisites`. (These are now
   returned by the tool — do not fall back to the manifest for them.)
2. **Steps (lossless raw)** — `manage_test_step action=list`. For each step capture the FK
   fields: `forLoopTestDataId`, `testDataProfileStepId`, `testDataFunctionId`,
   `stepGroupId`, `elementId`, `environmentId`, `naturalTextActionId`, and nested
   `testDataList[].{testDataProfileId, globalDataId, testDataType}`.
3. **Test-data profiles** — for every distinct case `test_data_id` and step
   `forLoopTestDataId`/`testDataProfileStepId`/nested `testDataProfileId`:
   `manage_test_data action=get test_data_id=<id> include_environments=true`. If
   `environments` is non-empty the profile is ENV-SCOPED (base `data` is empty) — read the
   rows per env with `manage_test_data action=get_effective test_data_id=<id>
   environment_ids=[<envId>]`.
4. **Environments** — `get_environments(page=N)` across ALL pages (paginated; `total` >>
   page size). Resolve the case-level `environment_id` and any step-level `environmentId`.
5. **Elements / screens / globals / actions / generators** — `manage_element`,
   `manage_screen`, `manage_global_variable list`, `list_natural_text_actions`,
   `list_data_generators`.

**Authoring / create (TARGET) — consolidated `manage_*`, CRUD in the new format:**
- `manage_test_case` — `list | get | create | delete | update | patch | set_prerequisites | bind_test_data` ✅.
- `manage_test_step` — `list | get | create | update | delete | bulk_delete | bulk_create | bulk_update | bulk_operation` ✅.
- `manage_element` — `list | get | get_by_name | create | update | delete | bulk_create | bulk_update | bulk_delete` ✅.
- `manage_screen` — `list | get | create | update` ✅. (No server delete endpoint.)
- `manage_test_data` — `list | get | create | update | delete | clone` ✅.
- `manage_environment` — `list | get | create | update | set_default | merge_params` ✅.
  `clone` and per-env test-data-profile attach are deferred (not needed for copy — the flow
  creates fresh profiles and merges params into the target default env). ⚠️
- `manage_global_variable` — `list | get | upsert | delete` ✅.

> `manage_test_case create` makes a **bare** case (no steps); write steps with
> `manage_test_step` afterwards. `manage_environment merge_params` is non-destructive by
> default (never overwrites an existing target key; never pass `vault` params).

## Algorithm — depth-first, leaves-first, two-pass

Maintain three dedup maps and one visiting set across the whole copy (they span the full
recursion, including nested step groups and prerequisites):

```
caseIdMap     : sourceCaseId  -> targetCaseId      # prereqs + step-group cases
stepGroupMap  : sourceCaseId  -> targetCaseId      # subset of caseIdMap that are step groups
profileMap    : sourceProfile -> targetProfile     # test-data profiles
visiting      : set(sourceCaseId)                  # CYCLE GUARD (the reference lacks this)
```

### `copyCase(sourceId)` — returns the target case id

1. **Dedup / cycle guard.** If `sourceId in caseIdMap`, return it. If `sourceId in visiting`,
   STOP — a prerequisite/step-group cycle exists; report it and abort (do not recurse).
   Add `sourceId` to `visiting`.

2. **Export source** (SOURCE conn): `export_test_case(sourceId)`. Keep the raw steps,
   the manifest, and `requires_manual_reentry`.

3. **Resolve prerequisites first (recursion).** For each `manifest.prerequisites[].id`,
   call `copyCase(prereqId)` → collect target prereq ids (leaves-first: prereqs exist
   before the parent binds them).

4. **Resolve step-group references (recursion).** For each step with `stepGroupId`, if not in
   `stepGroupMap`, `copyCase(stepGroupId)` and record `stepGroupMap[src]=tgt`. Nested step
   groups resolve naturally through the recursion.

5. **Resolve dependencies in the TARGET (create-by-name / id):**
   - **Screens** → `manage_screen action=create` (idempotent by name; reuse on duplicate).
   - **Elements** → for each `manifest.elements` name, `manage_element action=get_by_name`;
     if absent, `manage_element action=create` (or `bulk_create`) under the target screen.
     Record the new `elementId` per name.
   - **Test-data profiles** → for each `manifest.test_data_profiles`, read it on SOURCE
     (`manage_test_data action=get`) and `manage_test_data action=create` on TARGET; record
     `profileMap`. (No reuse-by-name on the server side — a fresh profile is expected.)
     **Link the profile to the case's target environment** — pass
     `environment_ids=[<targetEnvId>]` (the same env resolved for the case in step 5's
     Environment bullet). A profile that is not linked to the case's bound env is INVISIBLE
     in the step editor's "Value Source: Test Data Profile" dropdown even though the FOR_LOOP
     header still shows it. See the **environment invariant** hard rule.
   - **Global variables** → `manage_global_variable action=upsert` on TARGET for each used
     var (upsert is idempotent by name+workspace).
   - **Environment** → resolve the target environment; for each `manifest.env_params` key,
     ensure it exists via `manage_environment action=merge_params` (additive, non-destructive:
     never overwrite an existing target key; never create/overwrite `vault`-typed params —
     those are in `requires_manual_reentry`).

6. **Re-resolve catalog ids in the TARGET** (ids are tenant-specific): for every step,
   re-resolve `naturalTextActionId` via `list_natural_text_actions` (by displayName) and
   `testDataFunctionId` via `list_data_generators` (by name). A carried id from the source
   will be wrong in the target.

7. **Create the target case** (TARGET conn): `manage_test_case action=create` with the
   metadata. **Pass the remapped case-level `environment_id`** (the target env from step 5's
   Environment bullet) so the case is bound to its environment — the create action forwards it
   to the entity's `environmentId`. Skipping this leaves `environmentId=null`, the UI falls
   back to the workspace-default env, and every env-scoped profile the case uses disappears
   from the step editor (the exact symptom the environment invariant guards against). If it is
   a step group, ensure data-driven fields are cleared (`isDataDriven=false`,
   `testDataId/testDataStartIndex/testDataEndIndex=null`). Record `caseIdMap[sourceId]=targetId`;
   if step group, also `stepGroupMap`.

8. **Bind prerequisites:** `manage_test_case action=set_prerequisites` with the ordered
   target prereq ids (preserve order/position).

9. **Bind case-level test data:** if data-driven, `manage_test_case action=bind_test_data`
   with `profileMap[sourceTestDataId]` and the start/end indices.

10. **Write steps — PASS 1 (create):** build each step from the redacted raw step, applying
    the **remap table** (below), then `manage_test_step action=bulk_operation` (preserves
    `position`, resolves intra-batch `parentId`/`children` via `tempId`). Capture the
    `tempId → targetStepId` map from the response.

11. **Write steps — PASS 2 (relationships + conditionIf):** for each step, remap
    `parentId`, `preRequisiteStepId`, `testDataProfileStepId` (fallback: parent loop step),
    and re-key `testDataFunctionArgs` keys through the **global** step-id map (so nested
    step-group args resolve). Re-apply `conditionIf` on `CONDITION_IF` / `CONDITION_ELSE_IF`
    / `*IFNOT` steps (create nulls it) via `manage_test_step action=bulk_update`.

12. **Remove from `visiting`**, return `targetId`.

### Remap table (per step, before write)

| Field | Rule |
|---|---|
| `element` (name) | keep verbatim (binds by name in target WSV) |
| `elementId` | target id from step-5 element resolution, else `null` |
| `naturalTextActionId` | re-resolve by displayName on TARGET (step 6) |
| `testDataFunctionId` | re-resolve by name on TARGET (step 6) |
| `stepGroupId` | `stepGroupMap[src]` |
| `forLoopTestDataId`, `testDataProfileStepId`, case `testDataId` | `profileMap[src]` |
| `environmentId` (case-level AND step-level) | target environment id — set on the case at create (step 7) *and* on every step that carried one |
| `parentId`, `preRequisiteStepId` | `tempId`/target-step-id map (pass 2) |
| `testDataFunctionArgs` keys | re-key through the global step-id map (pass 2) |
| `action` (HTML), `event` | passthrough verbatim (already validated/redacted) |
| inline tokens `@\|..\|`, `*\|..\|*`, `$\|..\|` | keep verbatim — resolve iff the named target artifact was created in step 5 |

## Verify (the acceptance check)

After the root `copyCase` returns:
1. `export_test_case(targetId)` on TARGET.
2. **Masked deep-field diff** vs the source export: mask the remapped id fields (`id`,
   `testCaseId`, `elementId`, `stepGroupId`, `forLoopTestDataId`, `testDataProfileStepId`,
   `testDataId`, `environmentId`, `parentId`, `preRequisiteStepId`, and nested
   `testDataList[].{testDataProfileId,environmentId,globalDataId}`); assert deep equality of
   everything else (`action`, `event`, `conditionIf`, `testData*`, `position`, type sequence,
   nesting depth).
3. Assert step count equals the source's `step_count` (catches pagination/truncation).
4. **Assert the environment invariant.** `get_test_case(targetId).environment_id` is non-null
   iff the source's was; and for each profile in `profileMap.values()`,
   `manage_test_data action=get include_environments=true` lists the case's target env in its
   `environments`. A profile linked to a different env than the case (or a case with null env)
   fails this check even though the masked field-diff in step 2 passes.
5. Confirm every `requires_manual_reentry` secret is flagged (NOT copied) and ask the user to
   re-enter them in the target.

## Hard rules

- **Secrets are never copied.** Anything in `requires_manual_reentry` (passwords, auth
  headers, vault params) is surfaced for manual re-entry, never written.
- **Cycle guard is mandatory** — the reference implementation lacks it and will infinite-loop.
- **Not idempotent.** Re-running creates duplicate cases/steps/profiles. Track the created
  `targetId` and offer a "replace target" path (delete steps via `bulk_delete` then re-write)
  rather than blindly re-copying.
- **One session = one tenant.** Never try to reach two environments from a single connection.
- **Validate carried ids.** A `naturalTextActionId`/`testDataFunctionId`/`elementId` that is
  not re-resolved in the target is a silent dangling reference — re-resolve or fail loudly.
- **Environment invariant (mandatory).** The case must be bound to a target environment `E`
  (`environmentId=E`, step 7), AND every test-data profile the case references must be linked
  to that same `E` (`environment_ids=[E]`, step 5). If the case's `environmentId` is null, or a
  profile is linked to a *different* env than the case, the profile is present in the TDP list
  but **invisible inside the step editor** ("No test data profiles assigned to <env>") because
  the UI's env-scoped dropdown filters by the case's active environment. The FOR_LOOP header
  reading `forLoopTestDataId` directly will still show the profile — do not let that mask the
  break. Faithfulness rule: mirror the source — if source case `environment_id` is non-null,
  the target case's `environmentId` must be non-null and its profiles linked to it.
