@voltro/testing 0.33.0 → 0.34.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/dist/index.js CHANGED
@@ -1,14 +1,15 @@
1
1
  import { AUTO_FILLED_COLUMNS as e, allRegisteredTables as t, changeDelete as n, changeInsert as r, changeSoftDelete as i, changeUpdate as a, clearRelationsRegistry as o, missingRequiredColumns as s, registerRelations as c } from "@voltro/database";
2
2
  import { Cause as l, Effect as u, Exit as d, Layer as f, Schema as p } from "effect";
3
- import { SubjectService as m, anonymousSubject as h, checkGuardsEffect as g, composeRpcInterceptors as _, eventRoute as v, tenantScopedSubject as ee } from "@voltro/protocol";
4
- import { EventBus as y, InMemoryDataStore as te, applyRowFilterToDescriptor as ne, clearSystemStoreHandle as b, getRowFilter as x, getSystemStoreHandle as S, makeAppAccess as C, makeDataLoader as w, makeEffectStoreLayer as T, makeOutboxFacade as re, makeSchemaRegistry as ie, publishEvent as ae, resolveRowFilterScopeFor as oe, runProvidedEffect as E, runWithDeadlockRetry as se, setSystemStoreHandle as D, wrapStoreWithMixinBehaviour as O } from "@voltro/runtime";
5
- import { installEnvSnapshot as ce } from "@voltro/env";
6
- import le from "node:net";
7
- import { describe as ue } from "vitest";
8
- import { createLogger as de } from "@voltro/logger";
9
- import { CurrentWorkflowRunId as fe, inMemoryWorkflowEngineLayer as k, makeInMemoryRecorder as A } from "@voltro/workflow";
3
+ import { SubjectService as m, anonymousSubject as h, checkGuardsEffect as g, composeAuthStrategies as _, composeRpcInterceptors as v, eventRoute as ee, systemSubject as y, tenantScopedSubject as b } from "@voltro/protocol";
4
+ import { EventBus as te, InMemoryDataStore as ne, applyRowFilterToDescriptor as re, clearSystemStoreHandle as ie, getRowFilter as ae, getSystemStoreHandle as oe, makeAppAccess as se, makeDataLoader as x, makeEffectStoreLayer as ce, makeOutboxFacade as le, makeSchemaRegistry as ue, publishEvent as de, resolveRowFilterScopeFor as fe, runProvidedEffect as S, runWithDeadlockRetry as pe, setSystemStoreHandle as C, wrapStoreWithMixinBehaviour as w } from "@voltro/runtime";
5
+ import { installEnvSnapshot as me } from "@voltro/env";
6
+ import he from "node:net";
7
+ import { describe as ge } from "vitest";
8
+ import { collectPublicApiRoutes as _e, dispatchSharedPath as ve, restRoutesToHttpRoutes as ye } from "@voltro/protocol/rest";
9
+ import { createLogger as be } from "@voltro/logger";
10
+ import { CurrentWorkflowRunId as xe, inMemoryWorkflowEngineLayer as Se, makeInMemoryRecorder as Ce } from "@voltro/workflow";
10
11
  //#region src/fixtureRow.ts
11
- var j = new Set(e), M = 0, N = (e, t) => {
12
+ var we = new Set(e), T = 0, E = () => ++T, Te = (e, t) => {
12
13
  if (t.oneOf && t.oneOf.length > 0) return t.oneOf[0];
13
14
  if (t.enumValues && t.enumValues.length > 0) return t.enumValues[0];
14
15
  let n = t.unique === !0;
@@ -16,27 +17,95 @@ var j = new Set(e), M = 0, N = (e, t) => {
16
17
  case "text":
17
18
  case "reference":
18
19
  case "id":
19
- case "enum": return n ? `${e}-${++M}` : e;
20
+ case "enum": return n ? `${e}-${++T}` : e;
20
21
  case "integer":
21
- case "real": return n ? ++M : 0;
22
+ case "real": return n ? ++T : 0;
22
23
  case "decimal":
23
- case "bigint": return n ? String(++M) : "0";
24
+ case "bigint": return n ? String(++T) : "0";
24
25
  case "boolean": return !1;
25
26
  case "timestamp":
26
27
  case "date": return /* @__PURE__ */ new Date(0);
27
28
  default: throw Error(`fixtureRow: column '${e}' is a required '${t.type}' with no default, and fixtureRow can't synthesize a safe placeholder for that type. Pass it explicitly: fixtureRow(table, { ${e}: … }).`);
28
29
  }
29
- }, P = (e, t = {}) => {
30
+ }, D = (e, t = {}) => {
30
31
  let n = {};
31
- for (let r of s(e, t)) j.has(r) || (n[r] = N(r, e.fields[r]));
32
+ for (let r of s(e, t)) we.has(r) || (n[r] = Te(r, e.fields[r]));
32
33
  return {
33
34
  ...n,
34
35
  ...t
35
36
  };
36
- }, F = class {
37
+ }, O = (e) => {
38
+ let t = {};
39
+ for (let [n, r] of Object.entries(e)) t[n] = typeof r == "function" ? r(E()) : r;
40
+ return t;
41
+ }, Ee = (e) => {
42
+ for (let [t, n] of Object.entries(e.fields)) if (n.type === "id") return t;
43
+ return "id";
44
+ }, De = async (e, t, n, r, i) => {
45
+ let a = {};
46
+ for (let o of s(t, n)) {
47
+ let n = t.fields[o];
48
+ if (n === void 0 || n.type !== "reference" || n.references === void 0) continue;
49
+ let s = n.references();
50
+ if (i.includes(s.tableName)) throw Error(`defineFactory: creating '${t.tableName}' requires '${s.tableName}' via '${o}', and that reference is cyclic (${[...i, s.tableName].join(" → ")}). Insert one side first and pass the id: create(store, { ${o}: existing.id }).`);
51
+ let c = r[o];
52
+ a[o] = (c === void 0 ? await k(e, s, {}, {}, [...i, s.tableName]) : await c.create(e, {}))[Ee(s)];
53
+ }
54
+ return a;
55
+ }, k = async (e, t, n, r, i) => {
56
+ let a = {
57
+ ...await De(e, t, n, r, i),
58
+ ...n
59
+ };
60
+ return e.insert(t.tableName, D(t, a));
61
+ }, A = (e, t) => typeof e == "function" ? e(t) : e ?? {}, Oe = (e, t = {}) => {
62
+ let n = t.traits ?? {}, r = t.associations ?? {}, i = (t) => {
63
+ let a = (e = {}) => ({
64
+ ...O(t),
65
+ ...e
66
+ }), o = (t = {}) => D(e, a(t));
67
+ return {
68
+ table: e,
69
+ build: o,
70
+ buildList: (e, t) => Array.from({ length: e }, (e, n) => o(A(t, n))),
71
+ create: (t, n = {}) => k(t, e, a(n), r, [e.tableName]),
72
+ createList: async (t, n, i) => {
73
+ let o = [];
74
+ for (let s = 0; s < n; s++) o.push(await k(t, e, a(A(i, s)), r, [e.tableName]));
75
+ return o;
76
+ },
77
+ with: (...r) => {
78
+ let a = t;
79
+ for (let t of r) {
80
+ let r = n[t];
81
+ if (r === void 0) throw Error(`defineFactory: '${e.tableName}' has no trait '${t}'. Declared: ${Object.keys(n).join(", ") || "(none)"}.`);
82
+ a = {
83
+ ...a,
84
+ ...r
85
+ };
86
+ }
87
+ return i(a);
88
+ },
89
+ extend: (e) => i({
90
+ ...t,
91
+ ...e
92
+ })
93
+ };
94
+ };
95
+ return i(t.defaults ?? {});
96
+ }, j = Symbol.for("voltro/testing/mockClock.installed"), M = () => globalThis, ke = (e, t) => new Proxy(e, {
97
+ construct: (e, n, r) => Reflect.construct(e, n.length === 0 ? [t()] : n, r),
98
+ apply: () => new e(t()).toString(),
99
+ get: (e, n, r) => n === "now" ? () => t() : Reflect.get(e, n, r)
100
+ }), N = (e) => {
101
+ if (typeof e == "number") return e;
102
+ let t = e instanceof Date ? e.getTime() : Date.parse(e);
103
+ if (Number.isNaN(t)) throw Error(`mockClock: cannot read an instant from '${String(e)}'`);
104
+ return t;
105
+ }, P = class {
37
106
  currentMs;
38
- constructor(e = /* @__PURE__ */ new Date("2026-01-01T00:00:00Z")) {
39
- this.currentMs = typeof e == "number" ? e : e.getTime();
107
+ constructor(e = "2026-01-01T00:00:00Z") {
108
+ this.currentMs = N(e);
40
109
  }
41
110
  now() {
42
111
  return this.currentMs;
@@ -45,9 +114,47 @@ var j = new Set(e), M = 0, N = (e, t) => {
45
114
  return new Date(this.currentMs);
46
115
  }
47
116
  advance(e) {
48
- this.currentMs += typeof e == "number" ? e : I(e);
117
+ this.currentMs += typeof e == "number" ? e : Ne(e);
118
+ }
119
+ set(e) {
120
+ this.currentMs = N(e);
121
+ }
122
+ get installed() {
123
+ return M()[j]?.clock === this;
49
124
  }
50
- }, I = (e) => {
125
+ install() {
126
+ let e = M(), t = e[j];
127
+ if (t !== void 0) throw Error("mockClock: global time is already faked" + (t.clock === this ? " by this same clock" : " by another MockClock") + ". Uninstall it before installing again — a nested install restores the outer fake on the way out, which leaves the realm frozen with nothing pointing at why. `withFrozenTime` always restores, including on a throw.");
128
+ let n = globalThis.Date;
129
+ return e[j] = {
130
+ realDate: n,
131
+ clock: this
132
+ }, globalThis.Date = ke(n, () => this.currentMs), () => {
133
+ this.uninstall();
134
+ };
135
+ }
136
+ uninstall() {
137
+ let e = M(), t = e[j];
138
+ t === void 0 || t.clock !== this || (globalThis.Date = t.realDate, delete e[j]);
139
+ }
140
+ }, Ae = (e) => typeof e == "object" && !!e && typeof e.then == "function", je = (e, t) => {
141
+ let n = e instanceof P ? e : new P(e);
142
+ n.install();
143
+ let r = !1;
144
+ try {
145
+ let e = t(n);
146
+ return Ae(e) ? (r = !0, e.then((e) => (n.uninstall(), e), (e) => {
147
+ throw n.uninstall(), e;
148
+ })) : e;
149
+ } finally {
150
+ r || n.uninstall();
151
+ }
152
+ }, Me = (e) => u.acquireRelease(u.sync(() => {
153
+ let t = e instanceof P ? e : new P(e);
154
+ return t.install(), t;
155
+ }), (e) => u.sync(() => {
156
+ e.uninstall();
157
+ })), Ne = (e) => {
51
158
  let t = /^\s*(\d+(?:\.\d+)?)\s*(ms|s|m|h|d)\s*$/.exec(e);
52
159
  if (!t) throw Error(`mockClock: cannot parse duration '${e}'`);
53
160
  let n = Number(t[1]), r = t[2];
@@ -59,21 +166,7 @@ var j = new Set(e), M = 0, N = (e, t) => {
59
166
  case "d": return n * 864e5;
60
167
  default: throw Error(`mockClock: unknown unit '${r}'`);
61
168
  }
62
- }, L = class {
63
- sent = [];
64
- send(e) {
65
- this.sent.push({
66
- ...e,
67
- sentAt: /* @__PURE__ */ new Date()
68
- });
69
- }
70
- lastTo(e) {
71
- for (let t = this.sent.length - 1; t >= 0; t--) if (this.sent[t].to === e) return this.sent[t];
72
- }
73
- clear() {
74
- this.sent.length = 0;
75
- }
76
- }, R = class {
169
+ }, F = class {
77
170
  emitted = [];
78
171
  async emit(e, t) {
79
172
  let n = typeof e == "string" ? e : e.id ?? e.event ?? "(unnamed event)";
@@ -92,7 +185,7 @@ var j = new Set(e), M = 0, N = (e, t) => {
92
185
  clear() {
93
186
  this.emitted.length = 0;
94
187
  }
95
- }, z = class {
188
+ }, I = class {
96
189
  queue;
97
190
  calls = [];
98
191
  constructor(e) {
@@ -107,29 +200,32 @@ var j = new Set(e), M = 0, N = (e, t) => {
107
200
  remaining() {
108
201
  return this.queue.length;
109
202
  }
110
- }, B = (e) => e, V = "00000000000000000000000000000000", H = /* @__PURE__ */ new WeakMap(), U = /* @__PURE__ */ new WeakMap(), W = /* @__PURE__ */ new WeakMap(), G = /* @__PURE__ */ new WeakMap(), K = (e, t) => {
111
- let n = G.get(e) ?? [], r = t === "mutation" ? "interceptMutation" : t === "query" ? "interceptQuery" : "interceptAction", i = [];
203
+ }, Pe = (e) => e, L = "00000000000000000000000000000000", R = /* @__PURE__ */ new WeakMap(), z = /* @__PURE__ */ new WeakMap(), B = /* @__PURE__ */ new WeakMap(), V = /* @__PURE__ */ new WeakMap(), H = /* @__PURE__ */ new WeakMap(), U = (e, t) => {
204
+ let n = V.get(e) ?? [], r = t === "mutation" ? "interceptMutation" : t === "query" ? "interceptQuery" : "interceptAction", i = [];
112
205
  for (let e of n) {
113
206
  let t = e[r];
114
207
  typeof t == "function" && i.push(t);
115
208
  }
116
- return _(i);
117
- }, q = (e) => {
118
- let t = U.get(e);
209
+ return v(i);
210
+ }, W = (e) => {
211
+ let t = (V.get(e) ?? []).map((e) => e.services).filter((e) => e !== void 0), n = H.get(e) ?? [], r = [...t, ...n];
212
+ if (r.length !== 0) return f.mergeAll(...r);
213
+ }, G = (e) => {
214
+ let t = z.get(e);
119
215
  t !== void 0 && (t.length = 0);
120
- }, J = async (e) => {
121
- let t = U.get(e);
216
+ }, K = async (e) => {
217
+ let t = z.get(e);
122
218
  if (t === void 0 || t.length === 0) return;
123
219
  let n = [...t];
124
220
  t.length = 0;
125
221
  for (let e of n) await e();
126
- }, pe = (e) => W.get(e) ?? [], Y = async (e, t) => {
127
- let n = H.get(e);
222
+ }, Fe = (e) => B.get(e) ?? [], q = async (e, t) => {
223
+ let n = R.get(e);
128
224
  return n === void 0 ? e.store.transactional(async (n) => t({
129
225
  ...e,
130
226
  store: n
131
227
  })) : n(t);
132
- }, me = (e) => {
228
+ }, Ie = (e) => {
133
229
  let t = /* @__PURE__ */ new Map(), n = /* @__PURE__ */ new Map(), r = (n) => {
134
230
  let r = t.get(n);
135
231
  return r === void 0 ? !1 : r.expiresAt !== null && r.expiresAt <= e() ? (t.delete(n), !1) : !0;
@@ -168,7 +264,7 @@ var j = new Set(e), M = 0, N = (e, t) => {
168
264
  return i(e, o, n.ttlMs, n.tags), o;
169
265
  }
170
266
  };
171
- }, he = (e) => {
267
+ }, Le = (e) => {
172
268
  let t = /* @__PURE__ */ new Map(), n = (n) => {
173
269
  let r = t.get(n);
174
270
  return r === void 0 ? !1 : r.expiresAt !== null && r.expiresAt <= e() ? (t.delete(n), !1) : !0;
@@ -196,96 +292,98 @@ var j = new Set(e), M = 0, N = (e, t) => {
196
292
  t.clear();
197
293
  }
198
294
  };
199
- }, ge = async (e, t) => {
200
- let n = S();
201
- D(e);
295
+ }, Re = async (e, t) => {
296
+ let n = oe();
297
+ C(e);
202
298
  try {
203
299
  return await t();
204
300
  } finally {
205
- n === void 0 ? b() : D(n);
301
+ n === void 0 ? ie() : C(n);
206
302
  }
207
- }, _e = (e, t, n) => {
208
- let r = ve, i;
303
+ }, ze = (e, t, n) => {
304
+ let r = Be, i;
209
305
  return () => {
210
- let a = t ?? x();
211
- return (i === void 0 || a !== r) && (r = a, i = ge(n, () => E(oe(a, e, (e) => {
306
+ let a = t ?? ae();
307
+ return (i === void 0 || a !== r) && (r = a, i = Re(n, () => S(fe(a, e, (e) => {
212
308
  console.error("[voltro:testing] row filter failed to load — reads refused for this subject", e);
213
309
  })))), i;
214
310
  };
215
- }, ve = Symbol("unresolved"), X = (e, t) => new Proxy(e, { get: (e, n) => {
216
- if (n === "query") return async (n) => e.query(ne(await t(), n));
311
+ }, Be = Symbol("unresolved"), J = (e, t) => new Proxy(e, { get: (e, n) => {
312
+ if (n === "query") return async (n) => e.query(re(await t(), n));
217
313
  let r = Reflect.get(e, n, e);
218
314
  return typeof r == "function" ? r.bind(e) : r;
219
- } }), ye = (e = {}) => {
220
- if (ce({
315
+ } }), Ve = (e = {}) => {
316
+ if (me({
221
317
  ...process.env,
222
318
  ...e.env ?? {}
223
319
  }), e.relations !== void 0) {
224
320
  o();
225
321
  for (let t of e.relations) c(t);
226
322
  }
227
- let n = ie(e.tables ?? t()), r = new te(e.store ?? {}), i = {
323
+ let n = ue(e.tables ?? t()), r = new ne(e.store ?? {}), i = {
228
324
  dataStore: r,
229
325
  schemaRegistry: n
230
326
  };
231
- D(i);
232
- let a = new F(e.clockStart), s = new L(), l = new R(), u = new z(e.llmResponses ?? []), d = me(() => a.now()), f = he(() => a.now()), p = (t, o = r, c) => {
233
- let m = {
327
+ C(i);
328
+ let a = new P(e.clockStart), s = new F(), l = new I(e.llmResponses ?? []), u = Ie(() => a.now()), d = Le(() => a.now()), f = (t, o = r, c) => {
329
+ let p = {
234
330
  subject: t,
235
- traceId: V
236
- }, h = c?.queued ?? [], g = c?.nudges ?? [], _ = w({ store: o }), v = _e(t, e.rowFilter, i), y = {
331
+ traceId: L
332
+ }, m = c?.queued ?? [], h = c?.nudges ?? [], g = x({ store: o }), _ = ze(t, e.rowFilter, i), v = {
237
333
  clock: a,
238
- email: s,
239
- webhooks: l,
240
- llm: u,
334
+ webhooks: s,
335
+ llm: l,
241
336
  ...e.ai === void 0 ? {} : { ai: e.ai },
242
- request: m,
243
- access: C(t),
244
- cache: d,
245
- kv: f,
246
- store: O(X(o, v), {
337
+ request: p,
338
+ access: se(t),
339
+ cache: u,
340
+ kv: d,
341
+ store: w(J(o, _), {
247
342
  subject: t,
248
343
  schemaRegistry: n
249
344
  }),
250
- storeForTenant: (e) => O(X(o, v), {
251
- subject: ee(t, e),
345
+ storeForTenant: (e) => w(J(o, _), {
346
+ subject: b(t, e),
252
347
  schemaRegistry: n
253
348
  }),
254
- outbox: re({
349
+ outbox: le({
255
350
  store: o,
256
351
  subject: t,
257
- traceId: V,
352
+ traceId: L,
258
353
  nudge: () => {
259
- g.push(V);
354
+ h.push(L);
260
355
  },
261
356
  afterCommit: (e) => {
262
- h.push(e);
357
+ m.push(e);
263
358
  }
264
359
  }),
265
- load: _.load,
266
- loadMany: _.loadMany,
267
- withSubject: (e, t) => Promise.resolve(t(p(e, o, {
268
- queued: h,
269
- nudges: g
360
+ load: g.load,
361
+ loadMany: g.loadMany,
362
+ withSubject: (e, t) => Promise.resolve(t(f(e, o, {
363
+ queued: m,
364
+ nudges: h
270
365
  }))),
271
- withTenant: (e, n) => Promise.resolve(n(p({
366
+ withTenant: (e, n) => Promise.resolve(n(f({
272
367
  ...t,
273
368
  tenantId: e
274
369
  }, o, {
275
- queued: h,
276
- nudges: g
370
+ queued: m,
371
+ nudges: h
277
372
  })))
278
373
  };
279
- return U.set(y, h), W.set(y, g), G.set(y, e.plugins ?? []), H.set(y, (e) => o.transactional((n) => e(p(t, n, {
280
- queued: h,
281
- nudges: g
282
- })))), y;
374
+ return z.set(v, m), B.set(v, h), V.set(v, e.plugins ?? []), H.set(v, e.layers ?? []), R.set(v, (e) => o.transactional((n) => e(f(t, n, {
375
+ queued: m,
376
+ nudges: h
377
+ })))), v;
283
378
  };
284
- return p(e.subject ?? h(null));
285
- }, be = (e, t) => e.pipe(u.provide(T(t.store)), u.provideService(m, t.request.subject)), xe = async (e, t, n, r) => {
379
+ return f(e.subject ?? h(null));
380
+ }, He = (e, t) => {
381
+ let n = W(t);
382
+ return (n === void 0 ? e : u.provide(e, n)).pipe(u.provide(ce(t.store)), u.provideService(m, t.request.subject));
383
+ }, Y = async (e, t, n, r) => {
286
384
  let i = e.input, a = await p.decodeUnknownPromise(i)(n), o = e.kind, s = async (e) => {
287
385
  let n = t(a, e);
288
- return u.isEffect(n) ? E(be(n, e)) : n;
386
+ return u.isEffect(n) ? S(He(n, e)) : n;
289
387
  }, c = async () => {
290
388
  let t = e.guards;
291
389
  if (t !== void 0 && t.length > 0) {
@@ -293,9 +391,9 @@ var j = new Set(e), M = 0, N = (e, t) => {
293
391
  if (e !== null) throw e;
294
392
  }
295
393
  if (o !== "mutation") return s(r);
296
- let n = await se(async () => (q(r), Y(r, async (e) => s(e))), { delay: () => Promise.resolve() });
297
- return await J(r), n;
298
- }, f = o === "mutation" || o === "query" || o === "action" ? o : void 0, m = f === void 0 ? void 0 : K(r, f);
394
+ let n = await pe(async () => (G(r), q(r, async (e) => s(e))), { delay: () => Promise.resolve() });
395
+ return await K(r), n;
396
+ }, f = o === "mutation" || o === "query" || o === "action" ? o : void 0, m = f === void 0 ? void 0 : U(r, f);
299
397
  if (m === void 0 || f === void 0) return await c();
300
398
  let h = m(u.tryPromise({
301
399
  try: () => c(),
@@ -310,12 +408,12 @@ var j = new Set(e), M = 0, N = (e, t) => {
310
408
  }), _ = await u.runPromiseExit(h);
311
409
  if (d.isSuccess(_)) return _.value;
312
410
  throw l.squash(_.cause);
313
- }, Z = (e) => new Promise((t) => {
314
- let n = new le.Socket(), r = (e) => {
411
+ }, X = (e) => new Promise((t) => {
412
+ let n = new he.Socket(), r = (e) => {
315
413
  clearTimeout(i), n.destroy(), t(e);
316
414
  }, i = setTimeout(() => r(!1), e.timeoutMs ?? 750);
317
415
  n.once("connect", () => r(!0)), n.once("error", () => r(!1)), n.connect(e.port, e.host);
318
- }), Q = async (e, t, n, r) => {
416
+ }), Z = async (e, t, n, r) => {
319
417
  if (typeof t != "string" || typeof n != "function" || typeof r != "function") throw TypeError(`describeIfAvailable('${e}', ...): expected (label, dependency, probe, suite) — got dependency: ${typeof t}, probe: ${typeof n}, suite: ${typeof r}. Three arguments means the suite body is being used as the probe: it would run at file scope, unconditionally, and the skip would silently do nothing.`);
320
418
  let i = !1;
321
419
  try {
@@ -323,17 +421,115 @@ var j = new Set(e), M = 0, N = (e, t) => {
323
421
  } catch {
324
422
  i = !1;
325
423
  }
326
- ue.skipIf(!i)(i ? e : `${e} [SKIPPED: needs ${t}]`, r);
327
- }, Se = async (e, t, n) => {
328
- await Q(e, `${t.name} at ${t.host}:${t.port}`, () => Z(t), n);
329
- }, Ce = (e = {}) => {
330
- let t = e.tenantId ?? null, n = new y({
424
+ ge.skipIf(!i)(i ? e : `${e} [SKIPPED: needs ${t}]`, r);
425
+ }, Ue = async (e, t, n) => {
426
+ await Z(e, `${t.name} at ${t.host}:${t.port}`, () => X(t), n);
427
+ }, We = "test-tenant", Q = (e, t, n) => ({
428
+ type: e,
429
+ id: t,
430
+ tenantId: n.tenantId ?? "test-tenant",
431
+ scopes: n.scopes ?? [],
432
+ ...n.metadata === void 0 ? {} : { metadata: n.metadata }
433
+ }), Ge = (e, t = {}) => Q("user", e, t), Ke = (e, t = {}) => Q("apiKey", e, t), qe = (e, t = {}) => Q("serviceAccount", e, t), Je = (e = null) => h(e), Ye = (e = "job:test", t) => t === void 0 ? y(e) : y(e, t), Xe = (e) => {
434
+ let t = e.indexOf("?");
435
+ return t === -1 ? {
436
+ path: e,
437
+ query: ""
438
+ } : {
439
+ path: e.slice(0, t),
440
+ query: e.slice(t + 1)
441
+ };
442
+ }, Ze = (e, t) => {
443
+ let n = e.split("/").filter((e) => e.length > 0), r = t.split("/").filter((e) => e.length > 0);
444
+ return n.length === r.length && n.every((e, t) => e.startsWith(":") || e === r[t]);
445
+ }, $ = (e) => {
446
+ let t = {};
447
+ for (let [n, r] of Object.entries(e)) t[n.toLowerCase()] = r;
448
+ return t;
449
+ }, Qe = (e) => e === void 0 ? /* @__PURE__ */ new Uint8Array() : new TextEncoder().encode(JSON.stringify(e)), $e = (e) => e === void 0 ? "" : typeof e == "string" ? e : new TextDecoder().decode(e), et = (e) => {
450
+ let { ctx: t } = e, n = _e((e.publicApi ?? []).map((e) => ({
451
+ descriptor: e.descriptor,
452
+ invoke: (n, r) => t.withSubject(r.subject, (t) => Y(e.descriptor, e.handler, n, t))
453
+ }))), r = [...e.restRoutes ?? [], ...n], i = _(e.strategies ?? [], {
454
+ ...e.anonymousTenantRequired === void 0 ? {} : { anonymousTenantRequired: e.anonymousTenantRequired },
455
+ getStore: () => t.store
456
+ }), a = (n, o) => {
457
+ let s = {
458
+ store: t.store,
459
+ resolveSubject: n === void 0 ? async (e) => (await i({
460
+ headers: e,
461
+ clientId: -1
462
+ })).subject : () => n,
463
+ ...e.idempotency === void 0 ? {} : { idempotency: e.idempotency }
464
+ }, c = ye(r, s).map((e, t) => ({
465
+ pattern: r[t].path,
466
+ route: e
467
+ })), l = async (e, n, i = {}) => {
468
+ let { path: a, query: s } = Xe(n), l = c.filter((e) => Ze(e.pattern, a)).map((e) => e.route);
469
+ if (l.length === 0) return {
470
+ status: 404,
471
+ headers: {},
472
+ text: "",
473
+ body: {
474
+ error: "No route mounted for this path",
475
+ mounted: r.map((e) => e.path)
476
+ },
477
+ stream: !1
478
+ };
479
+ let u = await ve(l, {
480
+ method: e.toUpperCase(),
481
+ path: a,
482
+ query: s,
483
+ headers: {
484
+ ...$(o),
485
+ ...$(i.headers ?? {})
486
+ },
487
+ rawBody: Qe(i.body),
488
+ store: t.store
489
+ }), d = $(u.headers ?? {});
490
+ u.contentType !== void 0 && (d["content-type"] = u.contentType);
491
+ let f = $e(u.body), p = (u.contentType ?? "").includes("json");
492
+ return {
493
+ status: u.status,
494
+ headers: d,
495
+ text: f,
496
+ body: p && f !== "" ? JSON.parse(f) : void 0,
497
+ stream: u.stream !== void 0
498
+ };
499
+ };
500
+ return {
501
+ actingAs: (e) => a(e, o),
502
+ withHeaders: (e) => a(n, {
503
+ ...o,
504
+ ...e
505
+ }),
506
+ get: (e, t) => l("GET", e, t ?? {}),
507
+ delete: (e, t) => l("DELETE", e, t ?? {}),
508
+ post: (e, t, n) => l("POST", e, {
509
+ ...n ?? {},
510
+ body: t
511
+ }),
512
+ put: (e, t, n) => l("PUT", e, {
513
+ ...n ?? {},
514
+ body: t
515
+ }),
516
+ patch: (e, t, n) => l("PATCH", e, {
517
+ ...n ?? {},
518
+ body: t
519
+ }),
520
+ request: (e, t, n) => l(e, t, n ?? {}),
521
+ paths: r.map((e) => e.path)
522
+ };
523
+ };
524
+ return a(void 0, {});
525
+ }, tt = (e = {}) => {
526
+ let t = e.tenantId ?? null, n = new te({
331
527
  origin: e.origin ?? "test",
332
528
  ...e.ringSize === void 0 ? {} : { ringSize: e.ringSize }
333
529
  });
334
530
  return {
335
531
  publish: async (e, r, i) => {
336
- let a = await u.runPromise(u.either(ae({
532
+ let a = await u.runPromise(u.either(de({
337
533
  bus: n,
338
534
  tenantId: t
339
535
  }, e, r, i)));
@@ -382,7 +578,7 @@ var j = new Set(e), M = 0, N = (e, t) => {
382
578
  },
383
579
  skipSerials: (e, r, i, a) => {
384
580
  let o = {
385
- route: v(a?.tenantId ?? t, e.name, r),
581
+ route: ee(a?.tenantId ?? t, e.name, r),
386
582
  event: e.name,
387
583
  origin: "test-gap",
388
584
  n: i + 1,
@@ -396,17 +592,17 @@ var j = new Set(e), M = 0, N = (e, t) => {
396
592
  n.clear();
397
593
  }
398
594
  };
399
- }, $ = (e = {}) => {
595
+ }, nt = (e = {}) => {
400
596
  let t = e.id ?? "test", n = e.store ?? new Proxy({}, { get: (e, t) => {
401
597
  throw Error(`makeSubscribeContext: this subscriber reached ctx.store.${String(t)} and the test passed no store. Pass \`{ store: makeTestContext().store }\` so the subscriber and the code under test share one — an empty stand-in would let a read of the wrong table pass.`);
402
598
  } });
403
599
  return {
404
600
  id: t,
405
- log: e.log ?? de({ scope: `subscribe:${t}` }),
601
+ log: e.log ?? be({ scope: `subscribe:${t}` }),
406
602
  store: n,
407
603
  ...e.publish === void 0 ? {} : { publish: e.publish }
408
604
  };
409
- }, we = (e) => {
605
+ }, rt = (e) => {
410
606
  let t = /* @__PURE__ */ new Map();
411
607
  for (let n of e) {
412
608
  let e = t.get(n.stepName);
@@ -432,13 +628,13 @@ var j = new Set(e), M = 0, N = (e, t) => {
432
628
  });
433
629
  }
434
630
  return n.sort((e, t) => e._startedAt - t._startedAt), n.map(({ _startedAt: e, ...t }) => t);
435
- }, Te = (e) => {
631
+ }, it = (e) => {
436
632
  let t = e.workflows ?? [], n = /* @__PURE__ */ new Map(), r = 0;
437
633
  return {
438
634
  start: async (i, a) => {
439
635
  let o = t.find((e) => e.workflow.name === i);
440
636
  if (o === void 0) throw Error(`makeWorkflowRunner: no workflow named '${i}' — pass it in makeWorkflowRunner({ ctx, workflows: [{ workflow, execute }] }).`);
441
- let s = `wfrun_test_${++r}`, c = A(), l = o.workflow.toLayer(o.execute).pipe(f.provideMerge(k)), d = /* @__PURE__ */ new Date(), p = o.workflow.execute(a).pipe(u.locally(fe, s), u.provide(c.layer), u.provide(l), u.either), m = await u.runPromise(p), h = we(c.readSteps()), g = /* @__PURE__ */ new Date(), _;
637
+ let s = `wfrun_test_${++r}`, c = Ce(), l = o.workflow.toLayer(o.execute).pipe(f.provideMerge(Se)), d = /* @__PURE__ */ new Date(), p = o.workflow.execute(a).pipe(u.locally(xe, s), u.provide(c.layer), u.provide(l), u.either), m = await u.runPromise(p), h = rt(c.readSteps()), g = /* @__PURE__ */ new Date(), _;
442
638
  if (m._tag === "Right") _ = {
443
639
  status: "succeeded",
444
640
  output: m.right,
@@ -478,6 +674,6 @@ var j = new Set(e), M = 0, N = (e, t) => {
478
674
  },
479
675
  inspect: async (e) => n.get(e) ?? null
480
676
  };
481
- }, Ee = 1;
677
+ }, at = 1;
482
678
  //#endregion
483
- export { F as MockClock, L as MockEmail, z as MockLLM, R as MockWebhooks, Ee as TESTING_PRESET_VERSION, n as changeDelete, r as changeInsert, i as changeSoftDelete, a as changeUpdate, Q as describeIfAvailable, Se as describeIfReachable, P as fixtureRow, xe as invoke, Z as isTcpReachable, $ as makeSubscribeContext, ye as makeTestContext, Te as makeWorkflowRunner, B as mockStore, pe as outboxNudgesOf, q as resetAfterCommit, K as rpcInterceptorFor, J as runAfterCommit, Y as runInStoreTransaction, Ce as testEventBus };
679
+ export { P as MockClock, I as MockLLM, F as MockWebhooks, at as TESTING_PRESET_VERSION, We as TEST_TENANT_ID, Je as anonymous, Ke as apiKey, n as changeDelete, r as changeInsert, i as changeSoftDelete, a as changeUpdate, Oe as defineFactory, Z as describeIfAvailable, Ue as describeIfReachable, D as fixtureRow, Me as frozenTime, Y as invoke, X as isTcpReachable, nt as makeSubscribeContext, et as makeTestApp, Ve as makeTestContext, it as makeWorkflowRunner, Pe as mockStore, E as nextSequence, Fe as outboxNudgesOf, G as resetAfterCommit, U as rpcInterceptorFor, K as runAfterCommit, q as runInStoreTransaction, qe as serviceAccount, W as serviceLayerFor, Ye as system, tt as testEventBus, Ge as user, je as withFrozenTime };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@voltro/testing",
3
- "version": "0.33.0",
4
- "description": "Test utilities for Voltro apps — deterministic clock, captured emails, queued LLM responses, scoped subject/tenant runners, and a cross-dialect parity harness.",
3
+ "version": "0.34.0",
4
+ "description": "Test utilities for Voltro apps — deterministic clock, subject and row factories, a handler-level invoke and a request-level app harness, queued LLM responses, scoped subject/tenant runners, and a cross-dialect parity harness.",
5
5
  "keywords": [
6
6
  "voltro",
7
7
  "typescript",
@@ -43,15 +43,15 @@
43
43
  "node": ">=24.0.0"
44
44
  },
45
45
  "dependencies": {
46
- "@voltro/database": "0.33.0",
47
- "@voltro/env": "0.33.0",
48
- "@voltro/logger": "0.33.0",
49
- "@voltro/protocol": "0.33.0",
50
- "@voltro/runtime": "0.33.0",
51
- "@voltro/workflow": "0.33.0"
46
+ "@voltro/database": "0.34.0",
47
+ "@voltro/env": "0.34.0",
48
+ "@voltro/logger": "0.34.0",
49
+ "@voltro/protocol": "0.34.0",
50
+ "@voltro/runtime": "0.34.0",
51
+ "@voltro/workflow": "0.34.0"
52
52
  },
53
53
  "peerDependencies": {
54
- "@voltro/client": "0.33.0",
54
+ "@voltro/client": "0.34.0",
55
55
  "effect": "^3.22.0",
56
56
  "react": "^19.0.0",
57
57
  "@effect/sql": "^0.52.0"