@noy-db/test-adapter-conformance 0.6.0-pre.1
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/LICENSE +21 -0
- package/README.md +28 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +321 -0
- package/dist/index.js.map +1 -0
- package/package.json +57 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 vLannaAi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# @noy-db/test-adapter-conformance
|
|
2
|
+
|
|
3
|
+
The parameterized store-contract conformance suite for [noy-db](https://github.com/vLannaAi/noy-db)
|
|
4
|
+
adapters — **every `NoydbStore` implementation must pass it**, in this repo, in `noy-db-to`, and
|
|
5
|
+
out-of-tree.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { runStoreConformanceTests } from '@noy-db/test-adapter-conformance'
|
|
9
|
+
import { toMyBackend } from '../src/index.js'
|
|
10
|
+
|
|
11
|
+
runStoreConformanceTests('to-my-backend (mock)', async () => toMyBackend({ client: mockClient() }))
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
`runStoreConformanceTests(name, factory, cleanup?)` registers a vitest `describe` block covering:
|
|
15
|
+
basic CRUD, optimistic concurrency (`expectedVersion` → `ConflictError`), bulk `loadAll`/`saveAll`,
|
|
16
|
+
vault/collection isolation, edge cases (Unicode ids, 1 MB envelopes, `_del` markers), internal
|
|
17
|
+
`_`-collection filtering, and the optional-capability contract — including the
|
|
18
|
+
*declared ⇔ implemented* biconditional for `txAtomic` and behavioral `tx()` tests
|
|
19
|
+
(rollback-on-failure, atomic `expectedVersion` enforcement).
|
|
20
|
+
|
|
21
|
+
## Peer dependencies
|
|
22
|
+
|
|
23
|
+
- `vitest` ^3 — the suite registers vitest tests; call it from a vitest test file.
|
|
24
|
+
- `@noy-db/hub` — the store contract (`@noy-db/hub/to`) the suite asserts against.
|
|
25
|
+
|
|
26
|
+
## License
|
|
27
|
+
|
|
28
|
+
[MIT](./LICENSE) © vLannaAi
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { NoydbStore } from '@noy-db/hub/to';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Parameterized adapter conformance test suite.
|
|
5
|
+
* Every NOYDB adapter must pass all of these tests.
|
|
6
|
+
*/
|
|
7
|
+
declare function runStoreConformanceTests(name: string, factory: () => Promise<NoydbStore>, cleanup?: () => Promise<void>): void;
|
|
8
|
+
|
|
9
|
+
export { runStoreConformanceTests };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { describe, it, expect, beforeEach, afterAll } from "vitest";
|
|
3
|
+
import { ConflictError } from "@noy-db/hub/to";
|
|
4
|
+
function makeEnvelope(version, data = "test-data") {
|
|
5
|
+
return {
|
|
6
|
+
_noydb: 1,
|
|
7
|
+
_v: version,
|
|
8
|
+
_ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9
|
+
_iv: "dGVzdC1pdi0xMjM0",
|
|
10
|
+
// base64 of "test-iv-1234"
|
|
11
|
+
_data: Buffer.from(data).toString("base64")
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
function runStoreConformanceTests(name, factory, cleanup) {
|
|
15
|
+
describe(`Adapter Conformance: ${name}`, () => {
|
|
16
|
+
let adapter;
|
|
17
|
+
beforeEach(async () => {
|
|
18
|
+
adapter = await factory();
|
|
19
|
+
});
|
|
20
|
+
afterAll(async () => {
|
|
21
|
+
await cleanup?.();
|
|
22
|
+
});
|
|
23
|
+
describe("basic CRUD", () => {
|
|
24
|
+
it("put + get returns the same envelope", async () => {
|
|
25
|
+
const envelope = makeEnvelope(1);
|
|
26
|
+
await adapter.put("comp1", "coll1", "id1", envelope);
|
|
27
|
+
const result = await adapter.get("comp1", "coll1", "id1");
|
|
28
|
+
expect(result).toEqual(envelope);
|
|
29
|
+
});
|
|
30
|
+
it("get returns null for non-existent record", async () => {
|
|
31
|
+
const result = await adapter.get("comp1", "coll1", "nonexistent");
|
|
32
|
+
expect(result).toBeNull();
|
|
33
|
+
});
|
|
34
|
+
it("put overwrites existing record", async () => {
|
|
35
|
+
await adapter.put("comp1", "coll1", "id1", makeEnvelope(1, "first"));
|
|
36
|
+
const updated = makeEnvelope(2, "second");
|
|
37
|
+
await adapter.put("comp1", "coll1", "id1", updated);
|
|
38
|
+
const result = await adapter.get("comp1", "coll1", "id1");
|
|
39
|
+
expect(result).toEqual(updated);
|
|
40
|
+
});
|
|
41
|
+
it("delete removes a record", async () => {
|
|
42
|
+
await adapter.put("comp1", "coll1", "id1", makeEnvelope(1));
|
|
43
|
+
await adapter.delete("comp1", "coll1", "id1");
|
|
44
|
+
const result = await adapter.get("comp1", "coll1", "id1");
|
|
45
|
+
expect(result).toBeNull();
|
|
46
|
+
});
|
|
47
|
+
it("delete on non-existent record does not throw", async () => {
|
|
48
|
+
await expect(adapter.delete("comp1", "coll1", "nonexistent")).resolves.not.toThrow();
|
|
49
|
+
});
|
|
50
|
+
it("list returns all IDs in a collection", async () => {
|
|
51
|
+
await adapter.put("comp1", "coll1", "a", makeEnvelope(1));
|
|
52
|
+
await adapter.put("comp1", "coll1", "b", makeEnvelope(1));
|
|
53
|
+
await adapter.put("comp1", "coll1", "c", makeEnvelope(1));
|
|
54
|
+
const ids = await adapter.list("comp1", "coll1");
|
|
55
|
+
expect(ids.sort()).toEqual(["a", "b", "c"]);
|
|
56
|
+
});
|
|
57
|
+
it("list returns empty array for empty collection", async () => {
|
|
58
|
+
const ids = await adapter.list("comp1", "empty-coll");
|
|
59
|
+
expect(ids).toEqual([]);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
describe("optimistic concurrency", () => {
|
|
63
|
+
it("put with correct expectedVersion succeeds", async () => {
|
|
64
|
+
await adapter.put("comp1", "coll1", "id1", makeEnvelope(1));
|
|
65
|
+
await expect(
|
|
66
|
+
adapter.put("comp1", "coll1", "id1", makeEnvelope(2), 1)
|
|
67
|
+
).resolves.not.toThrow();
|
|
68
|
+
});
|
|
69
|
+
it("put with wrong expectedVersion throws ConflictError", async () => {
|
|
70
|
+
await adapter.put("comp1", "coll1", "id1", makeEnvelope(3));
|
|
71
|
+
await expect(
|
|
72
|
+
adapter.put("comp1", "coll1", "id1", makeEnvelope(4), 1)
|
|
73
|
+
).rejects.toThrow(ConflictError);
|
|
74
|
+
});
|
|
75
|
+
it("put without expectedVersion always succeeds (upsert)", async () => {
|
|
76
|
+
await adapter.put("comp1", "coll1", "id1", makeEnvelope(5));
|
|
77
|
+
await expect(
|
|
78
|
+
adapter.put("comp1", "coll1", "id1", makeEnvelope(6))
|
|
79
|
+
).resolves.not.toThrow();
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
describe("bulk operations", () => {
|
|
83
|
+
it("loadAll returns all collections and records", async () => {
|
|
84
|
+
await adapter.put("comp1", "invoices", "inv-1", makeEnvelope(1, "inv1"));
|
|
85
|
+
await adapter.put("comp1", "invoices", "inv-2", makeEnvelope(1, "inv2"));
|
|
86
|
+
await adapter.put("comp1", "payments", "pay-1", makeEnvelope(1, "pay1"));
|
|
87
|
+
const snapshot = await adapter.loadAll("comp1");
|
|
88
|
+
expect(Object.keys(snapshot).sort()).toEqual(["invoices", "payments"]);
|
|
89
|
+
expect(Object.keys(snapshot["invoices"]).sort()).toEqual(["inv-1", "inv-2"]);
|
|
90
|
+
expect(Object.keys(snapshot["payments"])).toEqual(["pay-1"]);
|
|
91
|
+
});
|
|
92
|
+
it("loadAll returns empty snapshot for empty compartment", async () => {
|
|
93
|
+
const snapshot = await adapter.loadAll("empty-comp");
|
|
94
|
+
expect(snapshot).toEqual({});
|
|
95
|
+
});
|
|
96
|
+
it("saveAll writes all collections", async () => {
|
|
97
|
+
const data = {
|
|
98
|
+
invoices: {
|
|
99
|
+
"inv-1": makeEnvelope(1, "saved-inv1")
|
|
100
|
+
},
|
|
101
|
+
payments: {
|
|
102
|
+
"pay-1": makeEnvelope(1, "saved-pay1")
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
await adapter.saveAll("comp1", data);
|
|
106
|
+
const inv = await adapter.get("comp1", "invoices", "inv-1");
|
|
107
|
+
expect(inv?._data).toBe(Buffer.from("saved-inv1").toString("base64"));
|
|
108
|
+
const pay = await adapter.get("comp1", "payments", "pay-1");
|
|
109
|
+
expect(pay?._data).toBe(Buffer.from("saved-pay1").toString("base64"));
|
|
110
|
+
});
|
|
111
|
+
it("saveAll followed by loadAll round-trips correctly", async () => {
|
|
112
|
+
const data = {
|
|
113
|
+
coll1: { "r1": makeEnvelope(1, "data1"), "r2": makeEnvelope(2, "data2") },
|
|
114
|
+
coll2: { "r3": makeEnvelope(1, "data3") }
|
|
115
|
+
};
|
|
116
|
+
await adapter.saveAll("rt-comp", data);
|
|
117
|
+
const loaded = await adapter.loadAll("rt-comp");
|
|
118
|
+
expect(loaded).toEqual(data);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
describe("isolation", () => {
|
|
122
|
+
it("records in different compartments are isolated", async () => {
|
|
123
|
+
await adapter.put("compA", "coll1", "id1", makeEnvelope(1, "A"));
|
|
124
|
+
await adapter.put("compB", "coll1", "id1", makeEnvelope(1, "B"));
|
|
125
|
+
const a = await adapter.get("compA", "coll1", "id1");
|
|
126
|
+
const b = await adapter.get("compB", "coll1", "id1");
|
|
127
|
+
expect(a?._data).not.toBe(b?._data);
|
|
128
|
+
});
|
|
129
|
+
it("records in different collections are isolated", async () => {
|
|
130
|
+
await adapter.put("comp1", "collA", "id1", makeEnvelope(1, "A"));
|
|
131
|
+
await adapter.put("comp1", "collB", "id1", makeEnvelope(1, "B"));
|
|
132
|
+
const a = await adapter.get("comp1", "collA", "id1");
|
|
133
|
+
const b = await adapter.get("comp1", "collB", "id1");
|
|
134
|
+
expect(a?._data).not.toBe(b?._data);
|
|
135
|
+
});
|
|
136
|
+
it("operations on one collection do not affect another", async () => {
|
|
137
|
+
await adapter.put("comp1", "coll1", "id1", makeEnvelope(1));
|
|
138
|
+
await adapter.put("comp1", "coll2", "id1", makeEnvelope(1));
|
|
139
|
+
await adapter.delete("comp1", "coll1", "id1");
|
|
140
|
+
const deleted = await adapter.get("comp1", "coll1", "id1");
|
|
141
|
+
const intact = await adapter.get("comp1", "coll2", "id1");
|
|
142
|
+
expect(deleted).toBeNull();
|
|
143
|
+
expect(intact).not.toBeNull();
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
describe("edge cases", () => {
|
|
147
|
+
it("handles record IDs with Unicode / Thai characters", async () => {
|
|
148
|
+
const id = "\u0E1A\u0E23\u0E34\u0E29\u0E31\u0E17-ABC-001";
|
|
149
|
+
await adapter.put("comp1", "coll1", id, makeEnvelope(1));
|
|
150
|
+
const result = await adapter.get("comp1", "coll1", id);
|
|
151
|
+
expect(result).not.toBeNull();
|
|
152
|
+
const ids = await adapter.list("comp1", "coll1");
|
|
153
|
+
expect(ids).toContain(id);
|
|
154
|
+
});
|
|
155
|
+
it("handles large envelopes (1MB+ _data field)", async () => {
|
|
156
|
+
const largeData = "x".repeat(1e6);
|
|
157
|
+
const envelope = makeEnvelope(1, largeData);
|
|
158
|
+
await adapter.put("comp1", "coll1", "large", envelope);
|
|
159
|
+
const result = await adapter.get("comp1", "coll1", "large");
|
|
160
|
+
expect(result?._data).toBe(Buffer.from(largeData).toString("base64"));
|
|
161
|
+
});
|
|
162
|
+
it("handles IDs with special characters", async () => {
|
|
163
|
+
const ids = ["with spaces", "with.dots", "with-dashes", "with_underscores", "MiXeD.CaSe-123"];
|
|
164
|
+
for (const id of ids) {
|
|
165
|
+
await adapter.put("comp1", "coll1", id, makeEnvelope(1, id));
|
|
166
|
+
}
|
|
167
|
+
const listed = await adapter.list("comp1", "coll1");
|
|
168
|
+
for (const id of ids) {
|
|
169
|
+
expect(listed).toContain(id);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
it("handles rapid sequential writes", async () => {
|
|
173
|
+
const promises = Array.from(
|
|
174
|
+
{ length: 100 },
|
|
175
|
+
(_, i) => adapter.put("comp1", "coll1", `rapid-${i}`, makeEnvelope(1, `data-${i}`))
|
|
176
|
+
);
|
|
177
|
+
await Promise.all(promises);
|
|
178
|
+
const ids = await adapter.list("comp1", "coll1");
|
|
179
|
+
expect(ids.length).toBe(100);
|
|
180
|
+
});
|
|
181
|
+
it("handles empty string values in envelope fields", async () => {
|
|
182
|
+
const envelope = {
|
|
183
|
+
_noydb: 1,
|
|
184
|
+
_v: 1,
|
|
185
|
+
_ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
186
|
+
_iv: "",
|
|
187
|
+
_data: ""
|
|
188
|
+
};
|
|
189
|
+
await adapter.put("comp1", "coll1", "empty", envelope);
|
|
190
|
+
const result = await adapter.get("comp1", "coll1", "empty");
|
|
191
|
+
expect(result?._iv).toBe("");
|
|
192
|
+
expect(result?._data).toBe("");
|
|
193
|
+
});
|
|
194
|
+
it("round-trips a delete-marker envelope (_del) byte-identically (#589)", async () => {
|
|
195
|
+
const marker = { _noydb: 1, _v: 6, _ts: (/* @__PURE__ */ new Date()).toISOString(), _iv: "", _data: "", _del: true };
|
|
196
|
+
await adapter.put("comp1", "coll1", "id1", marker);
|
|
197
|
+
const result = await adapter.get("comp1", "coll1", "id1");
|
|
198
|
+
expect(result).toEqual(marker);
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
describe("internal collection filtering", () => {
|
|
202
|
+
it("loadAll excludes _keyring collection", async () => {
|
|
203
|
+
await adapter.put("comp1", "invoices", "inv-1", makeEnvelope(1, "record"));
|
|
204
|
+
await adapter.put("comp1", "_keyring", "user-01", makeEnvelope(1, "keyring"));
|
|
205
|
+
const snapshot = await adapter.loadAll("comp1");
|
|
206
|
+
expect(snapshot["invoices"]).toBeDefined();
|
|
207
|
+
expect(snapshot["_keyring"]).toBeUndefined();
|
|
208
|
+
});
|
|
209
|
+
it("loadAll excludes _sync collection", async () => {
|
|
210
|
+
await adapter.put("comp1", "invoices", "inv-1", makeEnvelope(1, "record"));
|
|
211
|
+
await adapter.put("comp1", "_sync", "meta", makeEnvelope(1, "sync"));
|
|
212
|
+
const snapshot = await adapter.loadAll("comp1");
|
|
213
|
+
expect(snapshot["invoices"]).toBeDefined();
|
|
214
|
+
expect(snapshot["_sync"]).toBeUndefined();
|
|
215
|
+
});
|
|
216
|
+
it("get/put/delete still work on _keyring collection directly", async () => {
|
|
217
|
+
await adapter.put("comp1", "_keyring", "user-01", makeEnvelope(1, "keyring"));
|
|
218
|
+
const result = await adapter.get("comp1", "_keyring", "user-01");
|
|
219
|
+
expect(result).not.toBeNull();
|
|
220
|
+
await adapter.delete("comp1", "_keyring", "user-01");
|
|
221
|
+
const deleted = await adapter.get("comp1", "_keyring", "user-01");
|
|
222
|
+
expect(deleted).toBeNull();
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
describe("optional capabilities", () => {
|
|
226
|
+
it("declares txAtomic if and only if tx() is implemented", async () => {
|
|
227
|
+
const implemented = typeof adapter.tx === "function";
|
|
228
|
+
const declared = adapter.capabilities?.txAtomic === true;
|
|
229
|
+
expect(
|
|
230
|
+
declared,
|
|
231
|
+
implemented ? "store implements tx() but does not declare capabilities.txAtomic \u2014 the hub gates delegation on that bit, so the implementation would be skipped" : "store declares capabilities.txAtomic but has no tx() to delegate to"
|
|
232
|
+
).toBe(implemented);
|
|
233
|
+
});
|
|
234
|
+
it("ping() resolves without throwing, when implemented", async () => {
|
|
235
|
+
if (typeof adapter.ping !== "function") return;
|
|
236
|
+
await expect(adapter.ping()).resolves.not.toThrow();
|
|
237
|
+
});
|
|
238
|
+
it("listVaults() reports a vault that has been written to, when implemented", async () => {
|
|
239
|
+
if (typeof adapter.listVaults !== "function") return;
|
|
240
|
+
await adapter.put("comp-lv", "coll1", "id1", makeEnvelope(1));
|
|
241
|
+
const vaults = await adapter.listVaults();
|
|
242
|
+
expect(vaults).toContain("comp-lv");
|
|
243
|
+
});
|
|
244
|
+
it("tx() applies every op, when implemented", async () => {
|
|
245
|
+
if (typeof adapter.tx !== "function") return;
|
|
246
|
+
await adapter.tx([
|
|
247
|
+
{ type: "put", vault: "comp-tx", collection: "coll1", id: "a", envelope: makeEnvelope(1, "a") },
|
|
248
|
+
{ type: "put", vault: "comp-tx", collection: "coll1", id: "b", envelope: makeEnvelope(1, "b") }
|
|
249
|
+
]);
|
|
250
|
+
expect(await adapter.get("comp-tx", "coll1", "a")).not.toBeNull();
|
|
251
|
+
expect(await adapter.get("comp-tx", "coll1", "b")).not.toBeNull();
|
|
252
|
+
});
|
|
253
|
+
it("tx() enforces expectedVersion atomically \u2014 mismatch throws ConflictError with nothing applied, when implemented (#920)", async () => {
|
|
254
|
+
if (typeof adapter.tx !== "function") return;
|
|
255
|
+
await adapter.put("comp-txv", "coll1", "a", makeEnvelope(2, "committed"));
|
|
256
|
+
await adapter.tx([
|
|
257
|
+
{ type: "put", vault: "comp-txv", collection: "coll1", id: "a", envelope: makeEnvelope(3, "updated"), expectedVersion: 2 }
|
|
258
|
+
]);
|
|
259
|
+
expect((await adapter.get("comp-txv", "coll1", "a"))?._v).toBe(3);
|
|
260
|
+
await expect(
|
|
261
|
+
adapter.tx([
|
|
262
|
+
{ type: "put", vault: "comp-txv", collection: "coll1", id: "b", envelope: makeEnvelope(1, "sibling") },
|
|
263
|
+
{ type: "put", vault: "comp-txv", collection: "coll1", id: "a", envelope: makeEnvelope(9, "stale"), expectedVersion: 7 }
|
|
264
|
+
])
|
|
265
|
+
).rejects.toThrow(ConflictError);
|
|
266
|
+
expect((await adapter.get("comp-txv", "coll1", "a"))?._v).toBe(3);
|
|
267
|
+
expect(await adapter.get("comp-txv", "coll1", "b")).toBeNull();
|
|
268
|
+
});
|
|
269
|
+
it("tx() rolls back every op when one leg fails \u2014 zero partial writes, when implemented (#920)", async () => {
|
|
270
|
+
if (typeof adapter.tx !== "function") return;
|
|
271
|
+
await adapter.put("comp-txr", "coll1", "seed", makeEnvelope(1, "original"));
|
|
272
|
+
await adapter.put("comp-txr", "coll1", "blocker", makeEnvelope(1, "blocker"));
|
|
273
|
+
await expect(
|
|
274
|
+
adapter.tx([
|
|
275
|
+
{ type: "put", vault: "comp-txr", collection: "coll1", id: "fresh", envelope: makeEnvelope(1, "fresh") },
|
|
276
|
+
{ type: "delete", vault: "comp-txr", collection: "coll1", id: "seed" },
|
|
277
|
+
{ type: "put", vault: "comp-txr", collection: "coll1", id: "blocker", envelope: makeEnvelope(9, "clobber"), expectedVersion: 4 }
|
|
278
|
+
])
|
|
279
|
+
).rejects.toThrow();
|
|
280
|
+
expect(await adapter.get("comp-txr", "coll1", "fresh")).toBeNull();
|
|
281
|
+
const seed = await adapter.get("comp-txr", "coll1", "seed");
|
|
282
|
+
expect(seed?._data).toBe(Buffer.from("original").toString("base64"));
|
|
283
|
+
});
|
|
284
|
+
it("tx() rejects a put op missing its envelope without partial application, when implemented (#920)", async () => {
|
|
285
|
+
if (typeof adapter.tx !== "function") return;
|
|
286
|
+
await expect(
|
|
287
|
+
adapter.tx([
|
|
288
|
+
{ type: "put", vault: "comp-txe", collection: "coll1", id: "good", envelope: makeEnvelope(1, "good") },
|
|
289
|
+
{ type: "put", vault: "comp-txe", collection: "coll1", id: "bad" }
|
|
290
|
+
])
|
|
291
|
+
).rejects.toThrow();
|
|
292
|
+
expect(await adapter.get("comp-txe", "coll1", "good")).toBeNull();
|
|
293
|
+
});
|
|
294
|
+
it("getStoreTime() returns a non-decreasing interval, when implemented", async () => {
|
|
295
|
+
if (typeof adapter.getStoreTime !== "function") return;
|
|
296
|
+
const a = await adapter.getStoreTime();
|
|
297
|
+
const b = await adapter.getStoreTime();
|
|
298
|
+
expect(a.earliest).toBeLessThanOrEqual(a.latest);
|
|
299
|
+
expect(b.earliest).toBeGreaterThanOrEqual(a.earliest);
|
|
300
|
+
});
|
|
301
|
+
it("listPage() paginates and terminates, when implemented", async () => {
|
|
302
|
+
if (typeof adapter.listPage !== "function") return;
|
|
303
|
+
for (let i = 0; i < 3; i++) {
|
|
304
|
+
await adapter.put("comp-lp", "coll1", `id${i}`, makeEnvelope(1));
|
|
305
|
+
}
|
|
306
|
+
const first = await adapter.listPage("comp-lp", "coll1", void 0, 2);
|
|
307
|
+
expect(first.items.length).toBeLessThanOrEqual(2);
|
|
308
|
+
let cursor = first.nextCursor;
|
|
309
|
+
let guard = 0;
|
|
310
|
+
while (cursor && guard++ < 10) {
|
|
311
|
+
cursor = (await adapter.listPage("comp-lp", "coll1", cursor, 2)).nextCursor;
|
|
312
|
+
}
|
|
313
|
+
expect(cursor).toBeFalsy();
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
export {
|
|
319
|
+
runStoreConformanceTests
|
|
320
|
+
};
|
|
321
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { describe, it, expect, beforeEach, afterAll } from 'vitest'\nimport type { NoydbStore, EncryptedEnvelope } from '@noy-db/hub/to'\nimport { ConflictError } from '@noy-db/hub/to'\n\nfunction makeEnvelope(version: number, data = 'test-data'): EncryptedEnvelope {\n return {\n _noydb: 1,\n _v: version,\n _ts: new Date().toISOString(),\n _iv: 'dGVzdC1pdi0xMjM0', // base64 of \"test-iv-1234\"\n _data: Buffer.from(data).toString('base64'),\n }\n}\n\n/**\n * Parameterized adapter conformance test suite.\n * Every NOYDB adapter must pass all of these tests.\n */\nexport function runStoreConformanceTests(\n name: string,\n factory: () => Promise<NoydbStore>,\n cleanup?: () => Promise<void>,\n): void {\n describe(`Adapter Conformance: ${name}`, () => {\n let adapter: NoydbStore\n\n beforeEach(async () => {\n adapter = await factory()\n })\n\n afterAll(async () => {\n await cleanup?.()\n })\n\n // ─── Basic CRUD ────────────────────────────────────────────────\n\n describe('basic CRUD', () => {\n it('put + get returns the same envelope', async () => {\n const envelope = makeEnvelope(1)\n await adapter.put('comp1', 'coll1', 'id1', envelope)\n const result = await adapter.get('comp1', 'coll1', 'id1')\n expect(result).toEqual(envelope)\n })\n\n it('get returns null for non-existent record', async () => {\n const result = await adapter.get('comp1', 'coll1', 'nonexistent')\n expect(result).toBeNull()\n })\n\n it('put overwrites existing record', async () => {\n await adapter.put('comp1', 'coll1', 'id1', makeEnvelope(1, 'first'))\n const updated = makeEnvelope(2, 'second')\n await adapter.put('comp1', 'coll1', 'id1', updated)\n const result = await adapter.get('comp1', 'coll1', 'id1')\n expect(result).toEqual(updated)\n })\n\n it('delete removes a record', async () => {\n await adapter.put('comp1', 'coll1', 'id1', makeEnvelope(1))\n await adapter.delete('comp1', 'coll1', 'id1')\n const result = await adapter.get('comp1', 'coll1', 'id1')\n expect(result).toBeNull()\n })\n\n it('delete on non-existent record does not throw', async () => {\n await expect(adapter.delete('comp1', 'coll1', 'nonexistent')).resolves.not.toThrow()\n })\n\n it('list returns all IDs in a collection', async () => {\n await adapter.put('comp1', 'coll1', 'a', makeEnvelope(1))\n await adapter.put('comp1', 'coll1', 'b', makeEnvelope(1))\n await adapter.put('comp1', 'coll1', 'c', makeEnvelope(1))\n const ids = await adapter.list('comp1', 'coll1')\n expect(ids.sort()).toEqual(['a', 'b', 'c'])\n })\n\n it('list returns empty array for empty collection', async () => {\n const ids = await adapter.list('comp1', 'empty-coll')\n expect(ids).toEqual([])\n })\n })\n\n // ─── Optimistic Concurrency ────────────────────────────────────\n\n describe('optimistic concurrency', () => {\n it('put with correct expectedVersion succeeds', async () => {\n await adapter.put('comp1', 'coll1', 'id1', makeEnvelope(1))\n await expect(\n adapter.put('comp1', 'coll1', 'id1', makeEnvelope(2), 1),\n ).resolves.not.toThrow()\n })\n\n it('put with wrong expectedVersion throws ConflictError', async () => {\n await adapter.put('comp1', 'coll1', 'id1', makeEnvelope(3))\n await expect(\n adapter.put('comp1', 'coll1', 'id1', makeEnvelope(4), 1),\n ).rejects.toThrow(ConflictError)\n })\n\n it('put without expectedVersion always succeeds (upsert)', async () => {\n await adapter.put('comp1', 'coll1', 'id1', makeEnvelope(5))\n await expect(\n adapter.put('comp1', 'coll1', 'id1', makeEnvelope(6)),\n ).resolves.not.toThrow()\n })\n })\n\n // ─── Bulk Operations ───────────────────────────────────────────\n\n describe('bulk operations', () => {\n it('loadAll returns all collections and records', async () => {\n await adapter.put('comp1', 'invoices', 'inv-1', makeEnvelope(1, 'inv1'))\n await adapter.put('comp1', 'invoices', 'inv-2', makeEnvelope(1, 'inv2'))\n await adapter.put('comp1', 'payments', 'pay-1', makeEnvelope(1, 'pay1'))\n\n const snapshot = await adapter.loadAll('comp1')\n expect(Object.keys(snapshot).sort()).toEqual(['invoices', 'payments'])\n expect(Object.keys(snapshot['invoices']!).sort()).toEqual(['inv-1', 'inv-2'])\n expect(Object.keys(snapshot['payments']!)).toEqual(['pay-1'])\n })\n\n it('loadAll returns empty snapshot for empty compartment', async () => {\n const snapshot = await adapter.loadAll('empty-comp')\n expect(snapshot).toEqual({})\n })\n\n it('saveAll writes all collections', async () => {\n const data = {\n invoices: {\n 'inv-1': makeEnvelope(1, 'saved-inv1'),\n },\n payments: {\n 'pay-1': makeEnvelope(1, 'saved-pay1'),\n },\n }\n await adapter.saveAll('comp1', data)\n\n const inv = await adapter.get('comp1', 'invoices', 'inv-1')\n expect(inv?._data).toBe(Buffer.from('saved-inv1').toString('base64'))\n\n const pay = await adapter.get('comp1', 'payments', 'pay-1')\n expect(pay?._data).toBe(Buffer.from('saved-pay1').toString('base64'))\n })\n\n it('saveAll followed by loadAll round-trips correctly', async () => {\n const data = {\n coll1: { 'r1': makeEnvelope(1, 'data1'), 'r2': makeEnvelope(2, 'data2') },\n coll2: { 'r3': makeEnvelope(1, 'data3') },\n }\n await adapter.saveAll('rt-comp', data)\n const loaded = await adapter.loadAll('rt-comp')\n expect(loaded).toEqual(data)\n })\n })\n\n // ─── Isolation ─────────────────────────────────────────────────\n\n describe('isolation', () => {\n it('records in different compartments are isolated', async () => {\n await adapter.put('compA', 'coll1', 'id1', makeEnvelope(1, 'A'))\n await adapter.put('compB', 'coll1', 'id1', makeEnvelope(1, 'B'))\n\n const a = await adapter.get('compA', 'coll1', 'id1')\n const b = await adapter.get('compB', 'coll1', 'id1')\n expect(a?._data).not.toBe(b?._data)\n })\n\n it('records in different collections are isolated', async () => {\n await adapter.put('comp1', 'collA', 'id1', makeEnvelope(1, 'A'))\n await adapter.put('comp1', 'collB', 'id1', makeEnvelope(1, 'B'))\n\n const a = await adapter.get('comp1', 'collA', 'id1')\n const b = await adapter.get('comp1', 'collB', 'id1')\n expect(a?._data).not.toBe(b?._data)\n })\n\n it('operations on one collection do not affect another', async () => {\n await adapter.put('comp1', 'coll1', 'id1', makeEnvelope(1))\n await adapter.put('comp1', 'coll2', 'id1', makeEnvelope(1))\n await adapter.delete('comp1', 'coll1', 'id1')\n\n const deleted = await adapter.get('comp1', 'coll1', 'id1')\n const intact = await adapter.get('comp1', 'coll2', 'id1')\n expect(deleted).toBeNull()\n expect(intact).not.toBeNull()\n })\n })\n\n // ─── Edge Cases ────────────────────────────────────────────────\n\n describe('edge cases', () => {\n it('handles record IDs with Unicode / Thai characters', async () => {\n const id = 'บริษัท-ABC-001'\n await adapter.put('comp1', 'coll1', id, makeEnvelope(1))\n const result = await adapter.get('comp1', 'coll1', id)\n expect(result).not.toBeNull()\n const ids = await adapter.list('comp1', 'coll1')\n expect(ids).toContain(id)\n })\n\n it('handles large envelopes (1MB+ _data field)', async () => {\n const largeData = 'x'.repeat(1_000_000)\n const envelope = makeEnvelope(1, largeData)\n await adapter.put('comp1', 'coll1', 'large', envelope)\n const result = await adapter.get('comp1', 'coll1', 'large')\n expect(result?._data).toBe(Buffer.from(largeData).toString('base64'))\n })\n\n it('handles IDs with special characters', async () => {\n const ids = ['with spaces', 'with.dots', 'with-dashes', 'with_underscores', 'MiXeD.CaSe-123']\n for (const id of ids) {\n await adapter.put('comp1', 'coll1', id, makeEnvelope(1, id))\n }\n const listed = await adapter.list('comp1', 'coll1')\n for (const id of ids) {\n expect(listed).toContain(id)\n }\n })\n\n it('handles rapid sequential writes', async () => {\n const promises = Array.from({ length: 100 }, (_, i) =>\n adapter.put('comp1', 'coll1', `rapid-${i}`, makeEnvelope(1, `data-${i}`)),\n )\n await Promise.all(promises)\n const ids = await adapter.list('comp1', 'coll1')\n expect(ids.length).toBe(100)\n })\n\n it('handles empty string values in envelope fields', async () => {\n const envelope: EncryptedEnvelope = {\n _noydb: 1,\n _v: 1,\n _ts: new Date().toISOString(),\n _iv: '',\n _data: '',\n }\n await adapter.put('comp1', 'coll1', 'empty', envelope)\n const result = await adapter.get('comp1', 'coll1', 'empty')\n expect(result?._iv).toBe('')\n expect(result?._data).toBe('')\n })\n\n it('round-trips a delete-marker envelope (_del) byte-identically (#589)', async () => {\n const marker = { _noydb: 1 as const, _v: 6, _ts: new Date().toISOString(), _iv: '', _data: '', _del: true as const }\n await adapter.put('comp1', 'coll1', 'id1', marker)\n const result = await adapter.get('comp1', 'coll1', 'id1')\n expect(result).toEqual(marker) // _del must survive — a store that drops it breaks #589 convergence\n })\n })\n\n // ─── Internal Collection Filtering ─────────────────────────────\n\n describe('internal collection filtering', () => {\n it('loadAll excludes _keyring collection', async () => {\n await adapter.put('comp1', 'invoices', 'inv-1', makeEnvelope(1, 'record'))\n await adapter.put('comp1', '_keyring', 'user-01', makeEnvelope(1, 'keyring'))\n const snapshot = await adapter.loadAll('comp1')\n expect(snapshot['invoices']).toBeDefined()\n expect(snapshot['_keyring']).toBeUndefined()\n })\n\n it('loadAll excludes _sync collection', async () => {\n await adapter.put('comp1', 'invoices', 'inv-1', makeEnvelope(1, 'record'))\n await adapter.put('comp1', '_sync', 'meta', makeEnvelope(1, 'sync'))\n const snapshot = await adapter.loadAll('comp1')\n expect(snapshot['invoices']).toBeDefined()\n expect(snapshot['_sync']).toBeUndefined()\n })\n\n it('get/put/delete still work on _keyring collection directly', async () => {\n await adapter.put('comp1', '_keyring', 'user-01', makeEnvelope(1, 'keyring'))\n const result = await adapter.get('comp1', '_keyring', 'user-01')\n expect(result).not.toBeNull()\n await adapter.delete('comp1', '_keyring', 'user-01')\n const deleted = await adapter.get('comp1', '_keyring', 'user-01')\n expect(deleted).toBeNull()\n })\n })\n\n // ─── Optional capability surface (#845) ────────────────────────\n //\n // The six-method core is mandatory; `ping` / `listVaults` / `tx` /\n // `listPage` / `getStoreTime` are not. Stores diverge exactly here, and\n // until now the harness never looked — which is how `@noy-db/to-memory`\n // shipped a working `tx()` whose `txAtomic` capability was never declared,\n // while its JSDoc claimed otherwise. The hub reads that bit to decide\n // whether to delegate, so the implementation would simply have been\n // skipped.\n //\n // Each block runs only when the store implements the method, so this adds\n // no requirement. The load-bearing rule is the pairing assertion:\n // IMPLEMENTED ⇒ DECLARED. That is what catches this whole class of drift,\n // for every store present and future.\n\n describe('optional capabilities', () => {\n it('declares txAtomic if and only if tx() is implemented', async () => {\n const implemented = typeof adapter.tx === 'function'\n const declared = adapter.capabilities?.txAtomic === true\n expect(\n declared,\n implemented\n ? 'store implements tx() but does not declare capabilities.txAtomic — ' +\n 'the hub gates delegation on that bit, so the implementation would be skipped'\n : 'store declares capabilities.txAtomic but has no tx() to delegate to',\n ).toBe(implemented)\n })\n\n it('ping() resolves without throwing, when implemented', async () => {\n if (typeof adapter.ping !== 'function') return\n await expect(adapter.ping()).resolves.not.toThrow()\n })\n\n it('listVaults() reports a vault that has been written to, when implemented', async () => {\n if (typeof adapter.listVaults !== 'function') return\n await adapter.put('comp-lv', 'coll1', 'id1', makeEnvelope(1))\n const vaults = await adapter.listVaults()\n expect(vaults).toContain('comp-lv')\n })\n\n it('tx() applies every op, when implemented', async () => {\n if (typeof adapter.tx !== 'function') return\n await adapter.tx([\n { type: 'put', vault: 'comp-tx', collection: 'coll1', id: 'a', envelope: makeEnvelope(1, 'a') },\n { type: 'put', vault: 'comp-tx', collection: 'coll1', id: 'b', envelope: makeEnvelope(1, 'b') },\n ])\n expect(await adapter.get('comp-tx', 'coll1', 'a')).not.toBeNull()\n expect(await adapter.get('comp-tx', 'coll1', 'b')).not.toBeNull()\n })\n\n it('tx() enforces expectedVersion atomically — mismatch throws ConflictError with nothing applied, when implemented (#920)', async () => {\n if (typeof adapter.tx !== 'function') return\n await adapter.put('comp-txv', 'coll1', 'a', makeEnvelope(2, 'committed'))\n // A matching expectedVersion commits.\n await adapter.tx([\n { type: 'put', vault: 'comp-txv', collection: 'coll1', id: 'a', envelope: makeEnvelope(3, 'updated'), expectedVersion: 2 },\n ])\n expect((await adapter.get('comp-txv', 'coll1', 'a'))?._v).toBe(3)\n // A mismatch throws ConflictError and applies NOTHING — including sibling ops.\n await expect(\n adapter.tx([\n { type: 'put', vault: 'comp-txv', collection: 'coll1', id: 'b', envelope: makeEnvelope(1, 'sibling') },\n { type: 'put', vault: 'comp-txv', collection: 'coll1', id: 'a', envelope: makeEnvelope(9, 'stale'), expectedVersion: 7 },\n ]),\n ).rejects.toThrow(ConflictError)\n expect((await adapter.get('comp-txv', 'coll1', 'a'))?._v).toBe(3)\n expect(await adapter.get('comp-txv', 'coll1', 'b')).toBeNull()\n })\n\n it('tx() rolls back every op when one leg fails — zero partial writes, when implemented (#920)', async () => {\n if (typeof adapter.tx !== 'function') return\n await adapter.put('comp-txr', 'coll1', 'seed', makeEnvelope(1, 'original'))\n await adapter.put('comp-txr', 'coll1', 'blocker', makeEnvelope(1, 'blocker'))\n await expect(\n adapter.tx([\n { type: 'put', vault: 'comp-txr', collection: 'coll1', id: 'fresh', envelope: makeEnvelope(1, 'fresh') },\n { type: 'delete', vault: 'comp-txr', collection: 'coll1', id: 'seed' },\n { type: 'put', vault: 'comp-txr', collection: 'coll1', id: 'blocker', envelope: makeEnvelope(9, 'clobber'), expectedVersion: 4 },\n ]),\n ).rejects.toThrow()\n expect(await adapter.get('comp-txr', 'coll1', 'fresh')).toBeNull()\n const seed = await adapter.get('comp-txr', 'coll1', 'seed')\n expect(seed?._data).toBe(Buffer.from('original').toString('base64'))\n })\n\n it('tx() rejects a put op missing its envelope without partial application, when implemented (#920)', async () => {\n if (typeof adapter.tx !== 'function') return\n await expect(\n adapter.tx([\n { type: 'put', vault: 'comp-txe', collection: 'coll1', id: 'good', envelope: makeEnvelope(1, 'good') },\n { type: 'put', vault: 'comp-txe', collection: 'coll1', id: 'bad' },\n ]),\n ).rejects.toThrow()\n expect(await adapter.get('comp-txe', 'coll1', 'good')).toBeNull()\n })\n\n it('getStoreTime() returns a non-decreasing interval, when implemented', async () => {\n if (typeof adapter.getStoreTime !== 'function') return\n const a = await adapter.getStoreTime()\n const b = await adapter.getStoreTime()\n expect(a.earliest).toBeLessThanOrEqual(a.latest)\n expect(b.earliest).toBeGreaterThanOrEqual(a.earliest)\n })\n\n it('listPage() paginates and terminates, when implemented', async () => {\n if (typeof adapter.listPage !== 'function') return\n for (let i = 0; i < 3; i++) {\n await adapter.put('comp-lp', 'coll1', `id${i}`, makeEnvelope(1))\n }\n const first = await adapter.listPage('comp-lp', 'coll1', undefined, 2)\n expect(first.items.length).toBeLessThanOrEqual(2)\n // A cursor must eventually run out — no infinite paging.\n let cursor = first.nextCursor\n let guard = 0\n while (cursor && guard++ < 10) {\n cursor = (await adapter.listPage('comp-lp', 'coll1', cursor, 2)).nextCursor\n }\n expect(cursor).toBeFalsy()\n })\n })\n })\n}\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@noy-db/test-adapter-conformance",
|
|
3
|
+
"version": "0.6.0-pre.1",
|
|
4
|
+
"description": "Parameterized adapter contract tests for noy-db stores — the conformance suite every NoydbStore implementation must pass",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "vLannaAi <vicio@lanna.ai>",
|
|
7
|
+
"homepage": "https://github.com/vLannaAi/noy-db/tree/main/packages/test-adapter-conformance#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/vLannaAi/noy-db.git",
|
|
11
|
+
"directory": "packages/test-adapter-conformance"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/vLannaAi/noy-db/issues"
|
|
15
|
+
},
|
|
16
|
+
"type": "module",
|
|
17
|
+
"sideEffects": false,
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"default": "./dist/index.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"module": "./dist/index.js",
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"README.md",
|
|
29
|
+
"LICENSE"
|
|
30
|
+
],
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=22.0.0"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"vitest": "^3.0.0",
|
|
36
|
+
"@noy-db/hub": "0.6.0-pre.1"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"vitest": "^3.0.0",
|
|
40
|
+
"@noy-db/hub": "0.6.0-pre.1"
|
|
41
|
+
},
|
|
42
|
+
"keywords": [
|
|
43
|
+
"noy-db",
|
|
44
|
+
"conformance",
|
|
45
|
+
"adapter",
|
|
46
|
+
"testing",
|
|
47
|
+
"storage"
|
|
48
|
+
],
|
|
49
|
+
"publishConfig": {
|
|
50
|
+
"access": "public"
|
|
51
|
+
},
|
|
52
|
+
"scripts": {
|
|
53
|
+
"build": "tsup",
|
|
54
|
+
"test": "vitest run --passWithNoTests",
|
|
55
|
+
"typecheck": "tsc --noEmit"
|
|
56
|
+
}
|
|
57
|
+
}
|