@voltro/testing 0.85.0 → 0.87.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.
@@ -0,0 +1,65 @@
1
+ import { fixtureRow as e, nextSequence as t } from "./fixtureRow.js";
2
+ import { missingRequiredColumns as n } from "@voltro/database";
3
+ //#region src/factory.ts
4
+ var r = (e) => {
5
+ let n = {};
6
+ for (let [r, i] of Object.entries(e)) n[r] = typeof i == "function" ? i(t()) : i;
7
+ return n;
8
+ }, i = (e) => {
9
+ for (let [t, n] of Object.entries(e.fields)) if (n.type === "id") return t;
10
+ return "id";
11
+ }, a = async (e, t, r, a, s) => {
12
+ let c = {};
13
+ for (let l of n(t, r)) {
14
+ let n = t.fields[l];
15
+ if (n === void 0 || n.type !== "reference" || n.references === void 0) continue;
16
+ let r = n.references();
17
+ if (s.includes(r.tableName)) throw Error(`defineFactory: creating '${t.tableName}' requires '${r.tableName}' via '${l}', and that reference is cyclic (${[...s, r.tableName].join(" → ")}). Insert one side first and pass the id: create(store, { ${l}: existing.id }).`);
18
+ let u = a[l];
19
+ c[l] = (u === void 0 ? await o(e, r, {}, {}, [...s, r.tableName]) : await u.create(e, {}))[n.referenceColumn ?? i(r)];
20
+ }
21
+ return c;
22
+ }, o = async (t, n, r, i, o) => {
23
+ let s = {
24
+ ...await a(t, n, r, i, o),
25
+ ...r
26
+ };
27
+ return t.insert(n.tableName, e(n, s));
28
+ }, s = (e, t) => typeof e == "function" ? e(t) : e ?? {}, c = (t, n = {}) => {
29
+ let i = n.traits ?? {}, a = n.associations ?? {}, c = (n) => {
30
+ let l = (e = {}) => ({
31
+ ...r(n),
32
+ ...e
33
+ }), u = (n = {}) => e(t, l(n));
34
+ return {
35
+ table: t,
36
+ build: u,
37
+ buildList: (e, t) => Array.from({ length: e }, (e, n) => u(s(t, n))),
38
+ create: (e, n = {}) => o(e, t, l(n), a, [t.tableName]),
39
+ createList: async (e, n, r) => {
40
+ let i = [];
41
+ for (let c = 0; c < n; c++) i.push(await o(e, t, l(s(r, c)), a, [t.tableName]));
42
+ return i;
43
+ },
44
+ with: (...e) => {
45
+ let r = n;
46
+ for (let n of e) {
47
+ let e = i[n];
48
+ if (e === void 0) throw Error(`defineFactory: '${t.tableName}' has no trait '${n}'. Declared: ${Object.keys(i).join(", ") || "(none)"}.`);
49
+ r = {
50
+ ...r,
51
+ ...e
52
+ };
53
+ }
54
+ return c(r);
55
+ },
56
+ extend: (e) => c({
57
+ ...n,
58
+ ...e
59
+ })
60
+ };
61
+ };
62
+ return c(n.defaults ?? {});
63
+ };
64
+ //#endregion
65
+ export { c as defineFactory };
@@ -0,0 +1,73 @@
1
+ import { declaredEncryptedColumns as e, declaredRenames as t, getFieldCipher as n, isFieldKeyring as r, makeFieldKeyring as i, openCell as a, setFieldCipher as o } from "@voltro/runtime";
2
+ //#region src/fieldKeys.ts
3
+ var s = /* @__PURE__ */ new WeakMap(), c = (e) => {
4
+ let t = i(e);
5
+ return s.set(t, e), o(t), t;
6
+ }, l = (e, t = e.active) => {
7
+ let n = e.keys.find((e) => e.id === t);
8
+ if (n === void 0) throw Error(`the keyring does not hold its active key "${t}"`);
9
+ return i({
10
+ active: n.id,
11
+ keys: [n]
12
+ });
13
+ }, u = (e = (() => {
14
+ let e = n();
15
+ return r(e) ? e : void 0;
16
+ })(), t) => {
17
+ if (t !== void 0) return l(t);
18
+ if (e === void 0) throw Error("no field keyring is registered — pass `fieldKeys` to makeTestContext, or call registerTestFieldKeys");
19
+ let i = s.get(e);
20
+ if (i === void 0) throw Error("the registered field keyring was not built by the test helpers, so its keys are unknown here — register it with `fieldKeys` / registerTestFieldKeys");
21
+ return l(i, e.activeKeyId);
22
+ }, d = /* @__PURE__ */ new WeakMap(), f = (e, t) => {
23
+ d.set(e, t);
24
+ }, p = (e) => typeof e == "string" ? e : e?.tableName, m = async (n, r) => {
25
+ let i = d.get(n) ?? n;
26
+ if (typeof i.query != "function") throw Error("pass a test context (makeTestContext) or a DataStore");
27
+ let o = [...r.tables], s = e(o), c = t(o), l = u(r.keyring), f = 0, m = [];
28
+ for (let e of o) {
29
+ let t = p(e);
30
+ if (t === void 0) continue;
31
+ let n = await i.query({
32
+ table: t,
33
+ predicate: void 0,
34
+ order: [],
35
+ take: void 0,
36
+ skip: void 0,
37
+ projection: void 0
38
+ });
39
+ for (let e of n) {
40
+ let { id: n, ...r } = e;
41
+ for (let [i, o] of Object.entries(e)) {
42
+ if (o == null) continue;
43
+ let e = typeof o == "string" ? o : JSON.stringify(o);
44
+ if (!e.includes("enc:v")) continue;
45
+ let u = a(e, l, {
46
+ table: t,
47
+ column: i,
48
+ rowId: n,
49
+ row: r,
50
+ declared: s,
51
+ renames: c
52
+ });
53
+ f += u.opened, u.unopened > 0 && m.push({
54
+ table: t,
55
+ column: i,
56
+ rowId: n,
57
+ count: u.unopened
58
+ });
59
+ }
60
+ }
61
+ }
62
+ return {
63
+ activeKeyId: l.activeKeyId,
64
+ opened: f,
65
+ unopened: m
66
+ };
67
+ }, h = (e) => `${e.unopened.reduce((e, t) => e + t.count, 0)} sealed value(s) do not open with the active key ${e.activeKeyId} alone:\n` + e.unopened.slice(0, 20).map((e) => ` ${e.table}.${e.column}${e.rowId === void 0 ? "" : ` (row ${String(e.rowId)})`}: ${e.count}`).join("\n") + (e.unopened.length > 20 ? `\n … and ${e.unopened.length - 20} more place(s)` : ""), g = async (e, t) => {
68
+ let n = await m(e, t);
69
+ if (n.unopened.length > 0) throw Error(h(n));
70
+ return n;
71
+ };
72
+ //#endregion
73
+ export { u as activeKeyAlone, h as describeActiveKeyAloneFailures, g as expectSealedWithActiveKeyAlone, f as recordTestRawStore, c as registerTestFieldKeys, m as sealedValuesWithActiveKeyAlone };
@@ -0,0 +1,14 @@
1
+ import { activeKeyAlone as e } from "./fieldKeys.js";
2
+ import { Effect as t } from "effect";
3
+ import { verifySealedValues as n } from "@voltro/runtime";
4
+ //#region src/fieldKeysSql.ts
5
+ var r = async (r) => {
6
+ let i = e(r.keyring, r.fieldKeys), a = await t.runPromise(t.scoped(n(r.dialect, i, r.tables ?? [])).pipe(t.provide(r.layer))), o = {
7
+ activeKeyId: i.activeKeyId,
8
+ ...a
9
+ };
10
+ if (o.unopened.length > 0 || o.unreadable.length > 0) throw Error(`${o.unopened.reduce((e, t) => e + t.count, 0)} sealed value(s) do not open with the active key ${o.activeKeyId} alone` + (o.unopened.length === 0 ? "" : `:\n${o.unopened.map((e) => ` ${e.table}.${e.column}: ${e.count}`).join("\n")}`) + (o.unreadable.length === 0 ? "" : `\n${o.unreadable.length} column(s) could not be read:\n${o.unreadable.map((e) => ` ${e.table}.${e.column}: ${e.reason}`).join("\n")}`));
11
+ return o;
12
+ };
13
+ //#endregion
14
+ export { r as expectSealedWithActiveKeyAloneInSql };
@@ -0,0 +1,30 @@
1
+ import { AUTO_FILLED_COLUMNS as e, missingRequiredColumns as t } from "@voltro/database";
2
+ //#region src/fixtureRow.ts
3
+ var n = new Set(e), r = 0, i = () => ++r, a = (e, t) => {
4
+ if (t.oneOf && t.oneOf.length > 0) return t.oneOf[0];
5
+ if (t.enumValues && t.enumValues.length > 0) return t.enumValues[0];
6
+ let n = t.unique === !0;
7
+ switch (t.type) {
8
+ case "text":
9
+ case "id":
10
+ case "enum": return n ? `${e}-${++r}` : e;
11
+ case "reference": return t.referenceStorage === "numeric" ? n ? ++r : 0 : n ? `${e}-${++r}` : e;
12
+ case "integer":
13
+ case "real": return n ? ++r : 0;
14
+ case "decimal":
15
+ case "bigint": return n ? String(++r) : "0";
16
+ case "boolean": return !1;
17
+ case "timestamp":
18
+ case "date": return /* @__PURE__ */ new Date(0);
19
+ 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}: … }).`);
20
+ }
21
+ }, o = (e, r = {}) => {
22
+ let i = {};
23
+ for (let o of t(e, r)) n.has(o) || (i[o] = a(o, e.fields[o]));
24
+ return {
25
+ ...i,
26
+ ...r
27
+ };
28
+ };
29
+ //#endregion
30
+ export { o as fixtureRow, i as nextSequence };
@@ -0,0 +1,46 @@
1
+ import { makeVoltroTestClient as e } from "./client.js";
2
+ import { act as t, createElement as n } from "react";
3
+ import { useFormBinding as r } from "@voltro/client";
4
+ //#region src/formBindingHarness.tsx
5
+ var i = async (i, a = {}) => {
6
+ if (globalThis.document === void 0) throw Error("renderFormBinding needs a DOM to render hooks — add `// @vitest-environment jsdom` to the spec.");
7
+ let { createRoot: o } = await import("react-dom/client"), s = a.apiName ?? "app", c = e({
8
+ apiName: s,
9
+ ...a.client ?? {},
10
+ mutations: {
11
+ [i]: a.mutation ?? (() => ({ ok: !0 })),
12
+ ...a.client?.mutations ?? {}
13
+ }
14
+ }), l, u = () => (l = r(s, i, a.binding ?? {}), null), d = o(globalThis.document.createElement("div"));
15
+ return await t(async () => {
16
+ d.render(n(c.Provider, null, n(u)));
17
+ }), {
18
+ binding: () => l,
19
+ fill: (e, n) => {
20
+ t(() => {
21
+ l.field(e).setValue(n);
22
+ });
23
+ },
24
+ blur: (e) => {
25
+ t(() => {
26
+ l.field(e).onBlur();
27
+ });
28
+ },
29
+ submit: async () => {
30
+ let e;
31
+ return await t(async () => {
32
+ e = await l.submit();
33
+ }), e;
34
+ },
35
+ errors: () => l.errors,
36
+ state: () => l.state,
37
+ client: c,
38
+ unmount: () => {
39
+ t(() => {
40
+ d.unmount();
41
+ });
42
+ }
43
+ };
44
+ };
45
+ //#endregion
46
+ export { i as renderFormBinding };
@@ -0,0 +1,132 @@
1
+ import { eq as e, queryFor as t, registerTable as n } from "@voltro/database";
2
+ import { Effect as r, Layer as i, Option as a, Result as o, Stream as s } from "effect";
3
+ import { describe as c, expect as l, it as u } from "vitest";
4
+ import { randomUUID as d } from "node:crypto";
5
+ import { HotStore as f } from "@voltro/kv/hot/contract";
6
+ import { hotStoreConformance as p } from "@voltro/kv/hot/testing";
7
+ import { dataStoreHotStore as m, hotDataStoreTables as h, hotKvTable as g, hotZTable as _ } from "@voltro/runtime/hot";
8
+ //#region src/hotStoreSqlParity.ts
9
+ var v = (v) => {
10
+ h.forEach(n), p(v.name, () => {
11
+ let e = 0;
12
+ return {
13
+ layer: i.effect(f, r.acquireRelease(r.promise(v.make), (e) => r.promise(e.dispose)).pipe(r.map((t) => m(t.store, {
14
+ dialect: v.dialect,
15
+ namespace: d(),
16
+ now: () => e
17
+ })))),
18
+ advance: (t) => r.sync(() => {
19
+ e += t;
20
+ }),
21
+ capabilities: {
22
+ atomicMulti: !0,
23
+ crossNode: !0,
24
+ persistent: !0
25
+ }
26
+ };
27
+ });
28
+ let y = (e, t) => u(e, async () => {
29
+ let e = await v.make();
30
+ try {
31
+ await t(e);
32
+ } finally {
33
+ await e.dispose();
34
+ }
35
+ }, 6e4);
36
+ c(`HotStore SQL parity — ${v.name}`, () => {
37
+ y("idempotent table DDL preserves exact normalized scores and physical deletion", async (e) => {
38
+ let n = m(e.store, {
39
+ dialect: v.dialect,
40
+ namespace: "sql"
41
+ }), i = 1.2345678901234567;
42
+ await r.runPromise(n.zadd("player:subject", [{
43
+ member: "member",
44
+ score: i
45
+ }])), await e.migrate(), l(await e.store.query(t(_).descriptor)).toEqual([l.objectContaining({
46
+ member: "member",
47
+ score: i
48
+ })]), await r.runPromise(n.delete("player:subject")), l(await e.store.query(t(_).descriptor)).toEqual([]), l(await e.store.query(t(g).descriptor)).toEqual([l.objectContaining({
49
+ keyHash: "lock",
50
+ namespace: null,
51
+ key: null,
52
+ payload: null,
53
+ expiresAtMs: null
54
+ })]);
55
+ }), y("wrong-type batches roll back every preceding SQL write", async (n) => {
56
+ let i = m(n.store, {
57
+ dialect: v.dialect,
58
+ namespace: "atomic"
59
+ });
60
+ await r.runPromise(i.zadd("board", [{
61
+ member: "m",
62
+ score: 1
63
+ }]));
64
+ let s = await r.runPromise(i.multi([{
65
+ op: "incrBy",
66
+ key: "other",
67
+ delta: 1
68
+ }, {
69
+ op: "incrBy",
70
+ key: "board",
71
+ delta: 1
72
+ }]).pipe(r.result));
73
+ l(o.isFailure(s)).toBe(!0), l(await n.store.query(t(g).where(e("key", "other")).descriptor)).toEqual([]), l(a.getOrNull(await r.runPromise(i.zscore("board", "m")))).toBe(1);
74
+ }), y("namespace/key boundaries, case, wildcard prefixes and long Unicode values remain exact", async (e) => {
75
+ let t = m(e.store, {
76
+ dialect: v.dialect,
77
+ namespace: "n"
78
+ }), n = m(e.store, {
79
+ dialect: v.dialect,
80
+ namespace: "n:tenant"
81
+ }), i = m(e.store, {
82
+ dialect: v.dialect,
83
+ namespace: "N"
84
+ });
85
+ await r.runPromise(t.incrBy("tenant:key", 4)), await r.runPromise(n.incrBy("key", 2)), await r.runPromise(i.incrBy("tenant:key", 9)), await r.runPromise(t.incrBy("%_:key", 1)), await r.runPromise(t.incrBy("wildcard-key", 1));
86
+ let o = "🎮".repeat(2e3);
87
+ await r.runPromise(t.zadd(o, [{
88
+ member: o,
89
+ score: -1.5
90
+ }])), l(await r.runPromise(t.incrBy("tenant:key", 0))).toBe(4), l(await r.runPromise(n.incrBy("key", 0))).toBe(2), l(await r.runPromise(i.incrBy("tenant:key", 0))).toBe(9), l(await r.runPromise(s.runCollect(t.scanPrefix("%_:")))).toEqual(["%_:key"]), l(await r.runPromise(s.runCollect(i.scanPrefix("")))).toEqual(["tenant:key"]), l(a.getOrNull(await r.runPromise(t.zscore(o, o)))).toBe(-1.5);
91
+ }), y("independent connections retain every cold-key increment and have one lease winner", async (e) => {
92
+ let t = [e.store];
93
+ for (let n = 0; n < 3; n++) t.push(await e.connect());
94
+ let n = t.map((e) => m(e, {
95
+ dialect: v.dialect,
96
+ namespace: "connections"
97
+ })), i = await Promise.all(Array.from({ length: 32 }, (e, t) => r.runPromise(n[t % n.length].incrBy("counter", 1))));
98
+ l(new Set(i).size).toBe(32), l(await r.runPromise(n[0].incrBy("counter", 0))).toBe(32);
99
+ let a = await Promise.all(Array.from({ length: 32 }, (e, t) => r.runPromise(n[t % n.length].setNx("seat", String(t), 6e4))));
100
+ l(a.filter(Boolean)).toHaveLength(1);
101
+ }), y("concurrent first-use ports admit coordination before application writes", async (e) => {
102
+ let t = [e.store];
103
+ for (let n = 0; n < 3; n++) t.push(await e.connect());
104
+ let n = Array.from({ length: 32 }, (e, n) => m(t[n % t.length], {
105
+ dialect: v.dialect,
106
+ namespace: "first-use"
107
+ })), i = (await Promise.allSettled(n.map((e) => r.runPromise(e.incrBy("counter", 1))))).map((e) => {
108
+ if (e.status === "rejected") throw e.reason;
109
+ return e.value;
110
+ });
111
+ l(new Set(i).size).toBe(32), l(await r.runPromise(n[0].incrBy("counter", 0))).toBe(32);
112
+ }), y("new connections rebuild the port from persisted SQL and apply stored expiry", async (e) => {
113
+ let t = 0, n = m(e.store, {
114
+ dialect: v.dialect,
115
+ namespace: "reconnect",
116
+ now: () => t
117
+ });
118
+ await r.runPromise(n.incrBy("counter", 7)), await r.runPromise(n.zadd("board", [{
119
+ member: "𐀀",
120
+ score: 1.2345678901234567
121
+ }])), await r.runPromise(n.setNx("seat", "holder", 100));
122
+ let i = m(await e.connect(), {
123
+ dialect: v.dialect,
124
+ namespace: "reconnect",
125
+ now: () => t
126
+ });
127
+ l(await r.runPromise(i.incrBy("counter", 0))).toBe(7), l(a.getOrNull(await r.runPromise(i.zscore("board", "𐀀")))).toBe(1.2345678901234567), l(await r.runPromise(i.setNx("seat", "replacement", 100))).toBe(!1), t = 100, l(await r.runPromise(i.setNx("seat", "replacement", 100))).toBe(!0);
128
+ });
129
+ });
130
+ };
131
+ //#endregion
132
+ export { v as runHotStoreSqlParity };
@@ -0,0 +1,22 @@
1
+ import { Result as e, Schema as t } from "effect";
2
+ //#region src/hydrateFixtureRows.ts
3
+ var n = t.decodeUnknownResult(t.DateFromString), r = /^\d{4}-\d{2}-\d{2}$/, i = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/, a = (t, a) => {
4
+ if (typeof t != "string" || !i.test(t) && !(a === "date" && r.test(t))) return !1;
5
+ let o = t.slice(0, 10), s = n(o);
6
+ return e.isSuccess(s) && Number.isFinite(s.success.getTime()) && s.success.toISOString().slice(0, 10) === o;
7
+ }, o = (t, r) => {
8
+ let i = Object.entries(t.fields).filter(([, e]) => e.type === "timestamp" || e.type === "date");
9
+ return r.map((r, o) => {
10
+ let s = { ...r };
11
+ for (let [c, l] of i) {
12
+ let i = r[c];
13
+ if (i == null || i instanceof Date && Number.isFinite(i.getTime())) continue;
14
+ let u = n(i);
15
+ if (!a(i, l.type) || e.isFailure(u) || !Number.isFinite(u.success.getTime())) throw TypeError(`hydrateFixtureRows: table '${t.tableName}', row ${o}, column '${c}' requires a valid Date or ISO date-time with Z or an offset${l.type === "date" ? " (YYYY-MM-DD is also accepted as UTC midnight)" : ""}.`);
16
+ s[c] = u.success;
17
+ }
18
+ return s;
19
+ });
20
+ };
21
+ //#endregion
22
+ export { o as hydrateFixtureRows };
package/dist/index.d.ts CHANGED
@@ -6,22 +6,33 @@ import { ChangeEvent } from '@voltro/database';
6
6
  import { changeInsert } from '@voltro/database';
7
7
  import { changeSoftDelete } from '@voltro/database';
8
8
  import { changeUpdate } from '@voltro/database';
9
+ import { ComputedQueryCacheDeps } from '@voltro/runtime';
10
+ import { ConnectionRef } from '@voltro/kv/connection';
11
+ import { ConnectionRegistry } from '@voltro/kv/connection';
12
+ import { ConnectionRegistryOptions } from '@voltro/kv/connection';
13
+ import { ConnectionsConfig } from '@voltro/kv/connection';
14
+ import { DialectId } from '@voltro/database';
9
15
  import { Effect } from 'effect';
10
16
  import { EventBus } from '@voltro/runtime';
11
17
  import { EventDescriptor } from '@voltro/protocol';
12
18
  import { EventPublisher } from '@voltro/runtime';
19
+ import { FieldKeyring } from '@voltro/runtime';
20
+ import { FieldKeyringConfig } from '@voltro/runtime';
13
21
  import { InspectedStep } from '@voltro/workflow';
14
22
  import { InspectedWorkflow } from '@voltro/workflow';
15
23
  import { Layer } from 'effect';
24
+ import { MemoryRedisConnection } from '@voltro/kv/connection';
16
25
  import { ProcedureDescriptor } from '@voltro/protocol';
17
26
  import { PublicApiDescriptor } from '@voltro/protocol/rest';
18
27
  import { PublicApiProfileConfig } from '@voltro/protocol';
19
28
  import { PublicApiSpec } from '@voltro/protocol';
29
+ import { RedisConnections } from '@voltro/kv';
20
30
  import { RelationsSpec } from '@voltro/database';
21
31
  import { RestRouteDescriptor } from '@voltro/protocol/rest';
22
32
  import { RestServeBindings } from '@voltro/protocol/rest';
23
33
  import { Row } from '@voltro/database';
24
34
  import { RowFilter } from '@voltro/runtime';
35
+ import { RowFilterScope } from '@voltro/runtime';
25
36
  import { RpcConnection } from '@voltro/runtime';
26
37
  import { RpcInterceptor } from '@voltro/protocol';
27
38
  import { RpcKind } from '@voltro/protocol';
@@ -29,12 +40,46 @@ import { Schema } from 'effect';
29
40
  import { SchemaError } from 'effect/Schema';
30
41
  import { Scope } from 'effect';
31
42
  import { ServiceApi } from '@voltro/protocol';
43
+ import { SqlClient } from 'effect/sql';
32
44
  import { Subject } from '@voltro/protocol';
33
45
  import { SubscribeContext } from '@voltro/runtime';
34
46
  import { SyncLogger } from '@voltro/logger';
35
47
  import { TableLike } from '@voltro/database';
36
48
  import { VoltroPlugin } from '@voltro/protocol';
37
49
 
50
+ /**
51
+ * The keyring reduced to its active key — `fieldKeys` when given (a
52
+ * configuration, e.g. the one a rotation printed), else the registered one.
53
+ */
54
+ export declare const activeKeyAlone: (keyring?: FieldKeyring | undefined, fieldKeys?: FieldKeyringConfig) => FieldKeyring;
55
+
56
+ /** Values the active key alone does not open, per place. */
57
+ export declare interface ActiveKeyAloneReport {
58
+ readonly activeKeyId: string;
59
+ readonly opened: number;
60
+ readonly unopened: ReadonlyArray<{
61
+ readonly table: string;
62
+ readonly column: string;
63
+ readonly rowId: unknown;
64
+ readonly count: number;
65
+ }>;
66
+ }
67
+
68
+ export declare interface ActiveKeyAloneSqlReport {
69
+ readonly activeKeyId: string;
70
+ readonly opened: number;
71
+ readonly unopened: ReadonlyArray<{
72
+ readonly table: string;
73
+ readonly column: string;
74
+ readonly count: number;
75
+ }>;
76
+ readonly unreadable: ReadonlyArray<{
77
+ readonly table: string;
78
+ readonly column: string;
79
+ readonly reason: string;
80
+ }>;
81
+ }
82
+
38
83
  /** An unauthenticated caller, optionally carrying a tenant (what the
39
84
  * `x-tenant` header resolves to when no auth strategy matches). Delegates to
40
85
  * the protocol's own constructor so the harness cannot drift from it. */
@@ -142,6 +187,28 @@ export declare interface EmittedWebhook {
142
187
  * ergonomic without an `any` in the public type. */
143
188
  declare type ErasedWorkflowEffect = Effect.Effect<unknown, unknown, never>;
144
189
 
190
+ /** Assert that after a rotation every sealed value opens with the active key alone. Returns the report. */
191
+ export declare const expectSealedWithActiveKeyAlone: (source: object, options: {
192
+ readonly tables: Iterable<unknown>;
193
+ readonly keyring?: FieldKeyring;
194
+ }) => Promise<ActiveKeyAloneReport>;
195
+
196
+ /**
197
+ * Assert that every sealed value in the database `layer` connects to opens with
198
+ * the active key alone. `tables` are the app's declared tables (their
199
+ * `.encrypted()` columns are always checked); `keyring` defaults to the one
200
+ * registered with `fieldKeys` / `registerTestFieldKeys`. Columns that cannot be
201
+ * read fail the assertion too — a check that skipped them would pass vacuously.
202
+ */
203
+ export declare const expectSealedWithActiveKeyAloneInSql: (options: {
204
+ readonly dialect: DialectId;
205
+ readonly layer: Layer.Layer<SqlClient.SqlClient, unknown, never>;
206
+ readonly tables?: Iterable<unknown>;
207
+ readonly keyring?: FieldKeyring;
208
+ /** The keyring as configured (ids and keys, the active one named) — instead of a registered keyring. */
209
+ readonly fieldKeys?: FieldKeyringConfig;
210
+ }) => Promise<ActiveKeyAloneSqlReport>;
211
+
145
212
  export declare interface Factory {
146
213
  /** The table this factory builds rows for. */
147
214
  readonly table: TableLike;
@@ -482,6 +549,14 @@ export declare interface MakeTestContextOptions<RowFilterCtx = unknown> {
482
549
  * gate, and the gate is what turns the variable into a cipher.
483
550
  */
484
551
  readonly fieldEncryptionKey?: string;
552
+ /**
553
+ * A keyring instead of one key — several ids, one active — registered the way
554
+ * the boot registers `VOLTRO_FIELD_ENCRYPTION_KEYS` (`registerTestFieldKeys`).
555
+ * With it, `expectSealedWithActiveKeyAlone(ctx, { tables })` checks what a
556
+ * rotation ends with: every sealed value opens with the active key alone.
557
+ * Not together with `fieldEncryptionKey`.
558
+ */
559
+ readonly fieldKeys?: FieldKeyringConfig;
485
560
  /**
486
561
  * Plugins whose rpc interceptors (`interceptMutation` / `interceptQuery` /
487
562
  * `interceptAction`) wrap every `invoke` on this context — the same list an
@@ -528,7 +603,7 @@ export declare interface MakeTestContextOptions<RowFilterCtx = unknown> {
528
603
  * Merged LAST, so a user layer overrides a plugin layer declaring the same
529
604
  * Tag — the ordering `makeHandlerServiceLayer` uses on both boot paths.
530
605
  */
531
- readonly layers?: ReadonlyArray<Layer.Layer<unknown, never, never>>;
606
+ readonly layers?: ReadonlyArray<Layer.Layer<never, never, never>>;
532
607
  /**
533
608
  * Row-level security for this context, WITHOUT touching the process global.
534
609
  *
@@ -706,6 +781,8 @@ export declare interface PublicApiBinding {
706
781
  readonly handler: (input: never, ctx: TestContext) => unknown;
707
782
  }
708
783
 
784
+ export declare const queryCacheOf: (ctx: TestContext) => TestQueryCache | undefined;
785
+
709
786
  export declare interface ReachableTarget {
710
787
  readonly host: string;
711
788
  readonly port: number;
@@ -721,6 +798,14 @@ export declare interface RecordedDelivery<P = unknown> {
721
798
  readonly n: number;
722
799
  }
723
800
 
801
+ /**
802
+ * Register a keyring as this process's field cipher — what the boot builds from
803
+ * `VOLTRO_FIELD_ENCRYPTION_KEYS` — and remember its keys, so the active-key-alone
804
+ * check can open values with the active key and nothing else. Call it again with
805
+ * the next configuration to rotate inside a test.
806
+ */
807
+ export declare const registerTestFieldKeys: (config: FieldKeyringConfig) => FieldKeyring;
808
+
724
809
  /**
725
810
  * The engine BUILD behind a target, printed once per file.
726
811
  *
@@ -809,6 +894,16 @@ export declare const runInStoreTransaction: <T>(ctx: TestContext, work: (txCtx:
809
894
 
810
895
  export { SchemaError }
811
896
 
897
+ /**
898
+ * Every sealed value in `tables` — the app's declared tables, or names — opened
899
+ * with the active key alone; the values it does not open are listed. Reads the
900
+ * RAW rows (envelopes as stored), not through the store's decryption.
901
+ */
902
+ export declare const sealedValuesWithActiveKeyAlone: (source: object, options: {
903
+ readonly tables: Iterable<unknown>;
904
+ readonly keyring?: FieldKeyring;
905
+ }) => Promise<ActiveKeyAloneReport>;
906
+
812
907
  /** A machine subject scoped to ONE tenant (a per-tenant integration, a
813
908
  * `storeForTenant` view). NOT the cross-tenant `system()` below. */
814
909
  export declare const serviceAccount: (id: string, options?: TestSubjectOptions) => Subject;
@@ -1071,6 +1166,22 @@ export declare interface TestEventBusOptions {
1071
1166
 
1072
1167
  export declare const TESTING_PRESET_VERSION: 1;
1073
1168
 
1169
+ /**
1170
+ * The query cache a context's `invoke` serves `cache:` queries through — the
1171
+ * runtime's own `cachedComputedCall` over a real `@voltro/cache` `Cache` (memory
1172
+ * backend, ttl on `ctx.clock`). A write through any context of the tree drops
1173
+ * the entries tagged with its table, synchronously, so a test can assert
1174
+ * "write X invalidates query Y" and "a revoked grant refuses a hit" without a
1175
+ * redis and without waiting.
1176
+ */
1177
+ export declare interface TestQueryCache {
1178
+ readonly deps: ComputedQueryCacheDeps;
1179
+ /** This context's resolved row-filter visibility (`undefined`: no filter). */
1180
+ readonly rowFilter: () => Promise<RowFilterScope | undefined>;
1181
+ /** Results a cached compute did not keep, and why — the dev log's lines. */
1182
+ readonly notices: ReadonlyArray<string>;
1183
+ }
1184
+
1074
1185
  export declare interface TestRequestOptions {
1075
1186
  /** Extra headers for this call, merged over the app's (`actingAs` /
1076
1187
  * `withHeaders`) headers. Names are lowercased, as the transport delivers
@@ -1094,13 +1205,13 @@ export declare interface TestResponse {
1094
1205
  readonly headers: Readonly<Record<string, string | ReadonlyArray<string>>>;
1095
1206
  /** The raw response body as text (empty string when there was none). */
1096
1207
  readonly text: string;
1097
- /** The body parsed as JSON when the route answered with a JSON content-type,
1098
- * otherwise `undefined`. Every framework REST response is JSON; a streaming
1099
- * (SSE) route answers with neither — see `stream`. */
1208
+ /** Byte-exact buffered body, including a consumed finite byte stream. SSE remains unconsumed. */
1209
+ readonly bytes?: Uint8Array;
1210
+ /** Parsed JSON when the response has a JSON content-type; otherwise
1211
+ * `undefined`. SSE responses remain unconsumed. */
1100
1212
  readonly body: unknown;
1101
- /** `true` for a streaming (SSE) route: the response has no buffered body, so
1102
- * `text` is empty and `body` is `undefined`. Stated rather than silently
1103
- * looking like an empty 200. */
1213
+ /** True for SSE and byte streams. Finite byte streams populate `bytes` and
1214
+ * `text`; SSE remains unconsumed, with empty text and undefined body. */
1104
1215
  readonly stream: boolean;
1105
1216
  }
1106
1217
 
@@ -1138,6 +1249,13 @@ export declare interface TestSubscriber<P = unknown> {
1138
1249
  */
1139
1250
  export declare const user: (id: string, options?: TestSubjectOptions) => Subject;
1140
1251
 
1252
+ /** Replace named app entries with scoped local transports, without changing a
1253
+ * handler's references. Provide this layer last in a test context's layers. */
1254
+ export declare const withConnections: (overrides: Readonly<Record<ConnectionRef<"redis">, MemoryRedisConnection>>, options?: {
1255
+ readonly connections?: ConnectionsConfig;
1256
+ readonly connectAnalytics?: ConnectionRegistryOptions["connectAnalytics"];
1257
+ }) => Layer.Layer< ConnectionRegistry | RedisConnections, never, never>;
1258
+
1141
1259
  /**
1142
1260
  * Run `body` with global time frozen at `at`, then restore — whether the body
1143
1261
  * returns, throws, resolves or rejects.