@venn-lang/mock 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vinicius Borges
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,115 @@
1
+ # @venn-lang/mock
2
+
3
+ > The `mock` namespace: named mocks, HTTP interceptors, feature flags and a virtual clock.
4
+
5
+ Eight verbs writing to one in-process state object. There is no port and no network here: the whole
6
+ package is a typed way to record what a run should pretend is true, and a way to read that record
7
+ back from TypeScript.
8
+
9
+ ## Install
10
+
11
+ The package ships with the stdlib, so the CLI already loads it. Inside a `.vn` file, bring the
12
+ namespace in with `use`:
13
+
14
+ ```ruby
15
+ use "venn/mock"
16
+ ```
17
+
18
+ ## Usage
19
+
20
+ ```ruby
21
+ module demo.checkout
22
+
23
+ use "venn/mock"
24
+
25
+ setup {
26
+ mock.start "payments" { from: "./mocks/stripe.yaml" }
27
+ mock.intercept "POST" "**/charge" { respond: { status: 201, body: { id: "ch_1" } } }
28
+ mock.flag "new-checkout"
29
+ mock.flag "rollout" { value: 0.5 }
30
+ }
31
+
32
+ teardown { mock.reset }
33
+
34
+ flow "Checkout" {
35
+ step "hold time still" {
36
+ mock.clock.freeze 2026-07-23T12:00:00Z
37
+ let now = mock.clock.advance 1h
38
+ expect now > 0
39
+ }
40
+ }
41
+ ```
42
+
43
+ ## Verbs
44
+
45
+ | Verb | Positional arguments | Options | Result |
46
+ | --- | --- | --- | --- |
47
+ | `mock.start` | `name: string` | `from` | `mock.Mock` |
48
+ | `mock.stop` | none | none | `void` |
49
+ | `mock.intercept` | `method: string`, `url: string` | `respond` | `mock.Interceptor` |
50
+ | `mock.respond` | `status: number`, `body: dynamic` | `status`, `body` | `mock.Response` |
51
+ | `mock.clock.freeze` | `at: string \| number \| instant` | none | `number`, epoch ms |
52
+ | `mock.clock.advance` | `by: string \| number \| duration` | none | `number`, the new virtual now |
53
+ | `mock.flag` | `name: string` | `value` | `dynamic`, the value that was set |
54
+ | `mock.reset` | none | none | `void` |
55
+
56
+ Notes that matter in practice:
57
+
58
+ - `mock.stop` clears the registered mocks and interceptors but leaves flags and the clock alone.
59
+ `mock.reset` clears everything, including the frozen instant.
60
+ - `mock.flag "x"` with no `value` sets the flag to `true`.
61
+ - `mock.respond` accepts its two values positionally or by name, so
62
+ `mock.respond 201 { body: { ok: true } }` and `mock.respond { status: 201, body: { ok: true } }`
63
+ record the same thing. The `respond` option of `mock.intercept` takes either a full
64
+ `{ status, body }` map or a bare value, which is wrapped as a `200`.
65
+ - The clock verbs read the language's own literals. `mock.clock.freeze 2026-07-23T12:00:00Z` and
66
+ `mock.clock.advance 1h` pass an instant and a duration value, not strings, and both are understood.
67
+ An ISO string or a raw millisecond count works too.
68
+ - Freezing records the instant in mock state. It does not drive the host clock, and no other stdlib
69
+ plugin reads the interceptors yet: the state is a record, and TypeScript is what reads it.
70
+ - Option names come from each verb's schema, so a typo is `VN3001` with a "did you mean" hint before
71
+ the flow runs.
72
+
73
+ ## Types
74
+
75
+ | Name | Shape |
76
+ | --- | --- |
77
+ | `mock.Mock` | `{ name: string, from?: string }` |
78
+ | `mock.Interceptor` | `{ method: string, path: string, respond: mock.Response }` |
79
+ | `mock.Response` | `{ status: number, body: dynamic }` |
80
+
81
+ ## Reading the state back
82
+
83
+ Every verb reads and writes one process-wide `MockState`. A test that drives the plugin from
84
+ TypeScript inspects it directly:
85
+
86
+ ```ts
87
+ import { getMockState, resetMockState } from "@venn-lang/mock";
88
+
89
+ resetMockState();
90
+ // ... run the flow ...
91
+ const state = getMockState();
92
+ state.flags.get("new-checkout"); // true
93
+ state.intercepts[0]?.respond; // { status: 201, body: { id: "ch_1" } }
94
+ state.frozenInstant; // epoch ms, or undefined while the clock is live
95
+ ```
96
+
97
+ `resetMockState()` replaces the shared state with a fresh one, which is exactly what the
98
+ `mock.reset` verb does. Call it in a `beforeEach` so one test's flags never reach the next.
99
+
100
+ ## API
101
+
102
+ | Export | What it is |
103
+ | --- | --- |
104
+ | `mockPlugin` (also the default export) | The `PluginDefinition`: namespace `mock`, no required capability, eight actions. |
105
+ | `mockActions` | The action list, in registration order. |
106
+ | `getMockState()` | The process-wide `MockState` the verbs read and write. |
107
+ | `resetMockState()` | Replaces it with a fresh, empty state. |
108
+ | `createMockState()` | Builds a fresh, empty state without touching the shared one. |
109
+ | `MockState`, `NamedMock`, `Interceptor`, `MockResponse` | Types only. |
110
+
111
+ ## See also
112
+
113
+ - [`@venn-lang/http`](../std-http), whose verbs the interceptors are written for.
114
+ - [`@venn-lang/data`](../std-data), for deterministic fake values.
115
+ - [`@venn-lang/sdk`](../sdk), `defineAction` / `definePlugin`.
@@ -0,0 +1,71 @@
1
+ import { ActionDefinition, PluginDefinition } from "@venn-lang/sdk";
2
+ //#region src/actions/index.d.ts
3
+ /** Every verb in the `mock` namespace, in the order the editor lists them. */
4
+ declare const mockActions: ActionDefinition[];
5
+ //#endregion
6
+ //#region src/plugin.d.ts
7
+ /**
8
+ * The `mock` plugin: in-process mocking, feature flags and a virtual clock.
9
+ *
10
+ * Everything it does happens in memory, so it needs no host capability and
11
+ * opens no socket. It publishes `mock.Mock`, `mock.Interceptor` and
12
+ * `mock.Response` so a flow can name what its verbs hand back.
13
+ */
14
+ declare const mockPlugin: PluginDefinition;
15
+ //#endregion
16
+ //#region src/state/mock-state.types.d.ts
17
+ /** A canned response an interceptor replies with. */
18
+ interface MockResponse {
19
+ status: number;
20
+ body: unknown;
21
+ }
22
+ /** A registered interception: match a method and a path pattern, reply with `respond`. */
23
+ interface Interceptor {
24
+ method: string;
25
+ path: string;
26
+ respond: MockResponse;
27
+ }
28
+ /** A named mock service, optionally seeded from a source such as an OpenAPI file. */
29
+ interface NamedMock {
30
+ name: string;
31
+ from?: string;
32
+ }
33
+ /** The in-process, mutable state the `mock` namespace reads and writes. */
34
+ interface MockState {
35
+ mocks: Map<string, NamedMock>;
36
+ intercepts: Interceptor[];
37
+ flags: Map<string, unknown>;
38
+ /** Virtual "now" in epoch ms once frozen; `undefined` while the clock is live. */
39
+ frozenInstant?: number;
40
+ }
41
+ //#endregion
42
+ //#region src/state/create-mock-state.d.ts
43
+ /**
44
+ * Builds an empty mock state: no mocks, no interceptors, no flags, clock live.
45
+ *
46
+ * @returns A state nothing else holds. `getMockState` returns the shared one
47
+ * the verbs actually read.
48
+ */
49
+ declare function createMockState(): MockState;
50
+ //#endregion
51
+ //#region src/state/shared-state.d.ts
52
+ /**
53
+ * The mock state every `mock` verb reads and writes.
54
+ *
55
+ * One state per process, so everything running in that process sees the same
56
+ * mocks, flags and clock.
57
+ *
58
+ * @returns The live state. It is mutable: writing to it is how the verbs work.
59
+ */
60
+ declare function getMockState(): MockState;
61
+ /**
62
+ * Replaces the shared state with an empty one. Backs `mock.reset`, and lets a
63
+ * test start from a known state.
64
+ *
65
+ * Callers holding the object from a previous {@link getMockState} keep the old
66
+ * one, which no verb reads any more.
67
+ */
68
+ declare function resetMockState(): void;
69
+ //#endregion
70
+ export { type Interceptor, type MockResponse, type MockState, type NamedMock, createMockState, mockPlugin as default, mockPlugin, getMockState, mockActions, resetMockState };
71
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/actions/index.ts","../src/plugin.ts","../src/state/mock-state.types.ts","../src/state/create-mock-state.ts","../src/state/shared-state.ts"],"mappings":";;;cAOa,aAAa;;;;;;;;;;cCIb,YAAY;;;;UCVR;EACf;EACA;;;UAIe;EACf;EACA;EACA,SAAS;;;UAIM;EACf;EACA;;;UAIe;EACf,OAAO,YAAY;EACnB,YAAY;EACZ,OAAO;;EAEP;;;;;;;;;;iBCjBc,mBAAmB;;;;;;;;;;;iBCKnB,gBAAgB;;;;;;;;iBAWhB"}
package/dist/index.js ADDED
@@ -0,0 +1,280 @@
1
+ import { Duration, arg, defineAction, definePlugin, z } from "@venn-lang/sdk";
2
+ import { t } from "@venn-lang/types";
3
+ //#region src/state/create-mock-state.ts
4
+ /**
5
+ * Builds an empty mock state: no mocks, no interceptors, no flags, clock live.
6
+ *
7
+ * @returns A state nothing else holds. `getMockState` returns the shared one
8
+ * the verbs actually read.
9
+ */
10
+ function createMockState() {
11
+ return {
12
+ mocks: /* @__PURE__ */ new Map(),
13
+ intercepts: [],
14
+ flags: /* @__PURE__ */ new Map(),
15
+ frozenInstant: void 0
16
+ };
17
+ }
18
+ //#endregion
19
+ //#region src/state/shared-state.ts
20
+ let shared = createMockState();
21
+ /**
22
+ * The mock state every `mock` verb reads and writes.
23
+ *
24
+ * One state per process, so everything running in that process sees the same
25
+ * mocks, flags and clock.
26
+ *
27
+ * @returns The live state. It is mutable: writing to it is how the verbs work.
28
+ */
29
+ function getMockState() {
30
+ return shared;
31
+ }
32
+ /**
33
+ * Replaces the shared state with an empty one. Backs `mock.reset`, and lets a
34
+ * test start from a known state.
35
+ *
36
+ * Callers holding the object from a previous {@link getMockState} keep the old
37
+ * one, which no verb reads any more.
38
+ */
39
+ function resetMockState() {
40
+ shared = createMockState();
41
+ }
42
+ //#endregion
43
+ //#region src/actions/clock-actions.ts
44
+ /**
45
+ * `mock.clock.freeze("2026-01-01T00:00:00Z")`: pin virtual time to an instant.
46
+ *
47
+ * The instant is only recorded in mock state. It does not drive the host clock,
48
+ * so a verb that asks the host what time it is still gets the real answer.
49
+ */
50
+ const clockFreeze = defineAction({
51
+ name: "clock.freeze",
52
+ doc: "Freeze virtual time at the given instant (recorded in mock state only, for now).",
53
+ args: [arg("at", t.union(t.string, t.number, t.instant), "The instant to hold time at.")],
54
+ result: t.number,
55
+ run: (_ctx, input) => freezeClock(input)
56
+ });
57
+ function freezeClock(input) {
58
+ const instant = toInstant(input.args[0]);
59
+ getMockState().frozenInstant = instant;
60
+ return instant;
61
+ }
62
+ function toInstant(value) {
63
+ if (typeof value === "number") return value;
64
+ const epochMs = instantValue(value);
65
+ if (epochMs !== void 0) return epochMs;
66
+ const ms = Date.parse(String(value));
67
+ return Number.isNaN(ms) ? 0 : ms;
68
+ }
69
+ /**
70
+ * `mock.clock.advance("1h")`: move the stored virtual time forward by a span.
71
+ *
72
+ * Advancing an unfrozen clock counts from the epoch, since there is no instant
73
+ * to move from.
74
+ */
75
+ const clockAdvance = defineAction({
76
+ name: "clock.advance",
77
+ doc: "Advance the frozen virtual time by a duration, e.g. mock.clock.advance(\"1h\").",
78
+ args: [arg("by", t.union(t.string, t.number, t.duration), "How far to move time forward.")],
79
+ result: t.number,
80
+ run: (_ctx, input) => advanceClock(input)
81
+ });
82
+ function advanceClock(input) {
83
+ const state = getMockState();
84
+ const next = (state.frozenInstant ?? 0) + durationMs(input);
85
+ state.frozenInstant = next;
86
+ return next;
87
+ }
88
+ function durationMs(input) {
89
+ const params = input.params ?? {};
90
+ const given = input.args[0] ?? params.by;
91
+ const ms = durationValue(given);
92
+ if (ms !== void 0) return ms;
93
+ const parsed = Duration.safeParse(given);
94
+ return parsed.success ? parsed.data : 0;
95
+ }
96
+ /**
97
+ * The number inside a unit value, when the argument is one.
98
+ *
99
+ * `1h` and `2026-07-23T12:00:00Z` reach an action as `{ kind, ms }` and
100
+ * `{ kind, epochMs }`, the language's own literals rather than strings. Without
101
+ * this unwrapping they parse to `NaN` and the clock silently sits at the epoch.
102
+ */
103
+ function durationValue(value) {
104
+ const unit = value;
105
+ return unit?.kind === "duration" ? unit.ms ?? 0 : void 0;
106
+ }
107
+ function instantValue(value) {
108
+ const unit = value;
109
+ return unit?.kind === "instant" ? unit.epochMs ?? 0 : void 0;
110
+ }
111
+ //#endregion
112
+ //#region src/actions/flag-actions.ts
113
+ /**
114
+ * `mock.flag("new-checkout", { value: "b" })`: set a feature flag.
115
+ *
116
+ * The flag's value is an option rather than a second positional argument, so
117
+ * the common case, turning a flag on, reads as `mock.flag("new-checkout")`.
118
+ */
119
+ const flag = defineAction({
120
+ name: "flag",
121
+ doc: "Set a feature flag; the value comes from opts or defaults to true.",
122
+ params: z.object({ value: z.unknown().optional() }).optional(),
123
+ args: [arg("name", t.string, "Which flag. Its value is an option, true by default.")],
124
+ result: t.dynamic,
125
+ run: (_ctx, input) => setFlag(input)
126
+ });
127
+ function setFlag(input) {
128
+ const params = input.params ?? {};
129
+ const name = String(input.args[0] ?? "");
130
+ const value = params.value ?? true;
131
+ getMockState().flags.set(name, value);
132
+ return value;
133
+ }
134
+ //#endregion
135
+ //#region src/actions/intercept-actions.ts
136
+ /**
137
+ * `mock.intercept("POST", "/charge", { respond })`: catch a request matching a
138
+ * method and a path pattern, and answer it with a canned reply.
139
+ *
140
+ * A `respond` that is not already a response is taken as the body, with status
141
+ * 200, so a plain map can be handed straight in.
142
+ */
143
+ const intercept = defineAction({
144
+ name: "intercept",
145
+ doc: "Register an interceptor, e.g. mock.intercept(\"POST\", \"**/charge\", { respond }).",
146
+ params: z.object({ respond: z.unknown().optional() }).optional(),
147
+ args: [arg("method", t.string, "Which verb to catch: `GET`, `POST`, `*` for any."), arg("url", t.string, "Which URL to catch. A pattern is allowed.")],
148
+ result: t.ref("mock.Interceptor"),
149
+ run: (_ctx, input) => registerIntercept(input)
150
+ });
151
+ function registerIntercept(input) {
152
+ const params = input.params ?? {};
153
+ const entry = {
154
+ method: String(input.args[0] ?? "GET"),
155
+ path: String(input.args[1] ?? ""),
156
+ respond: toResponse(params.respond)
157
+ };
158
+ getMockState().intercepts.push(entry);
159
+ return entry;
160
+ }
161
+ /** `mock.respond(status, { body })`: build a canned reply for an interceptor. */
162
+ const respond = defineAction({
163
+ name: "respond",
164
+ doc: "Build a canned response for an interceptor.",
165
+ params: z.object({
166
+ status: z.number().optional(),
167
+ body: z.unknown().optional()
168
+ }).optional(),
169
+ args: [arg("status", t.number, "The status code to answer with."), arg("body", t.dynamic, "What to answer with. A map or list is sent as JSON.")],
170
+ result: t.ref("mock.Response"),
171
+ run: (_ctx, input) => buildResponse(input)
172
+ });
173
+ function buildResponse(input) {
174
+ const params = input.params ?? {};
175
+ return {
176
+ status: input.args[0] === void 0 ? params.status ?? 200 : Number(input.args[0]),
177
+ body: params.body ?? input.args[1] ?? null
178
+ };
179
+ }
180
+ function toResponse(value) {
181
+ if (isResponse(value)) return value;
182
+ return {
183
+ status: 200,
184
+ body: value ?? null
185
+ };
186
+ }
187
+ function isResponse(value) {
188
+ return typeof value === "object" && value !== null && "status" in value && "body" in value;
189
+ }
190
+ //#endregion
191
+ //#region src/actions/lifecycle-actions.ts
192
+ /**
193
+ * `mock.start("payments", { from: "openapi.yaml" })`: register a named mock
194
+ * service. Starting a name twice replaces the earlier registration.
195
+ */
196
+ const start = defineAction({
197
+ name: "start",
198
+ doc: "Register a named mock service.",
199
+ params: z.object({ from: z.string().optional() }).optional(),
200
+ args: [arg("name", t.string, "What to call this mock, so it can be reached later.")],
201
+ result: t.ref("mock.Mock"),
202
+ run: (_ctx, input) => startMock(input)
203
+ });
204
+ function startMock(input) {
205
+ const params = input.params ?? {};
206
+ const name = String(input.args[0] ?? "");
207
+ const mock = {
208
+ name,
209
+ from: params.from
210
+ };
211
+ getMockState().mocks.set(name, mock);
212
+ return mock;
213
+ }
214
+ /** `mock.stop()`: clear the active mocks and interceptors. Flags and clock survive. */
215
+ const stop = defineAction({
216
+ name: "stop",
217
+ doc: "Clear all registered mocks and interceptors.",
218
+ result: t.void,
219
+ run: () => stopMocks()
220
+ });
221
+ function stopMocks() {
222
+ const state = getMockState();
223
+ state.mocks.clear();
224
+ state.intercepts.length = 0;
225
+ }
226
+ //#endregion
227
+ //#region src/actions/index.ts
228
+ /** Every verb in the `mock` namespace, in the order the editor lists them. */
229
+ const mockActions = [
230
+ start,
231
+ stop,
232
+ intercept,
233
+ respond,
234
+ clockFreeze,
235
+ clockAdvance,
236
+ flag,
237
+ defineAction({
238
+ name: "reset",
239
+ doc: "Reset all mock state to empty.",
240
+ result: t.void,
241
+ run: () => resetMockState()
242
+ })
243
+ ];
244
+ //#endregion
245
+ //#region src/plugin.ts
246
+ /**
247
+ * The `mock` plugin: in-process mocking, feature flags and a virtual clock.
248
+ *
249
+ * Everything it does happens in memory, so it needs no host capability and
250
+ * opens no socket. It publishes `mock.Mock`, `mock.Interceptor` and
251
+ * `mock.Response` so a flow can name what its verbs hand back.
252
+ */
253
+ const mockPlugin = definePlugin({
254
+ name: "venn/mock",
255
+ version: "0.0.0",
256
+ namespace: "mock",
257
+ actions: mockActions,
258
+ typeDefs: {
259
+ /** A registered mock service. `from` names what it was seeded from. */
260
+ Mock: t.record({
261
+ name: t.string,
262
+ from: t.string
263
+ }, { optional: ["from"] }),
264
+ /** One registered interception: what to match, and what to answer with. */
265
+ Interceptor: t.record({
266
+ method: t.string,
267
+ path: t.string,
268
+ respond: t.ref("mock.Response")
269
+ }),
270
+ /** A canned reply. The body is whatever the flow put there. */
271
+ Response: t.record({
272
+ status: t.number,
273
+ body: t.dynamic
274
+ })
275
+ }
276
+ });
277
+ //#endregion
278
+ export { createMockState, mockPlugin as default, mockPlugin, getMockState, mockActions, resetMockState };
279
+
280
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/state/create-mock-state.ts","../src/state/shared-state.ts","../src/actions/clock-actions.ts","../src/actions/flag-actions.ts","../src/actions/intercept-actions.ts","../src/actions/lifecycle-actions.ts","../src/actions/index.ts","../src/types.ts","../src/plugin.ts"],"sourcesContent":["import type { MockState } from \"./mock-state.types.js\";\n\n/**\n * Builds an empty mock state: no mocks, no interceptors, no flags, clock live.\n *\n * @returns A state nothing else holds. `getMockState` returns the shared one\n * the verbs actually read.\n */\nexport function createMockState(): MockState {\n return {\n mocks: new Map(),\n intercepts: [],\n flags: new Map(),\n frozenInstant: undefined,\n };\n}\n","import { createMockState } from \"./create-mock-state.js\";\nimport type { MockState } from \"./mock-state.types.js\";\n\nlet shared: MockState = createMockState();\n\n/**\n * The mock state every `mock` verb reads and writes.\n *\n * One state per process, so everything running in that process sees the same\n * mocks, flags and clock.\n *\n * @returns The live state. It is mutable: writing to it is how the verbs work.\n */\nexport function getMockState(): MockState {\n return shared;\n}\n\n/**\n * Replaces the shared state with an empty one. Backs `mock.reset`, and lets a\n * test start from a known state.\n *\n * Callers holding the object from a previous {@link getMockState} keep the old\n * one, which no verb reads any more.\n */\nexport function resetMockState(): void {\n shared = createMockState();\n}\n","import {\n type ActionDefinition,\n type ActionInput,\n arg,\n Duration,\n defineAction,\n} from \"@venn-lang/sdk\";\nimport { t } from \"@venn-lang/types\";\nimport { getMockState } from \"../state/index.js\";\n\n/**\n * `mock.clock.freeze(\"2026-01-01T00:00:00Z\")`: pin virtual time to an instant.\n *\n * The instant is only recorded in mock state. It does not drive the host clock,\n * so a verb that asks the host what time it is still gets the real answer.\n */\nexport const clockFreeze: ActionDefinition = defineAction({\n name: \"clock.freeze\",\n doc: \"Freeze virtual time at the given instant (recorded in mock state only, for now).\",\n args: [arg(\"at\", t.union(t.string, t.number, t.instant), \"The instant to hold time at.\")],\n result: t.number,\n run: (_ctx, input) => freezeClock(input),\n});\n\nfunction freezeClock(input: ActionInput<unknown>): number {\n const instant = toInstant(input.args[0]);\n getMockState().frozenInstant = instant;\n return instant;\n}\n\nfunction toInstant(value: unknown): number {\n if (typeof value === \"number\") return value;\n const epochMs = instantValue(value);\n if (epochMs !== undefined) return epochMs;\n const ms = Date.parse(String(value));\n return Number.isNaN(ms) ? 0 : ms;\n}\n\n/**\n * `mock.clock.advance(\"1h\")`: move the stored virtual time forward by a span.\n *\n * Advancing an unfrozen clock counts from the epoch, since there is no instant\n * to move from.\n */\nexport const clockAdvance: ActionDefinition = defineAction({\n name: \"clock.advance\",\n doc: 'Advance the frozen virtual time by a duration, e.g. mock.clock.advance(\"1h\").',\n args: [arg(\"by\", t.union(t.string, t.number, t.duration), \"How far to move time forward.\")],\n result: t.number,\n run: (_ctx, input) => advanceClock(input),\n});\n\nfunction advanceClock(input: ActionInput<unknown>): number {\n const state = getMockState();\n const next = (state.frozenInstant ?? 0) + durationMs(input);\n state.frozenInstant = next;\n return next;\n}\n\nfunction durationMs(input: ActionInput<unknown>): number {\n const params = (input.params ?? {}) as { by?: unknown };\n const given = input.args[0] ?? params.by;\n const ms = durationValue(given);\n if (ms !== undefined) return ms;\n const parsed = Duration.safeParse(given);\n return parsed.success ? parsed.data : 0;\n}\n\n/**\n * The number inside a unit value, when the argument is one.\n *\n * `1h` and `2026-07-23T12:00:00Z` reach an action as `{ kind, ms }` and\n * `{ kind, epochMs }`, the language's own literals rather than strings. Without\n * this unwrapping they parse to `NaN` and the clock silently sits at the epoch.\n */\nfunction durationValue(value: unknown): number | undefined {\n const unit = value as { kind?: string; ms?: number } | null | undefined;\n return unit?.kind === \"duration\" ? (unit.ms ?? 0) : undefined;\n}\n\nfunction instantValue(value: unknown): number | undefined {\n const unit = value as { kind?: string; epochMs?: number } | null | undefined;\n return unit?.kind === \"instant\" ? (unit.epochMs ?? 0) : undefined;\n}\n","import { type ActionDefinition, type ActionInput, arg, defineAction, z } from \"@venn-lang/sdk\";\nimport { t } from \"@venn-lang/types\";\nimport { getMockState } from \"../state/index.js\";\n\n/**\n * `mock.flag(\"new-checkout\", { value: \"b\" })`: set a feature flag.\n *\n * The flag's value is an option rather than a second positional argument, so\n * the common case, turning a flag on, reads as `mock.flag(\"new-checkout\")`.\n */\nexport const flag: ActionDefinition = defineAction({\n name: \"flag\",\n doc: \"Set a feature flag; the value comes from opts or defaults to true.\",\n params: z.object({ value: z.unknown().optional() }).optional(),\n args: [arg(\"name\", t.string, \"Which flag. Its value is an option, true by default.\")],\n result: t.dynamic,\n run: (_ctx, input) => setFlag(input),\n});\n\nfunction setFlag(input: ActionInput<unknown>): unknown {\n const params = (input.params ?? {}) as { value?: unknown };\n const name = String(input.args[0] ?? \"\");\n const value = params.value ?? true;\n getMockState().flags.set(name, value);\n return value;\n}\n","import { type ActionDefinition, type ActionInput, arg, defineAction, z } from \"@venn-lang/sdk\";\nimport { t } from \"@venn-lang/types\";\nimport { getMockState, type Interceptor, type MockResponse } from \"../state/index.js\";\n\n/**\n * `mock.intercept(\"POST\", \"/charge\", { respond })`: catch a request matching a\n * method and a path pattern, and answer it with a canned reply.\n *\n * A `respond` that is not already a response is taken as the body, with status\n * 200, so a plain map can be handed straight in.\n */\nexport const intercept: ActionDefinition = defineAction({\n name: \"intercept\",\n doc: 'Register an interceptor, e.g. mock.intercept(\"POST\", \"**/charge\", { respond }).',\n params: z.object({ respond: z.unknown().optional() }).optional(),\n args: [\n arg(\"method\", t.string, \"Which verb to catch: `GET`, `POST`, `*` for any.\"),\n arg(\"url\", t.string, \"Which URL to catch. A pattern is allowed.\"),\n ],\n result: t.ref(\"mock.Interceptor\"),\n run: (_ctx, input) => registerIntercept(input),\n});\n\nfunction registerIntercept(input: ActionInput<unknown>): Interceptor {\n const params = (input.params ?? {}) as { respond?: unknown };\n const entry: Interceptor = {\n method: String(input.args[0] ?? \"GET\"),\n path: String(input.args[1] ?? \"\"),\n respond: toResponse(params.respond),\n };\n getMockState().intercepts.push(entry);\n return entry;\n}\n\n/** `mock.respond(status, { body })`: build a canned reply for an interceptor. */\nexport const respond: ActionDefinition = defineAction({\n name: \"respond\",\n doc: \"Build a canned response for an interceptor.\",\n params: z.object({ status: z.number().optional(), body: z.unknown().optional() }).optional(),\n // Status and body may arrive positionally or by name, and `run` reads\n // whichever came. The name wins for the body, the position wins for the status.\n args: [\n arg(\"status\", t.number, \"The status code to answer with.\"),\n arg(\"body\", t.dynamic, \"What to answer with. A map or list is sent as JSON.\"),\n ],\n result: t.ref(\"mock.Response\"),\n run: (_ctx, input) => buildResponse(input),\n});\n\nfunction buildResponse(input: ActionInput<unknown>): MockResponse {\n const params = (input.params ?? {}) as { status?: number; body?: unknown };\n const status = input.args[0] === undefined ? (params.status ?? 200) : Number(input.args[0]);\n return { status, body: params.body ?? input.args[1] ?? null };\n}\n\nfunction toResponse(value: unknown): MockResponse {\n if (isResponse(value)) return value;\n return { status: 200, body: value ?? null };\n}\n\nfunction isResponse(value: unknown): value is MockResponse {\n return typeof value === \"object\" && value !== null && \"status\" in value && \"body\" in value;\n}\n","import { type ActionDefinition, type ActionInput, arg, defineAction, z } from \"@venn-lang/sdk\";\nimport { t } from \"@venn-lang/types\";\nimport { getMockState, type NamedMock, resetMockState } from \"../state/index.js\";\n\n/**\n * `mock.start(\"payments\", { from: \"openapi.yaml\" })`: register a named mock\n * service. Starting a name twice replaces the earlier registration.\n */\nexport const start: ActionDefinition = defineAction({\n name: \"start\",\n doc: \"Register a named mock service.\",\n params: z.object({ from: z.string().optional() }).optional(),\n args: [arg(\"name\", t.string, \"What to call this mock, so it can be reached later.\")],\n result: t.ref(\"mock.Mock\"),\n run: (_ctx, input) => startMock(input),\n});\n\nfunction startMock(input: ActionInput<unknown>): NamedMock {\n const params = (input.params ?? {}) as { from?: string };\n const name = String(input.args[0] ?? \"\");\n const mock: NamedMock = { name, from: params.from };\n getMockState().mocks.set(name, mock);\n return mock;\n}\n\n/** `mock.stop()`: clear the active mocks and interceptors. Flags and clock survive. */\nexport const stop: ActionDefinition = defineAction({\n name: \"stop\",\n doc: \"Clear all registered mocks and interceptors.\",\n result: t.void,\n run: () => stopMocks(),\n});\n\nfunction stopMocks(): void {\n const state = getMockState();\n state.mocks.clear();\n state.intercepts.length = 0;\n}\n\n/** `mock.reset()`: clear every piece of mock state, the flags and the clock included. */\nexport const reset: ActionDefinition = defineAction({\n name: \"reset\",\n doc: \"Reset all mock state to empty.\",\n result: t.void,\n run: () => resetMockState(),\n});\n","import type { ActionDefinition } from \"@venn-lang/sdk\";\nimport { clockAdvance, clockFreeze } from \"./clock-actions.js\";\nimport { flag } from \"./flag-actions.js\";\nimport { intercept, respond } from \"./intercept-actions.js\";\nimport { reset, start, stop } from \"./lifecycle-actions.js\";\n\n/** Every verb in the `mock` namespace, in the order the editor lists them. */\nexport const mockActions: ActionDefinition[] = [\n start,\n stop,\n intercept,\n respond,\n clockFreeze,\n clockAdvance,\n flag,\n reset,\n];\n","import { type TypeSpec, t } from \"@venn-lang/types\";\n\n/**\n * The types `@venn-lang/mock` publishes: `mock.Mock`, `mock.Interceptor` and\n * `mock.Response`.\n *\n * They mirror `state/mock-state.types.ts` by hand, dropping the prefix the\n * namespace already supplies (`NamedMock` becomes `Mock`). Change one side and\n * the other has to follow.\n */\nexport const mockTypeDefs: Readonly<Record<string, TypeSpec>> = {\n /** A registered mock service. `from` names what it was seeded from. */\n Mock: t.record({ name: t.string, from: t.string }, { optional: [\"from\"] }),\n /** One registered interception: what to match, and what to answer with. */\n Interceptor: t.record({\n method: t.string,\n path: t.string,\n respond: t.ref(\"mock.Response\"),\n }),\n /** A canned reply. The body is whatever the flow put there. */\n Response: t.record({ status: t.number, body: t.dynamic }),\n};\n","import { definePlugin, type PluginDefinition } from \"@venn-lang/sdk\";\nimport { mockActions } from \"./actions/index.js\";\nimport { mockTypeDefs } from \"./types.js\";\n\n/**\n * The `mock` plugin: in-process mocking, feature flags and a virtual clock.\n *\n * Everything it does happens in memory, so it needs no host capability and\n * opens no socket. It publishes `mock.Mock`, `mock.Interceptor` and\n * `mock.Response` so a flow can name what its verbs hand back.\n */\nexport const mockPlugin: PluginDefinition = definePlugin({\n name: \"venn/mock\",\n version: \"0.0.0\",\n namespace: \"mock\",\n actions: mockActions,\n typeDefs: mockTypeDefs,\n});\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@venn-lang/mock",
3
+ "version": "0.1.0",
4
+ "description": "The mock namespace: named mocks, HTTP interceptors, feature flags and a virtual clock.",
5
+ "keywords": [
6
+ "venn",
7
+ "testing",
8
+ "e2e",
9
+ "mock"
10
+ ],
11
+ "homepage": "https://github.com/venn-lang/venn/tree/main/packages/std-mock#readme",
12
+ "bugs": "https://github.com/venn-lang/venn/issues",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/venn-lang/venn.git",
16
+ "directory": "packages/std-mock"
17
+ },
18
+ "license": "MIT",
19
+ "author": "Vinicius Borges",
20
+ "type": "module",
21
+ "sideEffects": false,
22
+ "exports": {
23
+ ".": {
24
+ "development": "./src/index.ts",
25
+ "types": "./dist/index.d.ts",
26
+ "import": "./dist/index.js",
27
+ "default": "./dist/index.js"
28
+ }
29
+ },
30
+ "files": [
31
+ "dist",
32
+ "src",
33
+ "!src/**/*.test.ts",
34
+ "!src/**/*.suite.ts"
35
+ ],
36
+ "publishConfig": {
37
+ "access": "public"
38
+ },
39
+ "dependencies": {
40
+ "@venn-lang/contracts": "0.1.0",
41
+ "@venn-lang/sdk": "0.1.0",
42
+ "@venn-lang/types": "0.1.0"
43
+ },
44
+ "devDependencies": {
45
+ "tsdown": "^0.22.14",
46
+ "typescript": "^7.0.2",
47
+ "vitest": "^4.1.10"
48
+ },
49
+ "scripts": {
50
+ "build": "tsdown",
51
+ "test": "vitest run",
52
+ "typecheck": "tsc --noEmit"
53
+ }
54
+ }
@@ -0,0 +1,84 @@
1
+ import {
2
+ type ActionDefinition,
3
+ type ActionInput,
4
+ arg,
5
+ Duration,
6
+ defineAction,
7
+ } from "@venn-lang/sdk";
8
+ import { t } from "@venn-lang/types";
9
+ import { getMockState } from "../state/index.js";
10
+
11
+ /**
12
+ * `mock.clock.freeze("2026-01-01T00:00:00Z")`: pin virtual time to an instant.
13
+ *
14
+ * The instant is only recorded in mock state. It does not drive the host clock,
15
+ * so a verb that asks the host what time it is still gets the real answer.
16
+ */
17
+ export const clockFreeze: ActionDefinition = defineAction({
18
+ name: "clock.freeze",
19
+ doc: "Freeze virtual time at the given instant (recorded in mock state only, for now).",
20
+ args: [arg("at", t.union(t.string, t.number, t.instant), "The instant to hold time at.")],
21
+ result: t.number,
22
+ run: (_ctx, input) => freezeClock(input),
23
+ });
24
+
25
+ function freezeClock(input: ActionInput<unknown>): number {
26
+ const instant = toInstant(input.args[0]);
27
+ getMockState().frozenInstant = instant;
28
+ return instant;
29
+ }
30
+
31
+ function toInstant(value: unknown): number {
32
+ if (typeof value === "number") return value;
33
+ const epochMs = instantValue(value);
34
+ if (epochMs !== undefined) return epochMs;
35
+ const ms = Date.parse(String(value));
36
+ return Number.isNaN(ms) ? 0 : ms;
37
+ }
38
+
39
+ /**
40
+ * `mock.clock.advance("1h")`: move the stored virtual time forward by a span.
41
+ *
42
+ * Advancing an unfrozen clock counts from the epoch, since there is no instant
43
+ * to move from.
44
+ */
45
+ export const clockAdvance: ActionDefinition = defineAction({
46
+ name: "clock.advance",
47
+ doc: 'Advance the frozen virtual time by a duration, e.g. mock.clock.advance("1h").',
48
+ args: [arg("by", t.union(t.string, t.number, t.duration), "How far to move time forward.")],
49
+ result: t.number,
50
+ run: (_ctx, input) => advanceClock(input),
51
+ });
52
+
53
+ function advanceClock(input: ActionInput<unknown>): number {
54
+ const state = getMockState();
55
+ const next = (state.frozenInstant ?? 0) + durationMs(input);
56
+ state.frozenInstant = next;
57
+ return next;
58
+ }
59
+
60
+ function durationMs(input: ActionInput<unknown>): number {
61
+ const params = (input.params ?? {}) as { by?: unknown };
62
+ const given = input.args[0] ?? params.by;
63
+ const ms = durationValue(given);
64
+ if (ms !== undefined) return ms;
65
+ const parsed = Duration.safeParse(given);
66
+ return parsed.success ? parsed.data : 0;
67
+ }
68
+
69
+ /**
70
+ * The number inside a unit value, when the argument is one.
71
+ *
72
+ * `1h` and `2026-07-23T12:00:00Z` reach an action as `{ kind, ms }` and
73
+ * `{ kind, epochMs }`, the language's own literals rather than strings. Without
74
+ * this unwrapping they parse to `NaN` and the clock silently sits at the epoch.
75
+ */
76
+ function durationValue(value: unknown): number | undefined {
77
+ const unit = value as { kind?: string; ms?: number } | null | undefined;
78
+ return unit?.kind === "duration" ? (unit.ms ?? 0) : undefined;
79
+ }
80
+
81
+ function instantValue(value: unknown): number | undefined {
82
+ const unit = value as { kind?: string; epochMs?: number } | null | undefined;
83
+ return unit?.kind === "instant" ? (unit.epochMs ?? 0) : undefined;
84
+ }
@@ -0,0 +1,26 @@
1
+ import { type ActionDefinition, type ActionInput, arg, defineAction, z } from "@venn-lang/sdk";
2
+ import { t } from "@venn-lang/types";
3
+ import { getMockState } from "../state/index.js";
4
+
5
+ /**
6
+ * `mock.flag("new-checkout", { value: "b" })`: set a feature flag.
7
+ *
8
+ * The flag's value is an option rather than a second positional argument, so
9
+ * the common case, turning a flag on, reads as `mock.flag("new-checkout")`.
10
+ */
11
+ export const flag: ActionDefinition = defineAction({
12
+ name: "flag",
13
+ doc: "Set a feature flag; the value comes from opts or defaults to true.",
14
+ params: z.object({ value: z.unknown().optional() }).optional(),
15
+ args: [arg("name", t.string, "Which flag. Its value is an option, true by default.")],
16
+ result: t.dynamic,
17
+ run: (_ctx, input) => setFlag(input),
18
+ });
19
+
20
+ function setFlag(input: ActionInput<unknown>): unknown {
21
+ const params = (input.params ?? {}) as { value?: unknown };
22
+ const name = String(input.args[0] ?? "");
23
+ const value = params.value ?? true;
24
+ getMockState().flags.set(name, value);
25
+ return value;
26
+ }
@@ -0,0 +1,17 @@
1
+ import type { ActionDefinition } from "@venn-lang/sdk";
2
+ import { clockAdvance, clockFreeze } from "./clock-actions.js";
3
+ import { flag } from "./flag-actions.js";
4
+ import { intercept, respond } from "./intercept-actions.js";
5
+ import { reset, start, stop } from "./lifecycle-actions.js";
6
+
7
+ /** Every verb in the `mock` namespace, in the order the editor lists them. */
8
+ export const mockActions: ActionDefinition[] = [
9
+ start,
10
+ stop,
11
+ intercept,
12
+ respond,
13
+ clockFreeze,
14
+ clockAdvance,
15
+ flag,
16
+ reset,
17
+ ];
@@ -0,0 +1,63 @@
1
+ import { type ActionDefinition, type ActionInput, arg, defineAction, z } from "@venn-lang/sdk";
2
+ import { t } from "@venn-lang/types";
3
+ import { getMockState, type Interceptor, type MockResponse } from "../state/index.js";
4
+
5
+ /**
6
+ * `mock.intercept("POST", "/charge", { respond })`: catch a request matching a
7
+ * method and a path pattern, and answer it with a canned reply.
8
+ *
9
+ * A `respond` that is not already a response is taken as the body, with status
10
+ * 200, so a plain map can be handed straight in.
11
+ */
12
+ export const intercept: ActionDefinition = defineAction({
13
+ name: "intercept",
14
+ doc: 'Register an interceptor, e.g. mock.intercept("POST", "**/charge", { respond }).',
15
+ params: z.object({ respond: z.unknown().optional() }).optional(),
16
+ args: [
17
+ arg("method", t.string, "Which verb to catch: `GET`, `POST`, `*` for any."),
18
+ arg("url", t.string, "Which URL to catch. A pattern is allowed."),
19
+ ],
20
+ result: t.ref("mock.Interceptor"),
21
+ run: (_ctx, input) => registerIntercept(input),
22
+ });
23
+
24
+ function registerIntercept(input: ActionInput<unknown>): Interceptor {
25
+ const params = (input.params ?? {}) as { respond?: unknown };
26
+ const entry: Interceptor = {
27
+ method: String(input.args[0] ?? "GET"),
28
+ path: String(input.args[1] ?? ""),
29
+ respond: toResponse(params.respond),
30
+ };
31
+ getMockState().intercepts.push(entry);
32
+ return entry;
33
+ }
34
+
35
+ /** `mock.respond(status, { body })`: build a canned reply for an interceptor. */
36
+ export const respond: ActionDefinition = defineAction({
37
+ name: "respond",
38
+ doc: "Build a canned response for an interceptor.",
39
+ params: z.object({ status: z.number().optional(), body: z.unknown().optional() }).optional(),
40
+ // Status and body may arrive positionally or by name, and `run` reads
41
+ // whichever came. The name wins for the body, the position wins for the status.
42
+ args: [
43
+ arg("status", t.number, "The status code to answer with."),
44
+ arg("body", t.dynamic, "What to answer with. A map or list is sent as JSON."),
45
+ ],
46
+ result: t.ref("mock.Response"),
47
+ run: (_ctx, input) => buildResponse(input),
48
+ });
49
+
50
+ function buildResponse(input: ActionInput<unknown>): MockResponse {
51
+ const params = (input.params ?? {}) as { status?: number; body?: unknown };
52
+ const status = input.args[0] === undefined ? (params.status ?? 200) : Number(input.args[0]);
53
+ return { status, body: params.body ?? input.args[1] ?? null };
54
+ }
55
+
56
+ function toResponse(value: unknown): MockResponse {
57
+ if (isResponse(value)) return value;
58
+ return { status: 200, body: value ?? null };
59
+ }
60
+
61
+ function isResponse(value: unknown): value is MockResponse {
62
+ return typeof value === "object" && value !== null && "status" in value && "body" in value;
63
+ }
@@ -0,0 +1,46 @@
1
+ import { type ActionDefinition, type ActionInput, arg, defineAction, z } from "@venn-lang/sdk";
2
+ import { t } from "@venn-lang/types";
3
+ import { getMockState, type NamedMock, resetMockState } from "../state/index.js";
4
+
5
+ /**
6
+ * `mock.start("payments", { from: "openapi.yaml" })`: register a named mock
7
+ * service. Starting a name twice replaces the earlier registration.
8
+ */
9
+ export const start: ActionDefinition = defineAction({
10
+ name: "start",
11
+ doc: "Register a named mock service.",
12
+ params: z.object({ from: z.string().optional() }).optional(),
13
+ args: [arg("name", t.string, "What to call this mock, so it can be reached later.")],
14
+ result: t.ref("mock.Mock"),
15
+ run: (_ctx, input) => startMock(input),
16
+ });
17
+
18
+ function startMock(input: ActionInput<unknown>): NamedMock {
19
+ const params = (input.params ?? {}) as { from?: string };
20
+ const name = String(input.args[0] ?? "");
21
+ const mock: NamedMock = { name, from: params.from };
22
+ getMockState().mocks.set(name, mock);
23
+ return mock;
24
+ }
25
+
26
+ /** `mock.stop()`: clear the active mocks and interceptors. Flags and clock survive. */
27
+ export const stop: ActionDefinition = defineAction({
28
+ name: "stop",
29
+ doc: "Clear all registered mocks and interceptors.",
30
+ result: t.void,
31
+ run: () => stopMocks(),
32
+ });
33
+
34
+ function stopMocks(): void {
35
+ const state = getMockState();
36
+ state.mocks.clear();
37
+ state.intercepts.length = 0;
38
+ }
39
+
40
+ /** `mock.reset()`: clear every piece of mock state, the flags and the clock included. */
41
+ export const reset: ActionDefinition = defineAction({
42
+ name: "reset",
43
+ doc: "Reset all mock state to empty.",
44
+ result: t.void,
45
+ run: () => resetMockState(),
46
+ });
package/src/index.ts ADDED
@@ -0,0 +1,8 @@
1
+ // The `mock` namespace: named mock services, HTTP interceptors, feature flags
2
+ // and a virtual clock, all in-process. State lives in one shared MockState the
3
+ // actions read and write; tests reach it through getMockState(). No network,
4
+ // no port.
5
+
6
+ export { mockActions } from "./actions/index.js";
7
+ export { mockPlugin, mockPlugin as default } from "./plugin.js";
8
+ export * from "./state/index.js";
package/src/plugin.ts ADDED
@@ -0,0 +1,18 @@
1
+ import { definePlugin, type PluginDefinition } from "@venn-lang/sdk";
2
+ import { mockActions } from "./actions/index.js";
3
+ import { mockTypeDefs } from "./types.js";
4
+
5
+ /**
6
+ * The `mock` plugin: in-process mocking, feature flags and a virtual clock.
7
+ *
8
+ * Everything it does happens in memory, so it needs no host capability and
9
+ * opens no socket. It publishes `mock.Mock`, `mock.Interceptor` and
10
+ * `mock.Response` so a flow can name what its verbs hand back.
11
+ */
12
+ export const mockPlugin: PluginDefinition = definePlugin({
13
+ name: "venn/mock",
14
+ version: "0.0.0",
15
+ namespace: "mock",
16
+ actions: mockActions,
17
+ typeDefs: mockTypeDefs,
18
+ });
@@ -0,0 +1,16 @@
1
+ import type { MockState } from "./mock-state.types.js";
2
+
3
+ /**
4
+ * Builds an empty mock state: no mocks, no interceptors, no flags, clock live.
5
+ *
6
+ * @returns A state nothing else holds. `getMockState` returns the shared one
7
+ * the verbs actually read.
8
+ */
9
+ export function createMockState(): MockState {
10
+ return {
11
+ mocks: new Map(),
12
+ intercepts: [],
13
+ flags: new Map(),
14
+ frozenInstant: undefined,
15
+ };
16
+ }
@@ -0,0 +1,3 @@
1
+ export { createMockState } from "./create-mock-state.js";
2
+ export type { Interceptor, MockResponse, MockState, NamedMock } from "./mock-state.types.js";
3
+ export { getMockState, resetMockState } from "./shared-state.js";
@@ -0,0 +1,27 @@
1
+ /** A canned response an interceptor replies with. */
2
+ export interface MockResponse {
3
+ status: number;
4
+ body: unknown;
5
+ }
6
+
7
+ /** A registered interception: match a method and a path pattern, reply with `respond`. */
8
+ export interface Interceptor {
9
+ method: string;
10
+ path: string;
11
+ respond: MockResponse;
12
+ }
13
+
14
+ /** A named mock service, optionally seeded from a source such as an OpenAPI file. */
15
+ export interface NamedMock {
16
+ name: string;
17
+ from?: string;
18
+ }
19
+
20
+ /** The in-process, mutable state the `mock` namespace reads and writes. */
21
+ export interface MockState {
22
+ mocks: Map<string, NamedMock>;
23
+ intercepts: Interceptor[];
24
+ flags: Map<string, unknown>;
25
+ /** Virtual "now" in epoch ms once frozen; `undefined` while the clock is live. */
26
+ frozenInstant?: number;
27
+ }
@@ -0,0 +1,27 @@
1
+ import { createMockState } from "./create-mock-state.js";
2
+ import type { MockState } from "./mock-state.types.js";
3
+
4
+ let shared: MockState = createMockState();
5
+
6
+ /**
7
+ * The mock state every `mock` verb reads and writes.
8
+ *
9
+ * One state per process, so everything running in that process sees the same
10
+ * mocks, flags and clock.
11
+ *
12
+ * @returns The live state. It is mutable: writing to it is how the verbs work.
13
+ */
14
+ export function getMockState(): MockState {
15
+ return shared;
16
+ }
17
+
18
+ /**
19
+ * Replaces the shared state with an empty one. Backs `mock.reset`, and lets a
20
+ * test start from a known state.
21
+ *
22
+ * Callers holding the object from a previous {@link getMockState} keep the old
23
+ * one, which no verb reads any more.
24
+ */
25
+ export function resetMockState(): void {
26
+ shared = createMockState();
27
+ }
package/src/types.ts ADDED
@@ -0,0 +1,22 @@
1
+ import { type TypeSpec, t } from "@venn-lang/types";
2
+
3
+ /**
4
+ * The types `@venn-lang/mock` publishes: `mock.Mock`, `mock.Interceptor` and
5
+ * `mock.Response`.
6
+ *
7
+ * They mirror `state/mock-state.types.ts` by hand, dropping the prefix the
8
+ * namespace already supplies (`NamedMock` becomes `Mock`). Change one side and
9
+ * the other has to follow.
10
+ */
11
+ export const mockTypeDefs: Readonly<Record<string, TypeSpec>> = {
12
+ /** A registered mock service. `from` names what it was seeded from. */
13
+ Mock: t.record({ name: t.string, from: t.string }, { optional: ["from"] }),
14
+ /** One registered interception: what to match, and what to answer with. */
15
+ Interceptor: t.record({
16
+ method: t.string,
17
+ path: t.string,
18
+ respond: t.ref("mock.Response"),
19
+ }),
20
+ /** A canned reply. The body is whatever the flow put there. */
21
+ Response: t.record({ status: t.number, body: t.dynamic }),
22
+ };