@voltro/testing 0.11.1 → 0.11.2

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/CHANGELOG.md CHANGED
@@ -39,6 +39,44 @@ _Changes staged for the next release accumulate here (rolled up from
39
39
 
40
40
  ---
41
41
 
42
+ ## [0.11.2] — 2026-07-24
43
+
44
+ ### Added
45
+
46
+ - **@voltro/i18n** — Two escapes for adopting typed messages (`createTypedMessages`, #16) app-wide (#19):
47
+
48
+ - **`t.dynamic(runtimeKey, values?)`** — a first-class escape for a genuinely runtime-computed key, on both `useT` and the `useTFn()` result. It takes a plain string with NO forced ICU args, so it doesn't fight the strict literal-key surface. Until now the natural escape — casting a computed key to the catalog key union — made things WORSE: that union spans placeholder-bearing keys, so the call then demanded a spurious 2nd ICU arg. `t.dynamic` is the documented, discoverable alternative. - **`LooseTFunction`** — the widened `(id: string, values?) => string` signature to type a `t` pass-through across a package boundary that can't import the app catalog, instead of falling back to `(...args: any[]) => string`. A strict `TypedTFunction` is deliberately NOT assignable to it (a narrowed key param can't satisfy a wider one — that would erase the checking); pass `t.dynamic` at the boundary, which IS a `LooseTFunction`.
49
+
50
+ Additive: `TypedTFunction<C>` gains a `.dynamic` member (the callable surface is unchanged, so `Parameters<TypedTFunction<C>>[0]` and existing typed call sites still resolve). `createTypedMessages` attaches `.dynamic` in place on the two translate functions — no new per-render closure, so a captured `t`'s identity stays stable.
51
+ - **@voltro/testing, @voltro/database** — `fixtureRow(table, overrides)` (`@voltro/testing`) completes a partial test row so it satisfies the 0.11.1 required-column insert validation — WITHOUT disabling the check. It fills every NOT-NULL, no-default, non-auto-stamped column the payload omits with a schema-typed placeholder (a `oneOf` column takes its first allowed value; a `unique` column gets a distinct value per call so two fixtures don't collide; `timestamp`/`date` get a fixed epoch), then merges your overrides on top (an explicit value always wins). It leaves out exactly what a caller may omit — nullable, defaulted, and framework auto-stamped columns (id / tenant / audit) — and refuses to guess a structured type (`json` / `bytes` / `vector` / `array` / `interval` / `raw`), throwing a message that names the column and says to pass it explicitly.
52
+
53
+ The motivating case: 0.11.1 made the in-memory/test store reject the same partial inserts real Postgres always would (correct — it surfaced a latent prod bug), which turned lean fixtures (`insert(users, { id })`, an omitted required FK) into `TableValidationFailed`. The wrong fix is a `validateInserts: false` knob — it re-hides that bug class, and a test store laxer than production is a fake testing itself. `fixtureRow` is the right one: it makes the fixture COMPLETE.
54
+
55
+ ```ts
56
+ await ctx.store.insert('journal_entries', fixtureRow(journalEntries, {
57
+ tenantId, amount: '100.00', // the columns THIS test cares about
58
+ })) // entryNumber, postedAt, … auto-filled + unique
59
+ ```
60
+
61
+ It is a runtime filler for the loose `store.insert(name, row)` path (what fixtures use). For COMPILE-time payload typing, use `insertRow` / `upsertRow` from `@voltro/database`. The auto-stamped column set it skips is now exported as `AUTO_FILLED_COLUMNS` from `@voltro/database` — the same list `InferInsertRow` derives its optional columns from, single-sourced so the two can't drift.
62
+
63
+ ### Changed
64
+
65
+ - **@voltro/cli** — `voltro build` now emits **directly-executable** boot bundles for BOTH app kinds: the web start bundle (`.framework/dist-web/startBundle/startEntry.js`) and the api serve bundle (`.framework/dist-api/serveBundle/serveEntry.js`) each carry a main-guard that boots the app when run as `node <entry>.js`, and stays inert when imported (the `voltro start` / `voltro serve` dev fast paths are unchanged). Production containers can now use `CMD ["node", "…/startEntry.js"]` (or `serveEntry.js`) instead of `pnpm voltro start` / `pnpm voltro serve` — no pnpm process, no `@voltro/cli` bin at runtime — which is what makes `voltro prune-runtime` safe to enable on both: with the self-contained bundle as the real entrypoint, the @vercel/nft trace roots there and legitimately drops `@voltro/cli` and the whole inlined framework tree (a static site's `node_modules` collapses to ~0; a memory api's 146 MB → 11 MB). `prune-runtime` now also roots the trace at the serve bundle. The serve entry chdir's to the app root BEFORE its app-module registry keys are computed from cwd, preserving relocation-safety. Existing `pnpm voltro start` / `pnpm voltro serve` entrypoints keep working. The standalone Dockerfiles gain a build-time boot smoke that fails the build unless the pruned tree reaches ready.
66
+
67
+ ### Fixed
68
+
69
+ - **@voltro/i18n** — `createTypedMessages` (#16) no longer extracts phantom required vars from a nested plural/select message (#19). For `'{count, plural, one {# day} other {# days total duration}}'`, the type-level `ICUVars` parse was reading a branch's TEXT (`"# days total duration"`) as a bogus required arg name, so `useT('key', { count })` failed to typecheck even though it renders perfectly — and a real var nested inside a branch was dropped. `ICUArgName` now resolves to `never` for any candidate that isn't a valid ICU identifier (`^[A-Za-z0-9_]+$`), so branch text — which contains spaces / `#` / `—` — is never mistaken for a var. Only the top-level arg (`count`) is required, matching what the message actually needs.
70
+
71
+ Scope note: a REAL var nested inside a plural branch (`other {# — {discipline}}`) is still not collected, so it reads as not-required rather than wrongly-required — the safe direction. Apps that pluralise in JS over simple `{count}` messages (the Voltro idiom) were already fully typed and are unaffected.
72
+ - **@voltro/database, @voltro/runtime** — `InferInsertRow` (and thus `insertRow` / `upsertRow`, #15) no longer requires a non-nullable DB-generated (`generatedAs`) column (#20). A stored/virtual generated column declared without `.nullable()` and without a default was typed **required**, but MariaDB/Postgres REJECT an explicit value for a generated column — so the type forced the caller to pass a value the database refuses at runtime. `.generatedAs()` now marks the column optional-for-insert exactly like a `.default()` column (the DB supplies it), so it may be omitted; the whole payload guard on every real column stays intact.
73
+
74
+ Two runtime halves complete it, so the loose `store.insert(name, row)` path agrees: the required-column validation (`missingRequiredColumns`) skips generated columns — omitting one is correct, never a missing-column error — and the store write path now STRIPS any value a caller supplied for a generated column before the INSERT reaches the dialect (tracked on the schema registry as `generatedColumns`), so a value from an untyped insert can't blow up on MariaDB. A generated column is never caller-supplied; the framework and the DB own it end to end.
75
+
76
+ The `.generatedAs()` return type narrows from `this` to `ColumnBuilder<…, true>` (the HasDefault flag) — a purely more-permissive refinement: it only makes the column omittable, so no existing code stops compiling.
77
+
78
+ ---
79
+
42
80
  ## [0.11.1] — 2026-07-23
43
81
 
44
82
  ### Added
@@ -71,6 +109,8 @@ _Changes staged for the next release accumulate here (rolled up from
71
109
  - **@voltro/database, @voltro/runtime** — `store.insert` / `upsert` / `insertIgnore` now raise a clear, typed `TableValidationFailed` naming the column when the payload omits one that is NOT NULL, has no default, and isn't auto-stamped — instead of a raw dialect `SqlError: Failed to execute statement` (`Field '…' doesn't have a default value`) surfaced only on the INSERT path (so it lay dormant until the first row with no existing cache entry). An upsert / insertIgnore whose payload is missing one of its own `conflictColumns` is likewise named at the call (an absent conflict key can't match its target). The check runs AFTER stamping, so auto-id / tenant / audit columns never trip it, and skips nullable, defaulted, and id (`idScheme`) columns — exactly the ones a caller may legitimately omit.
72
110
 
73
111
  Two pure helpers back it — `missingRequiredColumns(table, row)` and `missingConflictColumns(conflictColumns, row)` (exported from `@voltro/database`). This is the runtime half of the "handler data silently disagrees with the schema" class; a compile-time payload type needs the column DSL to track `hasDefault` at the type level, which is a separate change.
112
+
113
+ **Migration impact — behaviour-breaking for lenient test fixtures.** The in-memory/test store now rejects the same partial inserts a real Postgres always would, so it stops being laxer than production — which is the point (it surfaced at least one latent prod bug where a NOT-NULL `text().unique()` column was written without a value). But a fixture that inserted a partial row (`{ id }` parents, an omitted required FK) and passed against the old lenient memory store now throws `TableValidationFailed`. There is no code-level codemod — the fix is fixture DATA: fill the required columns. Use the new `fixtureRow(table, overrides)` helper in `@voltro/testing`, which fills every NOT-NULL-no-default column with a schema-typed placeholder and merges your overrides on top, so a fixture complies without disabling the check. There is deliberately no opt-out to turn the validation off: a test store that accepts rows production rejects is a fake testing itself.
74
114
  - **@voltro/database, @voltro/plugin-ai-flows, @voltro/plugin-audit, @voltro/plugin-deactivation, @voltro/plugin-soft-delete** — Compile-time payload typing for writes (#15) — the type-level half that the runtime `TableValidationFailed` guard flagged as a separate change. `insertRow` / `upsertRow` take the TABLE OBJECT (not a string name), so the payload is checked against `InferInsertRow<T>`: every column is required EXCEPT nullable ones, columns with a default, and the framework-filled id/tenant/audit columns. A missing NOT-NULL-no-default column — the exact `lastRefreshedAt` / `teamId` omission from the report — is now a COMPILE error at the call, not a runtime SqlError only on the INSERT path; `upsertRow`'s `conflictColumns` are constrained to the table's own columns too.
75
115
 
76
116
  import { insertRow } from '@voltro/database' await insertRow(ctx.store, roadmapEpicStats, { teamId, lastRefreshedAt: new Date() })
package/dist/dialect.js CHANGED
@@ -1,5 +1,5 @@
1
- import { _voltroUndoLogTable as e, applyInverses as t, synthesizeInverse as n } from "@voltro/runtime";
2
- import { boolean as r, eq as i, id as a, integer as o, table as s, text as c, timestamp as l } from "@voltro/database";
1
+ import { boolean as e, eq as t, id as n, integer as r, table as i, text as a, timestamp as o } from "@voltro/database";
2
+ import { _voltroUndoLogTable as s, applyInverses as c, synthesizeInverse as l } from "@voltro/runtime";
3
3
  import { afterEach as u, beforeEach as d, describe as f, expect as p, it as m } from "vitest";
4
4
  import { applySchema as h } from "@voltro/database/sql";
5
5
  //#region src/dialectParity.ts
@@ -10,33 +10,33 @@ var g = (e) => ({
10
10
  take: void 0,
11
11
  skip: void 0,
12
12
  projection: void 0
13
- }), _ = s("voltro_parity_a", {
14
- id: a(),
15
- title: c(),
16
- count: o()
17
- }), v = s("voltro_parity_b", {
18
- id: a(),
19
- title: c()
20
- }), y = s("voltro_parity_c", {
21
- id: a(),
22
- title: c(),
23
- count: o(),
24
- flag: r().default(!1)
25
- }), b = s("voltro_parity_d", {
26
- id: a(),
27
- title: c(),
28
- at: l().nullable(),
29
- n: o().nullable()
30
- }), x = (r) => {
31
- f(`dialect parity: ${r.name ?? r.dialect.id}`, () => {
32
- let a = null;
13
+ }), _ = i("voltro_parity_a", {
14
+ id: n(),
15
+ title: a(),
16
+ count: r()
17
+ }), v = i("voltro_parity_b", {
18
+ id: n(),
19
+ title: a()
20
+ }), y = i("voltro_parity_c", {
21
+ id: n(),
22
+ title: a(),
23
+ count: r(),
24
+ flag: e().default(!1)
25
+ }), b = i("voltro_parity_d", {
26
+ id: n(),
27
+ title: a(),
28
+ at: o().nullable(),
29
+ n: r().nullable()
30
+ }), x = (e) => {
31
+ f(`dialect parity: ${e.name ?? e.dialect.id}`, () => {
32
+ let n = null;
33
33
  d(async () => {
34
- await r.setup(), a = await r.make(), await a.migrate([
34
+ await e.setup(), n = await e.make(), await n.migrate([
35
35
  _,
36
36
  v,
37
37
  y,
38
38
  b,
39
- e
39
+ s
40
40
  ]);
41
41
  for (let e of [
42
42
  "voltro_parity_a",
@@ -45,15 +45,15 @@ var g = (e) => ({
45
45
  "voltro_parity_d",
46
46
  "_voltro_undo_log"
47
47
  ]) {
48
- let t = await a.store.query(g(e));
49
- for (let n of t) await a.store.delete(e, n.id);
48
+ let t = await n.store.query(g(e));
49
+ for (let r of t) await n.store.delete(e, r.id);
50
50
  }
51
51
  }), u(async () => {
52
- await a?.dispose(), await r.teardown(), a = null;
52
+ await n?.dispose(), await e.teardown(), n = null;
53
53
  }), m("DDL is idempotent — re-applying the schema is a no-op", async () => {
54
- await a.migrate([_, v]);
54
+ await n.migrate([_, v]);
55
55
  }), m("insert + query round-trips a row", async () => {
56
- p(await a.store.insert("voltro_parity_a", {
56
+ p(await n.store.insert("voltro_parity_a", {
57
57
  id: "a1",
58
58
  title: "one",
59
59
  count: 1
@@ -62,23 +62,23 @@ var g = (e) => ({
62
62
  title: "one",
63
63
  count: 1
64
64
  });
65
- let e = await a.store.query(g("voltro_parity_a"));
65
+ let e = await n.store.query(g("voltro_parity_a"));
66
66
  p(e.length).toBe(1), p(e[0]?.id).toBe("a1");
67
67
  }), m("update returns the post-image", async () => {
68
- await a.store.insert("voltro_parity_a", {
68
+ await n.store.insert("voltro_parity_a", {
69
69
  id: "a2",
70
70
  title: "pre",
71
71
  count: 1
72
- }), p(await a.store.update("voltro_parity_a", "a2", { title: "post" })).toMatchObject({
72
+ }), p(await n.store.update("voltro_parity_a", "a2", { title: "post" })).toMatchObject({
73
73
  id: "a2",
74
74
  title: "post"
75
75
  });
76
76
  }), m("delete removes the row + reports true", async () => {
77
- await a.store.insert("voltro_parity_a", {
77
+ await n.store.insert("voltro_parity_a", {
78
78
  id: "a3",
79
79
  title: "x",
80
80
  count: 1
81
- }), p(await a.store.delete("voltro_parity_a", "a3")).toBe(!0), p((await a.store.query(g("voltro_parity_a"))).length).toBe(0);
81
+ }), p(await n.store.delete("voltro_parity_a", "a3")).toBe(!0), p((await n.store.query(g("voltro_parity_a"))).length).toBe(0);
82
82
  }), m("undo: the _voltro_undo_log TEXT changes column round-trips a JSON ChangeSet", async () => {
83
83
  let e = [{
84
84
  table: "voltro_parity_c",
@@ -97,7 +97,7 @@ var g = (e) => ({
97
97
  flag: !1
98
98
  }
99
99
  }];
100
- await a.store.insert("_voltro_undo_log", {
100
+ await n.store.insert("_voltro_undo_log", {
101
101
  id: "undo_p1",
102
102
  tag: "parityC.update",
103
103
  label: null,
@@ -109,7 +109,7 @@ var g = (e) => ({
109
109
  undone: !1,
110
110
  createdAt: /* @__PURE__ */ new Date()
111
111
  });
112
- let t = await a.store.query(g("_voltro_undo_log"));
112
+ let t = await n.store.query(g("_voltro_undo_log"));
113
113
  p(t.length).toBe(1), p(JSON.parse(t[0].changes)).toEqual(e);
114
114
  }), m("undo: reverts an INSERT (inverse delete) on this dialect", async () => {
115
115
  let e = {
@@ -118,7 +118,7 @@ var g = (e) => ({
118
118
  changes: [{
119
119
  table: "voltro_parity_c",
120
120
  op: "insert",
121
- next: await a.store.insert("voltro_parity_c", {
121
+ next: await n.store.insert("voltro_parity_c", {
122
122
  id: "c2",
123
123
  title: "new",
124
124
  count: 1,
@@ -126,23 +126,23 @@ var g = (e) => ({
126
126
  })
127
127
  }]
128
128
  };
129
- await t(a.store, n(e)), p((await a.store.query(g("voltro_parity_c"))).find((e) => e.id === "c2")).toBeUndefined();
129
+ await c(n.store, l(e)), p((await n.store.query(g("voltro_parity_c"))).find((e) => e.id === "c2")).toBeUndefined();
130
130
  }), m("undo: reverts an UPDATE including the BOOLEAN, restoring the captured value FAITHFULLY", async () => {
131
- await a.store.insert("voltro_parity_c", {
131
+ await n.store.insert("voltro_parity_c", {
132
132
  id: "c3",
133
133
  title: "t",
134
134
  count: 1,
135
135
  flag: !0
136
136
  });
137
- let e = (await a.store.query({
137
+ let e = (await n.store.query({
138
138
  ...g("voltro_parity_c"),
139
- predicate: i("id", "c3")
140
- }))[0], r = await a.store.update("voltro_parity_c", "c3", {
139
+ predicate: t("id", "c3")
140
+ }))[0], r = await n.store.update("voltro_parity_c", "c3", {
141
141
  count: 2,
142
142
  flag: !1
143
143
  });
144
144
  p(r.flag).not.toEqual(e.flag);
145
- let o = {
145
+ let i = {
146
146
  invocationId: "inv2",
147
147
  subject: null,
148
148
  changes: [{
@@ -153,20 +153,20 @@ var g = (e) => ({
153
153
  next: r
154
154
  }]
155
155
  };
156
- await t(a.store, n(o));
157
- let s = (await a.store.query({
156
+ await c(n.store, l(i));
157
+ let a = (await n.store.query({
158
158
  ...g("voltro_parity_c"),
159
- predicate: i("id", "c3")
159
+ predicate: t("id", "c3")
160
160
  }))[0];
161
- p(s.flag).toEqual(e.flag), p(s.count).toEqual(e.count);
161
+ p(a.flag).toEqual(e.flag), p(a.count).toEqual(e.count);
162
162
  }), m("undo: reverts a DELETE (inverse re-insert) on this dialect", async () => {
163
- let e = await a.store.insert("voltro_parity_c", {
163
+ let e = await n.store.insert("voltro_parity_c", {
164
164
  id: "c4",
165
165
  title: "gone",
166
166
  count: 9,
167
167
  flag: !0
168
168
  });
169
- await a.store.delete("voltro_parity_c", "c4");
169
+ await n.store.delete("voltro_parity_c", "c4");
170
170
  let r = {
171
171
  invocationId: "inv3",
172
172
  subject: null,
@@ -177,19 +177,19 @@ var g = (e) => ({
177
177
  prev: e
178
178
  }]
179
179
  };
180
- await t(a.store, n(r));
181
- let o = (await a.store.query({
180
+ await c(n.store, l(r));
181
+ let i = (await n.store.query({
182
182
  ...g("voltro_parity_c"),
183
- predicate: i("id", "c4")
183
+ predicate: t("id", "c4")
184
184
  }))[0];
185
- p(o.id).toBe("c4"), p(o.title).toBe("gone"), p(o.flag).toEqual(e.flag);
185
+ p(i.id).toBe("c4"), p(i.title).toBe("gone"), p(i.flag).toEqual(e.flag);
186
186
  }), m("onChange fires insert + update + delete events", async () => {
187
- let e = [], t = a.store.onChange((t) => e.push(t.op));
188
- await a.store.insert("voltro_parity_a", {
187
+ let e = [], t = n.store.onChange((t) => e.push(t.op));
188
+ await n.store.insert("voltro_parity_a", {
189
189
  id: "a4",
190
190
  title: "a",
191
191
  count: 1
192
- }), await a.store.update("voltro_parity_a", "a4", { title: "b" }), await a.store.delete("voltro_parity_a", "a4"), t(), p(e).toEqual([
192
+ }), await n.store.update("voltro_parity_a", "a4", { title: "b" }), await n.store.delete("voltro_parity_a", "a4"), t(), p(e).toEqual([
193
193
  "insert",
194
194
  "update",
195
195
  "delete"
@@ -199,22 +199,22 @@ var g = (e) => ({
199
199
  e.push(t.name);
200
200
  };
201
201
  process.on("warning", t);
202
- let n = Array.from({ length: 32 }, () => a.store.onChange(() => {}));
202
+ let r = Array.from({ length: 32 }, () => n.store.onChange(() => {}));
203
203
  await new Promise((e) => setImmediate(e));
204
- for (let e of n) e();
204
+ for (let e of r) e();
205
205
  process.off("warning", t), p(e.filter((e) => e === "MaxListenersExceededWarning"), "the change bus still warns at 11 listeners — `raiseChangeListenerCeiling` is not wired into this dialect’s store").toEqual([]);
206
206
  }), m("transactional rolls back on throw + drops queued events", async () => {
207
- let e = [], t = a.store.onChange((t) => e.push(t.op));
208
- await p(a.store.transactional(async (e) => {
207
+ let e = [], t = n.store.onChange((t) => e.push(t.op));
208
+ await p(n.store.transactional(async (e) => {
209
209
  throw await e.insert("voltro_parity_a", {
210
210
  id: "rollback",
211
211
  title: "r",
212
212
  count: 0
213
213
  }), Error("intentional rollback");
214
- })).rejects.toThrow("intentional rollback"), t(), p(e).toEqual([]), p((await a.store.query(g("voltro_parity_a"))).length).toBe(0);
214
+ })).rejects.toThrow("intentional rollback"), t(), p(e).toEqual([]), p((await n.store.query(g("voltro_parity_a"))).length).toBe(0);
215
215
  }), m("transactional commit drains queued events in order", async () => {
216
- let e = [], t = a.store.onChange((t) => e.push(t.op));
217
- await a.store.transactional(async (e) => {
216
+ let e = [], t = n.store.onChange((t) => e.push(t.op));
217
+ await n.store.transactional(async (e) => {
218
218
  await e.insert("voltro_parity_a", {
219
219
  id: "commit1",
220
220
  title: "a",
@@ -226,11 +226,11 @@ var g = (e) => ({
226
226
  });
227
227
  }), t(), p(e).toEqual(["insert", "insert"]);
228
228
  }), m("upsert inserts a new row, then updates on conflict — no duplicate", async () => {
229
- await a.store.upsert("voltro_parity_a", {
229
+ await n.store.upsert("voltro_parity_a", {
230
230
  id: "u1",
231
231
  title: "first",
232
232
  count: 1
233
- }, { conflictColumns: ["id"] }), await a.store.upsert("voltro_parity_a", {
233
+ }, { conflictColumns: ["id"] }), await n.store.upsert("voltro_parity_a", {
234
234
  id: "u1",
235
235
  title: "second",
236
236
  count: 2
@@ -238,14 +238,14 @@ var g = (e) => ({
238
238
  conflictColumns: ["id"],
239
239
  update: ["title", "count"]
240
240
  });
241
- let e = await a.store.query(g("voltro_parity_a"));
241
+ let e = await n.store.query(g("voltro_parity_a"));
242
242
  p(e.length).toBe(1), p(e[0]).toMatchObject({
243
243
  id: "u1",
244
244
  title: "second",
245
245
  count: 2
246
246
  });
247
247
  }), m("insert + update bind an explicit NULL into a datetime2/int column", async () => {
248
- p(await a.store.insert("voltro_parity_d", {
248
+ p(await n.store.insert("voltro_parity_d", {
249
249
  id: "nd1",
250
250
  title: "nulls",
251
251
  at: null,
@@ -255,12 +255,12 @@ var g = (e) => ({
255
255
  title: "nulls",
256
256
  at: null,
257
257
  n: null
258
- }), await a.store.insert("voltro_parity_d", {
258
+ }), await n.store.insert("voltro_parity_d", {
259
259
  id: "nd2",
260
260
  title: "set",
261
261
  at: /* @__PURE__ */ new Date(),
262
262
  n: 7
263
- }), p(await a.store.update("voltro_parity_d", "nd2", {
263
+ }), p(await n.store.update("voltro_parity_d", "nd2", {
264
264
  at: null,
265
265
  n: null
266
266
  })).toMatchObject({
@@ -269,23 +269,23 @@ var g = (e) => ({
269
269
  n: null
270
270
  });
271
271
  }), m("updateMany updates only the rows matching the predicate + returns the count", async () => {
272
- await a.store.insert("voltro_parity_a", {
272
+ await n.store.insert("voltro_parity_a", {
273
273
  id: "m1",
274
274
  title: "x",
275
275
  count: 5
276
- }), await a.store.insert("voltro_parity_a", {
276
+ }), await n.store.insert("voltro_parity_a", {
277
277
  id: "m2",
278
278
  title: "y",
279
279
  count: 5
280
- }), await a.store.insert("voltro_parity_a", {
280
+ }), await n.store.insert("voltro_parity_a", {
281
281
  id: "m3",
282
282
  title: "z",
283
283
  count: 9
284
- }), p(await a.store.updateMany("voltro_parity_a", { title: "hit" }, { where: {
284
+ }), p(await n.store.updateMany("voltro_parity_a", { title: "hit" }, { where: {
285
285
  column: "count",
286
286
  op: "eq",
287
287
  value: 5
288
- } })).toBe(2), p((await a.store.query({
288
+ } })).toBe(2), p((await n.store.query({
289
289
  ...g("voltro_parity_a"),
290
290
  predicate: {
291
291
  column: "title",
@@ -294,7 +294,7 @@ var g = (e) => ({
294
294
  }
295
295
  })).length).toBe(2);
296
296
  }), m("updateMany with an AND predicate is an atomic compare-and-set (the wakeup-claim pattern)", async () => {
297
- await a.store.insert("voltro_parity_a", {
297
+ await n.store.insert("voltro_parity_a", {
298
298
  id: "cas",
299
299
  title: "pending",
300
300
  count: 1
@@ -308,63 +308,63 @@ var g = (e) => ({
308
308
  op: "eq",
309
309
  value: e
310
310
  }] });
311
- p(await a.store.updateMany("voltro_parity_a", { title: "claimed" }, { where: e("pending") })).toBe(1), p(await a.store.updateMany("voltro_parity_a", { title: "claimed-again" }, { where: e("pending") })).toBe(0);
311
+ p(await n.store.updateMany("voltro_parity_a", { title: "claimed" }, { where: e("pending") })).toBe(1), p(await n.store.updateMany("voltro_parity_a", { title: "claimed-again" }, { where: e("pending") })).toBe(0);
312
312
  }), m("deleteMany removes only the rows matching the predicate + returns the count", async () => {
313
- await a.store.insert("voltro_parity_a", {
313
+ await n.store.insert("voltro_parity_a", {
314
314
  id: "d1",
315
315
  title: "x",
316
316
  count: 5
317
- }), await a.store.insert("voltro_parity_a", {
317
+ }), await n.store.insert("voltro_parity_a", {
318
318
  id: "d2",
319
319
  title: "y",
320
320
  count: 5
321
- }), await a.store.insert("voltro_parity_a", {
321
+ }), await n.store.insert("voltro_parity_a", {
322
322
  id: "d3",
323
323
  title: "z",
324
324
  count: 9
325
- }), p(await a.store.deleteMany("voltro_parity_a", { where: {
325
+ }), p(await n.store.deleteMany("voltro_parity_a", { where: {
326
326
  column: "count",
327
327
  op: "eq",
328
328
  value: 5
329
- } })).toBe(2), p((await a.store.query(g("voltro_parity_a"))).map((e) => e.id)).toEqual(["d3"]);
329
+ } })).toBe(2), p((await n.store.query(g("voltro_parity_a"))).map((e) => e.id)).toEqual(["d3"]);
330
330
  }), m("deleteMany emits one delete ChangeEvent per removed row (old-image)", async () => {
331
- await a.store.insert("voltro_parity_a", {
331
+ await n.store.insert("voltro_parity_a", {
332
332
  id: "e1",
333
333
  title: "gone",
334
334
  count: 1
335
- }), await a.store.insert("voltro_parity_a", {
335
+ }), await n.store.insert("voltro_parity_a", {
336
336
  id: "e2",
337
337
  title: "gone",
338
338
  count: 1
339
- }), await a.store.insert("voltro_parity_a", {
339
+ }), await n.store.insert("voltro_parity_a", {
340
340
  id: "e3",
341
341
  title: "stay",
342
342
  count: 2
343
343
  });
344
- let e = [], t = a.store.onChange((t) => e.push({
344
+ let e = [], t = n.store.onChange((t) => e.push({
345
345
  op: t.op,
346
346
  id: t.old?.id
347
- })), n = await a.store.deleteMany("voltro_parity_a", { where: {
347
+ })), r = await n.store.deleteMany("voltro_parity_a", { where: {
348
348
  column: "title",
349
349
  op: "eq",
350
350
  value: "gone"
351
351
  } });
352
- t(), p(n).toBe(2);
353
- let r = e.filter((e) => e.op === "delete");
354
- p(r).toHaveLength(2), p(r.map((e) => e.id).sort()).toEqual(["e1", "e2"]);
352
+ t(), p(r).toBe(2);
353
+ let i = e.filter((e) => e.op === "delete");
354
+ p(i).toHaveLength(2), p(i.map((e) => e.id).sort()).toEqual(["e1", "e2"]);
355
355
  }), m("deleteMany with no match returns 0 and removes nothing", async () => {
356
- await a.store.insert("voltro_parity_a", {
356
+ await n.store.insert("voltro_parity_a", {
357
357
  id: "k1",
358
358
  title: "keep",
359
359
  count: 1
360
- }), p(await a.store.deleteMany("voltro_parity_a", { where: {
360
+ }), p(await n.store.deleteMany("voltro_parity_a", { where: {
361
361
  column: "title",
362
362
  op: "eq",
363
363
  value: "absent"
364
- } })).toBe(0), p((await a.store.query(g("voltro_parity_a"))).length).toBe(1);
364
+ } })).toBe(0), p((await n.store.query(g("voltro_parity_a"))).length).toBe(1);
365
365
  }), m("retryFilter recognises the dialect's own transient codes", () => {
366
- let e = r.dialect.retryFilter(/* @__PURE__ */ Error("garbage"));
367
- p(["retry", "noRetry"]).toContain(e);
366
+ let t = e.dialect.retryFilter(/* @__PURE__ */ Error("garbage"));
367
+ p(["retry", "noRetry"]).toContain(t);
368
368
  });
369
369
  });
370
370
  };
package/dist/index.d.ts CHANGED
@@ -23,6 +23,15 @@ import { VoltroPlugin } from '@voltro/protocol';
23
23
  * ergonomic without an `any` in the public type. */
24
24
  declare type ErasedWorkflowEffect = Effect.Effect<unknown, unknown, never>;
25
25
 
26
+ /**
27
+ * Complete a partial row for `table` so it satisfies the required-column insert
28
+ * validation. Fills every NOT-NULL, no-default, non-auto-stamped column that
29
+ * `overrides` doesn't already supply, then merges `overrides` on top (an explicit
30
+ * value — including `null` — always wins). Returns a plain row for the loose
31
+ * `store.insert(name, row)` path.
32
+ */
33
+ export declare const fixtureRow: (table: TableLike, overrides?: Row) => Row;
34
+
26
35
  /**
27
36
  * What `invoke` resolves to for a handler returning `Result`: an `Effect`'s
28
37
  * SUCCESS value, an awaited `Promise`, or the value itself.
package/dist/index.js CHANGED
@@ -1,11 +1,36 @@
1
- import { Cause as e, Effect as t, Exit as n, Layer as r, Schema as i } from "effect";
2
- import { SubjectService as a, anonymousSubject as o, checkGuardsEffect as s, composeRpcInterceptors as c } from "@voltro/protocol";
3
- import { InMemoryDataStore as l, applyRowFilterToDescriptor as u, clearSystemStoreHandle as d, getRowFilter as f, getSystemStoreHandle as p, makeAppAccess as m, makeDataLoader as h, makeEffectStoreLayer as g, makeOutboxFacade as _, makeSchemaRegistry as v, resolveRowFilterScopeFor as y, runProvidedEffect as b, runWithDeadlockRetry as x, setSystemStoreHandle as S, wrapStoreWithMixinBehaviour as C } from "@voltro/runtime";
4
- import { allRegisteredTables as w, clearRelationsRegistry as T, registerRelations as E } from "@voltro/database";
5
- import { installEnvSnapshot as D } from "@voltro/env";
6
- import { CurrentWorkflowRunId as O, inMemoryWorkflowEngineLayer as k, makeInMemoryRecorder as A } from "@voltro/workflow";
7
- //#region src/mockClock.ts
8
- var j = class {
1
+ import { AUTO_FILLED_COLUMNS as e, allRegisteredTables as t, clearRelationsRegistry as n, missingRequiredColumns as r, registerRelations as i } from "@voltro/database";
2
+ import { Cause as a, Effect as o, Exit as s, Layer as c, Schema as l } from "effect";
3
+ import { SubjectService as u, anonymousSubject as d, checkGuardsEffect as f, composeRpcInterceptors as p } from "@voltro/protocol";
4
+ import { InMemoryDataStore as m, applyRowFilterToDescriptor as h, clearSystemStoreHandle as g, getRowFilter as _, getSystemStoreHandle as v, makeAppAccess as y, makeDataLoader as b, makeEffectStoreLayer as ee, makeOutboxFacade as x, makeSchemaRegistry as S, resolveRowFilterScopeFor as C, runProvidedEffect as w, runWithDeadlockRetry as T, setSystemStoreHandle as E, wrapStoreWithMixinBehaviour as D } from "@voltro/runtime";
5
+ import { installEnvSnapshot as O } from "@voltro/env";
6
+ import { CurrentWorkflowRunId as k, inMemoryWorkflowEngineLayer as A, makeInMemoryRecorder as j } from "@voltro/workflow";
7
+ //#region src/fixtureRow.ts
8
+ var M = new Set(e), N = 0, P = (e, t) => {
9
+ if (t.oneOf && t.oneOf.length > 0) return t.oneOf[0];
10
+ if (t.enumValues && t.enumValues.length > 0) return t.enumValues[0];
11
+ let n = t.unique === !0;
12
+ switch (t.type) {
13
+ case "text":
14
+ case "reference":
15
+ case "id":
16
+ case "enum": return n ? `${e}-${++N}` : e;
17
+ case "integer":
18
+ case "real": return n ? ++N : 0;
19
+ case "decimal":
20
+ case "bigint": return n ? String(++N) : "0";
21
+ case "boolean": return !1;
22
+ case "timestamp":
23
+ case "date": return /* @__PURE__ */ new Date(0);
24
+ 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}: … }).`);
25
+ }
26
+ }, F = (e, t = {}) => {
27
+ let n = {};
28
+ for (let i of r(e, t)) M.has(i) || (n[i] = P(i, e.fields[i]));
29
+ return {
30
+ ...n,
31
+ ...t
32
+ };
33
+ }, I = class {
9
34
  currentMs;
10
35
  constructor(e = /* @__PURE__ */ new Date("2026-01-01T00:00:00Z")) {
11
36
  this.currentMs = typeof e == "number" ? e : e.getTime();
@@ -17,9 +42,9 @@ var j = class {
17
42
  return new Date(this.currentMs);
18
43
  }
19
44
  advance(e) {
20
- this.currentMs += typeof e == "number" ? e : M(e);
45
+ this.currentMs += typeof e == "number" ? e : L(e);
21
46
  }
22
- }, M = (e) => {
47
+ }, L = (e) => {
23
48
  let t = /^\s*(\d+(?:\.\d+)?)\s*(ms|s|m|h|d)\s*$/.exec(e);
24
49
  if (!t) throw Error(`mockClock: cannot parse duration '${e}'`);
25
50
  let n = Number(t[1]), r = t[2];
@@ -31,7 +56,7 @@ var j = class {
31
56
  case "d": return n * 864e5;
32
57
  default: throw Error(`mockClock: unknown unit '${r}'`);
33
58
  }
34
- }, N = class {
59
+ }, R = class {
35
60
  sent = [];
36
61
  send(e) {
37
62
  this.sent.push({
@@ -45,7 +70,7 @@ var j = class {
45
70
  clear() {
46
71
  this.sent.length = 0;
47
72
  }
48
- }, P = class {
73
+ }, z = class {
49
74
  queue;
50
75
  calls = [];
51
76
  constructor(e) {
@@ -60,29 +85,29 @@ var j = class {
60
85
  remaining() {
61
86
  return this.queue.length;
62
87
  }
63
- }, F = (e) => e, I = "00000000000000000000000000000000", L = /* @__PURE__ */ new WeakMap(), R = /* @__PURE__ */ new WeakMap(), z = /* @__PURE__ */ new WeakMap(), B = /* @__PURE__ */ new WeakMap(), V = (e, t) => {
64
- let n = B.get(e) ?? [], r = t === "mutation" ? "interceptMutation" : t === "query" ? "interceptQuery" : "interceptAction", i = [];
88
+ }, B = (e) => e, V = "00000000000000000000000000000000", H = /* @__PURE__ */ new WeakMap(), U = /* @__PURE__ */ new WeakMap(), W = /* @__PURE__ */ new WeakMap(), G = /* @__PURE__ */ new WeakMap(), K = (e, t) => {
89
+ let n = G.get(e) ?? [], r = t === "mutation" ? "interceptMutation" : t === "query" ? "interceptQuery" : "interceptAction", i = [];
65
90
  for (let e of n) {
66
91
  let t = e[r];
67
92
  typeof t == "function" && i.push(t);
68
93
  }
69
- return c(i);
70
- }, H = (e) => {
71
- let t = R.get(e);
94
+ return p(i);
95
+ }, q = (e) => {
96
+ let t = U.get(e);
72
97
  t !== void 0 && (t.length = 0);
73
- }, U = async (e) => {
74
- let t = R.get(e);
98
+ }, J = async (e) => {
99
+ let t = U.get(e);
75
100
  if (t === void 0 || t.length === 0) return;
76
101
  let n = [...t];
77
102
  t.length = 0;
78
103
  for (let e of n) await e();
79
- }, W = (e) => z.get(e) ?? [], G = async (e, t) => {
80
- let n = L.get(e);
104
+ }, Y = (e) => W.get(e) ?? [], X = async (e, t) => {
105
+ let n = H.get(e);
81
106
  return n === void 0 ? e.store.transactional(async (n) => t({
82
107
  ...e,
83
108
  store: n
84
109
  })) : n(t);
85
- }, K = (e) => {
110
+ }, Z = (e) => {
86
111
  let t = /* @__PURE__ */ new Map(), n = /* @__PURE__ */ new Map(), r = (n) => {
87
112
  let r = t.get(n);
88
113
  return r === void 0 ? !1 : r.expiresAt !== null && r.expiresAt <= e() ? (t.delete(n), !1) : !0;
@@ -121,7 +146,7 @@ var j = class {
121
146
  return i(e, o, n.ttlMs, n.tags), o;
122
147
  }
123
148
  };
124
- }, q = (e) => {
149
+ }, Q = (e) => {
125
150
  let t = /* @__PURE__ */ new Map(), n = (n) => {
126
151
  let r = t.get(n);
127
152
  return r === void 0 ? !1 : r.expiresAt !== null && r.expiresAt <= e() ? (t.delete(n), !1) : !0;
@@ -149,116 +174,116 @@ var j = class {
149
174
  t.clear();
150
175
  }
151
176
  };
152
- }, J = async (e, t) => {
153
- let n = p();
154
- S(e);
177
+ }, te = async (e, t) => {
178
+ let n = v();
179
+ E(e);
155
180
  try {
156
181
  return await t();
157
182
  } finally {
158
- n === void 0 ? d() : S(n);
183
+ n === void 0 ? g() : E(n);
159
184
  }
160
- }, Y = (e, t, n) => {
161
- let r = X, i;
185
+ }, ne = (e, t, n) => {
186
+ let r = re, i;
162
187
  return () => {
163
- let a = t ?? f();
164
- return (i === void 0 || a !== r) && (r = a, i = J(n, () => b(y(a, e, (e) => {
188
+ let a = t ?? _();
189
+ return (i === void 0 || a !== r) && (r = a, i = te(n, () => w(C(a, e, (e) => {
165
190
  console.error("[voltro:testing] row filter failed to load — reads refused for this subject", e);
166
191
  })))), i;
167
192
  };
168
- }, X = Symbol("unresolved"), Z = (e, t) => new Proxy(e, { get: (e, n) => {
169
- if (n === "query") return async (n) => e.query(u(await t(), n));
193
+ }, re = Symbol("unresolved"), ie = (e, t) => new Proxy(e, { get: (e, n) => {
194
+ if (n === "query") return async (n) => e.query(h(await t(), n));
170
195
  let r = Reflect.get(e, n, e);
171
196
  return typeof r == "function" ? r.bind(e) : r;
172
- } }), Q = (e = {}) => {
173
- if (D({
197
+ } }), ae = (e = {}) => {
198
+ if (O({
174
199
  ...process.env,
175
200
  ...e.env ?? {}
176
201
  }), e.relations !== void 0) {
177
- T();
178
- for (let t of e.relations) E(t);
202
+ n();
203
+ for (let t of e.relations) i(t);
179
204
  }
180
- let t = v(e.tables ?? w()), n = new l(e.store ?? {}), r = {
181
- dataStore: n,
182
- schemaRegistry: t
205
+ let r = S(e.tables ?? t()), a = new m(e.store ?? {}), o = {
206
+ dataStore: a,
207
+ schemaRegistry: r
183
208
  };
184
- S(r);
185
- let i = new j(e.clockStart), a = new N(), s = new P(e.llmResponses ?? []), c = K(() => i.now()), u = q(() => i.now()), d = (o, l = n, f) => {
186
- let p = {
187
- subject: o,
188
- traceId: I
189
- }, g = f?.queued ?? [], v = f?.nudges ?? [], y = h({ store: l }), b = Y(o, e.rowFilter, r), x = {
190
- clock: i,
191
- email: a,
192
- llm: s,
209
+ E(o);
210
+ let s = new I(e.clockStart), c = new R(), l = new z(e.llmResponses ?? []), u = Z(() => s.now()), f = Q(() => s.now()), p = (t, n = a, i) => {
211
+ let d = {
212
+ subject: t,
213
+ traceId: V
214
+ }, m = i?.queued ?? [], h = i?.nudges ?? [], g = b({ store: n }), _ = ne(t, e.rowFilter, o), v = {
215
+ clock: s,
216
+ email: c,
217
+ llm: l,
193
218
  ...e.ai === void 0 ? {} : { ai: e.ai },
194
- request: p,
195
- access: m(o),
196
- cache: c,
197
- kv: u,
198
- store: C(Z(l, b), {
199
- subject: o,
200
- schemaRegistry: t
219
+ request: d,
220
+ access: y(t),
221
+ cache: u,
222
+ kv: f,
223
+ store: D(ie(n, _), {
224
+ subject: t,
225
+ schemaRegistry: r
201
226
  }),
202
- outbox: _({
203
- store: l,
204
- subject: o,
205
- traceId: I,
227
+ outbox: x({
228
+ store: n,
229
+ subject: t,
230
+ traceId: V,
206
231
  nudge: () => {
207
- v.push(I);
232
+ h.push(V);
208
233
  },
209
234
  afterCommit: (e) => {
210
- g.push(e);
235
+ m.push(e);
211
236
  }
212
237
  }),
213
- load: y.load,
214
- loadMany: y.loadMany,
215
- withSubject: (e, t) => Promise.resolve(t(d(e, l, {
216
- queued: g,
217
- nudges: v
238
+ load: g.load,
239
+ loadMany: g.loadMany,
240
+ withSubject: (e, t) => Promise.resolve(t(p(e, n, {
241
+ queued: m,
242
+ nudges: h
218
243
  }))),
219
- withTenant: (e, t) => Promise.resolve(t(d({
220
- ...o,
244
+ withTenant: (e, r) => Promise.resolve(r(p({
245
+ ...t,
221
246
  tenantId: e
222
- }, l, {
223
- queued: g,
224
- nudges: v
247
+ }, n, {
248
+ queued: m,
249
+ nudges: h
225
250
  })))
226
251
  };
227
- return R.set(x, g), z.set(x, v), B.set(x, e.plugins ?? []), L.set(x, (e) => l.transactional((t) => e(d(o, t, {
228
- queued: g,
229
- nudges: v
230
- })))), x;
252
+ return U.set(v, m), W.set(v, h), G.set(v, e.plugins ?? []), H.set(v, (e) => n.transactional((n) => e(p(t, n, {
253
+ queued: m,
254
+ nudges: h
255
+ })))), v;
231
256
  };
232
- return d(e.subject ?? o(null));
233
- }, $ = (e, n) => e.pipe(t.provide(g(n.store)), t.provideService(a, n.request.subject)), ee = async (r, a, o, c) => {
234
- let l = r.input, u = await i.decodeUnknownPromise(l)(o), d = r.kind, f = async (e) => {
235
- let n = a(u, e);
236
- return t.isEffect(n) ? b($(n, e)) : n;
257
+ return p(e.subject ?? d(null));
258
+ }, oe = (e, t) => e.pipe(o.provide(ee(t.store)), o.provideService(u, t.request.subject)), $ = async (e, t, n, r) => {
259
+ let i = e.input, c = await l.decodeUnknownPromise(i)(n), u = e.kind, d = async (e) => {
260
+ let n = t(c, e);
261
+ return o.isEffect(n) ? w(oe(n, e)) : n;
237
262
  }, p = async () => {
238
- let e = r.guards;
239
- if (e !== void 0 && e.length > 0) {
240
- let n = await t.runPromise(s(c.request.subject, e, u));
241
- if (n !== null) throw n;
263
+ let t = e.guards;
264
+ if (t !== void 0 && t.length > 0) {
265
+ let e = await o.runPromise(f(r.request.subject, t, c));
266
+ if (e !== null) throw e;
242
267
  }
243
- if (d !== "mutation") return f(c);
244
- let n = await x(async () => (H(c), G(c, async (e) => f(e))), { delay: () => Promise.resolve() });
245
- return await U(c), n;
246
- }, m = d === "mutation" || d === "query" || d === "action" ? d : void 0, h = m === void 0 ? void 0 : V(c, m);
268
+ if (u !== "mutation") return d(r);
269
+ let n = await T(async () => (q(r), X(r, async (e) => d(e))), { delay: () => Promise.resolve() });
270
+ return await J(r), n;
271
+ }, m = u === "mutation" || u === "query" || u === "action" ? u : void 0, h = m === void 0 ? void 0 : K(r, m);
247
272
  if (h === void 0 || m === void 0) return await p();
248
- let g = h(t.tryPromise({
273
+ let g = h(o.tryPromise({
249
274
  try: () => p(),
250
275
  catch: (e) => e
251
276
  }), {
252
- tag: r.name ?? "",
277
+ tag: e.name ?? "",
253
278
  kind: m,
254
- input: u,
255
- subject: c.request.subject,
256
- traceId: c.request.traceId,
257
- ...c.request.spanId === void 0 ? {} : { spanId: c.request.spanId }
258
- }), _ = await t.runPromiseExit(g);
259
- if (n.isSuccess(_)) return _.value;
260
- throw e.squash(_.cause);
261
- }, te = (e) => {
279
+ input: c,
280
+ subject: r.request.subject,
281
+ traceId: r.request.traceId,
282
+ ...r.request.spanId === void 0 ? {} : { spanId: r.request.spanId }
283
+ }), _ = await o.runPromiseExit(g);
284
+ if (s.isSuccess(_)) return _.value;
285
+ throw a.squash(_.cause);
286
+ }, se = (e) => {
262
287
  let t = /* @__PURE__ */ new Map();
263
288
  for (let n of e) {
264
289
  let e = t.get(n.stepName);
@@ -284,13 +309,13 @@ var j = class {
284
309
  });
285
310
  }
286
311
  return n.sort((e, t) => e._startedAt - t._startedAt), n.map(({ _startedAt: e, ...t }) => t);
287
- }, ne = (e) => {
288
- let n = e.workflows ?? [], i = /* @__PURE__ */ new Map(), a = 0;
312
+ }, ce = (e) => {
313
+ let t = e.workflows ?? [], n = /* @__PURE__ */ new Map(), r = 0;
289
314
  return {
290
- start: async (o, s) => {
291
- let c = n.find((e) => e.workflow.name === o);
292
- if (c === void 0) throw Error(`makeWorkflowRunner: no workflow named '${o}' — pass it in makeWorkflowRunner({ ctx, workflows: [{ workflow, execute }] }).`);
293
- let l = `wfrun_test_${++a}`, u = A(), d = c.workflow.toLayer(c.execute).pipe(r.provideMerge(k)), f = /* @__PURE__ */ new Date(), p = c.workflow.execute(s).pipe(t.locally(O, l), t.provide(u.layer), t.provide(d), t.either), m = await t.runPromise(p), h = te(u.readSteps()), g = /* @__PURE__ */ new Date(), _;
315
+ start: async (i, a) => {
316
+ let s = t.find((e) => e.workflow.name === i);
317
+ if (s === void 0) throw Error(`makeWorkflowRunner: no workflow named '${i}' — pass it in makeWorkflowRunner({ ctx, workflows: [{ workflow, execute }] }).`);
318
+ let l = `wfrun_test_${++r}`, u = j(), d = s.workflow.toLayer(s.execute).pipe(c.provideMerge(A)), f = /* @__PURE__ */ new Date(), p = s.workflow.execute(a).pipe(o.locally(k, l), o.provide(u.layer), o.provide(d), o.either), m = await o.runPromise(p), h = se(u.readSteps()), g = /* @__PURE__ */ new Date(), _;
294
319
  if (m._tag === "Right") _ = {
295
320
  status: "succeeded",
296
321
  output: m.right,
@@ -311,12 +336,12 @@ var j = class {
311
336
  runId: l
312
337
  };
313
338
  }
314
- return i.set(l, {
339
+ return n.set(l, {
315
340
  id: l,
316
341
  executionId: l,
317
- name: o,
342
+ name: i,
318
343
  status: _.status,
319
- input: s,
344
+ input: a,
320
345
  output: _.output,
321
346
  subject: e.ctx.request.subject,
322
347
  source: "test-runner",
@@ -328,8 +353,8 @@ var j = class {
328
353
  parentClosePolicy: null
329
354
  }), _;
330
355
  },
331
- inspect: async (e) => i.get(e) ?? null
356
+ inspect: async (e) => n.get(e) ?? null
332
357
  };
333
- }, re = 1;
358
+ }, le = 1;
334
359
  //#endregion
335
- export { j as MockClock, N as MockEmail, P as MockLLM, re as TESTING_PRESET_VERSION, ee as invoke, Q as makeTestContext, ne as makeWorkflowRunner, F as mockStore, W as outboxNudgesOf, H as resetAfterCommit, V as rpcInterceptorFor, U as runAfterCommit, G as runInStoreTransaction };
360
+ export { I as MockClock, R as MockEmail, z as MockLLM, le as TESTING_PRESET_VERSION, F as fixtureRow, $ as invoke, ae as makeTestContext, ce as makeWorkflowRunner, B as mockStore, Y as outboxNudgesOf, q as resetAfterCommit, K as rpcInterceptorFor, J as runAfterCommit, X as runInStoreTransaction };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voltro/testing",
3
- "version": "0.11.1",
3
+ "version": "0.11.2",
4
4
  "description": "Test utilities for Voltro apps — deterministic clock, captured emails, queued LLM responses, scoped subject/tenant runners, and a cross-dialect parity harness.",
5
5
  "keywords": [
6
6
  "voltro",
@@ -42,16 +42,16 @@
42
42
  "node": ">=24.0.0"
43
43
  },
44
44
  "dependencies": {
45
- "@voltro/database": "0.11.1",
46
- "@voltro/env": "0.11.1",
47
- "@voltro/protocol": "0.11.1",
48
- "@voltro/runtime": "0.11.1",
49
- "@voltro/workflow": "0.11.1"
45
+ "@voltro/database": "0.11.2",
46
+ "@voltro/env": "0.11.2",
47
+ "@voltro/protocol": "0.11.2",
48
+ "@voltro/runtime": "0.11.2",
49
+ "@voltro/workflow": "0.11.2"
50
50
  },
51
51
  "peerDependencies": {
52
52
  "effect": "^3.21.4",
53
53
  "react": "^19.0.0",
54
- "@voltro/client": "0.11.1"
54
+ "@voltro/client": "0.11.2"
55
55
  },
56
56
  "peerDependenciesMeta": {
57
57
  "react": {