---
name: fluent-test-data
description: Discover retailer configuration and generate valid test entities dynamically via GraphQL. No hardcoded refs — everything is queried from the live environment. Triggers on "create test order", "test data", "create test entity", "discover products", "discover locations".
user-invocable: true
allowed-tools: Bash, Read, Write, Edit, Glob, Grep
argument-hint: [entity-type] [--type HD|CC|SFS] [--count N]
cursor-tier: auto
---

# Dynamic Test Data Generator

Discover retailer configuration (locations, products, catalogues, networks, inventory, customers) via live GraphQL queries and generate valid entity creation payloads. Everything is queried — nothing is hardcoded.

**Knowledge dependency:** For the full `createOrder` input schema, mutation patterns (HD, CC, MULTI, mixed), fulfilmentChoice wiring, and common mistakes, see `knowledge/platform/create-order-reference.md`.

## Pre-flight: Feedback Check

Before starting, check for past learnings from this skill:

1. Check if `accounts/<PROFILE>/feedback/fluent-test-data.jsonl` exists
2. If yes, read last 20 records
3. Filter to: FAILURE or USER_CORRECTED outcomes, confidence = CONFIRMED or PROMOTED
4. Extract top 3 relevant learnings — use as internal guidance during execution
5. Do NOT show raw feedback to the user — silently adjust approach based on past issues

## Ownership Boundary

This skill owns environment discovery and entity payload generation.

Canonical MCP extension tool syntax/limits are owned by `/fluent-mcp-tools`.

## When to Use

- Before running E2E tests — discover what's available in the target environment
- Creating test orders, fulfilments, or other entities with valid refs
- Exploring retailer configuration (what locations exist, what products are in stock)
- Generating bulk test data for load/stress testing

## Core Principle

**Never hardcode entity refs, product SKUs, location refs, or addresses.** Always discover them from the live environment via GraphQL. Different retailers have different catalogues, locations, and products — hardcoded values will fail.

## Pre-flight: GraphQL Validation Gate (MANDATORY)

See `knowledge/platform/graphql-preflight.md`.

Before executing ANY query or mutation constructed by the LLM:

1. **Validate** via `graphql.validate({ query: "<constructed-query>" })` — check the `valid` field (NOT `ok`)
2. **Or build** via `graphql.buildQuery({ root: "<entity>", fields: [...] })` which returns a schema-validated query
3. If `valid: false`, read `errors` and fix before executing

This applies to: discovery queries (Phase 1), entity creation mutations (Phase 2), and ref collision checks. One invalid field in a Fluent query nullifies the entire response silently.

**If MCP is not connected:** Skip validation with a warning, but document the unvalidated queries for post-hoc review.

## Phase 1: Environment Discovery

Run these queries in sequence. Each subsequent query uses refs discovered in the previous step.

### Step 1 — Retailer Context

```graphql
{
  me {
    id
    username
    primaryRetailer { id ref tradingName }
  }
}
```

Captures: `retailerId`, `retailerRef`. If `primaryRetailer` is null, use `FLUENT_RETAILER_ID` from config.

### Step 1B — Retailer Scope Safety Gate (mandatory)

Before selecting any location, catalogue, product, or customer ref, verify scope:

1. **Prefer retailer-scoped query args when schema supports them.**
   - Use `graphql.introspect` to check whether fields accept retailer-scoped filters.
2. **If schema does not support retailer filters**, treat unfiltered results as potentially cross-retailer.
3. **Validate candidate refs in context of target retailer** before use:
   - Confirm the candidate is used by entities already created for the target retailer (for example, recent orders/fulfilments).
   - If uncertain, pick another candidate and re-check.

If this safety gate is skipped, the order may be created but downstream fulfilment creation can fail because refs belong to a different retailer scope.

### Step 2 — Locations (warehouses and stores)

```graphql
{
  locations(first: 20) {
    edges {
      node {
        id
        ref
        name
        type
        status
        primaryAddress { street city state postcode country }
      }
    }
  }
}
```

Filter results by status = `ACTIVE`. Group by type:
- `WAREHOUSE` — for HD (home delivery) sourcing
- `STORE` — for CC (click & collect) or SFS (ship from store)

Pick the **first active warehouse** for HD tests. Pick the **first active store** for CC tests.

### Step 3 — Networks

```graphql
{
  networks(first: 20) {
    edges {
      node {
        id
        ref
        type
        status
      }
    }
  }
}
```

Look for:
- HD network (type contains `HD` or ref contains `HD`)
- CC network (type contains `CC` or ref contains `CC`)

### Step 4 — Product Catalogues

```graphql
{
  productCatalogues(first: 10) {
    edges {
      node {
        id
        ref
        type
        status
      }
    }
  }
}
```

Pick the **first active product catalogue** (usually ref like `PC:MASTER:*` or similar).

### Step 5 — Products with inventory

```graphql
{
  standardProducts(first: 10, catalogue: { ref: "<CATALOGUE_REF>" }) {
    edges {
      node {
        id
        ref
        name
        status
        variants(first: 5) {
          edges {
            node {
              id
              ref
              name
              status
            }
          }
        }
      }
    }
  }
}
```

If no `standardProducts`, try `variantProducts` directly:

```graphql
{
  variantProducts(first: 10, catalogue: { ref: "<CATALOGUE_REF>" }) {
    edges {
      node {
        id
        ref
        name
        status
      }
    }
  }
}
```

Pick the **first active product**. Note its `ref` — this becomes the `productRef` in order items.

### Step 6 — Inventory Positions (verify stock)

```graphql
{
  inventoryPositions(first: 5, ref: ["<PRODUCT_REF>"], locationRef: "<LOCATION_REF>") {
    edges {
      node {
        id
        ref
        type
        status
        onHand
        quantity {
          ... on Count { value }
        }
      }
    }
  }
}
```

If onHand > 0 at the target location, the product is in stock. If not, try another product or location.

### Step 7 — Virtual Catalogues

```graphql
{
  virtualCatalogues(first: 5) {
    edges {
      node {
        id
        ref
        type
        status
      }
    }
  }
}
```

### Step 8 — Existing Customers

```graphql
{
  customers(first: 5) {
    edges {
      node {
        id
        ref
        firstName
        lastName
        primaryEmail
      }
    }
  }
}
```

Pick the **first customer**. If none exist, create one (see Entity Creation below).

### Step 9 — Settings (optional — for consignment prefix, etc.)

```graphql
{
  settings(first: 50, context: "RETAILER") {
    edges {
      node {
        id
        name
        value
        context
      }
    }
  }
}
```

Look for settings related to consignment prefix, fulfilment routing, etc.

## Phase 2: Build Discovery Summary

After running all queries, compile a summary:

```
=== Environment Discovery ===
Retailer: <ref> (ID: <id>)

Locations:
  Warehouses: <ref1> (<name1>), <ref2> (<name2>)
  Stores: <ref3> (<name3>), <ref4> (<name4>)

Networks:
  HD: <network_ref>
  CC: <network_ref>

Product Catalogue: <catalogue_ref>

Products (in stock):
  <product_ref> at <location_ref> (onHand: <N>)

Virtual Catalogue: <vc_ref>

Customer: <customer_id> (<name>)

Settings:
  consignmentPrefix: <value> (or default "A_")
```

This summary is the input to entity creation. Every ref comes from live queries — nothing invented.

## Phase 3: Entity Creation

**Full createOrder mutation reference:** `knowledge/platform/create-order-reference.md` — complete type tree, all patterns (HD, CC, split shipment, financials), input-vs-return field differences, and gotchas (wrapper IDs, fulfilmentChoice singular vs plural, AttributeInput.value as Json scalar).

### Create Test Order (HD)

Use discovered values to build the mutation. The `<TIMESTAMP>` should be generated as `YYYYMMDD_HHmmss` at execution time.

```graphql
mutation($input: CreateOrderInput!) {
  createOrder(input: $input) {
    id
    ref
    status
  }
}
```

Variables — all values from discovery:

```json
{
  "input": {
    "ref": "E2E_HD_<TIMESTAMP>",
    "type": "MULTI",
    "retailer": { "id": "<discovered.retailerId>" },
    "customer": { "id": "<discovered.customerId>" },
    "items": [
      {
        "ref": "<discovered.productRef>",
        "quantity": 1,
        "productRef": "<discovered.productRef>",
        "productCatalogueRef": "<discovered.catalogueRef>",
        "fulfilmentChoiceRef": "E2E_HD_<TIMESTAMP>-FC-HD"
      }
    ],
    "fulfilmentChoice": {
      "ref": "E2E_HD_<TIMESTAMP>-FC-HD",
      "type": "HD",
      "deliveryType": "STANDARD",
      "deliveryAddress": {
        "ref": "E2E_HD_<TIMESTAMP>-ADDR",
        "name": "<discovered.location.primaryAddress.name or 'Test Delivery'>",
        "street": "<discovered.location.primaryAddress.street or '1 Discovery Lane'>",
        "city": "<discovered.location.primaryAddress.city or 'Sydney'>",
        "postcode": "<discovered.location.primaryAddress.postcode or '2000'>",
        "country": "<discovered.location.primaryAddress.country or 'AU'>"
      },
      "attributes": [
        { "name": "sourcingLocationRef", "type": "STRING", "value": "<discovered.warehouseRef>" },
        { "name": "consignmentPrefix", "type": "STRING", "value": "<discovered.consignmentPrefix or 'A_'>" }
      ]
    }
  }
}
```

### Create Test Order (CC — Click & Collect)

Same as HD, except:
- `fulfilmentChoice.type` = `"CC"`
- `deliveryType` = `"NONE"` (customer picks up)
- `pickupLocationRef` attribute instead of delivery address
- Use a `STORE` location from discovery

```json
{
  "input": {
    "ref": "E2E_CC_<TIMESTAMP>",
    "type": "MULTI",
    "retailer": { "id": "<discovered.retailerId>" },
    "customer": { "id": "<discovered.customerId>" },
    "items": [
      {
        "ref": "<discovered.productRef>",
        "quantity": 1,
        "productRef": "<discovered.productRef>",
        "productCatalogueRef": "<discovered.catalogueRef>",
        "fulfilmentChoiceRef": "E2E_CC_<TIMESTAMP>-FC-CC"
      }
    ],
    "fulfilmentChoice": {
      "ref": "E2E_CC_<TIMESTAMP>-FC-CC",
      "type": "CC",
      "deliveryType": "NONE",
      "pickupLocationRef": "<discovered.storeRef>",
      "attributes": [
        { "name": "sourcingLocationRef", "type": "STRING", "value": "<discovered.storeRef>" },
        { "name": "consignmentPrefix", "type": "STRING", "value": "<discovered.consignmentPrefix or 'A_'>" }
      ]
    }
  }
}
```

### Create Test Order (ORDER::MULTI — explicit FC routing inputs)

Use this when testing `ORDER::MULTI` where downstream FC routing requires fulfilment-choice attributes:

```json
{
  "input": {
    "ref": "E2E_MULTI_<TIMESTAMP>",
    "type": "MULTI",
    "retailer": { "id": "<discovered.retailerId>" },
    "customer": { "id": "<discovered.customerId>" },
    "items": [
      {
        "ref": "<discovered.productRef>",
        "quantity": 1,
        "productRef": "<discovered.productRef>",
        "productCatalogueRef": "<discovered.catalogueRef>",
        "fulfilmentChoiceRef": "E2E_MULTI_<TIMESTAMP>-FC-HD"
      }
    ],
    "fulfilmentChoice": {
      "ref": "E2E_MULTI_<TIMESTAMP>-FC-HD",
      "type": "HD",
      "deliveryType": "STANDARD",
      "deliveryAddress": {
        "ref": "E2E_MULTI_<TIMESTAMP>-ADDR",
        "name": "Test Delivery",
        "street": "1 Discovery Lane",
        "city": "Sydney",
        "postcode": "2000",
        "country": "AU"
      },
      "attributes": [
        { "name": "sourcingLocationRef", "type": "STRING", "value": "<discovered.warehouseRef>" },
        { "name": "consignmentPrefix", "type": "STRING", "value": "<discovered.consignmentPrefix or 'A_'>" }
      ]
    }
  }
}
```

For `ORDER::MULTI`, `sourcingLocationRef` is a critical input for downstream fulfilment creation rules.

### Create Customer (if none found)

```graphql
mutation($input: CreateCustomerInput!) {
  createCustomer(input: $input) {
    id
    username
    firstName
    lastName
    primaryEmail
  }
}
```

Variables:
```json
{
  "input": {
    "username": "e2e-test-<TIMESTAMP>",
    "firstName": "E2E",
    "lastName": "Test",
    "primaryEmail": "e2e-test@example.com",
    "promotionOptIn": false,
    "retailer": { "id": "<discovered.retailerId>" }
  }
}
```

## Phase 4: Bulk Creation

For creating multiple test entities:

1. Run discovery once (Phase 1-2)
2. For each entity, generate a unique ref with incrementing counter: `E2E_HD_<TIMESTAMP>_001`, `_002`, etc.
3. Use `graphql.batchMutate` for up to 50 entities per batch:

```json
{
  "mutation": "createOrder",
  "inputs": [
    { "ref": "E2E_HD_<TS>_001", ... },
    { "ref": "E2E_HD_<TS>_002", ... }
  ],
  "returnFields": ["id", "ref", "status"]
}
```

## Schema Constraints

These constraints are enforced by the GraphQL schema. Violating them produces cryptic errors.

### Customer Creation
- **NO `ref` field** — `username` is the identifier, not `ref`
- **`promotionOptIn` (Boolean!) is required** — use `false` for test customers
- **`firstName` and `lastName` are optional** (despite appearing essential)
- **`username` must be unique** — append timestamps: `e2e-test-<YYYYMMDD_HHmmss>`

### Order Creation
- **`retailer.id` must be a String**: `{ "id": "2" }` not `{ "id": 2 }`
- **`customer.id` must be a String** — use the ID from discovery or customer creation
- **`productCatalogueRef`** must match an existing catalogue's exact `ref` — discover it, don't guess
- **`fulfilmentChoice.ref`** must be unique per order — generate from order ref

### Settings Queries (used in discovery)
- **`context` is a plain String**: `context: "RETAILER"` not `context: { contextType: "RETAILER" }`
- **Pagination** requires `query($cursor: String)` variable declaration with `variables: { cursor: null }`

### Ref Collision Prevention
Before creating any entity, check if the ref already exists:
```graphql
{ orders(first: 1, ref: ["<proposed_ref>"]) { edges { node { id ref status } } } }
```
If edges is non-empty, append a counter or new timestamp.

## Troubleshooting Discovery

| Problem | Cause | Fix |
|---------|-------|-----|
| No locations found | Retailer has no locations configured | Check `locations` with no filter, or create via `/fluent-retailer-config` |
| No products found | Wrong catalogue ref or empty catalogue | Try other catalogues, check `productCatalogues` for all options |
| Zero inventory | Product exists but no stock at location | Check other locations, or create inventory position via `/fluent-retailer-config` |
| No customers | Retailer has no customer entities | Create one using the mutation above (remember: no `ref`, need `promotionOptIn`) |
| Schema error on mutation | Fluent version difference or custom schema | Use `graphql.introspect` to check exact input fields |
| `productCatalogueRef` rejected | Catalogue might use `ref` format `PC:NAME:VERSION` | Query catalogues and use exact ref format returned |
| Customer creation fails with "ref" error | `ref` field doesn't exist on `CreateCustomerInput` | Remove `ref`, use `username` as the identifier |
| Customer creation fails with required field | Missing `promotionOptIn` | Add `"promotionOptIn": false` to input |
| Order reaches ON_VALIDATION/IN_PROGRESS but no fulfilment created | Discovered refs may be from a different retailer scope | Re-run Step 1B scope checks; replace `productCatalogueRef` and `sourcingLocationRef` with retailer-valid refs |

## Tips

- **Run discovery fresh each time** — Locations, products, and inventory can change between sessions
- **Prefer products with inventory** — Orders for out-of-stock products may fail allocation
- **Use warehouse addresses for delivery** — When no other address is available, use the sourcing location's address as a fallback
- **Check workflow type** — Different retailers may use `HD`, `CC`, `SFS`, or custom subtypes. Query `workflow.transitions` to discover what's configured
- **Use strict user-action contract with transitions** — Treat `userActions[].eventName` and `attributes` as authoritative; do not infer from labels or invent payload keys
- **Schema varies by account** — Always use `graphql.introspect` if a mutation fails; field names may differ
- **Cross-reference with `/fluent-retailer-config`** for entity creation constraints — it has the full validated Schema Patterns table

## Post-Execution: Feedback Capture

After completing this skill (whether success or failure), write a feedback record:

1. **Classify outcome:** `SUCCESS` (completed cleanly), `PARTIAL_SUCCESS` (completed after fixing issues), `FAILURE` (could not complete), `BLOCKED` (prereq missing), `USER_CORRECTED` (user overrode approach)
2. **Build record:** Create a `feedback-record-v1` JSON object with:
   - `skill`: `"fluent-test-data"`
   - `feature`: feature slug if applicable, null otherwise
   - `phases`: trace of each phase with PASS/FAIL/SKIP
   - `learnings`: any gotchas discovered during execution
   - `userCorrections`: any corrections the user provided
3. **Redact secrets:** Strip tokens, passwords, API keys. Keep business identifiers.
4. **Append** one JSON line to `accounts/<PROFILE>/feedback/fluent-test-data.jsonl`
5. **Update** `accounts/<PROFILE>/feedback/index.json` counters (create with `mkdir -p` if missing)

**Skip capture if:** The execution was trivially simple (e.g., user asked a question, no actual write operation happened).

## Known Pitfalls
<!-- feedback-promoted: managed section, updated by feedback loop -->

_(No promoted learnings yet.)_

<!-- end feedback-promoted -->
