Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CONTRIBUTORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ its **Status** is what changes.

| Mock service | Contributor | Affiliation | Status |
|---|---|---|---|
| `zendesk_support` | Yuxuan Zhang ([@reacher-z](https://github.com/reacher-z)) | Independent | staged |
<!-- | `acme_crm` | Jane Doe ([@janedoe](https://github.com/janedoe)) | Independent | staged | -->

_Opens with the first community mock. Be the first._
Expand Down
96 changes: 96 additions & 0 deletions real_replica_bench/mock_services/contrib/zendesk_support/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# Zendesk Support API mock

This directory contains an original, deterministic, offline replica of a useful
subset of the Zendesk Support ticket API. It is intended for benchmark tasks in
which an agent must triage customer-support work: search tickets, assign an
agent or group, apply tags, add public or internal comments, and move a ticket
through a valid status workflow.

The implementation is not affiliated with or endorsed by Zendesk. It contains
no Zendesk code, captured responses, logos, fonts, screenshots, or other
third-party assets. All names, accounts, tickets, and messages in the committed
seed are synthetic and use the reserved `.test` top-level domain.

## Runtime contract

- Requested port: `3117` (override with `PORT`)
- Bind address: `127.0.0.1` (override with `HOST`)
- Health check: `GET /health`
- Launcher: `node /opt/mock_services/zendesk_support/server.js`
- Agent authentication: `Authorization: Bearer $SUPPORT_API_TOKEN`
- Verifier readout: `GET /__bench/state` with
`Authorization: Bearer $MOCK_VERIFIER_TOKEN`
- Seed override: `SUPPORT_MOCK_SEED=/absolute/path/to/seed.json`

Standalone development defaults are `local-agent-token` and `bench-verifier`.
A benchmark task should always inject different task-local values and expose
only `SUPPORT_API_TOKEN` to the agent.

The server has no package dependencies and makes no network requests. Every
mutation advances a seed-owned logical clock by exactly one minute. IDs come
from committed counters, so the same seed plus the same sequence of requests
produces byte-for-byte identical state and audit entries.

## Agent API

The mock implements JSON endpoints under `/api/v2/`:

| Method and path | Behavior |
|---|---|
| `GET /tickets.json` | List, filter, sort, and paginate tickets |
| `GET /tickets/:id.json` | Read one ticket and its comments |
| `POST /tickets.json` | Create a ticket and initial comment |
| `PUT /tickets/:id.json` | Assign, classify, tag, comment, or change status |
| `GET /search.json?query=...` | Search subject, description, tags, and custom fields |
| `GET /users.json` | List synthetic users and roles |
| `GET /organizations.json` | List synthetic customer organizations |
| `GET /groups.json` | List support groups |
| `GET /ticket_fields.json` | Discover closed-set custom-field definitions |

Ticket status, priority, type, assignee role, group, organization, tags, custom
fields, and status transitions are validated on the server. In particular,
`closed` tickets are immutable, and a ticket cannot jump directly from `open`
to `closed`. These rules apply to raw HTTP clients as well as future UIs.

Example:

```bash
curl -sS http://127.0.0.1:3117/api/v2/tickets.json?status=new \
-H 'Authorization: Bearer local-agent-token'

curl -sS -X PUT http://127.0.0.1:3117/api/v2/tickets/1005.json \
-H 'Authorization: Bearer local-agent-token' \
-H 'Content-Type: application/json' \
--data '{"ticket":{"assignee_id":103,"status":"open","comment":{"body":"I am preparing the receipt.","public":true}}}'
```

## Verifier and seed shape

`GET /__bench/state` returns the full authoritative state, including users,
organizations, groups, custom-field definitions, tickets, comments, the logical
clock and next-ID counters, and an append-only mutation audit. Verifiers should
score this readout rather than agent output.

Verifier-only control routes are:

- `GET /__bench/audit`
- `POST /__bench/reset`
- `POST /__bench/seed` with either a state object or `{ "state": ... }`

The committed seed is [`seeds/default.json`](seeds/default.json). A task-specific
seed must preserve the same top-level arrays and metadata. Seed loading checks
duplicate IDs, enum values, required text, and all ticket/comment references
before the server accepts the state.

## Local validation

```bash
cd real_replica_bench/mock_services/contrib/zendesk_support
npm test
PORT=3117 SUPPORT_API_TOKEN=local-agent-token \
MOCK_VERIFIER_TOKEN=bench-verifier npm start
```

The integration tests exercise authentication separation, filtering and search,
deterministic create/update behavior, invalid references, status transitions,
closed-ticket immutability, audit evidence, and exact reset behavior.
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"name": "realreplicabench-zendesk-support-mock",
"version": "0.1.0",
"private": true,
"description": "Deterministic offline support-ticket API mock for RealReplicaBench",
"scripts": {
"start": "node server.js",
"test": "node --test test.js"
},
"engines": {
"node": ">=20"
},
"license": "Apache-2.0"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
{
"meta": {
"seed_version": "1",
"clock": "2026-08-01T09:00:00.000Z",
"next_ids": {
"ticket": 1006,
"comment": 5010
}
},
"users": [
{
"id": 101,
"name": "Mina Patel",
"email": "mina.patel@example.test",
"role": "admin",
"active": true
},
{
"id": 102,
"name": "Jon Bell",
"email": "jon.bell@example.test",
"role": "agent",
"active": true
},
{
"id": 103,
"name": "Rosa Kim",
"email": "rosa.kim@example.test",
"role": "agent",
"active": true
},
{
"id": 201,
"name": "Avery Chen",
"email": "avery.chen@example.test",
"role": "end-user",
"active": true,
"organization_id": 301
},
{
"id": 202,
"name": "Sam Okafor",
"email": "sam.okafor@example.test",
"role": "end-user",
"active": true,
"organization_id": 302
}
],
"organizations": [
{
"id": 301,
"name": "Northwind Bikes",
"details": "Synthetic retail account",
"tags": ["retail", "priority-account"]
},
{
"id": 302,
"name": "Cedar Labs",
"details": "Synthetic research account",
"tags": ["research"]
}
],
"groups": [
{
"id": 401,
"name": "Order Support"
},
{
"id": 402,
"name": "Billing Escalations"
}
],
"custom_field_definitions": [
{
"id": 9001,
"name": "order_id",
"type": "order-id"
},
{
"id": 9002,
"name": "refund_reason",
"type": "enum",
"options": ["damaged", "late-delivery", "missing-item", "other"]
},
{
"id": 9003,
"name": "refund_amount",
"type": "amount"
}
],
"tickets": [
{
"id": 1001,
"subject": "Damaged rear light in order ORD-41027",
"description": "The rear light arrived with a cracked lens.",
"status": "open",
"priority": "high",
"type": "incident",
"requester_id": 201,
"submitter_id": 201,
"assignee_id": 102,
"group_id": 401,
"organization_id": 301,
"tags": ["damage", "order"],
"custom_fields": [
{"id": 9001, "value": "ORD-41027"},
{"id": 9002, "value": "damaged"},
{"id": 9003, "value": 39.5}
],
"via": {"channel": "web"},
"created_at": "2026-08-01T08:00:00.000Z",
"updated_at": "2026-08-01T08:20:00.000Z"
},
{
"id": 1002,
"subject": "Invoice contains duplicate charge",
"description": "July invoice appears to bill the workspace twice.",
"status": "pending",
"priority": "urgent",
"type": "problem",
"requester_id": 202,
"submitter_id": 202,
"assignee_id": 103,
"group_id": 402,
"organization_id": 302,
"tags": ["billing", "duplicate-charge"],
"custom_fields": [],
"via": {"channel": "email"},
"created_at": "2026-07-31T15:00:00.000Z",
"updated_at": "2026-08-01T07:45:00.000Z"
},
{
"id": 1003,
"subject": "Tracking page has not updated",
"description": "Shipment status has remained unchanged for three days.",
"status": "hold",
"priority": "normal",
"type": "question",
"requester_id": 201,
"submitter_id": 201,
"assignee_id": 102,
"group_id": 401,
"organization_id": 301,
"tags": ["delivery", "tracking"],
"custom_fields": [
{"id": 9001, "value": "ORD-40988"},
{"id": 9002, "value": "late-delivery"}
],
"via": {"channel": "web"},
"created_at": "2026-07-29T10:00:00.000Z",
"updated_at": "2026-07-31T09:10:00.000Z"
},
{
"id": 1004,
"subject": "Update shipping address before dispatch",
"description": "Please use the address already saved on the account.",
"status": "solved",
"priority": "normal",
"type": "task",
"requester_id": 201,
"submitter_id": 201,
"assignee_id": 103,
"group_id": 401,
"organization_id": 301,
"tags": ["address-change", "order"],
"custom_fields": [
{"id": 9001, "value": "ORD-41002"}
],
"via": {"channel": "email"},
"created_at": "2026-07-30T11:00:00.000Z",
"updated_at": "2026-07-30T12:30:00.000Z"
},
{
"id": 1005,
"subject": "Request a copy of the receipt",
"description": "A PDF receipt is needed for expense reporting.",
"status": "new",
"priority": "low",
"type": "question",
"requester_id": 202,
"submitter_id": 202,
"assignee_id": null,
"group_id": 402,
"organization_id": 302,
"tags": ["billing", "receipt"],
"custom_fields": [],
"via": {"channel": "web"},
"created_at": "2026-08-01T08:45:00.000Z",
"updated_at": "2026-08-01T08:45:00.000Z"
}
],
"comments": [
{
"id": 5001,
"ticket_id": 1001,
"author_id": 201,
"body": "The rear light arrived with a cracked lens.",
"public": true,
"created_at": "2026-08-01T08:00:00.000Z"
},
{
"id": 5002,
"ticket_id": 1001,
"author_id": 102,
"body": "Thanks for the photo description. I am checking replacement stock.",
"public": true,
"created_at": "2026-08-01T08:20:00.000Z"
},
{
"id": 5003,
"ticket_id": 1002,
"author_id": 202,
"body": "July invoice appears to bill the workspace twice.",
"public": true,
"created_at": "2026-07-31T15:00:00.000Z"
},
{
"id": 5004,
"ticket_id": 1002,
"author_id": 103,
"body": "Finance is comparing the two transaction references.",
"public": false,
"created_at": "2026-08-01T07:45:00.000Z"
},
{
"id": 5005,
"ticket_id": 1003,
"author_id": 201,
"body": "Shipment status has remained unchanged for three days.",
"public": true,
"created_at": "2026-07-29T10:00:00.000Z"
},
{
"id": 5006,
"ticket_id": 1003,
"author_id": 102,
"body": "The carrier trace is open; holding until it responds.",
"public": true,
"created_at": "2026-07-31T09:10:00.000Z"
},
{
"id": 5007,
"ticket_id": 1004,
"author_id": 201,
"body": "Please use the address already saved on the account.",
"public": true,
"created_at": "2026-07-30T11:00:00.000Z"
},
{
"id": 5008,
"ticket_id": 1004,
"author_id": 103,
"body": "The dispatch address was updated before label creation.",
"public": true,
"created_at": "2026-07-30T12:30:00.000Z"
},
{
"id": 5009,
"ticket_id": 1005,
"author_id": 202,
"body": "A PDF receipt is needed for expense reporting.",
"public": true,
"created_at": "2026-08-01T08:45:00.000Z"
}
],
"audit": []
}
Loading