Skip to content

Commit 320bfa5

Browse files
Copilotpmcelhaney
andauthored
Merge remote-tracking branch 'origin/main' into copilot/convert-openapi-document-to-class
Co-authored-by: pmcelhaney <51504+pmcelhaney@users.noreply.github.com>
2 parents d600126 + 258de47 commit 320bfa5

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+3295
-231
lines changed

.changeset/add-internal-jsdocs.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"counterfact": patch
3+
---
4+
5+
Add JSDoc comments throughout the codebase, covering all major classes, functions, and interfaces in `src/server/`, `src/typescript-generator/`, `src/repl/`, and `src/util/`.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"counterfact": patch
3+
---
4+
5+
Make the hexagon in the REPL prompt the same shade of blue as the logo (#0071b5).
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"counterfact": patch
3+
---
4+
5+
Moved `openapi-example.yaml` from the repository root into `test/fixtures/openapi-example.yaml` and expanded it with many OpenAPI edge cases: CRUD operations on `/users` and `/users/{userId}`, polymorphic events via `oneOf`/`allOf`/`discriminator`, nullable fields, enum types, integer formats, file upload via `multipart/form-data`, cookie parameters, deprecated endpoints, multiple response content types, a no-body `204` health-check endpoint, and free-form `additionalProperties` objects.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"counterfact": patch
3+
---
4+
5+
Rename the `.apply` REPL command to `.scenario`. Update all references in code, tests, and documentation.

docs/adr/001-apply-command-with-function-injection.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# ADR 001: .apply Command Design — Minimalist Function Injection
1+
# ADR 001: .scenario Command Design — Minimalist Function Injection
22

33
## Status
44

@@ -8,7 +8,7 @@ Accepted
88

99
Counterfact's REPL lets developers interact with the running mock server from the terminal. A common need is to transition the server into a specific state (e.g. "all pets sold", "service unavailable") in a reproducible, shareable way. Today, operators must manually call REPL commands one by one; there is no mechanism to save and replay a named scenario.
1010

11-
The `.apply` command is proposed to address this: given a path argument, it loads and executes a user-authored script that mutates REPL context and routes, then reports what changed.
11+
The `.scenario` command is proposed to address this: given a path argument, it loads and executes a user-authored script that mutates REPL context and routes, then reports what changed.
1212

1313
Three designs were proposed as working documents in `.github/issue-proposals/`:
1414

@@ -27,7 +27,7 @@ Three designs were proposed as working documents in `.github/issue-proposals/`:
2727

2828
**Solution 1 (Minimalist Function Injection) is selected.**
2929

30-
An apply script is a TypeScript file with one or more named function exports. When `.apply <path>` is run, Counterfact splits the argument on `/`, uses the last segment as the function name and the rest as the file path (relative to `<basePath>/repl/`), dynamically imports the module, and calls the named function with a live `ApplyContext` (`$`) object:
30+
A scenario script is a TypeScript file with one or more named function exports. When `.scenario <path>` is run, Counterfact splits the argument on `/`, uses the last segment as the function name and the rest as the file path (relative to `<basePath>/repl/`), dynamically imports the module, and calls the named function with a live `ApplyContext` (`$`) object:
3131

3232
```ts
3333
// repl/sold-pets.ts
@@ -55,7 +55,7 @@ Scripts export named functions that receive `$: ApplyContext`. Counterfact resol
5555

5656
### Solution 2: Scenario Class with Lifecycle Hooks
5757

58-
Scripts export a named class that implements a `Scenario` interface with `setup()` and optional `teardown()` methods. Counterfact instantiates the class, calls `setup()`, and tracks applied instances in a map for later `.unapply`. A static `dependencies` array enables ordered composition.
58+
Scripts export a named class that implements a `Scenario` interface with `setup()` and optional `teardown()` methods. Counterfact instantiates the class, calls `setup()`, and tracks applied instances in a map for later `.unscenario`. A static `dependencies` array enables ordered composition.
5959

6060
**Why not chosen:** Class syntax and lifecycle coupling add complexity that is not justified until the need for teardown and dependency ordering is proven in practice. These concerns can be layered on top of Solution 1 once the basic command exists.
6161

@@ -81,7 +81,7 @@ Identical surface syntax to Solution 1, but Counterfact wraps `context` and `rou
8181

8282
### Risks and downsides
8383

84-
- Without lifecycle hooks, accumulated state across many `.apply` calls may be difficult to reason about.
84+
- Without lifecycle hooks, accumulated state across many `.scenario` calls may be difficult to reason about.
8585
- If teardown proves to be a common need, adding it later will require extending the API in a backward-compatible way.
8686
- Proxy-based auto-diffing (Solution 3) remains attractive for DX; deferring it means script authors will need to be disciplined about documenting context changes in the short term.
8787

docs/features/repl.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -56,21 +56,21 @@ await req.send()
5656

5757
See the [Route Builder guide](./route-builder.md) for full documentation.
5858

59-
## Scenario scripts with `.apply`
59+
## Scenario scripts with `.scenario`
6060

61-
For more complex setups you can automate REPL interactions by writing _scenario scripts_ — plain TypeScript files that export named functions. Run them with `.apply`:
61+
For more complex setups you can automate REPL interactions by writing _scenario scripts_ — plain TypeScript files that export named functions. Run them with `.scenario`:
6262

6363
```
64-
⬣> .apply soldPets
64+
⬣> .scenario soldPets
6565
```
6666

67-
**Path resolution:** the argument to `.apply` is a slash-separated path. The last segment is the function name; everything before it is the file path, resolved relative to `<basePath>/scenarios/` (with `index.ts` as the default file).
67+
**Path resolution:** the argument to `.scenario` is a slash-separated path. The last segment is the function name; everything before it is the file path, resolved relative to `<basePath>/scenarios/` (with `index.ts` as the default file).
6868

6969
| Command | File | Function |
7070
|---|---|---|
71-
| `.apply foo` | `scenarios/index.ts` | `foo` |
72-
| `.apply foo/bar` | `scenarios/foo.ts` | `bar` |
73-
| `.apply foo/bar/baz` | `scenarios/foo/bar.ts` | `baz` |
71+
| `.scenario soldPets` | `scenarios/index.ts` | `soldPets` |
72+
| `.scenario pets/resetAll` | `scenarios/pets.ts` | `resetAll` |
73+
| `.scenario pets/orders/pending` | `scenarios/pets/orders.ts` | `pending` |
7474

7575
A scenario function receives a single argument with `{ context, loadContext, routes, route }`:
7676

openapi-example.yaml

Lines changed: 0 additions & 128 deletions
This file was deleted.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@
7676
"lint:quickfix": "eslint --fix . eslint --fix demo-ts --rule=\"import/namespace: 0,etc/no-deprecated:0,import/no-cycle:0,no-explicit-type-exports/no-explicit-type-exports:0,import/no-deprecated:0,import/no-self-import:0,import/default:0,import/no-named-as-default:0\" --ignore-pattern dist --ignore-pattern out",
7777
"go:petstore": "yarn build && yarn counterfact https://petstore3.swagger.io/api/v3/openapi.json out",
7878
"go:petstore2": "yarn build && yarn counterfact https://petstore.swagger.io/v2/swagger.json out",
79-
"go:example": "yarn build && node ./bin/counterfact.js ./openapi-example.yaml out",
79+
"go:example": "yarn build && node ./bin/counterfact.js ./test/fixtures/openapi-example.yaml out",
8080
"counterfact": "./bin/counterfact.js",
8181
"postinstall": "patch-package"
8282
},

src/app.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,16 @@ export type MockRequest = DispatcherRequest & { rawPath: string };
3838

3939
const mswHandlers: MswHandlerMap = {};
4040

41+
/**
42+
* Dispatches a single MSW (Mock Service Worker) intercepted request to the
43+
* matching Counterfact route handler registered via {@link createMswHandlers}.
44+
*
45+
* @param request - The intercepted request, including the HTTP method, path,
46+
* headers, query, body, and a `rawPath` that preserves the original URL
47+
* before base-path stripping.
48+
* @returns The response produced by the matching handler, or a 404 object when
49+
* no handler has been registered for the given method and path.
50+
*/
4151
export async function handleMswRequest(request: MockRequest) {
4252
const { method, rawPath } = request;
4353
const handler = mswHandlers[`${method}:${rawPath}`];
@@ -48,6 +58,18 @@ export async function handleMswRequest(request: MockRequest) {
4858
return { error: `No handler found for ${method} ${rawPath}`, status: 404 };
4959
}
5060

61+
/**
62+
* Loads an OpenAPI document, registers all routes from it as MSW handlers, and
63+
* returns the list of registered routes so callers (e.g. Vitest Browser mode)
64+
* can mount them on their own request-interception layer.
65+
*
66+
* @param config - Counterfact configuration; `openApiPath` and `basePath` are
67+
* the most important fields for this function.
68+
* @param ModuleLoaderClass - Injectable module-loader constructor, primarily
69+
* used in tests to substitute a test-friendly implementation.
70+
* @returns An array of `{ method, path }` objects describing every registered
71+
* MSW handler.
72+
*/
5173
export async function createMswHandlers(
5274
config: Config,
5375
ModuleLoaderClass = ModuleLoader,
@@ -98,6 +120,22 @@ export async function createMswHandlers(
98120
return handlers;
99121
}
100122

123+
/**
124+
* Creates and configures a full Counterfact server instance.
125+
*
126+
* Sets up the route registry, context registry, scenario registry, code
127+
* generator, transpiler, module loader, Koa application, and OpenAPI watcher.
128+
* The returned object exposes handles for starting the server, stopping it, and
129+
* launching the interactive REPL.
130+
*
131+
* @param config - Runtime configuration (port, paths, feature flags, etc.).
132+
* @returns An object containing the configured sub-systems and two entry-point
133+
* functions:
134+
* - `start(options)` — generates/watches code and optionally starts the HTTP
135+
* server; returns a `stop()` handle.
136+
* - `startRepl()` — launches the interactive Node.js REPL connected to the
137+
* live server state.
138+
*/
101139
export async function counterfact(config: Config) {
102140
const modulesPath = config.basePath;
103141

src/repl/raw-http-client.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,16 @@ function stringifyBody(body: string | object) {
6262
return JSON.stringify(body);
6363
}
6464

65+
/**
66+
* A minimal HTTP/1.1 client that communicates over a raw TCP socket.
67+
*
68+
* Used in the Counterfact REPL (`client.*`) to send requests to the local mock
69+
* server and pretty-print the request and response to `stdout` with ANSI
70+
* colours.
71+
*
72+
* Unlike `fetch` or Axios, `RawHttpClient` does not buffer or parse the
73+
* response — the raw HTTP response string is returned from every method.
74+
*/
6575
export class RawHttpClient {
6676
host: string;
6777
port: number;
@@ -72,38 +82,47 @@ export class RawHttpClient {
7282
this.port = port;
7383
}
7484

85+
/** Sends a `GET` request and returns the raw HTTP response string. */
7586
get(path: string, headers = {}) {
7687
return this.#send("GET", path, "", headers);
7788
}
7889

90+
/** Sends a `HEAD` request and returns the raw HTTP response string. */
7991
head(path: string, headers = {}) {
8092
return this.#send("HEAD", path, "", headers);
8193
}
8294

95+
/** Sends a `POST` request with `body` and returns the raw HTTP response string. */
8396
post(path: string, body: string | object = "", headers = {}) {
8497
return this.#send("POST", path, body, headers);
8598
}
8699

100+
/** Sends a `PUT` request with `body` and returns the raw HTTP response string. */
87101
put(path: string, body: string | object = "", headers = {}) {
88102
return this.#send("PUT", path, body, headers);
89103
}
90104

105+
/** Sends a `DELETE` request and returns the raw HTTP response string. */
91106
delete(path: string, headers = {}) {
92107
return this.#send("DELETE", path, "", headers);
93108
}
94109

110+
/** Sends a `CONNECT` request and returns the raw HTTP response string. */
95111
connect(path: string, headers = {}) {
96112
return this.#send("CONNECT", path, "", headers);
97113
}
98114

115+
/** Sends an `OPTIONS` request and returns the raw HTTP response string. */
99116
options(path: string, headers = {}) {
100117
return this.#send("OPTIONS", path, "", headers);
101118
}
102119

120+
/** Sends a `TRACE` request and returns the raw HTTP response string. */
103121
trace(path: string, headers = {}) {
104122
return this.#send("TRACE", path, "", headers);
105123
}
106124

125+
/** Sends a `PATCH` request with `body` and returns the raw HTTP response string. */
107126
patch(path: string, body: string | object = "", headers = {}) {
108127
return this.#send("PATCH", path, body, headers);
109128
}

0 commit comments

Comments
 (0)