definitely-fine 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 +281 -0
- package/dist/index.d.ts +604 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +729 -0
- package/dist/index.js.map +1 -0
- package/package.json +29 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 definitely-fine contributors
|
|
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,281 @@
|
|
|
1
|
+
# definitely-fine
|
|
2
|
+
|
|
3
|
+
`definitely-fine` is the core package for defining, saving, loading, and activating typed runtime scenarios.
|
|
4
|
+
|
|
5
|
+
It is useful when you want your tests to steer selected application behavior by contract, while the application still executes its normal code paths.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm add definitely-fine
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Important
|
|
14
|
+
|
|
15
|
+
> [!IMPORTANT]
|
|
16
|
+
> Disable runtime interception in production.
|
|
17
|
+
>
|
|
18
|
+
> If a production process should never honor saved scenarios, create the runtime with `enabled: false`.
|
|
19
|
+
> This makes every wrapper fall through to the original implementation even if a scenario id is present.
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { createRuntime } from "definitely-fine";
|
|
23
|
+
|
|
24
|
+
const runtime = createRuntime<DemoContract>({
|
|
25
|
+
enabled: process.env.NODE_ENV !== "production",
|
|
26
|
+
});
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Use this as the default recommendation for application code. Treat interception as test-only unless you have an explicit non-production use case.
|
|
30
|
+
|
|
31
|
+
## Core Idea
|
|
32
|
+
|
|
33
|
+
You define a contract for the behavior you want to intercept, create a scenario that targets specific function or service calls, save that scenario, then wrap your real implementation with a runtime.
|
|
34
|
+
|
|
35
|
+
At execution time, an active scenario id decides whether the runtime should intercept a call or fall through to the real implementation.
|
|
36
|
+
|
|
37
|
+
```mermaid
|
|
38
|
+
flowchart LR
|
|
39
|
+
subgraph W[Writer process]
|
|
40
|
+
W1[createScenario]
|
|
41
|
+
W2[define rules]
|
|
42
|
+
W3[save scenario]
|
|
43
|
+
W4[scenario id]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
subgraph S[Shared scenario storage]
|
|
47
|
+
S1[(persisted scenario JSON)]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
subgraph R[Runtime process]
|
|
51
|
+
R1[runWithRuntimeScenarioContext]
|
|
52
|
+
R2[createRuntime wrappers]
|
|
53
|
+
R3[wrapped function or service]
|
|
54
|
+
R4[real implementation]
|
|
55
|
+
R5[scenario-defined return or throw]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
W1 --> W2 --> W3
|
|
59
|
+
W3 --> S1
|
|
60
|
+
W3 --> W4
|
|
61
|
+
W4 --> R1
|
|
62
|
+
R1 --> R2 --> R3
|
|
63
|
+
R3 -- load active scenario --> S1
|
|
64
|
+
R3 --> R4
|
|
65
|
+
R3 --> R5
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Minimal Example
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
import {
|
|
72
|
+
createRuntime,
|
|
73
|
+
createScenario,
|
|
74
|
+
runWithRuntimeScenarioContext,
|
|
75
|
+
} from "definitely-fine";
|
|
76
|
+
|
|
77
|
+
type DemoContract = {
|
|
78
|
+
services: {
|
|
79
|
+
math: {
|
|
80
|
+
double(value: number): number;
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
functions: {
|
|
84
|
+
generateId(prefix: string): string;
|
|
85
|
+
};
|
|
86
|
+
errors: Record<string, never>;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const scenario = createScenario<DemoContract>();
|
|
90
|
+
|
|
91
|
+
scenario.fn("generateId").onCall(1).returns("generated-1");
|
|
92
|
+
scenario.service("math").method("double").onCall(2).returns(99);
|
|
93
|
+
|
|
94
|
+
await scenario.save();
|
|
95
|
+
|
|
96
|
+
const runtime = createRuntime<DemoContract>();
|
|
97
|
+
|
|
98
|
+
const generateId = runtime.wrapSyncFunction("generateId", (prefix) => {
|
|
99
|
+
return `${prefix}-live`;
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const math = runtime.wrapSyncService("math", {
|
|
103
|
+
double(value: number): number {
|
|
104
|
+
return value * 2;
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const result = runWithRuntimeScenarioContext(
|
|
109
|
+
{ scenarioId: scenario.id },
|
|
110
|
+
() => {
|
|
111
|
+
return {
|
|
112
|
+
id: generateId("user"),
|
|
113
|
+
firstDouble: math.double(2),
|
|
114
|
+
secondDouble: math.double(2),
|
|
115
|
+
};
|
|
116
|
+
},
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
console.log(result);
|
|
120
|
+
// {
|
|
121
|
+
// id: "generated-1",
|
|
122
|
+
// firstDouble: 4,
|
|
123
|
+
// secondDouble: 99,
|
|
124
|
+
// }
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Main APIs
|
|
128
|
+
|
|
129
|
+
### `createScenario()`
|
|
130
|
+
|
|
131
|
+
Creates a scenario builder with a stable id.
|
|
132
|
+
|
|
133
|
+
Use `fn()` when targeting a standalone function:
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
import { createScenario } from "definitely-fine";
|
|
137
|
+
|
|
138
|
+
const scenario = createScenario<DemoContract>();
|
|
139
|
+
|
|
140
|
+
scenario.fn("generateId").onCall(1).returns("generated-1");
|
|
141
|
+
scenario.fn("generateId").onCall(2).throwsMessage("blocked");
|
|
142
|
+
|
|
143
|
+
await scenario.save();
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Use `service().method()` when targeting methods on a wrapped service object:
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
import { createScenario } from "definitely-fine";
|
|
150
|
+
|
|
151
|
+
const scenario = createScenario<DemoContract>();
|
|
152
|
+
|
|
153
|
+
scenario.service("math").method("double").onCall(1).returns(10);
|
|
154
|
+
|
|
155
|
+
await scenario.save();
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### `createRuntime()`
|
|
159
|
+
|
|
160
|
+
Creates a runtime that loads persisted scenarios and wraps implementations.
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
import { createRuntime } from "definitely-fine";
|
|
164
|
+
|
|
165
|
+
const runtime = createRuntime<DemoContract>();
|
|
166
|
+
|
|
167
|
+
const generateId = runtime.wrapSyncFunction("generateId", (prefix) => {
|
|
168
|
+
return `${prefix}-live`;
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const math = runtime.wrapSyncService("math", {
|
|
172
|
+
double(value: number): number {
|
|
173
|
+
return value * 2;
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
```mermaid
|
|
179
|
+
sequenceDiagram
|
|
180
|
+
participant App as App process
|
|
181
|
+
participant Runtime as definitely-fine runtime
|
|
182
|
+
participant Store as Scenario storage
|
|
183
|
+
|
|
184
|
+
App->>Runtime: call wrapped function or service method
|
|
185
|
+
Runtime->>Runtime: resolve active scenario id
|
|
186
|
+
alt no active scenario id
|
|
187
|
+
Runtime-->>App: call original implementation
|
|
188
|
+
else active scenario id present
|
|
189
|
+
Runtime->>Store: load persisted scenario
|
|
190
|
+
Store-->>Runtime: scenario JSON or missing
|
|
191
|
+
alt matching rule for this call
|
|
192
|
+
Runtime-->>App: mocked return or configured throw
|
|
193
|
+
else no matching rule
|
|
194
|
+
Runtime-->>App: call original implementation
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Disable interception explicitly when the runtime must never load or honor scenarios:
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
const runtime = createRuntime<DemoContract>({
|
|
203
|
+
enabled: false,
|
|
204
|
+
});
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### `runWithRuntimeScenarioContext()`
|
|
208
|
+
|
|
209
|
+
Activates a scenario id for the current execution scope.
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
const result = runWithRuntimeScenarioContext(
|
|
213
|
+
{ scenarioId: scenario.id },
|
|
214
|
+
() => {
|
|
215
|
+
return generateId("user");
|
|
216
|
+
},
|
|
217
|
+
);
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### `getRuntimeScenarioId()`
|
|
221
|
+
|
|
222
|
+
Returns the active scenario id inside wrapped code.
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
import { getRuntimeScenarioId } from "definitely-fine";
|
|
226
|
+
|
|
227
|
+
const generateId = runtime.wrapSyncFunction("generateId", (prefix) => {
|
|
228
|
+
return `${prefix}-${getRuntimeScenarioId() ?? "live"}`;
|
|
229
|
+
});
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## Storage
|
|
233
|
+
|
|
234
|
+
By default, the built-in JSON adapter infers its storage directory automatically. When inference succeeds, scenarios are stored under `node_modules/.cache/definitely-fine/scenarios`.
|
|
235
|
+
|
|
236
|
+
That means this is valid without providing any storage options:
|
|
237
|
+
|
|
238
|
+
```ts
|
|
239
|
+
import { createRuntime, createScenario } from "definitely-fine";
|
|
240
|
+
|
|
241
|
+
const scenario = createScenario<DemoContract>();
|
|
242
|
+
const runtime = createRuntime<DemoContract>();
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
import { createRuntime, createScenario } from "definitely-fine";
|
|
247
|
+
|
|
248
|
+
const scenario = createScenario<DemoContract>({
|
|
249
|
+
directory: ".definitely-fine",
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
const runtime = createRuntime<DemoContract>({
|
|
253
|
+
directory: ".definitely-fine",
|
|
254
|
+
});
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
If you want full control over persistence, pass a custom `adapter` instead of relying on the built-in file-backed adapter.
|
|
258
|
+
|
|
259
|
+
```ts
|
|
260
|
+
import {
|
|
261
|
+
JsonScenarioStorageAdapter,
|
|
262
|
+
createRuntime,
|
|
263
|
+
createScenario,
|
|
264
|
+
} from "definitely-fine";
|
|
265
|
+
|
|
266
|
+
const adapter = new JsonScenarioStorageAdapter({});
|
|
267
|
+
|
|
268
|
+
const scenario = createScenario<DemoContract>({ adapter });
|
|
269
|
+
const runtime = createRuntime<DemoContract>({ adapter });
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
## When To Use It
|
|
273
|
+
|
|
274
|
+
- You want typed, call-specific overrides such as "return this value on the third call".
|
|
275
|
+
- You want browser tests to influence server-side behavior without adding test-only request plumbing everywhere.
|
|
276
|
+
- You want app code to keep running through real route handlers, actions, and services.
|
|
277
|
+
|
|
278
|
+
## Related Packages
|
|
279
|
+
|
|
280
|
+
- [`@definitely-fine/nextjs`](../nextjs/README.md) propagates scenario ids through Next.js route handlers and server actions.
|
|
281
|
+
- [`@definitely-fine/playwright`](../playwright/README.md) helps Playwright tests create browser contexts with the active scenario header already set.
|