Skip to content
Draft
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
5 changes: 5 additions & 0 deletions .changeset/add-startup-scenario.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"counterfact": minor
---

Add startup scenario: export a function named `startup` from `scenarios/index.ts` and it will run automatically when the server initializes, right before the REPL starts. Use it to seed dummy data so the server is immediately useful without any manual REPL commands. If `startup` is not exported, the server starts normally with no error.
42 changes: 42 additions & 0 deletions docs/features/repl.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,51 @@ After the command runs you can immediately use anything stored in `$.routes`:

The `Scenario` type and `ApplyContext` interface are generated automatically into `types/scenario-context.ts` when you run Counterfact with type generation enabled.

## Startup scenario

The `startup` export in `scenarios/index.ts` is special: it runs automatically when the server initializes, right before the REPL prompt appears. Use it to seed dummy data so the server is immediately useful without any manual REPL commands.

```ts
// scenarios/index.ts
import type { Scenario } from "../types/scenario-context.js";

export const startup: Scenario = ($) => {
$.context.addPet({ name: "Fluffy", status: "available", photoUrls: [] });
$.context.addPet({ name: "Rex", status: "sold", photoUrls: [] });
};
```

**Delegating to other scenario functions** keeps `startup` focused and readable. Pass `$` (and any extra arguments) to each helper:

```ts
// scenarios/index.ts
import type { Scenario } from "../types/scenario-context.js";
import { addPets } from "./pets.js";
import { addOrders } from "./orders.js";

export const startup: Scenario = ($) => {
addPets($, 20, "dog");
addOrders($, 5);
};
```

```ts
// scenarios/pets.ts
import type { ApplyContext } from "../types/scenario-context.js";

export function addPets($: ApplyContext, count: number, species: string) {
for (let i = 0; i < count; i++) {
$.context.addPet({ name: `${species} ${i + 1}`, status: "available", photoUrls: [] });
}
}
```

If `startup` is not exported from `scenarios/index.ts`, it is silently skipped — no error is thrown.

## See also

- [Route Builder](./route-builder.md) — fluent request builder with OpenAPI introspection
- [State](./state.md) — the context objects you interact with from the REPL
- [Patterns: Scenario Scripts](../patterns/scenario-scripts.md)
- [Patterns: Live Server Inspection with the REPL](../patterns/repl-inspection.md)
- [Usage](../usage.md)
3 changes: 2 additions & 1 deletion docs/patterns/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@

A pattern is a reusable solution to a recurring problem when building API simulations with Counterfact. Each pattern below describes a context, the problem it addresses, the solution, and its consequences.

Most projects start with [Explore a New API](./explore-new-api.md) or [Executable Spec](./executable-spec.md) to get a running server from an OpenAPI spec with no code. From there, [Mock APIs with Dummy Data](./mock-with-dummy-data.md) and [AI-Assisted Implementation](./ai-assisted-implementation.md) are the natural next steps for adding realistic responses — the former by hand, the latter with an AI agent doing the heavy lifting. As the mock grows, [Federated Context Files](./federated-context.md) and [Test the Context, Not the Handlers](./test-context-not-handlers.md) keep the stateful logic organized and reliable. Throughout all of this, [Live Server Inspection with the REPL](./repl-inspection.md) is Counterfact's most distinctive feature: it lets you seed data, send requests, and toggle behavior in real time without restarting. [Simulate Failures and Edge Cases](./simulate-failures.md) and [Simulate Realistic Latency](./simulate-latency.md) extend any mock to cover error paths and performance characteristics that real services exhibit. [Reference Implementation](./reference-implementation.md) and [Executable Spec](./executable-spec.md) make the mock a first-class artifact that teams can rely on as the API evolves. Finally, [Agentic Sandbox](./agentic-sandbox.md) and [Hybrid Proxy](./hybrid-proxy.md) address the two common integration strategies — isolating an AI agent from the real service, or blending mock and live traffic across endpoints. [Automated Integration Tests](./automated-integration-tests.md) shows how to embed the mock server in a test suite using the programmatic API, while [Custom Middleware](./custom-middleware.md) covers cross-cutting concerns like authentication and response headers without touching individual handlers.
Most projects start with [Explore a New API](./explore-new-api.md) or [Executable Spec](./executable-spec.md) to get a running server from an OpenAPI spec with no code. From there, [Mock APIs with Dummy Data](./mock-with-dummy-data.md) and [AI-Assisted Implementation](./ai-assisted-implementation.md) are the natural next steps for adding realistic responses — the former by hand, the latter with an AI agent doing the heavy lifting. [Scenario Scripts](./scenario-scripts.md) let you automate repetitive REPL interactions — seeding data, resetting state, building reusable request sequences — and the `startup` export runs one of those scripts automatically on every server start. As the mock grows, [Federated Context Files](./federated-context.md) and [Test the Context, Not the Handlers](./test-context-not-handlers.md) keep the stateful logic organized and reliable. Throughout all of this, [Live Server Inspection with the REPL](./repl-inspection.md) is Counterfact's most distinctive feature: it lets you seed data, send requests, and toggle behavior in real time without restarting. [Simulate Failures and Edge Cases](./simulate-failures.md) and [Simulate Realistic Latency](./simulate-latency.md) extend any mock to cover error paths and performance characteristics that real services exhibit. [Reference Implementation](./reference-implementation.md) and [Executable Spec](./executable-spec.md) make the mock a first-class artifact that teams can rely on as the API evolves. Finally, [Agentic Sandbox](./agentic-sandbox.md) and [Hybrid Proxy](./hybrid-proxy.md) address the two common integration strategies — isolating an AI agent from the real service, or blending mock and live traffic across endpoints. [Automated Integration Tests](./automated-integration-tests.md) shows how to embed the mock server in a test suite using the programmatic API, while [Custom Middleware](./custom-middleware.md) covers cross-cutting concerns like authentication and response headers without touching individual handlers.

| Pattern | When to use it |
|---|---|
| [Explore a New API](./explore-new-api.md) | You have a spec but no running backend or production access |
| [Executable Spec](./executable-spec.md) | You want immediate feedback on how spec changes affect the running server during API design |
| [Mock APIs with Dummy Data](./mock-with-dummy-data.md) | You need realistic-looking responses to build a UI, run a demo, or write assertions |
| [Scenario Scripts](./scenario-scripts.md) | You want to automate REPL interactions, seed data on startup, or build reusable state configurations |
| [AI-Assisted Implementation](./ai-assisted-implementation.md) | You want an AI agent to replace random responses with working handler logic |
| [Federated Context Files](./federated-context.md) | You want each domain to own its state, with explicit cross-domain dependencies |
| [Test the Context, Not the Handlers](./test-context-not-handlers.md) | You want to keep shared stateful logic reliable as the mock grows |
Expand Down
133 changes: 133 additions & 0 deletions docs/patterns/scenario-scripts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# Scenario Scripts

You want to automate repetitive REPL interactions — seeding data, configuring state, building reusable request sequences — so you don't have to type the same commands every session.

## Problem

The REPL is powerful for ad-hoc exploration, but many setup tasks are predictable: seed a set of pets before a demo, reset state to a known baseline for a test, or configure a reusable route builder for a common request. Retyping these commands every time the server restarts is tedious and error-prone.

## Solution

Write _scenario scripts_ — TypeScript files in the `scenarios/` directory that export named functions. Each function receives a single `$` argument that exposes the full live context and route builder. Run any export on demand from the REPL with `.scenario`, or have one run automatically at startup.

### Writing a scenario

Scenario functions are typed with the generated `Scenario` type:

```ts
// scenarios/index.ts
import type { Scenario } from "../types/scenario-context.js";

export const soldPets: Scenario = ($) => {
// Mutate context directly — the same object route handlers see as $.context
$.context.petService.reset();
$.context.petService.addPet({ id: 1, status: "sold" });
$.context.petService.addPet({ id: 2, status: "available" });

// Store a pre-configured route builder in the REPL environment
$.routes.findSold = $
.route("/pet/findByStatus")
.method("get")
.query({ status: "sold" });
};
```

### Running scenarios from the REPL

Use the `.scenario` command. The argument is a slash-separated path where the last segment is the function name and everything before it is the file path relative to `scenarios/` (with `index.ts` as the default):

```
⬣> .scenario soldPets
Applied soldPets
```

| Command | File | Function |
|---|---|---|
| `.scenario soldPets` | `scenarios/index.ts` | `soldPets` |
| `.scenario pets/resetAll` | `scenarios/pets.ts` | `resetAll` |
| `.scenario pets/orders/pending` | `scenarios/pets/orders.ts` | `pending` |

After running, anything stored in `$.routes` is immediately available in the REPL:

```js
⬣> routes.findSold.send()
```

### Automatic startup

Export a function named `startup` from `scenarios/index.ts` and Counterfact calls it automatically when the server initializes, right before the REPL prompt appears — no manual command required:

```ts
// scenarios/index.ts
import type { Scenario } from "../types/scenario-context.js";

export const startup: Scenario = ($) => {
$.context.addPet({ name: "Fluffy", status: "available", photoUrls: [] });
$.context.addPet({ name: "Rex", status: "sold", photoUrls: [] });
};
```

If `startup` is not exported, the server starts normally with no error.

### Delegating to helpers with extra arguments

Split large scenarios into focused helper functions in separate files and pass `$` (and any extra arguments) through. This keeps each file small and lets you call the same helpers from both `startup` and the REPL with different parameters:

```ts
// scenarios/index.ts
import type { Scenario } from "../types/scenario-context.js";
import { addPets } from "./pets.js";
import { addOrders } from "./orders.js";

export const startup: Scenario = ($) => {
addPets($, 20, "dog"); // seed 20 dogs at startup
addOrders($, 5);
};
```

```ts
// scenarios/pets.ts
import type { ApplyContext } from "../types/scenario-context.js";

export function addPets($: ApplyContext, count: number, species: string) {
for (let i = 0; i < count; i++) {
$.context.addPet({
name: `${species} ${i + 1}`,
status: "available",
photoUrls: [],
});
}
}
```

```ts
// scenarios/orders.ts
import type { ApplyContext } from "../types/scenario-context.js";

export function addOrders($: ApplyContext, count: number) {
for (let i = 0; i < count; i++) {
$.context.addOrder({ petId: i + 1, quantity: 1, status: "placed" });
}
}
```

Helpers can also be called as standalone `.scenario` commands:

```
⬣> .scenario pets/addPets
```

## Consequences

- Scenario functions are plain TypeScript — no special framework, fully type-checked, easy to test in isolation.
- `.scenario` provides on-demand state changes without editing handler files or restarting the server.
- `startup` gives the server a deterministic initial state on every restart, eliminating manual seeding steps.
- Helper functions accepting extra arguments let the same logic be used for both realistic production volumes (at startup) and minimal data (in the REPL during debugging).
- Scenarios live alongside your handler code, making them easy to discover and keep in sync with the API.

## Related Patterns

- [Mock APIs with Dummy Data](./mock-with-dummy-data.md) — the full range of approaches for populating server responses
- [Live Server Inspection with the REPL](./repl-inspection.md) — interactive exploration and state manipulation at runtime
- [Federated Context Files](./federated-context.md) — organize stateful logic that scenarios can seed
- [Simulate Failures and Edge Cases](./simulate-failures.md) — use scenarios to flip failure flags on demand
40 changes: 40 additions & 0 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import nodePath from "node:path";
import { createHttpTerminator, type HttpTerminator } from "http-terminator";

import { startRepl as startReplServer } from "./repl/repl.js";
import { createRouteFunction } from "./repl/route-builder.js";
import type { Config } from "./server/config.js";
import { ContextRegistry } from "./server/context-registry.js";
import { createKoaApp } from "./server/create-koa-app.js";
Expand All @@ -21,6 +22,38 @@ import { runtimeCanExecuteErasableTs } from "./util/runtime-can-execute-erasable

export { loadOpenApiDocument } from "./server/load-openapi-document.js";

type ApplyContext = {
context: Record<string, unknown>;
loadContext: (path: string) => Record<string, unknown>;
route: (path: string) => unknown;
routes: Record<string, unknown>;
};

export async function runStartupScenario(
scenarioRegistry: ScenarioRegistry,
contextRegistry: ContextRegistry,
config: Config,
openApiDocument?: Parameters<typeof createRouteFunction>[2],
): Promise<void> {
const indexModule = scenarioRegistry.getModule("index");

if (!indexModule || typeof indexModule["startup"] !== "function") {
return;
}

const applyContext: ApplyContext = {
context: contextRegistry.find("/") as Record<string, unknown>,
loadContext: (path: string) =>
contextRegistry.find(path) as Record<string, unknown>,
route: createRouteFunction(config.port, "localhost", openApiDocument),
routes: {},
};

await (indexModule["startup"] as (ctx: ApplyContext) => Promise<void> | void)(
applyContext,
);
}

type MswHandlerMap = {
[key: string]: (request: MockRequest) => Promise<unknown>;
};
Expand Down Expand Up @@ -182,6 +215,13 @@ export async function counterfact(config: Config) {
await moduleLoader.load();
await moduleLoader.watch();

await runStartupScenario(
scenarioRegistry,
contextRegistry,
config,
openApiDocument,
);

const server = koaApp.listen({
port: config.port,
});
Expand Down
15 changes: 15 additions & 0 deletions src/typescript-generator/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,21 @@ const DEFAULT_SCENARIOS_INDEX = `import type { Scenario } from "../types/scenari
* $.routes.myRequest = $.route("/pets").method("get");
*/

/**
* startup() runs automatically when the server initializes, right before the
* REPL starts. Use it to seed dummy data so the server is ready to use
* immediately. It receives the same $ argument as all other scenario functions.
*
* Tip: delegate to other scenario functions and pass $ along so each function
* stays focused on a single concern. You can also pass additional arguments to
* configure them, e.g. addPets($, 20, "dog").
*
* If you don't need a startup scenario, delete this function or leave it empty.
*/
export const startup: Scenario = ($) => {
void $;
};

/**
* An example scenario. To use it in the REPL, type:
* .scenario help
Expand Down
Loading
Loading