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

# Testing

> There is no test mode. Test on your own assistant, from localhost, with a test customer.

There are no test keys either. An assistant reads your real database, and every question is charged to your workspace. The assistant's caps are your safety net.

## Run it on localhost

In **Doors → Allowed origins**, add your local origin:

```text theme={"system"}
http://localhost:3000
```

An origin is a scheme, a host and a port, with no path. `http` is accepted. `http://localhost:3000` and `http://127.0.0.1:3000` are two different origins: add the one in your address bar. Remove local origins before you go live.

> [!TIP]
>
> A dataset has one assistant. To test on a staging site, add the staging origin next to your production origins.

## Use a test customer

On the per-user door, open sessions for an account you own, such as a demo customer in your database. Its conversation is kept like any other, one per assistant and per partition. Erase it when you are done:

```ts theme={"system"}
import { Concatflow } from "@concatflow/node";

const concatflow = new Concatflow({ apiKey: process.env.CONCATFLOW_API_KEY! });

await concatflow.conversations.erase({
  assistantId: process.env.CONCATFLOW_ASSISTANT_ID!,
  partition: { customer_id: "TEST_CUSTOMER_ID" },
});
```

Erasing a partition with no conversation also succeeds, so this runs safely after every test.

## Test your session route

Call your route the way your page does, with your login cookie:

```bash theme={"system"}
curl -X POST http://localhost:3000/api/concatflow/session \
  -H "Cookie: YOUR_SESSION_COOKIE"
```

```json theme={"system"}
{ "token": "…", "expiresAt": "2026-10-23T18:15:00.000000+00:00" }
```

Then break it on purpose, once each:

| Change | Expect |
| - | - |
| Rename a key in `partitions` | `403 partition_keys_mismatch` |
| Turn the per-user door off in **Doors** | `409 door_closed` |
| Use a revoked API key | `401 invalid_api_key` |

Your route should turn each one into an error your page handles. See [Errors](https://docs.concatflow.com/reference/node/errors.md).

## Test renewal without waiting 15 minutes

Hand the assistant a token Concatflow will refuse, then open it:

```js theme={"system"}
assistant.setSession("expired");
assistant.open();
```

The assistant gets the same answer as for an expired token. It calls your `onSessionExpired`, takes the new token, and the panel works. If `onSessionExpired` is missing or throws, the assistant stops answering: that is what your customers would see after 15 minutes. See [Session renewal](https://docs.concatflow.com/guides/session-renewal.md).

## Test isolation with two customers

Log in as customer A and ask a question only A's data can answer. Log in as customer B in another browser and ask the same question. B must see neither A's rows nor A's questions. It is the last step of the [Go-live checklist](https://docs.concatflow.com/guides/go-live.md).

## Unit-test your route

`@concatflow/node` calls the global `fetch`. Stub it to test your code without a network or an API key. With Node's own test runner:

```ts session.test.ts theme={"system"}
import assert from "node:assert/strict";
import { mock, test } from "node:test";
import { Concatflow, ConcatflowError } from "@concatflow/node";

const concatflow = new Concatflow({ apiKey: "lyra_sk_TEST" });
const params = { assistantId: "YOUR_ASSISTANT_ID", partitions: { customer_id: "4712" } };

test("opens a session for the customer", async (t) => {
  const fetch = t.mock.method(globalThis, "fetch", async () =>
    Response.json({ token: "tok_test", expires_at: "2026-10-23T18:15:00+00:00" }, { status: 201 }),
  );

  const session = await concatflow.sessions.create(params);

  assert.deepEqual(session, { token: "tok_test", expiresAt: "2026-10-23T18:15:00+00:00" });
  const [, init] = fetch.mock.calls[0]!.arguments as [string, RequestInit];
  assert.deepEqual(JSON.parse(String(init.body)).partitions, { customer_id: "4712" });
});

test("surfaces a refused partition", async (t) => {
  t.mock.method(globalThis, "fetch", async () =>
    Response.json({ detail: { code: "partition_keys_mismatch", expected: ["customer_id"] } }, { status: 403 }),
  );

  await assert.rejects(concatflow.sessions.create(params), (err) => {
    assert.ok(err instanceof ConcatflowError);
    assert.equal(err.status, 403);
    assert.equal(err.code, "partition_keys_mismatch");
    return true;
  });
});

mock.reset();
```

```bash theme={"system"}
node --test
```

## Limits you will meet while testing

These limits protect your credits. They apply to testers like anyone else: a whole team behind one office IP shares the per-IP limit.

| Limit | Value | Door |
| - | - | - |
| Questions per IP, per assistant | 30 per hour | both |
| Questions per visitor or per customer | **Rate** in **Doors**, 60 per hour by default | both |
| Questions per anonymous session | 20 | public |
| New anonymous sessions per IP | 10 per hour | public |
| Monthly cap, cap per end customer | **Limits** tab | both |

When a limit is reached, the assistant stays on the page and answers with a short message instead. That reply calls no model and costs nothing. Wait an hour, or raise **Rate** while you test.


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