@voltro/testing 0.86.0 → 0.88.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/CHANGELOG.md +5484 -0
- package/README.md +11 -0
- package/THIRD-PARTY-NOTICES.md +1511 -276
- package/dist/client.d.ts +5 -0
- package/dist/client.js +70 -58
- package/dist/crdt.js +1 -2
- package/dist/dialect.d.ts +20 -0
- package/dist/dialect.js +992 -694
- package/dist/factory.js +1 -2
- package/dist/fieldKeys.js +72 -0
- package/dist/fieldKeysSql.js +13 -0
- package/dist/fixtureRow.js +1 -2
- package/dist/formBindingHarness.js +1 -2
- package/dist/hotStoreSqlParity.js +135 -0
- package/dist/hydrateFixtureRows.js +1 -2
- package/dist/index.d.ts +119 -1
- package/dist/index.js +18 -16
- package/dist/invoke.js +77 -40
- package/dist/mockClock.js +1 -2
- package/dist/mockLLM.js +1 -2
- package/dist/mockWebhooks.js +1 -2
- package/dist/platform.js +1 -2
- package/dist/reachable.js +1 -2
- package/dist/serviceApiMock.js +1 -2
- package/dist/subject.js +1 -2
- package/dist/subscribeContext.js +1 -2
- package/dist/system.d.ts +99 -0
- package/dist/system.js +93 -0
- package/dist/testApps.js +1 -2
- package/dist/testContext.js +133 -113
- package/dist/testEventBus.js +1 -2
- package/dist/testRequest.js +56 -55
- package/dist/withConnections.js +25 -0
- package/dist/workflowRunner.js +1 -2
- package/package.json +17 -10
package/dist/factory.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { fixtureRow as e, nextSequence as t } from "./fixtureRow.js";
|
|
2
2
|
import { missingRequiredColumns as n } from "@voltro/database";
|
|
3
|
-
|
|
3
|
+
// src/factory.ts
|
|
4
4
|
var r = (e) => {
|
|
5
5
|
let n = {};
|
|
6
6
|
for (let [r, i] of Object.entries(e)) n[r] = typeof i == "function" ? i(t()) : i;
|
|
@@ -61,5 +61,4 @@ var r = (e) => {
|
|
|
61
61
|
};
|
|
62
62
|
return c(n.defaults ?? {});
|
|
63
63
|
};
|
|
64
|
-
//#endregion
|
|
65
64
|
export { c as defineFactory };
|
|
@@ -0,0 +1,72 @@
|
|
|
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
|
+
// 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
|
+
export { u as activeKeyAlone, h as describeActiveKeyAloneFailures, g as expectSealedWithActiveKeyAlone, f as recordTestRawStore, c as registerTestFieldKeys, m as sealedValuesWithActiveKeyAlone };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { activeKeyAlone as e } from "./fieldKeys.js";
|
|
2
|
+
import { Effect as t } from "effect";
|
|
3
|
+
import { verifySealedValues as n } from "@voltro/runtime";
|
|
4
|
+
// 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
|
+
export { r as expectSealedWithActiveKeyAloneInSql };
|
package/dist/fixtureRow.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AUTO_FILLED_COLUMNS as e, missingRequiredColumns as t } from "@voltro/database";
|
|
2
|
-
|
|
2
|
+
// src/fixtureRow.ts
|
|
3
3
|
var n = new Set(e), r = 0, i = () => ++r, a = (e, t) => {
|
|
4
4
|
if (t.oneOf && t.oneOf.length > 0) return t.oneOf[0];
|
|
5
5
|
if (t.enumValues && t.enumValues.length > 0) return t.enumValues[0];
|
|
@@ -26,5 +26,4 @@ var n = new Set(e), r = 0, i = () => ++r, a = (e, t) => {
|
|
|
26
26
|
...r
|
|
27
27
|
};
|
|
28
28
|
};
|
|
29
|
-
//#endregion
|
|
30
29
|
export { o as fixtureRow, i as nextSequence };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { makeVoltroTestClient as e } from "./client.js";
|
|
2
2
|
import { act as t, createElement as n } from "react";
|
|
3
3
|
import { useFormBinding as r } from "@voltro/client";
|
|
4
|
-
|
|
4
|
+
// src/formBindingHarness.tsx
|
|
5
5
|
var i = async (i, a = {}) => {
|
|
6
6
|
if (globalThis.document === void 0) throw Error("renderFormBinding needs a DOM to render hooks — add `// @vitest-environment jsdom` to the spec.");
|
|
7
7
|
let { createRoot: o } = await import("react-dom/client"), s = a.apiName ?? "app", c = e({
|
|
@@ -42,5 +42,4 @@ var i = async (i, a = {}) => {
|
|
|
42
42
|
}
|
|
43
43
|
};
|
|
44
44
|
};
|
|
45
|
-
//#endregion
|
|
46
45
|
export { i as renderFormBinding };
|
|
@@ -0,0 +1,135 @@
|
|
|
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
|
+
// 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
|
+
describe: c,
|
|
29
|
+
it: u,
|
|
30
|
+
expect: l
|
|
31
|
+
});
|
|
32
|
+
let y = (e, t) => u(e, async () => {
|
|
33
|
+
let e = await v.make();
|
|
34
|
+
try {
|
|
35
|
+
await t(e);
|
|
36
|
+
} finally {
|
|
37
|
+
await e.dispose();
|
|
38
|
+
}
|
|
39
|
+
}, 6e4);
|
|
40
|
+
c(`HotStore SQL parity — ${v.name}`, () => {
|
|
41
|
+
y("idempotent table DDL preserves exact normalized scores and physical deletion", async (e) => {
|
|
42
|
+
let n = m(e.store, {
|
|
43
|
+
dialect: v.dialect,
|
|
44
|
+
namespace: "sql"
|
|
45
|
+
}), i = 1.2345678901234567;
|
|
46
|
+
await r.runPromise(n.zadd("player:subject", [{
|
|
47
|
+
member: "member",
|
|
48
|
+
score: i
|
|
49
|
+
}])), await e.migrate(), l(await e.store.query(t(_).descriptor)).toEqual([l.objectContaining({
|
|
50
|
+
member: "member",
|
|
51
|
+
score: i
|
|
52
|
+
})]), 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({
|
|
53
|
+
keyHash: "lock",
|
|
54
|
+
namespace: null,
|
|
55
|
+
key: null,
|
|
56
|
+
payload: null,
|
|
57
|
+
expiresAtMs: null
|
|
58
|
+
})]);
|
|
59
|
+
}), y("wrong-type batches roll back every preceding SQL write", async (n) => {
|
|
60
|
+
let i = m(n.store, {
|
|
61
|
+
dialect: v.dialect,
|
|
62
|
+
namespace: "atomic"
|
|
63
|
+
});
|
|
64
|
+
await r.runPromise(i.zadd("board", [{
|
|
65
|
+
member: "m",
|
|
66
|
+
score: 1
|
|
67
|
+
}]));
|
|
68
|
+
let s = await r.runPromise(i.multi([{
|
|
69
|
+
op: "incrBy",
|
|
70
|
+
key: "other",
|
|
71
|
+
delta: 1
|
|
72
|
+
}, {
|
|
73
|
+
op: "incrBy",
|
|
74
|
+
key: "board",
|
|
75
|
+
delta: 1
|
|
76
|
+
}]).pipe(r.result));
|
|
77
|
+
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);
|
|
78
|
+
}), y("namespace/key boundaries, case, wildcard prefixes and long Unicode values remain exact", async (e) => {
|
|
79
|
+
let t = m(e.store, {
|
|
80
|
+
dialect: v.dialect,
|
|
81
|
+
namespace: "n"
|
|
82
|
+
}), n = m(e.store, {
|
|
83
|
+
dialect: v.dialect,
|
|
84
|
+
namespace: "n:tenant"
|
|
85
|
+
}), i = m(e.store, {
|
|
86
|
+
dialect: v.dialect,
|
|
87
|
+
namespace: "N"
|
|
88
|
+
});
|
|
89
|
+
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));
|
|
90
|
+
let o = "🎮".repeat(2e3);
|
|
91
|
+
await r.runPromise(t.zadd(o, [{
|
|
92
|
+
member: o,
|
|
93
|
+
score: -1.5
|
|
94
|
+
}])), 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);
|
|
95
|
+
}), y("independent connections retain every cold-key increment and have one lease winner", async (e) => {
|
|
96
|
+
let t = [e.store];
|
|
97
|
+
for (let n = 0; n < 3; n++) t.push(await e.connect());
|
|
98
|
+
let n = t.map((e) => m(e, {
|
|
99
|
+
dialect: v.dialect,
|
|
100
|
+
namespace: "connections"
|
|
101
|
+
})), i = await Promise.all(Array.from({ length: 32 }, (e, t) => r.runPromise(n[t % n.length].incrBy("counter", 1))));
|
|
102
|
+
l(new Set(i).size).toBe(32), l(await r.runPromise(n[0].incrBy("counter", 0))).toBe(32);
|
|
103
|
+
let a = await Promise.all(Array.from({ length: 32 }, (e, t) => r.runPromise(n[t % n.length].setNx("seat", String(t), 6e4))));
|
|
104
|
+
l(a.filter(Boolean)).toHaveLength(1);
|
|
105
|
+
}), y("concurrent first-use ports admit coordination before application writes", async (e) => {
|
|
106
|
+
let t = [e.store];
|
|
107
|
+
for (let n = 0; n < 3; n++) t.push(await e.connect());
|
|
108
|
+
let n = Array.from({ length: 32 }, (e, n) => m(t[n % t.length], {
|
|
109
|
+
dialect: v.dialect,
|
|
110
|
+
namespace: "first-use"
|
|
111
|
+
})), i = (await Promise.allSettled(n.map((e) => r.runPromise(e.incrBy("counter", 1))))).map((e) => {
|
|
112
|
+
if (e.status === "rejected") throw e.reason;
|
|
113
|
+
return e.value;
|
|
114
|
+
});
|
|
115
|
+
l(new Set(i).size).toBe(32), l(await r.runPromise(n[0].incrBy("counter", 0))).toBe(32);
|
|
116
|
+
}), y("new connections rebuild the port from persisted SQL and apply stored expiry", async (e) => {
|
|
117
|
+
let t = 0, n = m(e.store, {
|
|
118
|
+
dialect: v.dialect,
|
|
119
|
+
namespace: "reconnect",
|
|
120
|
+
now: () => t
|
|
121
|
+
});
|
|
122
|
+
await r.runPromise(n.incrBy("counter", 7)), await r.runPromise(n.zadd("board", [{
|
|
123
|
+
member: "𐀀",
|
|
124
|
+
score: 1.2345678901234567
|
|
125
|
+
}])), await r.runPromise(n.setNx("seat", "holder", 100));
|
|
126
|
+
let i = m(await e.connect(), {
|
|
127
|
+
dialect: v.dialect,
|
|
128
|
+
namespace: "reconnect",
|
|
129
|
+
now: () => t
|
|
130
|
+
});
|
|
131
|
+
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);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
};
|
|
135
|
+
export { v as runHotStoreSqlParity };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Result as e, Schema as t } from "effect";
|
|
2
|
-
|
|
2
|
+
// src/hydrateFixtureRows.ts
|
|
3
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
4
|
if (typeof t != "string" || !i.test(t) && !(a === "date" && r.test(t))) return !1;
|
|
5
5
|
let o = t.slice(0, 10), s = n(o);
|
|
@@ -18,5 +18,4 @@ var n = t.decodeUnknownResult(t.DateFromString), r = /^\d{4}-\d{2}-\d{2}$/, i =
|
|
|
18
18
|
return s;
|
|
19
19
|
});
|
|
20
20
|
};
|
|
21
|
-
//#endregion
|
|
22
21
|
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<
|
|
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
|
|
@@ -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.
|
package/dist/index.js
CHANGED
|
@@ -2,19 +2,21 @@ import { fixtureRow as e, nextSequence as t } from "./fixtureRow.js";
|
|
|
2
2
|
import { hydrateFixtureRows as n } from "./hydrateFixtureRows.js";
|
|
3
3
|
import { defineFactory as r } from "./factory.js";
|
|
4
4
|
import { MockClock as i, frozenTime as a, withFrozenTime as o } from "./mockClock.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
5
|
+
import { activeKeyAlone as s, expectSealedWithActiveKeyAlone as c, registerTestFieldKeys as l, sealedValuesWithActiveKeyAlone as u } from "./fieldKeys.js";
|
|
6
|
+
import { MockWebhooks as d } from "./mockWebhooks.js";
|
|
7
|
+
import { testEventBus as f } from "./testEventBus.js";
|
|
8
|
+
import { MockLLM as p } from "./mockLLM.js";
|
|
9
|
+
import { makeTestContext as m, markRestInvocation as h, mockStore as g, outboxNudgesOf as _, pluginsOf as v, queryCacheOf as y, resetAfterCommit as b, restInvocationOf as x, rpcInterceptorFor as S, runAfterCommit as C, runInStoreTransaction as w, serviceLayerFor as T, withRpcConnection as E } from "./testContext.js";
|
|
10
|
+
import { invoke as D } from "./invoke.js";
|
|
11
|
+
import { describeIfAvailable as O, describeIfReachable as k, isTcpReachable as A, reportEngineVersion as j } from "./reachable.js";
|
|
12
|
+
import { TEST_TENANT_ID as M, anonymous as N, apiKey as P, serviceAccount as F, system as I, user as L } from "./subject.js";
|
|
13
|
+
import { makeTestApp as R } from "./testRequest.js";
|
|
14
|
+
import { expectSealedWithActiveKeyAloneInSql as z } from "./fieldKeysSql.js";
|
|
15
|
+
import { changeDelete as B, changeInsert as V, changeSoftDelete as H, changeUpdate as U, makeSubscribeContext as W } from "./subscribeContext.js";
|
|
16
|
+
import { makeWorkflowRunner as G } from "./workflowRunner.js";
|
|
17
|
+
import { makeServiceApiMock as K } from "./serviceApiMock.js";
|
|
18
|
+
import { startTestApps as q } from "./testApps.js";
|
|
19
|
+
import { withConnections as J } from "./withConnections.js";
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var Y = 1;
|
|
22
|
+
export { i as MockClock, p as MockLLM, d as MockWebhooks, Y as TESTING_PRESET_VERSION, M as TEST_TENANT_ID, s as activeKeyAlone, N as anonymous, P as apiKey, B as changeDelete, V as changeInsert, H as changeSoftDelete, U as changeUpdate, r as defineFactory, O as describeIfAvailable, k as describeIfReachable, c as expectSealedWithActiveKeyAlone, z as expectSealedWithActiveKeyAloneInSql, e as fixtureRow, a as frozenTime, n as hydrateFixtureRows, D as invoke, A as isTcpReachable, K as makeServiceApiMock, W as makeSubscribeContext, R as makeTestApp, m as makeTestContext, G as makeWorkflowRunner, h as markRestInvocation, g as mockStore, t as nextSequence, _ as outboxNudgesOf, v as pluginsOf, y as queryCacheOf, l as registerTestFieldKeys, j as reportEngineVersion, b as resetAfterCommit, x as restInvocationOf, S as rpcInterceptorFor, C as runAfterCommit, w as runInStoreTransaction, u as sealedValuesWithActiveKeyAlone, F as serviceAccount, T as serviceLayerFor, q as startTestApps, I as system, f as testEventBus, L as user, J as withConnections, o as withFrozenTime, E as withRpcConnection };
|
package/dist/invoke.js
CHANGED
|
@@ -1,44 +1,81 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Cause as
|
|
3
|
-
import { SubjectService as
|
|
4
|
-
import { makeEffectStoreLayer as
|
|
5
|
-
|
|
6
|
-
var
|
|
7
|
-
let n =
|
|
8
|
-
return (n === void 0 ? e :
|
|
9
|
-
},
|
|
10
|
-
let
|
|
11
|
-
let t =
|
|
12
|
-
return
|
|
13
|
-
},
|
|
14
|
-
let t =
|
|
15
|
-
if (t
|
|
16
|
-
|
|
17
|
-
|
|
1
|
+
import { queryCacheOf as e, resetAfterCommit as t, restInvocationOf as n, rpcInterceptorFor as r, runAfterCommit as i, runInStoreTransaction as a, serviceLayerFor as o } from "./testContext.js";
|
|
2
|
+
import { Cause as s, Effect as c, Exit as l, Schema as u } from "effect";
|
|
3
|
+
import { SubjectService as d, checkGuardsEffect as f } from "@voltro/protocol";
|
|
4
|
+
import { cachedComputedCall as p, computedCacheKey as m, makeEffectStoreLayer as h, runProvidedEffect as g, runWithDeadlockRetry as _ } from "@voltro/runtime";
|
|
5
|
+
// src/invoke.ts
|
|
6
|
+
var v = (e, t) => {
|
|
7
|
+
let n = o(t);
|
|
8
|
+
return (n === void 0 ? e : c.provide(e, n)).pipe(c.provide(h(t.store)), c.provideService(d, t.request.subject));
|
|
9
|
+
}, y = async (o, h, y, x) => {
|
|
10
|
+
let S = o.input, C = await u.decodeUnknownPromise(S)(y), w = o.kind, T = async (e) => {
|
|
11
|
+
let t = h(C, e);
|
|
12
|
+
return c.isEffect(t) ? g(v(t, e)) : t;
|
|
13
|
+
}, E = async () => {
|
|
14
|
+
let t = e(x), n = o.cache;
|
|
15
|
+
if (t === void 0 || n === void 0) return b;
|
|
16
|
+
let r = t.deps.policyFor(n);
|
|
17
|
+
if (r === void 0) return b;
|
|
18
|
+
let i = o, a = i.name ?? "", s = i.source === void 0 ? [] : typeof i.source == "string" ? [i.source] : [...i.source], l = await t.rowFilter(), u = m({
|
|
19
|
+
name: a,
|
|
20
|
+
codeIdentity: t.deps.codeIdentity,
|
|
21
|
+
output: i.output,
|
|
22
|
+
input: i.input,
|
|
23
|
+
decodedInput: C,
|
|
24
|
+
scope: r.scope,
|
|
25
|
+
subject: x.request.subject,
|
|
26
|
+
rowFilter: l,
|
|
27
|
+
declaredSources: s
|
|
28
|
+
});
|
|
29
|
+
return "bypass" in u ? b : g(p({
|
|
30
|
+
cache: t.deps,
|
|
31
|
+
policy: r,
|
|
32
|
+
key: u.key,
|
|
33
|
+
name: a,
|
|
34
|
+
sources: s,
|
|
35
|
+
output: i.output,
|
|
36
|
+
subject: x.request.subject,
|
|
37
|
+
rowFilter: l,
|
|
38
|
+
ctx: x,
|
|
39
|
+
execute: async (e, t) => {
|
|
40
|
+
let n = h(C, e);
|
|
41
|
+
return c.isEffect(n) ? g(v(n.pipe(c.provideService(d, t)), x)) : n;
|
|
42
|
+
},
|
|
43
|
+
onDescriptor: () => {},
|
|
44
|
+
isDescriptor: () => !1,
|
|
45
|
+
unwrap: (e) => e
|
|
46
|
+
}).pipe(c.map(({ value: e }) => e)));
|
|
47
|
+
}, D = async () => {
|
|
48
|
+
let e = o.guards;
|
|
49
|
+
if (e !== void 0 && e.length > 0) {
|
|
50
|
+
let t = await c.runPromise(f(x.request.subject, e, C));
|
|
51
|
+
if (t !== null) throw t;
|
|
18
52
|
}
|
|
19
|
-
if (
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
53
|
+
if (w === "query") {
|
|
54
|
+
let e = await E();
|
|
55
|
+
if (e !== b) return e;
|
|
56
|
+
}
|
|
57
|
+
if (w !== "mutation") return T(x);
|
|
58
|
+
let n = await _(async () => a(x, async (e) => (t(x), T(e))), { delay: () => Promise.resolve() });
|
|
59
|
+
return await i(x), n;
|
|
60
|
+
}, O = w === "mutation" || w === "query" || w === "action" ? w : void 0, k = O === void 0 ? void 0 : r(x, O);
|
|
61
|
+
if (k === void 0 || O === void 0) return await D();
|
|
62
|
+
let A = c.tryPromise({
|
|
63
|
+
try: () => D(),
|
|
26
64
|
catch: (e) => e
|
|
27
|
-
}),
|
|
28
|
-
tag:
|
|
29
|
-
kind:
|
|
30
|
-
input:
|
|
31
|
-
subject:
|
|
32
|
-
traceId:
|
|
33
|
-
...
|
|
34
|
-
...
|
|
65
|
+
}), j = n(x), M = k(A, {
|
|
66
|
+
tag: o.name ?? "",
|
|
67
|
+
kind: O,
|
|
68
|
+
input: C,
|
|
69
|
+
subject: x.request.subject,
|
|
70
|
+
traceId: x.request.traceId,
|
|
71
|
+
...x.request.spanId === void 0 ? {} : { spanId: x.request.spanId },
|
|
72
|
+
...j === void 0 ? {} : {
|
|
35
73
|
surface: "rest",
|
|
36
|
-
publicApi:
|
|
74
|
+
publicApi: j.publicApi
|
|
37
75
|
},
|
|
38
|
-
...
|
|
39
|
-
}),
|
|
40
|
-
if (
|
|
41
|
-
throw
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
export { g as invoke };
|
|
76
|
+
...j?.remoteAddr === void 0 ? {} : { remoteAddr: j.remoteAddr }
|
|
77
|
+
}), N = await c.runPromiseExit(M);
|
|
78
|
+
if (l.isSuccess(N)) return N.value;
|
|
79
|
+
throw s.squash(N.cause);
|
|
80
|
+
}, b = Symbol("voltro:testing:not-cached");
|
|
81
|
+
export { y as invoke };
|