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 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.