@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 +21 -0
- package/README.md +115 -0
- package/dist/index.d.ts +71 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +280 -0
- package/dist/index.js.map +1 -0
- package/package.json +54 -0
- package/src/actions/clock-actions.ts +84 -0
- package/src/actions/flag-actions.ts +26 -0
- package/src/actions/index.ts +17 -0
- package/src/actions/intercept-actions.ts +63 -0
- package/src/actions/lifecycle-actions.ts +46 -0
- package/src/index.ts +8 -0
- package/src/plugin.ts +18 -0
- package/src/state/create-mock-state.ts +16 -0
- package/src/state/index.ts +3 -0
- package/src/state/mock-state.types.ts +27 -0
- package/src/state/shared-state.ts +27 -0
- package/src/types.ts +22 -0
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`.
|
package/dist/index.d.ts
ADDED
|
@@ -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,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
|
+
};
|