> ## Documentation Index
> Fetch the complete documentation index at: https://docs.stateset.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Integration Test Plan

> Build a test plan from isolated commerce assertions through restart recovery, hosted API checks, and observed cross-service outcomes.

Use a separate test for each boundary your integration crosses. A local engine test, an
authenticated HTTP request, and a completed provider operation establish different things.
This guide helps you choose the assertions and includes a complete payment-replay test that
runs across two Node processes.

## Start with the behavior you need to prove

| Question | Test boundary | Evidence to assert |
| - | - | - |
| Does our input mapping preserve money and IDs? | Application code | Exact expected fields and decimal strings |
| Does a rejected order leave stock unchanged? | Embedded engine | Error code, order count, and stock quantities |
| Does a retry reuse the original payment after restart? | Persistent engine database | Same payment ID and one stored payment across processes |
| Does this key reach the correct service? | Hosted API | Actual HTTP status and response fields for that service |
| Did an asynchronous workflow finish? | Worker and state store | Terminal operation state plus the resulting records |
| Did a provider perform the external action? | Provider integration | Provider result correlated with the local operation |

Start with the [integration test suite](/guides/api-testing) and
[error-handling tests](/guides/error-handling-best-practices). They cover stock reservation,
rejection without partial writes, cancellation, payment replay, and unexpected-error propagation.
Those tests need no hosted credentials.

## Test replay after a process restart

An in-process replay test can miss accidental dependence on application memory. This program
starts one process to create a payment, waits for it to exit, and starts another to replay the
original input against the same database. Each test run gets a new temporary directory.

Use Node.js **20.20.0+**, npm **10+**, and **`@stateset/embedded` 1.35.1**:

```bash theme={null}
mkdir stateset-restart-tests
cd stateset-restart-tests
npm init -y
npm install --save-dev @stateset/embedded@1.35.1
```

Save this as `restart.test.mjs`:

```javascript theme={null}
import test from 'node:test';
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';

// Both workers run this same code, with their action and input supplied over stdin.
const worker = `
import { readFileSync } from 'node:fs';
import { Commerce } from '@stateset/embedded';
const request = JSON.parse(readFileSync(0, 'utf8'));
const commerce = new Commerce(request.database);
let input = request.input;
if (request.action === 'create') {
  const customer = await commerce.customers.create({
    email: 'ada@example.com', firstName: 'Ada', lastName: 'Lovelace',
  });
  const order = await commerce.orders.createExact({
    customerId: customer.id,
    currency: 'USD',
    items: [{ sku: 'MUG-001', name: 'Ceramic mug', quantity: 2, unitPrice: '12.50' }],
  });
  input = {
    customerId: customer.id,
    orderId: order.id,
    amount: order.totalAmountExact,
    currency: 'USD',
    paymentMethod: 'card',
    idempotencyKey: 'restart-payment-001',
  };
}
const payment = await commerce.payments.createExact(input);
console.log(JSON.stringify({
  input,
  paymentId: payment.id,
  amount: payment.amountExact,
  status: payment.status,
  paymentCount: await commerce.payments.count(),
  orderCount: await commerce.orders.count(),
}));
`;

test('payment replay survives a process restart without another record', () => {
  const directory = mkdtempSync(join(tmpdir(), 'stateset-restart-'));
  const database = join(directory, 'commerce.db');
  function runWorker(action, input) {
    const result = spawnSync(process.execPath, ['--input-type=module', '--eval', worker], {
      input: JSON.stringify({ action, database, input }),
      encoding: 'utf8',
      timeout: 30000,
    });
    assert.ifError(result.error);
    assert.equal(result.status, 0, result.stderr || 'Worker did not exit successfully');
    return JSON.parse(result.stdout);
  }
  try {
    const created = runWorker('create');
    assert.ok(created.paymentId);
    assert.equal(created.amount, '25.00');
    assert.equal(created.status, 'pending');
    assert.equal(created.paymentCount, 1);
    assert.equal(created.orderCount, 1);

    const replayed = runWorker('replay', created.input);
    assert.equal(replayed.paymentId, created.paymentId);
    assert.equal(replayed.amount, '25.00');
    assert.equal(replayed.status, 'pending');
    assert.equal(replayed.paymentCount, 1);
    assert.equal(replayed.orderCount, 1);
  } finally {
    // The workers have exited; delete only the temporary directory this test created.
    rmSync(directory, { recursive: true, force: true, maxRetries: 3 });
  }
});
```

Run from the project directory where you installed the package:

```bash theme={null}
node --test restart.test.mjs
```

**Success:** **1 test passes**, the process exits with status `0`, and the temporary database
is removed. The subprocesses inherit the working directory so they can resolve the installed
package. A worker error or timeout fails the test rather than being treated as an empty result.

This verifies a clean process restart using a persisted database and the original key and
payload. It does not test an interrupted transaction, concurrent writers, disk failure, or an
external charge. The payment intentionally remains `pending`.

The key is fixed because every run uses a fresh database. In an application, persist one key
per intended operation along with its original request; do not reuse this teaching key for
unrelated payments. See [payment and refund behavior](/guides/payments-quickstart).

## Add hosted API checks as a separate layer

Select the service from the [API directory](/api-reference/introduction). Record its host,
authentication header, key scope, and test workspace. There is no universal StateSet sandbox
host or shared test key for all engines.

For ResponseCX, the [developer guide's first request](/development#make-a-first-request-from-your-application)
reads the workspace with `agents:read`. Assert the response fields described by the
[workspace contract](/api-reference/responsecx/workspace-list); do not hardcode the agent count
of a shared workspace.

For a write check, follow the [ResponseCX quickstart](/quickstart): create an inactive agent,
save its returned ID, then read it back and compare its configuration. Agent creation,
activation, and a customer-channel interaction are separate checks. A `201` response alone
does not establish that an agent can answer a customer correctly.

A test that requires a key must report a missing credential clearly. If your normal suite
intentionally skips hosted checks, report them as skipped and run the selected hosted job
before claiming that integration was verified.

## Verify asynchronous outcomes

Use the start operation's returned ID to correlate later reads and events. An accepted start
request proves acceptance, not completion. Define a deadline and inspect the workflow's actual
state until it reaches the expected terminal state or the deadline expires.

For each workflow, write down:

1. The accepted starting state and the operation ID returned by the service.
2. The completion or failure states named by that service's contract.
3. The database records or provider results that must exist after completion.
4. The outcome of a repeated request or duplicate event.
5. The cleanup or reconciliation needed when only part of the workflow finishes.

The [Temporal walkthrough](/guides/temporal-first-durable-workflow) shows start and status
requests for that engine. The [error-handling guide](/guides/error-handling-best-practices)
explains why a timeout requires reconciliation before repeating a write.

## Expand failure coverage intentionally

| Scenario | Assertion to add |
| - | - |
| Duplicate customer input | Reused identity or the expected conflict, without an unintended profile update |
| Insufficient stock | Expected rejection and no partial order or allocation |
| Replayed payment | Same payment ID and unchanged record count |
| Refund before payment completion | Rejection and no assumption that a provider refund occurred |
| Return received without required dispositions | Expected completion rejection; record remains in the observed state |
| Unexpected application exception | Original failure propagates, with no silent success or automatic second write |
| Lost response or duplicate event | Reconciliation or replay follows that operation's documented identity rules |

Use a test double to trigger a lost response in your own adapter, but label that coverage as
application behavior. It does not establish a remote service's deduplication or delivery
semantics. Verify those against the service separately.

## Measure performance after correctness

Choose the exact operation, dataset size, concurrency, and environment before comparing runs.
Measure failures as well as successful requests. Report failed attempts divided by **all
attempts**; dividing by successful requests gives the wrong error rate.

Include latency percentiles, throughput, timeout count, and failure count. Keep expected
business rejections separate from infrastructure failures so a workload dominated by
out-of-stock requests does not look like successful order creation.

A local SQLite result does not establish hosted API capacity. Use the intended service's
contract and an agreed test environment for remote load tests; the Sandbox API provides
isolated runtimes, not a generic commerce `/orders` endpoint.

## Keep CI results interpretable

Run local tests without service credentials on each change. The
[integration-testing guide](/guides/api-testing#add-the-suite-to-ci) provides a complete CI job.
Add `node --test restart.test.mjs` to that job after saving the program above and committing
your package lockfile.

Keep hosted and provider checks in separately named jobs with their own configuration. When
a job fails, retain its runtime/package version, operation name, assertion, and redacted
correlation IDs. Report cleanup failures separately so they do not hide the original failure.
Do not rerun an entire write workflow automatically to make a failed build green.

## Next steps

* [Integration tests](/guides/api-testing): isolated tests for the local engine.
* [Error handling and retries](/guides/error-handling-best-practices): bounded retries and uncertain outcomes.
* [Persisted quickstart](/stateset-icommerce/stateset-icommerce-quickstart): inspect an order from a second process.
* [Support checklist](/support#report-an-api-problem): collect a useful failure reproduction.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.