@fragno-dev/test 1.0.2 → 2.0.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.
Files changed (42) hide show
  1. package/.turbo/turbo-build.log +31 -15
  2. package/CHANGELOG.md +43 -0
  3. package/dist/adapters.d.ts +21 -3
  4. package/dist/adapters.d.ts.map +1 -1
  5. package/dist/adapters.js +125 -31
  6. package/dist/adapters.js.map +1 -1
  7. package/dist/db-test.d.ts.map +1 -1
  8. package/dist/db-test.js +33 -2
  9. package/dist/db-test.js.map +1 -1
  10. package/dist/durable-hooks.d.ts +7 -0
  11. package/dist/durable-hooks.d.ts.map +1 -0
  12. package/dist/durable-hooks.js +12 -0
  13. package/dist/durable-hooks.js.map +1 -0
  14. package/dist/index.d.ts +8 -4
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +6 -2
  17. package/dist/index.js.map +1 -1
  18. package/dist/model-checker-actors.d.ts +41 -0
  19. package/dist/model-checker-actors.d.ts.map +1 -0
  20. package/dist/model-checker-actors.js +406 -0
  21. package/dist/model-checker-actors.js.map +1 -0
  22. package/dist/model-checker-adapter.d.ts +32 -0
  23. package/dist/model-checker-adapter.d.ts.map +1 -0
  24. package/dist/model-checker-adapter.js +109 -0
  25. package/dist/model-checker-adapter.js.map +1 -0
  26. package/dist/model-checker.d.ts +128 -0
  27. package/dist/model-checker.d.ts.map +1 -0
  28. package/dist/model-checker.js +443 -0
  29. package/dist/model-checker.js.map +1 -0
  30. package/package.json +12 -11
  31. package/src/adapter-conformance.test.ts +322 -0
  32. package/src/adapters.ts +199 -36
  33. package/src/db-test.test.ts +2 -2
  34. package/src/db-test.ts +53 -3
  35. package/src/durable-hooks.ts +13 -0
  36. package/src/index.test.ts +84 -7
  37. package/src/index.ts +39 -4
  38. package/src/model-checker-actors.test.ts +78 -0
  39. package/src/model-checker-actors.ts +642 -0
  40. package/src/model-checker-adapter.ts +200 -0
  41. package/src/model-checker.test.ts +399 -0
  42. package/src/model-checker.ts +799 -0
@@ -0,0 +1,200 @@
1
+ import type {
2
+ DatabaseAdapter,
3
+ DatabaseAdapterMetadata,
4
+ DatabaseContextStorage,
5
+ } from "@fragno-dev/db/adapters";
6
+ import {
7
+ fragnoDatabaseAdapterNameFakeSymbol,
8
+ fragnoDatabaseAdapterVersionFakeSymbol,
9
+ } from "@fragno-dev/db/adapters";
10
+ import type { AnySchema } from "@fragno-dev/db/schema";
11
+ import type { SimpleQueryInterface } from "@fragno-dev/db/query";
12
+ import type { RequestContextStorage } from "@fragno-dev/core/internal/request-context-storage";
13
+ import type {
14
+ UOWInstrumentation,
15
+ UOWInstrumentationContext,
16
+ UOWInstrumentationInjection,
17
+ UOWInstrumentationFinalizer,
18
+ } from "@fragno-dev/db/unit-of-work";
19
+ import type { ModelCheckerPhase } from "./model-checker";
20
+
21
+ type SchedulerHook = (ctx: UOWInstrumentationContext, phase: ModelCheckerPhase) => Promise<void>;
22
+
23
+ export type ModelCheckerScheduler = {
24
+ beforePhase: SchedulerHook;
25
+ afterPhase: SchedulerHook;
26
+ };
27
+
28
+ const isInstrumentationInjection = (value: unknown): value is UOWInstrumentationInjection => {
29
+ if (!value || typeof value !== "object") {
30
+ return false;
31
+ }
32
+ const candidate = value as { type?: string };
33
+ return candidate.type === "conflict" || candidate.type === "error";
34
+ };
35
+
36
+ const mergeHook = (
37
+ primary: UOWInstrumentation[keyof UOWInstrumentation] | undefined,
38
+ secondary: UOWInstrumentation[keyof UOWInstrumentation] | undefined,
39
+ options: { alwaysCallSecondary?: boolean } = {},
40
+ ): UOWInstrumentation[keyof UOWInstrumentation] | undefined => {
41
+ if (!primary && !secondary) {
42
+ return undefined;
43
+ }
44
+
45
+ const hook = async (
46
+ ctx: UOWInstrumentationContext,
47
+ ): Promise<void | UOWInstrumentationInjection> => {
48
+ let result: void | UOWInstrumentationInjection;
49
+ if (options.alwaysCallSecondary) {
50
+ try {
51
+ result = await primary?.(ctx);
52
+ } finally {
53
+ await secondary?.(ctx);
54
+ }
55
+ } else {
56
+ result = await primary?.(ctx);
57
+ if (!isInstrumentationInjection(result)) {
58
+ const secondaryResult = await secondary?.(ctx);
59
+ if (isInstrumentationInjection(secondaryResult)) {
60
+ return secondaryResult;
61
+ }
62
+ }
63
+ }
64
+
65
+ if (isInstrumentationInjection(result)) {
66
+ return result;
67
+ }
68
+
69
+ return undefined;
70
+ };
71
+
72
+ return hook as UOWInstrumentation[keyof UOWInstrumentation];
73
+ };
74
+
75
+ const mergeInstrumentation = (
76
+ existing: UOWInstrumentation | undefined,
77
+ scheduler: ModelCheckerScheduler | null,
78
+ ): {
79
+ instrumentation?: UOWInstrumentation;
80
+ instrumentationFinalizer?: UOWInstrumentationFinalizer;
81
+ } => {
82
+ if (!scheduler) {
83
+ return { instrumentation: existing };
84
+ }
85
+
86
+ const schedulerBeforeRetrieve = (ctx: UOWInstrumentationContext) =>
87
+ scheduler.beforePhase(ctx, "retrieve");
88
+ const schedulerAfterRetrieve = (ctx: UOWInstrumentationContext) =>
89
+ scheduler.afterPhase(ctx, "retrieve");
90
+ const schedulerBeforeMutate = (ctx: UOWInstrumentationContext) =>
91
+ scheduler.beforePhase(ctx, "mutate");
92
+ const schedulerAfterMutate = (ctx: UOWInstrumentationContext) =>
93
+ scheduler.afterPhase(ctx, "mutate");
94
+
95
+ return {
96
+ instrumentation: {
97
+ beforeRetrieve: mergeHook(schedulerBeforeRetrieve, existing?.beforeRetrieve),
98
+ afterRetrieve: existing?.afterRetrieve,
99
+ beforeMutate: mergeHook(schedulerBeforeMutate, existing?.beforeMutate),
100
+ afterMutate: existing?.afterMutate,
101
+ },
102
+ instrumentationFinalizer: {
103
+ afterRetrieve: schedulerAfterRetrieve,
104
+ afterMutate: schedulerAfterMutate,
105
+ },
106
+ };
107
+ };
108
+
109
+ export class ModelCheckerAdapter<TUowConfig = void> implements DatabaseAdapter<TUowConfig> {
110
+ [fragnoDatabaseAdapterNameFakeSymbol]: string;
111
+ [fragnoDatabaseAdapterVersionFakeSymbol]: number;
112
+
113
+ readonly contextStorage: RequestContextStorage<DatabaseContextStorage>;
114
+ readonly adapterMetadata?: DatabaseAdapterMetadata;
115
+ readonly namingStrategy: DatabaseAdapter<TUowConfig>["namingStrategy"];
116
+ prepareMigrations?: DatabaseAdapter<TUowConfig>["prepareMigrations"];
117
+
118
+ #baseAdapter: DatabaseAdapter<TUowConfig>;
119
+ #scheduler: ModelCheckerScheduler | null = null;
120
+
121
+ constructor(baseAdapter: DatabaseAdapter<TUowConfig>) {
122
+ this.#baseAdapter = baseAdapter;
123
+ this.contextStorage = baseAdapter.contextStorage;
124
+ this.adapterMetadata = baseAdapter.adapterMetadata;
125
+ this.namingStrategy = baseAdapter.namingStrategy;
126
+ this[fragnoDatabaseAdapterNameFakeSymbol] = baseAdapter[fragnoDatabaseAdapterNameFakeSymbol];
127
+ this[fragnoDatabaseAdapterVersionFakeSymbol] =
128
+ baseAdapter[fragnoDatabaseAdapterVersionFakeSymbol];
129
+ this.prepareMigrations = baseAdapter.prepareMigrations?.bind(baseAdapter);
130
+ }
131
+
132
+ setScheduler(scheduler: ModelCheckerScheduler | null): void {
133
+ this.#scheduler = scheduler;
134
+ }
135
+
136
+ getHookProcessingAdapter(): DatabaseAdapter<TUowConfig> {
137
+ return this.#baseAdapter;
138
+ }
139
+
140
+ getSchemaVersion(namespace: string): Promise<string | undefined> {
141
+ return this.#baseAdapter.getSchemaVersion(namespace);
142
+ }
143
+
144
+ createQueryEngine<const T extends AnySchema>(
145
+ schema: T,
146
+ namespace: string | null,
147
+ ): SimpleQueryInterface<T, TUowConfig> {
148
+ const engine = this.#baseAdapter.createQueryEngine(schema, namespace);
149
+
150
+ const wrappedCreateUnitOfWork = (name?: string, config?: TUowConfig) => {
151
+ const scheduler = this.#scheduler;
152
+ if (!scheduler) {
153
+ return engine.createUnitOfWork(name, config);
154
+ }
155
+
156
+ const configWithInstrumentation = (() => {
157
+ if (!config || typeof config !== "object") {
158
+ const merged = mergeInstrumentation(undefined, scheduler);
159
+ return merged as TUowConfig;
160
+ }
161
+
162
+ const typedConfig = config as {
163
+ instrumentation?: UOWInstrumentation;
164
+ instrumentationFinalizer?: UOWInstrumentationFinalizer;
165
+ };
166
+ const merged = mergeInstrumentation(typedConfig.instrumentation, scheduler);
167
+ return {
168
+ ...typedConfig,
169
+ instrumentation: merged.instrumentation,
170
+ instrumentationFinalizer:
171
+ merged.instrumentationFinalizer ?? typedConfig.instrumentationFinalizer,
172
+ } as TUowConfig;
173
+ })();
174
+
175
+ return engine.createUnitOfWork(name, configWithInstrumentation);
176
+ };
177
+
178
+ const proxy = new Proxy(engine, {
179
+ get(target, prop, receiver) {
180
+ if (prop === "createUnitOfWork") {
181
+ return wrappedCreateUnitOfWork;
182
+ }
183
+ const value = Reflect.get(target, prop, receiver);
184
+ if (typeof value === "function") {
185
+ return value.bind(target);
186
+ }
187
+ return value;
188
+ },
189
+ });
190
+ return proxy;
191
+ }
192
+
193
+ isConnectionHealthy(): Promise<boolean> {
194
+ return this.#baseAdapter.isConnectionHealthy();
195
+ }
196
+
197
+ close(): Promise<void> {
198
+ return this.#baseAdapter.close();
199
+ }
200
+ }
@@ -0,0 +1,399 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { InMemoryAdapter } from "@fragno-dev/db";
3
+ import { column, idColumn, schema, type FragnoId } from "@fragno-dev/db/schema";
4
+ import type { UnitOfWorkConfig } from "@fragno-dev/db/unit-of-work";
5
+ import {
6
+ createRawUowTransaction,
7
+ defaultStateHasher,
8
+ defaultTraceHasher,
9
+ runModelChecker,
10
+ type ModelCheckerTraceEvent,
11
+ type ModelCheckerStateHasherContext,
12
+ } from "./model-checker";
13
+
14
+ const testSchema = schema("test", (s) =>
15
+ s.addTable("users", (t) =>
16
+ t
17
+ .addColumn("id", idColumn())
18
+ .addColumn("name", column("string"))
19
+ .createIndex("idx_users_name", ["name"]),
20
+ ),
21
+ );
22
+
23
+ const createContext = async () => {
24
+ const adapter = new InMemoryAdapter({ idSeed: "model-checker" });
25
+ const queryEngine = adapter.createQueryEngine(testSchema, "model-checker");
26
+ return {
27
+ ctx: {
28
+ queryEngine,
29
+ createUnitOfWork: queryEngine.createUnitOfWork,
30
+ },
31
+ cleanup: async () => {
32
+ await adapter.close();
33
+ },
34
+ };
35
+ };
36
+
37
+ describe("model checker", () => {
38
+ it("generates all interleavings for two two-step transactions", async () => {
39
+ const result = await runModelChecker({
40
+ schema: testSchema,
41
+ mode: "exhaustive",
42
+ history: false,
43
+ createContext,
44
+ setup: async (ctx) => {
45
+ await ctx.queryEngine.create("users", { name: "seed" });
46
+ },
47
+ buildTransactions: (ctx) => {
48
+ const buildTx = (label: string) => {
49
+ let uow = ctx.createUnitOfWork(`${label}-retrieve`);
50
+ let userCount = 0;
51
+ return {
52
+ retrieve: async () => {
53
+ uow.find("users", (b) => b.whereIndex("primary"));
54
+ const results = (await uow.executeRetrieve()) as unknown[];
55
+ const rows = (results[0] ?? []) as Array<{ name: string }>;
56
+ userCount = rows.length;
57
+ return userCount;
58
+ },
59
+ mutate: async () => {
60
+ uow.create("users", { name: `${label}-${userCount}` });
61
+ const { success } = await uow.executeMutations();
62
+ if (!success) {
63
+ throw new Error("Mutation failed");
64
+ }
65
+ },
66
+ };
67
+ };
68
+
69
+ return [buildTx("alpha"), buildTx("beta")];
70
+ },
71
+ });
72
+
73
+ expect(result.schedules).toHaveLength(6);
74
+ for (const schedule of result.schedules) {
75
+ expect(schedule.schedule).toHaveLength(4);
76
+ }
77
+ });
78
+
79
+ it("hashes state changes deterministically", async () => {
80
+ const { ctx, cleanup } = await createContext();
81
+ try {
82
+ const emptyHash = await defaultStateHasher({
83
+ schema: testSchema,
84
+ queryEngine: ctx.queryEngine,
85
+ });
86
+ await ctx.queryEngine.create("users", { name: "after" });
87
+ const nextHash = await defaultStateHasher({
88
+ schema: testSchema,
89
+ queryEngine: ctx.queryEngine,
90
+ });
91
+ expect(emptyHash).not.toEqual(nextHash);
92
+ } finally {
93
+ await cleanup();
94
+ }
95
+ });
96
+
97
+ it("generates deterministic random schedules with a seed", async () => {
98
+ const buildTransactions = () => [
99
+ {
100
+ retrieve: async () => "alpha",
101
+ mutate: async () => undefined,
102
+ },
103
+ {
104
+ retrieve: async () => "beta",
105
+ mutate: async () => undefined,
106
+ },
107
+ ];
108
+
109
+ const run = async () =>
110
+ runModelChecker({
111
+ schema: testSchema,
112
+ mode: "random",
113
+ seed: 123,
114
+ maxSchedules: 3,
115
+ history: false,
116
+ createContext,
117
+ buildTransactions,
118
+ });
119
+
120
+ const [first, second] = await Promise.all([run(), run()]);
121
+
122
+ expect(first.schedules).toEqual(second.schedules);
123
+ });
124
+
125
+ it("stops infinite mode when the frontier is exhausted", async () => {
126
+ const result = await runModelChecker({
127
+ schema: testSchema,
128
+ mode: "infinite",
129
+ seed: 7,
130
+ maxSchedules: 10,
131
+ stopWhenFrontierExhausted: true,
132
+ createContext,
133
+ buildTransactions: () => [
134
+ {
135
+ retrieve: async () => "only-step",
136
+ },
137
+ ],
138
+ });
139
+
140
+ expect(result.schedules).toHaveLength(2);
141
+ expect(result.visitedPaths).toBe(1);
142
+ });
143
+
144
+ it("builds raw UOW transactions with a shared unit of work", async () => {
145
+ let createUnitOfWorkCalls = 0;
146
+
147
+ const result = await runModelChecker({
148
+ schema: testSchema,
149
+ history: false,
150
+ createContext: async () => {
151
+ const adapter = new InMemoryAdapter({ idSeed: "model-checker" });
152
+ const queryEngine = adapter.createQueryEngine(testSchema, "model-checker");
153
+ const createUnitOfWork = vi.fn(queryEngine.createUnitOfWork.bind(queryEngine));
154
+ createUnitOfWork.mockImplementation((name, config) => {
155
+ createUnitOfWorkCalls += 1;
156
+ return queryEngine.createUnitOfWork(name, config);
157
+ });
158
+ return {
159
+ ctx: {
160
+ queryEngine,
161
+ createUnitOfWork,
162
+ },
163
+ cleanup: async () => {
164
+ await adapter.close();
165
+ },
166
+ };
167
+ },
168
+ buildTransactions: (_ctx) => [
169
+ createRawUowTransaction<unknown, unknown, typeof testSchema, UnitOfWorkConfig>({
170
+ name: "alpha",
171
+ retrieve: async (uow) => {
172
+ uow.find("users", (b) => b.whereIndex("primary"));
173
+ const results = (await uow.executeRetrieve()) as unknown[];
174
+ const rows = (results[0] ?? []) as Array<{ name: string }>;
175
+ return rows.length;
176
+ },
177
+ mutate: async (uow, txCtx) => {
178
+ uow.create("users", { name: `alpha-${txCtx.retrieveResult}` });
179
+ const { success } = await uow.executeMutations();
180
+ if (!success) {
181
+ throw new Error("Mutation failed");
182
+ }
183
+ },
184
+ }),
185
+ ],
186
+ });
187
+
188
+ expect(result.schedules).toHaveLength(1);
189
+ expect(createUnitOfWorkCalls).toBe(1);
190
+ });
191
+
192
+ it("surfaces invariant violations across interleavings", async () => {
193
+ const reservationSchema = schema("reservation", (s) =>
194
+ s
195
+ .addTable("stock", (t) => t.addColumn("id", idColumn()).addColumn("remaining", "integer"))
196
+ .addTable("orders", (t) => t.addColumn("id", idColumn()).addColumn("note", "string")),
197
+ );
198
+
199
+ const createContext = async () => {
200
+ const adapter = new InMemoryAdapter({ idSeed: "model-checker-invariant" });
201
+ const queryEngine = adapter.createQueryEngine(reservationSchema, "model-checker-invariant");
202
+ return {
203
+ ctx: {
204
+ queryEngine,
205
+ createUnitOfWork: queryEngine.createUnitOfWork,
206
+ },
207
+ cleanup: async () => {
208
+ await adapter.close();
209
+ },
210
+ };
211
+ };
212
+
213
+ const maxOrders = 1;
214
+ const stateHasher = async (
215
+ ctx: ModelCheckerStateHasherContext<typeof reservationSchema, UnitOfWorkConfig>,
216
+ ) => {
217
+ const orders = await ctx.queryEngine.find("orders", (b) => b.whereIndex("primary"));
218
+ if (orders.length > maxOrders) {
219
+ throw new Error(
220
+ `Invariant violated: expected at most ${maxOrders} orders, found ${orders.length}`,
221
+ );
222
+ }
223
+ return defaultStateHasher(ctx);
224
+ };
225
+
226
+ const buildBuyer = (label: string) =>
227
+ createRawUowTransaction<
228
+ { stockId: string | FragnoId; remaining: number },
229
+ void,
230
+ typeof reservationSchema,
231
+ UnitOfWorkConfig
232
+ >({
233
+ name: label,
234
+ retrieve: async (uow) => {
235
+ uow.find("stock", (b) => b.whereIndex("primary"));
236
+ const results = (await uow.executeRetrieve()) as unknown[];
237
+ const rows = (results[0] ?? []) as Array<{ id: string | FragnoId; remaining: number }>;
238
+ const stock = rows[0];
239
+ if (!stock) {
240
+ throw new Error("Stock row missing");
241
+ }
242
+ return { stockId: stock.id, remaining: stock.remaining };
243
+ },
244
+ mutate: async (uow, txCtx) => {
245
+ if (txCtx.retrieveResult.remaining <= 0) {
246
+ return;
247
+ }
248
+ uow.create("orders", { note: `order-${label}` });
249
+ uow.update("stock", txCtx.retrieveResult.stockId, (b) =>
250
+ b.set({ remaining: txCtx.retrieveResult.remaining - 1 }),
251
+ );
252
+ const { success } = await uow.executeMutations();
253
+ if (!success) {
254
+ throw new Error("Mutation failed");
255
+ }
256
+ },
257
+ });
258
+
259
+ await expect(
260
+ runModelChecker({
261
+ schema: reservationSchema,
262
+ mode: "exhaustive",
263
+ history: false,
264
+ stateHasher,
265
+ createContext,
266
+ setup: async (ctx) => {
267
+ await ctx.queryEngine.create("stock", { remaining: maxOrders });
268
+ },
269
+ buildTransactions: () => [buildBuyer("buyer-a"), buildBuyer("buyer-b")],
270
+ }),
271
+ ).rejects.toThrow("Invariant violated");
272
+ });
273
+
274
+ it("records trace events for UOW outputs and mutations", async () => {
275
+ const events: ModelCheckerTraceEvent[] = [];
276
+ const runtime = {
277
+ time: {
278
+ now: () => new Date("2025-01-01T00:00:00.000Z"),
279
+ },
280
+ random: {
281
+ float: () => 0.25,
282
+ uuid: () => "00000000-0000-4000-8000-000000000000",
283
+ cuid: () => "cuid_0000000000000000",
284
+ },
285
+ };
286
+
287
+ await runModelChecker({
288
+ schema: testSchema,
289
+ history: false,
290
+ runtime,
291
+ traceRecorder: (event) => events.push(event),
292
+ createContext,
293
+ setup: async (ctx) => {
294
+ await ctx.queryEngine.create("users", { name: "seed" });
295
+ },
296
+ buildTransactions: () => [
297
+ createRawUowTransaction<unknown, unknown, typeof testSchema, UnitOfWorkConfig>({
298
+ name: "trace-tx",
299
+ retrieve: async (uow, ctx) => {
300
+ ctx.runtime?.random.float();
301
+ uow.find("users", (b) => b.whereIndex("primary"));
302
+ await uow.executeRetrieve();
303
+ return "done";
304
+ },
305
+ mutate: async (uow, ctx) => {
306
+ ctx.runtime?.random.uuid();
307
+ ctx.runtime?.random.cuid();
308
+ ctx.runtime?.time.now();
309
+ uow.create("users", { name: "trace" });
310
+ await uow.executeMutations();
311
+ },
312
+ }),
313
+ ],
314
+ });
315
+
316
+ const eventTypes = new Set(events.map((event) => event.type));
317
+ expect(eventTypes).toContain("schedule-step");
318
+ expect(eventTypes).toContain("retrieve-output");
319
+ expect(eventTypes).toContain("mutation-input");
320
+ expect(eventTypes).toContain("mutation-result");
321
+ expect(eventTypes).toContain("runtime");
322
+ });
323
+
324
+ it("records runtime trace events during model checker runs", async () => {
325
+ const events: ModelCheckerTraceEvent[] = [];
326
+ const runtime = {
327
+ time: {
328
+ now: () => new Date("2025-01-01T00:00:00.000Z"),
329
+ },
330
+ random: {
331
+ float: () => 0.5,
332
+ uuid: () => "11111111-1111-4111-8111-111111111111",
333
+ cuid: () => "cuid_1111111111111111",
334
+ },
335
+ };
336
+
337
+ await runModelChecker({
338
+ schema: testSchema,
339
+ history: false,
340
+ runtime,
341
+ traceRecorder: (event) => events.push(event),
342
+ createContext,
343
+ buildTransactions: () => [
344
+ createRawUowTransaction<unknown, unknown, typeof testSchema, UnitOfWorkConfig>({
345
+ name: "runtime-tx",
346
+ retrieve: async (_uow, ctx) => {
347
+ ctx.runtime?.random.float();
348
+ ctx.runtime?.random.uuid();
349
+ ctx.runtime?.time.now();
350
+ return "ok";
351
+ },
352
+ }),
353
+ ],
354
+ });
355
+
356
+ const runtimeEvents = events.filter((event) => event.type === "runtime");
357
+ expect(runtimeEvents.length).toBeGreaterThan(0);
358
+ });
359
+
360
+ it("produces deterministic trace hashes", async () => {
361
+ const runtime = {
362
+ time: {
363
+ now: () => new Date("2025-01-01T00:00:00.000Z"),
364
+ },
365
+ random: {
366
+ float: () => 0.75,
367
+ uuid: () => "22222222-2222-4222-8222-222222222222",
368
+ cuid: () => "cuid_2222222222222222",
369
+ },
370
+ };
371
+
372
+ const run = () =>
373
+ runModelChecker({
374
+ schema: testSchema,
375
+ history: false,
376
+ runtime,
377
+ traceHasher: defaultTraceHasher,
378
+ createContext,
379
+ buildTransactions: () => [
380
+ createRawUowTransaction<unknown, unknown, typeof testSchema, UnitOfWorkConfig>({
381
+ name: "hash-tx",
382
+ retrieve: async (uow, ctx) => {
383
+ ctx.runtime?.random.float();
384
+ uow.find("users", (b) => b.whereIndex("primary"));
385
+ await uow.executeRetrieve();
386
+ return "ok";
387
+ },
388
+ mutate: async (uow) => {
389
+ uow.create("users", { id: "hash-id", name: "hash" });
390
+ await uow.executeMutations();
391
+ },
392
+ }),
393
+ ],
394
+ });
395
+
396
+ const [first, second] = await Promise.all([run(), run()]);
397
+ expect(first.schedules[0]?.traceHash).toEqual(second.schedules[0]?.traceHash);
398
+ });
399
+ });