@voidhash/mimic-effect 0.0.9 → 1.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +136 -90
- package/README.md +385 -0
- package/dist/ColdStorage.cjs +60 -0
- package/dist/ColdStorage.d.cts +53 -0
- package/dist/ColdStorage.d.cts.map +1 -0
- package/dist/ColdStorage.d.mts +53 -0
- package/dist/ColdStorage.d.mts.map +1 -0
- package/dist/ColdStorage.mjs +60 -0
- package/dist/ColdStorage.mjs.map +1 -0
- package/dist/DocumentManager.cjs +263 -82
- package/dist/DocumentManager.d.cts +44 -22
- package/dist/DocumentManager.d.cts.map +1 -1
- package/dist/DocumentManager.d.mts +44 -22
- package/dist/DocumentManager.d.mts.map +1 -1
- package/dist/DocumentManager.mjs +259 -67
- package/dist/DocumentManager.mjs.map +1 -1
- package/dist/Errors.cjs +54 -0
- package/dist/Errors.d.cts +96 -0
- package/dist/Errors.d.cts.map +1 -0
- package/dist/Errors.d.mts +96 -0
- package/dist/Errors.d.mts.map +1 -0
- package/dist/Errors.mjs +48 -0
- package/dist/Errors.mjs.map +1 -0
- package/dist/HotStorage.cjs +100 -0
- package/dist/HotStorage.d.cts +70 -0
- package/dist/HotStorage.d.cts.map +1 -0
- package/dist/HotStorage.d.mts +70 -0
- package/dist/HotStorage.d.mts.map +1 -0
- package/dist/HotStorage.mjs +100 -0
- package/dist/HotStorage.mjs.map +1 -0
- package/dist/Metrics.cjs +143 -0
- package/dist/Metrics.d.cts +31 -0
- package/dist/Metrics.d.cts.map +1 -0
- package/dist/Metrics.d.mts +31 -0
- package/dist/Metrics.d.mts.map +1 -0
- package/dist/Metrics.mjs +126 -0
- package/dist/Metrics.mjs.map +1 -0
- package/dist/MimicAuthService.cjs +61 -45
- package/dist/MimicAuthService.d.cts +61 -48
- package/dist/MimicAuthService.d.cts.map +1 -1
- package/dist/MimicAuthService.d.mts +61 -48
- package/dist/MimicAuthService.d.mts.map +1 -1
- package/dist/MimicAuthService.mjs +60 -36
- package/dist/MimicAuthService.mjs.map +1 -1
- package/dist/MimicClusterServerEngine.cjs +521 -0
- package/dist/MimicClusterServerEngine.d.cts +17 -0
- package/dist/MimicClusterServerEngine.d.cts.map +1 -0
- package/dist/MimicClusterServerEngine.d.mts +17 -0
- package/dist/MimicClusterServerEngine.d.mts.map +1 -0
- package/dist/MimicClusterServerEngine.mjs +523 -0
- package/dist/MimicClusterServerEngine.mjs.map +1 -0
- package/dist/MimicServer.cjs +205 -96
- package/dist/MimicServer.d.cts +9 -110
- package/dist/MimicServer.d.cts.map +1 -1
- package/dist/MimicServer.d.mts +9 -110
- package/dist/MimicServer.d.mts.map +1 -1
- package/dist/MimicServer.mjs +206 -90
- package/dist/MimicServer.mjs.map +1 -1
- package/dist/MimicServerEngine.cjs +97 -0
- package/dist/MimicServerEngine.d.cts +78 -0
- package/dist/MimicServerEngine.d.cts.map +1 -0
- package/dist/MimicServerEngine.d.mts +78 -0
- package/dist/MimicServerEngine.d.mts.map +1 -0
- package/dist/MimicServerEngine.mjs +97 -0
- package/dist/MimicServerEngine.mjs.map +1 -0
- package/dist/PresenceManager.cjs +75 -91
- package/dist/PresenceManager.d.cts +17 -66
- package/dist/PresenceManager.d.cts.map +1 -1
- package/dist/PresenceManager.d.mts +17 -66
- package/dist/PresenceManager.d.mts.map +1 -1
- package/dist/PresenceManager.mjs +74 -78
- package/dist/PresenceManager.mjs.map +1 -1
- package/dist/Protocol.cjs +146 -0
- package/dist/Protocol.d.cts +203 -0
- package/dist/Protocol.d.cts.map +1 -0
- package/dist/Protocol.d.mts +203 -0
- package/dist/Protocol.d.mts.map +1 -0
- package/dist/Protocol.mjs +132 -0
- package/dist/Protocol.mjs.map +1 -0
- package/dist/Types.d.cts +172 -0
- package/dist/Types.d.cts.map +1 -0
- package/dist/Types.d.mts +172 -0
- package/dist/Types.d.mts.map +1 -0
- package/dist/_virtual/rolldown_runtime.cjs +1 -25
- package/dist/_virtual/rolldown_runtime.mjs +4 -1
- package/dist/index.cjs +37 -75
- package/dist/index.d.cts +13 -12
- package/dist/index.d.mts +13 -12
- package/dist/index.mjs +12 -12
- package/dist/testing/ColdStorageTestSuite.cjs +508 -0
- package/dist/testing/ColdStorageTestSuite.d.cts +36 -0
- package/dist/testing/ColdStorageTestSuite.d.cts.map +1 -0
- package/dist/testing/ColdStorageTestSuite.d.mts +36 -0
- package/dist/testing/ColdStorageTestSuite.d.mts.map +1 -0
- package/dist/testing/ColdStorageTestSuite.mjs +508 -0
- package/dist/testing/ColdStorageTestSuite.mjs.map +1 -0
- package/dist/testing/FailingStorage.cjs +135 -0
- package/dist/testing/FailingStorage.d.cts +43 -0
- package/dist/testing/FailingStorage.d.cts.map +1 -0
- package/dist/testing/FailingStorage.d.mts +43 -0
- package/dist/testing/FailingStorage.d.mts.map +1 -0
- package/dist/testing/FailingStorage.mjs +136 -0
- package/dist/testing/FailingStorage.mjs.map +1 -0
- package/dist/testing/HotStorageTestSuite.cjs +585 -0
- package/dist/testing/HotStorageTestSuite.d.cts +40 -0
- package/dist/testing/HotStorageTestSuite.d.cts.map +1 -0
- package/dist/testing/HotStorageTestSuite.d.mts +40 -0
- package/dist/testing/HotStorageTestSuite.d.mts.map +1 -0
- package/dist/testing/HotStorageTestSuite.mjs +585 -0
- package/dist/testing/HotStorageTestSuite.mjs.map +1 -0
- package/dist/testing/StorageIntegrationTestSuite.cjs +349 -0
- package/dist/testing/StorageIntegrationTestSuite.d.cts +35 -0
- package/dist/testing/StorageIntegrationTestSuite.d.cts.map +1 -0
- package/dist/testing/StorageIntegrationTestSuite.d.mts +35 -0
- package/dist/testing/StorageIntegrationTestSuite.d.mts.map +1 -0
- package/dist/testing/StorageIntegrationTestSuite.mjs +349 -0
- package/dist/testing/StorageIntegrationTestSuite.mjs.map +1 -0
- package/dist/testing/assertions.cjs +114 -0
- package/dist/testing/assertions.mjs +109 -0
- package/dist/testing/assertions.mjs.map +1 -0
- package/dist/testing/index.cjs +14 -0
- package/dist/testing/index.d.cts +6 -0
- package/dist/testing/index.d.mts +6 -0
- package/dist/testing/index.mjs +7 -0
- package/dist/testing/types.cjs +15 -0
- package/dist/testing/types.d.cts +90 -0
- package/dist/testing/types.d.cts.map +1 -0
- package/dist/testing/types.d.mts +90 -0
- package/dist/testing/types.d.mts.map +1 -0
- package/dist/testing/types.mjs +16 -0
- package/dist/testing/types.mjs.map +1 -0
- package/package.json +18 -3
- package/src/ColdStorage.ts +136 -0
- package/src/DocumentManager.ts +550 -190
- package/src/Errors.ts +114 -0
- package/src/HotStorage.ts +239 -0
- package/src/Metrics.ts +187 -0
- package/src/MimicAuthService.ts +126 -64
- package/src/MimicClusterServerEngine.ts +946 -0
- package/src/MimicServer.ts +448 -195
- package/src/MimicServerEngine.ts +276 -0
- package/src/PresenceManager.ts +169 -240
- package/src/Protocol.ts +350 -0
- package/src/Types.ts +231 -0
- package/src/index.ts +57 -23
- package/src/testing/ColdStorageTestSuite.ts +589 -0
- package/src/testing/FailingStorage.ts +286 -0
- package/src/testing/HotStorageTestSuite.ts +762 -0
- package/src/testing/StorageIntegrationTestSuite.ts +504 -0
- package/src/testing/assertions.ts +181 -0
- package/src/testing/index.ts +83 -0
- package/src/testing/types.ts +100 -0
- package/tests/ColdStorage.test.ts +24 -0
- package/tests/DocumentManager.test.ts +158 -287
- package/tests/HotStorage.test.ts +24 -0
- package/tests/MimicAuthService.test.ts +102 -134
- package/tests/MimicClusterServerEngine.test.ts +587 -0
- package/tests/MimicServer.test.ts +90 -226
- package/tests/MimicServerEngine.test.ts +521 -0
- package/tests/PresenceManager.test.ts +22 -63
- package/tests/Protocol.test.ts +190 -0
- package/tests/StorageIntegration.test.ts +259 -0
- package/tsconfig.json +1 -1
- package/tsdown.config.ts +1 -1
- package/dist/DocumentProtocol.cjs +0 -94
- package/dist/DocumentProtocol.d.cts +0 -113
- package/dist/DocumentProtocol.d.cts.map +0 -1
- package/dist/DocumentProtocol.d.mts +0 -113
- package/dist/DocumentProtocol.d.mts.map +0 -1
- package/dist/DocumentProtocol.mjs +0 -89
- package/dist/DocumentProtocol.mjs.map +0 -1
- package/dist/MimicConfig.cjs +0 -60
- package/dist/MimicConfig.d.cts +0 -141
- package/dist/MimicConfig.d.cts.map +0 -1
- package/dist/MimicConfig.d.mts +0 -141
- package/dist/MimicConfig.d.mts.map +0 -1
- package/dist/MimicConfig.mjs +0 -50
- package/dist/MimicConfig.mjs.map +0 -1
- package/dist/MimicDataStorage.cjs +0 -83
- package/dist/MimicDataStorage.d.cts +0 -113
- package/dist/MimicDataStorage.d.cts.map +0 -1
- package/dist/MimicDataStorage.d.mts +0 -113
- package/dist/MimicDataStorage.d.mts.map +0 -1
- package/dist/MimicDataStorage.mjs +0 -74
- package/dist/MimicDataStorage.mjs.map +0 -1
- package/dist/WebSocketHandler.cjs +0 -365
- package/dist/WebSocketHandler.d.cts +0 -34
- package/dist/WebSocketHandler.d.cts.map +0 -1
- package/dist/WebSocketHandler.d.mts +0 -34
- package/dist/WebSocketHandler.d.mts.map +0 -1
- package/dist/WebSocketHandler.mjs +0 -355
- package/dist/WebSocketHandler.mjs.map +0 -1
- package/dist/auth/NoAuth.cjs +0 -43
- package/dist/auth/NoAuth.d.cts +0 -22
- package/dist/auth/NoAuth.d.cts.map +0 -1
- package/dist/auth/NoAuth.d.mts +0 -22
- package/dist/auth/NoAuth.d.mts.map +0 -1
- package/dist/auth/NoAuth.mjs +0 -36
- package/dist/auth/NoAuth.mjs.map +0 -1
- package/dist/errors.cjs +0 -74
- package/dist/errors.d.cts +0 -89
- package/dist/errors.d.cts.map +0 -1
- package/dist/errors.d.mts +0 -89
- package/dist/errors.d.mts.map +0 -1
- package/dist/errors.mjs +0 -67
- package/dist/errors.mjs.map +0 -1
- package/dist/storage/InMemoryDataStorage.cjs +0 -57
- package/dist/storage/InMemoryDataStorage.d.cts +0 -19
- package/dist/storage/InMemoryDataStorage.d.cts.map +0 -1
- package/dist/storage/InMemoryDataStorage.d.mts +0 -19
- package/dist/storage/InMemoryDataStorage.d.mts.map +0 -1
- package/dist/storage/InMemoryDataStorage.mjs +0 -48
- package/dist/storage/InMemoryDataStorage.mjs.map +0 -1
- package/src/DocumentProtocol.ts +0 -112
- package/src/MimicConfig.ts +0 -211
- package/src/MimicDataStorage.ts +0 -157
- package/src/WebSocketHandler.ts +0 -735
- package/src/auth/NoAuth.ts +0 -46
- package/src/errors.ts +0 -113
- package/src/storage/InMemoryDataStorage.ts +0 -66
- package/tests/DocumentProtocol.test.ts +0 -113
- package/tests/InMemoryDataStorage.test.ts +0 -190
- package/tests/MimicConfig.test.ts +0 -290
- package/tests/MimicDataStorage.test.ts +0 -190
- package/tests/NoAuth.test.ts +0 -94
- package/tests/WebSocketHandler.test.ts +0 -321
- package/tests/errors.test.ts +0 -77
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import { ColdStorageTag } from "../ColdStorage.mjs";
|
|
2
|
+
import { HotStorageTag } from "../HotStorage.mjs";
|
|
3
|
+
import { assertDefined, assertEmpty, assertEqual, assertLength, assertTrue, assertUndefined } from "./assertions.mjs";
|
|
4
|
+
import { Effect } from "effect";
|
|
5
|
+
|
|
6
|
+
//#region src/testing/StorageIntegrationTestSuite.ts
|
|
7
|
+
/**
|
|
8
|
+
* @voidhash/mimic-effect/testing - StorageIntegrationTestSuite
|
|
9
|
+
*
|
|
10
|
+
* Integration tests for verifying Hot/Cold storage coordination.
|
|
11
|
+
* Tests snapshot + WAL replay, failure handling, and version verification.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* import { StorageIntegrationTestSuite } from "@voidhash/mimic-effect/testing";
|
|
16
|
+
* import { describe, it } from "vitest";
|
|
17
|
+
* import { Effect, Layer } from "effect";
|
|
18
|
+
* import { ColdStorage, HotStorage } from "@voidhash/mimic-effect";
|
|
19
|
+
*
|
|
20
|
+
* describe("Storage Integration", () => {
|
|
21
|
+
* const layer = Layer.mergeAll(
|
|
22
|
+
* ColdStorage.InMemory.make(),
|
|
23
|
+
* HotStorage.InMemory.make()
|
|
24
|
+
* );
|
|
25
|
+
*
|
|
26
|
+
* for (const test of StorageIntegrationTestSuite.makeTests()) {
|
|
27
|
+
* it(test.name, () =>
|
|
28
|
+
* Effect.runPromise(test.run.pipe(Effect.provide(layer)))
|
|
29
|
+
* );
|
|
30
|
+
* }
|
|
31
|
+
* });
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
const Categories = {
|
|
35
|
+
SNAPSHOT_WAL_COORDINATION: "Snapshot + WAL Coordination",
|
|
36
|
+
VERSION_VERIFICATION: "Version Verification",
|
|
37
|
+
RECOVERY_SCENARIOS: "Recovery Scenarios"
|
|
38
|
+
};
|
|
39
|
+
const makeSnapshot = (version, state = { data: `v${version}` }) => ({
|
|
40
|
+
state,
|
|
41
|
+
version,
|
|
42
|
+
schemaVersion: 1,
|
|
43
|
+
savedAt: Date.now()
|
|
44
|
+
});
|
|
45
|
+
const makeWalEntry = (version, ops = [{
|
|
46
|
+
type: "set",
|
|
47
|
+
path: ["data"],
|
|
48
|
+
value: `v${version}`
|
|
49
|
+
}]) => ({
|
|
50
|
+
transaction: {
|
|
51
|
+
id: `tx-${version}`,
|
|
52
|
+
ops,
|
|
53
|
+
timestamp: Date.now()
|
|
54
|
+
},
|
|
55
|
+
version,
|
|
56
|
+
timestamp: Date.now()
|
|
57
|
+
});
|
|
58
|
+
const snapshotWalCoordinationTests = [
|
|
59
|
+
{
|
|
60
|
+
name: "load empty document returns undefined snapshot and empty WAL",
|
|
61
|
+
category: Categories.SNAPSHOT_WAL_COORDINATION,
|
|
62
|
+
run: Effect.gen(function* () {
|
|
63
|
+
const cold = yield* ColdStorageTag;
|
|
64
|
+
const hot = yield* HotStorageTag;
|
|
65
|
+
const snapshot = yield* cold.load("empty-doc");
|
|
66
|
+
const walEntries = yield* hot.getEntries("empty-doc", 0);
|
|
67
|
+
assertUndefined(snapshot, "Snapshot should be undefined for new doc");
|
|
68
|
+
assertEmpty(walEntries, "WAL should be empty for new doc");
|
|
69
|
+
})
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: "restore from snapshot only (no WAL)",
|
|
73
|
+
category: Categories.SNAPSHOT_WAL_COORDINATION,
|
|
74
|
+
run: Effect.gen(function* () {
|
|
75
|
+
const cold = yield* ColdStorageTag;
|
|
76
|
+
const hot = yield* HotStorageTag;
|
|
77
|
+
const docId = "snapshot-only";
|
|
78
|
+
const snapshot = makeSnapshot(5, { title: "Hello" });
|
|
79
|
+
yield* cold.save(docId, snapshot);
|
|
80
|
+
const loaded = yield* cold.load(docId);
|
|
81
|
+
const walEntries = yield* hot.getEntries(docId, 5);
|
|
82
|
+
assertDefined(loaded, "Snapshot should be loaded");
|
|
83
|
+
assertEqual(loaded.version, 5, "Snapshot version should match");
|
|
84
|
+
assertEqual(loaded.state, { title: "Hello" }, "Snapshot state should match");
|
|
85
|
+
assertEmpty(walEntries, "WAL should be empty after snapshot version");
|
|
86
|
+
})
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: "restore from WAL only (no snapshot)",
|
|
90
|
+
category: Categories.SNAPSHOT_WAL_COORDINATION,
|
|
91
|
+
run: Effect.gen(function* () {
|
|
92
|
+
const cold = yield* ColdStorageTag;
|
|
93
|
+
const hot = yield* HotStorageTag;
|
|
94
|
+
const docId = "wal-only";
|
|
95
|
+
yield* hot.append(docId, makeWalEntry(1));
|
|
96
|
+
yield* hot.append(docId, makeWalEntry(2));
|
|
97
|
+
yield* hot.append(docId, makeWalEntry(3));
|
|
98
|
+
const snapshot = yield* cold.load(docId);
|
|
99
|
+
const walEntries = yield* hot.getEntries(docId, 0);
|
|
100
|
+
assertUndefined(snapshot, "No snapshot should exist");
|
|
101
|
+
assertLength(walEntries, 3, "Should have 3 WAL entries");
|
|
102
|
+
assertEqual(walEntries[0].version, 1, "First entry should be v1");
|
|
103
|
+
assertEqual(walEntries[2].version, 3, "Last entry should be v3");
|
|
104
|
+
})
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
name: "restore from snapshot + WAL replay",
|
|
108
|
+
category: Categories.SNAPSHOT_WAL_COORDINATION,
|
|
109
|
+
run: Effect.gen(function* () {
|
|
110
|
+
const cold = yield* ColdStorageTag;
|
|
111
|
+
const hot = yield* HotStorageTag;
|
|
112
|
+
const docId = "snapshot-plus-wal";
|
|
113
|
+
yield* cold.save(docId, makeSnapshot(5));
|
|
114
|
+
yield* hot.append(docId, makeWalEntry(6));
|
|
115
|
+
yield* hot.append(docId, makeWalEntry(7));
|
|
116
|
+
yield* hot.append(docId, makeWalEntry(8));
|
|
117
|
+
const snapshot = yield* cold.load(docId);
|
|
118
|
+
const walEntries = yield* hot.getEntries(docId, snapshot.version);
|
|
119
|
+
assertEqual(snapshot.version, 5, "Snapshot at v5");
|
|
120
|
+
assertLength(walEntries, 3, "3 WAL entries after snapshot");
|
|
121
|
+
assertEqual(walEntries[0].version, 6, "First WAL entry is v6");
|
|
122
|
+
assertEqual(walEntries[2].version, 8, "Last WAL entry is v8");
|
|
123
|
+
})
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
name: "truncate WAL after snapshot",
|
|
127
|
+
category: Categories.SNAPSHOT_WAL_COORDINATION,
|
|
128
|
+
run: Effect.gen(function* () {
|
|
129
|
+
const cold = yield* ColdStorageTag;
|
|
130
|
+
const hot = yield* HotStorageTag;
|
|
131
|
+
const docId = "truncate-test";
|
|
132
|
+
for (let i = 1; i <= 5; i++) yield* hot.append(docId, makeWalEntry(i));
|
|
133
|
+
yield* cold.save(docId, makeSnapshot(3));
|
|
134
|
+
yield* hot.truncate(docId, 3);
|
|
135
|
+
const walEntries = yield* hot.getEntries(docId, 0);
|
|
136
|
+
assertLength(walEntries, 2, "Only v4 and v5 should remain");
|
|
137
|
+
assertEqual(walEntries[0].version, 4, "First remaining is v4");
|
|
138
|
+
assertEqual(walEntries[1].version, 5, "Last remaining is v5");
|
|
139
|
+
})
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: "snapshot overwrites previous snapshot",
|
|
143
|
+
category: Categories.SNAPSHOT_WAL_COORDINATION,
|
|
144
|
+
run: Effect.gen(function* () {
|
|
145
|
+
const cold = yield* ColdStorageTag;
|
|
146
|
+
const docId = "overwrite-test";
|
|
147
|
+
yield* cold.save(docId, makeSnapshot(1, { old: true }));
|
|
148
|
+
yield* cold.save(docId, makeSnapshot(5, { new: true }));
|
|
149
|
+
const loaded = yield* cold.load(docId);
|
|
150
|
+
assertEqual(loaded.version, 5, "Should have newer version");
|
|
151
|
+
assertEqual(loaded.state, { new: true }, "Should have newer state");
|
|
152
|
+
})
|
|
153
|
+
}
|
|
154
|
+
];
|
|
155
|
+
const versionVerificationTests = [
|
|
156
|
+
{
|
|
157
|
+
name: "WAL entries are ordered by version",
|
|
158
|
+
category: Categories.VERSION_VERIFICATION,
|
|
159
|
+
run: Effect.gen(function* () {
|
|
160
|
+
const hot = yield* HotStorageTag;
|
|
161
|
+
const docId = "ordering-test";
|
|
162
|
+
yield* hot.append(docId, makeWalEntry(3));
|
|
163
|
+
yield* hot.append(docId, makeWalEntry(1));
|
|
164
|
+
yield* hot.append(docId, makeWalEntry(2));
|
|
165
|
+
const entries = yield* hot.getEntries(docId, 0);
|
|
166
|
+
assertLength(entries, 3, "All entries should be returned");
|
|
167
|
+
assertEqual(entries[0].version, 1, "First should be v1");
|
|
168
|
+
assertEqual(entries[1].version, 2, "Second should be v2");
|
|
169
|
+
assertEqual(entries[2].version, 3, "Third should be v3");
|
|
170
|
+
})
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
name: "getEntries filters by sinceVersion correctly",
|
|
174
|
+
category: Categories.VERSION_VERIFICATION,
|
|
175
|
+
run: Effect.gen(function* () {
|
|
176
|
+
const hot = yield* HotStorageTag;
|
|
177
|
+
const docId = "filter-test";
|
|
178
|
+
for (let i = 1; i <= 10; i++) yield* hot.append(docId, makeWalEntry(i));
|
|
179
|
+
const fromV5 = yield* hot.getEntries(docId, 5);
|
|
180
|
+
const fromV8 = yield* hot.getEntries(docId, 8);
|
|
181
|
+
const fromV10 = yield* hot.getEntries(docId, 10);
|
|
182
|
+
assertLength(fromV5, 5, "v6-v10 = 5 entries");
|
|
183
|
+
assertEqual(fromV5[0].version, 6, "First entry after v5 is v6");
|
|
184
|
+
assertLength(fromV8, 2, "v9-v10 = 2 entries");
|
|
185
|
+
assertEqual(fromV8[0].version, 9, "First entry after v8 is v9");
|
|
186
|
+
assertEmpty(fromV10, "No entries after v10");
|
|
187
|
+
})
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
name: "detect version gap between snapshot and WAL",
|
|
191
|
+
category: Categories.VERSION_VERIFICATION,
|
|
192
|
+
run: Effect.gen(function* () {
|
|
193
|
+
const cold = yield* ColdStorageTag;
|
|
194
|
+
const hot = yield* HotStorageTag;
|
|
195
|
+
const docId = "gap-detection";
|
|
196
|
+
yield* cold.save(docId, makeSnapshot(5));
|
|
197
|
+
yield* hot.append(docId, makeWalEntry(7));
|
|
198
|
+
yield* hot.append(docId, makeWalEntry(8));
|
|
199
|
+
const snapshot = yield* cold.load(docId);
|
|
200
|
+
const walEntries = yield* hot.getEntries(docId, snapshot.version);
|
|
201
|
+
assertEqual(snapshot.version, 5, "Snapshot at v5");
|
|
202
|
+
assertLength(walEntries, 2, "Two WAL entries");
|
|
203
|
+
assertTrue(walEntries[0].version !== snapshot.version + 1, "Should detect gap (v7 != v6)");
|
|
204
|
+
})
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
name: "detect internal WAL gaps",
|
|
208
|
+
category: Categories.VERSION_VERIFICATION,
|
|
209
|
+
run: Effect.gen(function* () {
|
|
210
|
+
const hot = yield* HotStorageTag;
|
|
211
|
+
const docId = "internal-gap";
|
|
212
|
+
yield* hot.append(docId, makeWalEntry(1));
|
|
213
|
+
yield* hot.append(docId, makeWalEntry(2));
|
|
214
|
+
yield* hot.append(docId, makeWalEntry(4));
|
|
215
|
+
yield* hot.append(docId, makeWalEntry(5));
|
|
216
|
+
const entries = yield* hot.getEntries(docId, 0);
|
|
217
|
+
let gapFound = false;
|
|
218
|
+
for (let i = 1; i < entries.length; i++) {
|
|
219
|
+
const prev = entries[i - 1].version;
|
|
220
|
+
if (entries[i].version !== prev + 1) {
|
|
221
|
+
gapFound = true;
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
assertTrue(gapFound, "Should detect internal gap between v2 and v4");
|
|
226
|
+
})
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
name: "no gap when WAL is continuous",
|
|
230
|
+
category: Categories.VERSION_VERIFICATION,
|
|
231
|
+
run: Effect.gen(function* () {
|
|
232
|
+
const cold = yield* ColdStorageTag;
|
|
233
|
+
const hot = yield* HotStorageTag;
|
|
234
|
+
const docId = "no-gap";
|
|
235
|
+
yield* cold.save(docId, makeSnapshot(5));
|
|
236
|
+
yield* hot.append(docId, makeWalEntry(6));
|
|
237
|
+
yield* hot.append(docId, makeWalEntry(7));
|
|
238
|
+
yield* hot.append(docId, makeWalEntry(8));
|
|
239
|
+
const snapshot = yield* cold.load(docId);
|
|
240
|
+
assertTrue(!((yield* hot.getEntries(docId, snapshot.version))[0].version !== snapshot.version + 1), "Should not detect gap (v6 == v6)");
|
|
241
|
+
})
|
|
242
|
+
}
|
|
243
|
+
];
|
|
244
|
+
const recoveryScenarioTests = [
|
|
245
|
+
{
|
|
246
|
+
name: "full recovery: snapshot + WAL + new transactions",
|
|
247
|
+
category: Categories.RECOVERY_SCENARIOS,
|
|
248
|
+
run: Effect.gen(function* () {
|
|
249
|
+
const cold = yield* ColdStorageTag;
|
|
250
|
+
const hot = yield* HotStorageTag;
|
|
251
|
+
const docId = "full-recovery";
|
|
252
|
+
yield* cold.save(docId, makeSnapshot(3, { count: 3 }));
|
|
253
|
+
yield* hot.append(docId, makeWalEntry(4));
|
|
254
|
+
yield* hot.append(docId, makeWalEntry(5));
|
|
255
|
+
const snapshot = yield* cold.load(docId);
|
|
256
|
+
const walEntries = yield* hot.getEntries(docId, snapshot.version);
|
|
257
|
+
assertEqual(snapshot.version, 3, "Snapshot version");
|
|
258
|
+
assertLength(walEntries, 2, "WAL entries to replay");
|
|
259
|
+
yield* hot.append(docId, makeWalEntry(6));
|
|
260
|
+
const newWal = yield* hot.getEntries(docId, 5);
|
|
261
|
+
assertLength(newWal, 1, "One new entry after recovery");
|
|
262
|
+
assertEqual(newWal[0].version, 6, "New entry is v6");
|
|
263
|
+
})
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
name: "recovery from only WAL (cold start)",
|
|
267
|
+
category: Categories.RECOVERY_SCENARIOS,
|
|
268
|
+
run: Effect.gen(function* () {
|
|
269
|
+
const cold = yield* ColdStorageTag;
|
|
270
|
+
const hot = yield* HotStorageTag;
|
|
271
|
+
const docId = "cold-start";
|
|
272
|
+
yield* hot.append(docId, makeWalEntry(1));
|
|
273
|
+
yield* hot.append(docId, makeWalEntry(2));
|
|
274
|
+
const snapshot = yield* cold.load(docId);
|
|
275
|
+
const walEntries = yield* hot.getEntries(docId, 0);
|
|
276
|
+
assertUndefined(snapshot, "No snapshot exists");
|
|
277
|
+
assertLength(walEntries, 2, "All WAL entries from beginning");
|
|
278
|
+
})
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
name: "recovery after truncation failure (WAL has old entries)",
|
|
282
|
+
category: Categories.RECOVERY_SCENARIOS,
|
|
283
|
+
run: Effect.gen(function* () {
|
|
284
|
+
const cold = yield* ColdStorageTag;
|
|
285
|
+
const hot = yield* HotStorageTag;
|
|
286
|
+
const docId = "truncate-failed";
|
|
287
|
+
yield* hot.append(docId, makeWalEntry(3));
|
|
288
|
+
yield* hot.append(docId, makeWalEntry(4));
|
|
289
|
+
yield* hot.append(docId, makeWalEntry(5));
|
|
290
|
+
yield* hot.append(docId, makeWalEntry(6));
|
|
291
|
+
yield* cold.save(docId, makeSnapshot(5));
|
|
292
|
+
const snapshot = yield* cold.load(docId);
|
|
293
|
+
const walEntries = yield* hot.getEntries(docId, snapshot.version);
|
|
294
|
+
assertEqual(snapshot.version, 5, "Snapshot at v5");
|
|
295
|
+
assertLength(walEntries, 1, "Only v6 should be replayed");
|
|
296
|
+
assertEqual(walEntries[0].version, 6, "Entry is v6");
|
|
297
|
+
})
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
name: "idempotent snapshot save",
|
|
301
|
+
category: Categories.RECOVERY_SCENARIOS,
|
|
302
|
+
run: Effect.gen(function* () {
|
|
303
|
+
const cold = yield* ColdStorageTag;
|
|
304
|
+
const docId = "idempotent";
|
|
305
|
+
const snapshot1 = makeSnapshot(5, { first: true });
|
|
306
|
+
const snapshot2 = makeSnapshot(5, { second: true });
|
|
307
|
+
yield* cold.save(docId, snapshot1);
|
|
308
|
+
yield* cold.save(docId, snapshot2);
|
|
309
|
+
assertEqual((yield* cold.load(docId)).state, { second: true }, "Second save overwrites");
|
|
310
|
+
})
|
|
311
|
+
}
|
|
312
|
+
];
|
|
313
|
+
/**
|
|
314
|
+
* Generate all integration test cases
|
|
315
|
+
*/
|
|
316
|
+
const makeTests = () => [
|
|
317
|
+
...snapshotWalCoordinationTests,
|
|
318
|
+
...versionVerificationTests,
|
|
319
|
+
...recoveryScenarioTests
|
|
320
|
+
];
|
|
321
|
+
/**
|
|
322
|
+
* Run all integration tests and collect results
|
|
323
|
+
*/
|
|
324
|
+
const runAll = (layer) => Effect.gen(function* () {
|
|
325
|
+
const tests = makeTests();
|
|
326
|
+
const results = [];
|
|
327
|
+
for (const test of tests) {
|
|
328
|
+
const result = yield* Effect.either(test.run.pipe(Effect.provide(layer)));
|
|
329
|
+
if (result._tag === "Right") results.push({
|
|
330
|
+
name: test.name,
|
|
331
|
+
passed: true
|
|
332
|
+
});
|
|
333
|
+
else results.push({
|
|
334
|
+
name: test.name,
|
|
335
|
+
passed: false,
|
|
336
|
+
error: result.left
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
return results;
|
|
340
|
+
});
|
|
341
|
+
const StorageIntegrationTestSuite = {
|
|
342
|
+
Categories,
|
|
343
|
+
makeTests,
|
|
344
|
+
runAll
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
//#endregion
|
|
348
|
+
export { Categories, StorageIntegrationTestSuite };
|
|
349
|
+
//# sourceMappingURL=StorageIntegrationTestSuite.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"StorageIntegrationTestSuite.mjs","names":["snapshotWalCoordinationTests: IntegrationTestCase[]","versionVerificationTests: IntegrationTestCase[]","recoveryScenarioTests: IntegrationTestCase[]","results: Array<{ name: string; passed: boolean; error?: unknown }>"],"sources":["../../src/testing/StorageIntegrationTestSuite.ts"],"sourcesContent":["/**\n * @voidhash/mimic-effect/testing - StorageIntegrationTestSuite\n *\n * Integration tests for verifying Hot/Cold storage coordination.\n * Tests snapshot + WAL replay, failure handling, and version verification.\n *\n * @example\n * ```typescript\n * import { StorageIntegrationTestSuite } from \"@voidhash/mimic-effect/testing\";\n * import { describe, it } from \"vitest\";\n * import { Effect, Layer } from \"effect\";\n * import { ColdStorage, HotStorage } from \"@voidhash/mimic-effect\";\n *\n * describe(\"Storage Integration\", () => {\n * const layer = Layer.mergeAll(\n * ColdStorage.InMemory.make(),\n * HotStorage.InMemory.make()\n * );\n *\n * for (const test of StorageIntegrationTestSuite.makeTests()) {\n * it(test.name, () =>\n * Effect.runPromise(test.run.pipe(Effect.provide(layer)))\n * );\n * }\n * });\n * ```\n */\nimport { Effect } from \"effect\";\nimport { ColdStorageTag } from \"../ColdStorage\";\nimport { HotStorageTag } from \"../HotStorage\";\nimport { ColdStorageError, HotStorageError } from \"../Errors\";\nimport type { StoredDocument, WalEntry } from \"../Types\";\nimport type { StorageTestCase } from \"./types\";\nimport {\n assertEqual,\n assertTrue,\n assertLength,\n assertEmpty,\n assertDefined,\n assertUndefined,\n} from \"./assertions\";\n\n// =============================================================================\n// Test Categories\n// =============================================================================\n\nexport const Categories = {\n SNAPSHOT_WAL_COORDINATION: \"Snapshot + WAL Coordination\",\n VERSION_VERIFICATION: \"Version Verification\",\n RECOVERY_SCENARIOS: \"Recovery Scenarios\",\n} as const;\n\n// =============================================================================\n// Test Helpers\n// =============================================================================\n\nconst makeSnapshot = (\n version: number,\n state: unknown = { data: `v${version}` }\n): StoredDocument => ({\n state,\n version,\n schemaVersion: 1,\n savedAt: Date.now(),\n});\n\nconst makeWalEntry = (\n version: number,\n ops: unknown[] = [{ type: \"set\", path: [\"data\"], value: `v${version}` }]\n): WalEntry => ({\n transaction: {\n id: `tx-${version}`,\n ops,\n timestamp: Date.now(),\n },\n version,\n timestamp: Date.now(),\n});\n\n// =============================================================================\n// Test Definitions\n// =============================================================================\n\ntype IntegrationTestCase = StorageTestCase<ColdStorageError | HotStorageError, ColdStorageTag | HotStorageTag>;\n\nconst snapshotWalCoordinationTests: IntegrationTestCase[] = [\n {\n name: \"load empty document returns undefined snapshot and empty WAL\",\n category: Categories.SNAPSHOT_WAL_COORDINATION,\n run: Effect.gen(function* () {\n const cold = yield* ColdStorageTag;\n const hot = yield* HotStorageTag;\n\n const snapshot = yield* cold.load(\"empty-doc\");\n const walEntries = yield* hot.getEntries(\"empty-doc\", 0);\n\n assertUndefined(snapshot, \"Snapshot should be undefined for new doc\");\n assertEmpty(walEntries, \"WAL should be empty for new doc\");\n }),\n },\n\n {\n name: \"restore from snapshot only (no WAL)\",\n category: Categories.SNAPSHOT_WAL_COORDINATION,\n run: Effect.gen(function* () {\n const cold = yield* ColdStorageTag;\n const hot = yield* HotStorageTag;\n\n const docId = \"snapshot-only\";\n const snapshot = makeSnapshot(5, { title: \"Hello\" });\n\n yield* cold.save(docId, snapshot);\n\n const loaded = yield* cold.load(docId);\n const walEntries = yield* hot.getEntries(docId, 5);\n\n assertDefined(loaded, \"Snapshot should be loaded\");\n assertEqual(loaded!.version, 5, \"Snapshot version should match\");\n assertEqual(loaded!.state, { title: \"Hello\" }, \"Snapshot state should match\");\n assertEmpty(walEntries, \"WAL should be empty after snapshot version\");\n }),\n },\n\n {\n name: \"restore from WAL only (no snapshot)\",\n category: Categories.SNAPSHOT_WAL_COORDINATION,\n run: Effect.gen(function* () {\n const cold = yield* ColdStorageTag;\n const hot = yield* HotStorageTag;\n\n const docId = \"wal-only\";\n\n yield* hot.append(docId, makeWalEntry(1));\n yield* hot.append(docId, makeWalEntry(2));\n yield* hot.append(docId, makeWalEntry(3));\n\n const snapshot = yield* cold.load(docId);\n const walEntries = yield* hot.getEntries(docId, 0);\n\n assertUndefined(snapshot, \"No snapshot should exist\");\n assertLength(walEntries, 3, \"Should have 3 WAL entries\");\n assertEqual(walEntries[0]!.version, 1, \"First entry should be v1\");\n assertEqual(walEntries[2]!.version, 3, \"Last entry should be v3\");\n }),\n },\n\n {\n name: \"restore from snapshot + WAL replay\",\n category: Categories.SNAPSHOT_WAL_COORDINATION,\n run: Effect.gen(function* () {\n const cold = yield* ColdStorageTag;\n const hot = yield* HotStorageTag;\n\n const docId = \"snapshot-plus-wal\";\n\n // Save snapshot at v5\n yield* cold.save(docId, makeSnapshot(5));\n\n // Add WAL entries for v6, v7, v8\n yield* hot.append(docId, makeWalEntry(6));\n yield* hot.append(docId, makeWalEntry(7));\n yield* hot.append(docId, makeWalEntry(8));\n\n const snapshot = yield* cold.load(docId);\n const walEntries = yield* hot.getEntries(docId, snapshot!.version);\n\n assertEqual(snapshot!.version, 5, \"Snapshot at v5\");\n assertLength(walEntries, 3, \"3 WAL entries after snapshot\");\n assertEqual(walEntries[0]!.version, 6, \"First WAL entry is v6\");\n assertEqual(walEntries[2]!.version, 8, \"Last WAL entry is v8\");\n }),\n },\n\n {\n name: \"truncate WAL after snapshot\",\n category: Categories.SNAPSHOT_WAL_COORDINATION,\n run: Effect.gen(function* () {\n const cold = yield* ColdStorageTag;\n const hot = yield* HotStorageTag;\n\n const docId = \"truncate-test\";\n\n // Add WAL entries 1-5\n for (let i = 1; i <= 5; i++) {\n yield* hot.append(docId, makeWalEntry(i));\n }\n\n // Save snapshot at v3\n yield* cold.save(docId, makeSnapshot(3));\n\n // Truncate WAL up to v3\n yield* hot.truncate(docId, 3);\n\n const walEntries = yield* hot.getEntries(docId, 0);\n\n assertLength(walEntries, 2, \"Only v4 and v5 should remain\");\n assertEqual(walEntries[0]!.version, 4, \"First remaining is v4\");\n assertEqual(walEntries[1]!.version, 5, \"Last remaining is v5\");\n }),\n },\n\n {\n name: \"snapshot overwrites previous snapshot\",\n category: Categories.SNAPSHOT_WAL_COORDINATION,\n run: Effect.gen(function* () {\n const cold = yield* ColdStorageTag;\n\n const docId = \"overwrite-test\";\n\n yield* cold.save(docId, makeSnapshot(1, { old: true }));\n yield* cold.save(docId, makeSnapshot(5, { new: true }));\n\n const loaded = yield* cold.load(docId);\n\n assertEqual(loaded!.version, 5, \"Should have newer version\");\n assertEqual(loaded!.state, { new: true }, \"Should have newer state\");\n }),\n },\n];\n\nconst versionVerificationTests: IntegrationTestCase[] = [\n {\n name: \"WAL entries are ordered by version\",\n category: Categories.VERSION_VERIFICATION,\n run: Effect.gen(function* () {\n const hot = yield* HotStorageTag;\n\n const docId = \"ordering-test\";\n\n // Append out of order\n yield* hot.append(docId, makeWalEntry(3));\n yield* hot.append(docId, makeWalEntry(1));\n yield* hot.append(docId, makeWalEntry(2));\n\n const entries = yield* hot.getEntries(docId, 0);\n\n assertLength(entries, 3, \"All entries should be returned\");\n assertEqual(entries[0]!.version, 1, \"First should be v1\");\n assertEqual(entries[1]!.version, 2, \"Second should be v2\");\n assertEqual(entries[2]!.version, 3, \"Third should be v3\");\n }),\n },\n\n {\n name: \"getEntries filters by sinceVersion correctly\",\n category: Categories.VERSION_VERIFICATION,\n run: Effect.gen(function* () {\n const hot = yield* HotStorageTag;\n\n const docId = \"filter-test\";\n\n for (let i = 1; i <= 10; i++) {\n yield* hot.append(docId, makeWalEntry(i));\n }\n\n const fromV5 = yield* hot.getEntries(docId, 5);\n const fromV8 = yield* hot.getEntries(docId, 8);\n const fromV10 = yield* hot.getEntries(docId, 10);\n\n assertLength(fromV5, 5, \"v6-v10 = 5 entries\");\n assertEqual(fromV5[0]!.version, 6, \"First entry after v5 is v6\");\n\n assertLength(fromV8, 2, \"v9-v10 = 2 entries\");\n assertEqual(fromV8[0]!.version, 9, \"First entry after v8 is v9\");\n\n assertEmpty(fromV10, \"No entries after v10\");\n }),\n },\n\n {\n name: \"detect version gap between snapshot and WAL\",\n category: Categories.VERSION_VERIFICATION,\n run: Effect.gen(function* () {\n const cold = yield* ColdStorageTag;\n const hot = yield* HotStorageTag;\n\n const docId = \"gap-detection\";\n\n // Snapshot at v5\n yield* cold.save(docId, makeSnapshot(5));\n\n // WAL starts at v7 (gap: v6 missing)\n yield* hot.append(docId, makeWalEntry(7));\n yield* hot.append(docId, makeWalEntry(8));\n\n const snapshot = yield* cold.load(docId);\n const walEntries = yield* hot.getEntries(docId, snapshot!.version);\n\n assertEqual(snapshot!.version, 5, \"Snapshot at v5\");\n assertLength(walEntries, 2, \"Two WAL entries\");\n\n // Gap detection: first WAL entry should be v6, but it's v7\n const firstWalVersion = walEntries[0]!.version;\n const expectedFirst = snapshot!.version + 1;\n const hasGap = firstWalVersion !== expectedFirst;\n\n assertTrue(hasGap, \"Should detect gap (v7 != v6)\");\n }),\n },\n\n {\n name: \"detect internal WAL gaps\",\n category: Categories.VERSION_VERIFICATION,\n run: Effect.gen(function* () {\n const hot = yield* HotStorageTag;\n\n const docId = \"internal-gap\";\n\n yield* hot.append(docId, makeWalEntry(1));\n yield* hot.append(docId, makeWalEntry(2));\n // Skip v3\n yield* hot.append(docId, makeWalEntry(4));\n yield* hot.append(docId, makeWalEntry(5));\n\n const entries = yield* hot.getEntries(docId, 0);\n\n // Check for internal gaps\n let gapFound = false;\n for (let i = 1; i < entries.length; i++) {\n const prev = entries[i - 1]!.version;\n const curr = entries[i]!.version;\n if (curr !== prev + 1) {\n gapFound = true;\n break;\n }\n }\n\n assertTrue(gapFound, \"Should detect internal gap between v2 and v4\");\n }),\n },\n\n {\n name: \"no gap when WAL is continuous\",\n category: Categories.VERSION_VERIFICATION,\n run: Effect.gen(function* () {\n const cold = yield* ColdStorageTag;\n const hot = yield* HotStorageTag;\n\n const docId = \"no-gap\";\n\n yield* cold.save(docId, makeSnapshot(5));\n yield* hot.append(docId, makeWalEntry(6));\n yield* hot.append(docId, makeWalEntry(7));\n yield* hot.append(docId, makeWalEntry(8));\n\n const snapshot = yield* cold.load(docId);\n const walEntries = yield* hot.getEntries(docId, snapshot!.version);\n\n const firstWalVersion = walEntries[0]!.version;\n const expectedFirst = snapshot!.version + 1;\n const hasGap = firstWalVersion !== expectedFirst;\n\n assertTrue(!hasGap, \"Should not detect gap (v6 == v6)\");\n }),\n },\n];\n\nconst recoveryScenarioTests: IntegrationTestCase[] = [\n {\n name: \"full recovery: snapshot + WAL + new transactions\",\n category: Categories.RECOVERY_SCENARIOS,\n run: Effect.gen(function* () {\n const cold = yield* ColdStorageTag;\n const hot = yield* HotStorageTag;\n\n const docId = \"full-recovery\";\n\n // Initial state: snapshot at v3, WAL v4-v5\n yield* cold.save(docId, makeSnapshot(3, { count: 3 }));\n yield* hot.append(docId, makeWalEntry(4));\n yield* hot.append(docId, makeWalEntry(5));\n\n // \"Recovery\" - load snapshot and WAL\n const snapshot = yield* cold.load(docId);\n const walEntries = yield* hot.getEntries(docId, snapshot!.version);\n\n assertEqual(snapshot!.version, 3, \"Snapshot version\");\n assertLength(walEntries, 2, \"WAL entries to replay\");\n\n // Simulate new transaction after recovery\n yield* hot.append(docId, makeWalEntry(6));\n\n const newWal = yield* hot.getEntries(docId, 5);\n assertLength(newWal, 1, \"One new entry after recovery\");\n assertEqual(newWal[0]!.version, 6, \"New entry is v6\");\n }),\n },\n\n {\n name: \"recovery from only WAL (cold start)\",\n category: Categories.RECOVERY_SCENARIOS,\n run: Effect.gen(function* () {\n const cold = yield* ColdStorageTag;\n const hot = yield* HotStorageTag;\n\n const docId = \"cold-start\";\n\n // Only WAL entries, no snapshot (new document that hasn't been snapshotted)\n yield* hot.append(docId, makeWalEntry(1));\n yield* hot.append(docId, makeWalEntry(2));\n\n const snapshot = yield* cold.load(docId);\n const walEntries = yield* hot.getEntries(docId, 0);\n\n assertUndefined(snapshot, \"No snapshot exists\");\n assertLength(walEntries, 2, \"All WAL entries from beginning\");\n }),\n },\n\n {\n name: \"recovery after truncation failure (WAL has old entries)\",\n category: Categories.RECOVERY_SCENARIOS,\n run: Effect.gen(function* () {\n const cold = yield* ColdStorageTag;\n const hot = yield* HotStorageTag;\n\n const docId = \"truncate-failed\";\n\n // Simulate: snapshot saved at v5, but truncate failed\n // So WAL still has v3, v4, v5, v6\n yield* hot.append(docId, makeWalEntry(3));\n yield* hot.append(docId, makeWalEntry(4));\n yield* hot.append(docId, makeWalEntry(5));\n yield* hot.append(docId, makeWalEntry(6));\n\n yield* cold.save(docId, makeSnapshot(5));\n\n // Recovery should only replay v6\n const snapshot = yield* cold.load(docId);\n const walEntries = yield* hot.getEntries(docId, snapshot!.version);\n\n assertEqual(snapshot!.version, 5, \"Snapshot at v5\");\n assertLength(walEntries, 1, \"Only v6 should be replayed\");\n assertEqual(walEntries[0]!.version, 6, \"Entry is v6\");\n }),\n },\n\n {\n name: \"idempotent snapshot save\",\n category: Categories.RECOVERY_SCENARIOS,\n run: Effect.gen(function* () {\n const cold = yield* ColdStorageTag;\n\n const docId = \"idempotent\";\n\n const snapshot1 = makeSnapshot(5, { first: true });\n const snapshot2 = makeSnapshot(5, { second: true });\n\n yield* cold.save(docId, snapshot1);\n yield* cold.save(docId, snapshot2);\n\n const loaded = yield* cold.load(docId);\n\n // Last write wins\n assertEqual(loaded!.state, { second: true }, \"Second save overwrites\");\n }),\n },\n];\n\n// =============================================================================\n// Test Suite Export\n// =============================================================================\n\n/**\n * Generate all integration test cases\n */\nexport const makeTests = (): IntegrationTestCase[] => [\n ...snapshotWalCoordinationTests,\n ...versionVerificationTests,\n ...recoveryScenarioTests,\n];\n\n/**\n * Run all integration tests and collect results\n */\nexport const runAll = <R>(\n layer: import(\"effect\").Layer.Layer<ColdStorageTag | HotStorageTag, never, R>\n) =>\n Effect.gen(function* () {\n const tests = makeTests();\n const results: Array<{ name: string; passed: boolean; error?: unknown }> = [];\n\n for (const test of tests) {\n const result = yield* Effect.either(test.run.pipe(Effect.provide(layer)));\n\n if (result._tag === \"Right\") {\n results.push({ name: test.name, passed: true });\n } else {\n results.push({ name: test.name, passed: false, error: result.left });\n }\n }\n\n return results;\n });\n\n// =============================================================================\n// Export Namespace\n// =============================================================================\n\nexport const StorageIntegrationTestSuite = {\n Categories,\n makeTests,\n runAll,\n};\n"],"mappings":"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"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
const require_types = require('./types.cjs');
|
|
2
|
+
let effect = require("effect");
|
|
3
|
+
|
|
4
|
+
//#region src/testing/assertions.ts
|
|
5
|
+
/**
|
|
6
|
+
* @voidhash/mimic-effect/testing - Assertion Helpers
|
|
7
|
+
*
|
|
8
|
+
* Internal assertion helpers used by the test suites.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Deep equality check that handles objects, arrays, and primitives.
|
|
12
|
+
*/
|
|
13
|
+
const isDeepEqual = (a, b) => {
|
|
14
|
+
if (a === b) return true;
|
|
15
|
+
if (a === null || b === null) return a === b;
|
|
16
|
+
if (a === void 0 || b === void 0) return a === b;
|
|
17
|
+
if (typeof a !== typeof b) return false;
|
|
18
|
+
if (typeof a === "number" && typeof b === "number") {
|
|
19
|
+
if (Number.isNaN(a) && Number.isNaN(b)) return true;
|
|
20
|
+
return a === b;
|
|
21
|
+
}
|
|
22
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
23
|
+
if (a.length !== b.length) return false;
|
|
24
|
+
for (let i = 0; i < a.length; i++) if (!isDeepEqual(a[i], b[i])) return false;
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
if (typeof a === "object" && typeof b === "object") {
|
|
28
|
+
const aObj = a;
|
|
29
|
+
const bObj = b;
|
|
30
|
+
const aKeys = Object.keys(aObj);
|
|
31
|
+
const bKeys = Object.keys(bObj);
|
|
32
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
33
|
+
for (const key of aKeys) {
|
|
34
|
+
if (!Object.prototype.hasOwnProperty.call(bObj, key)) return false;
|
|
35
|
+
if (!isDeepEqual(aObj[key], bObj[key])) return false;
|
|
36
|
+
}
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
return false;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Assert that two values are deeply equal.
|
|
43
|
+
*/
|
|
44
|
+
const assertEqual = (actual, expected, message) => effect.Effect.gen(function* () {
|
|
45
|
+
if (!isDeepEqual(actual, expected)) yield* effect.Effect.fail(new require_types.TestError({
|
|
46
|
+
message,
|
|
47
|
+
expected,
|
|
48
|
+
actual
|
|
49
|
+
}));
|
|
50
|
+
});
|
|
51
|
+
/**
|
|
52
|
+
* Assert that a condition is true.
|
|
53
|
+
*/
|
|
54
|
+
const assertTrue = (condition, message) => effect.Effect.gen(function* () {
|
|
55
|
+
if (!condition) yield* effect.Effect.fail(new require_types.TestError({ message }));
|
|
56
|
+
});
|
|
57
|
+
/**
|
|
58
|
+
* Assert that a value is undefined.
|
|
59
|
+
*/
|
|
60
|
+
const assertUndefined = (value, message) => effect.Effect.gen(function* () {
|
|
61
|
+
if (value !== void 0) yield* effect.Effect.fail(new require_types.TestError({
|
|
62
|
+
message,
|
|
63
|
+
expected: void 0,
|
|
64
|
+
actual: value
|
|
65
|
+
}));
|
|
66
|
+
});
|
|
67
|
+
/**
|
|
68
|
+
* Assert that a value is defined (not undefined).
|
|
69
|
+
*/
|
|
70
|
+
const assertDefined = (value, message) => effect.Effect.gen(function* () {
|
|
71
|
+
if (value === void 0) yield* effect.Effect.fail(new require_types.TestError({
|
|
72
|
+
message,
|
|
73
|
+
expected: "defined value",
|
|
74
|
+
actual: void 0
|
|
75
|
+
}));
|
|
76
|
+
return value;
|
|
77
|
+
});
|
|
78
|
+
/**
|
|
79
|
+
* Assert that an array has the expected length.
|
|
80
|
+
*/
|
|
81
|
+
const assertLength = (array, expectedLength, message) => effect.Effect.gen(function* () {
|
|
82
|
+
if (array.length !== expectedLength) yield* effect.Effect.fail(new require_types.TestError({
|
|
83
|
+
message,
|
|
84
|
+
expected: expectedLength,
|
|
85
|
+
actual: array.length
|
|
86
|
+
}));
|
|
87
|
+
});
|
|
88
|
+
/**
|
|
89
|
+
* Assert that an array is empty.
|
|
90
|
+
*/
|
|
91
|
+
const assertEmpty = (array, message) => assertLength(array, 0, message);
|
|
92
|
+
/**
|
|
93
|
+
* Assert that an array is sorted by a key.
|
|
94
|
+
*/
|
|
95
|
+
const assertSortedBy = (array, key, message) => effect.Effect.gen(function* () {
|
|
96
|
+
for (let i = 1; i < array.length; i++) {
|
|
97
|
+
const prev = array[i - 1][key];
|
|
98
|
+
const curr = array[i][key];
|
|
99
|
+
if (prev > curr) yield* effect.Effect.fail(new require_types.TestError({
|
|
100
|
+
message,
|
|
101
|
+
expected: `array sorted by ${String(key)}`,
|
|
102
|
+
actual: `element at index ${i - 1} (${prev}) > element at index ${i} (${curr})`
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
//#endregion
|
|
108
|
+
exports.assertDefined = assertDefined;
|
|
109
|
+
exports.assertEmpty = assertEmpty;
|
|
110
|
+
exports.assertEqual = assertEqual;
|
|
111
|
+
exports.assertLength = assertLength;
|
|
112
|
+
exports.assertSortedBy = assertSortedBy;
|
|
113
|
+
exports.assertTrue = assertTrue;
|
|
114
|
+
exports.assertUndefined = assertUndefined;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { TestError } from "./types.mjs";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
|
|
4
|
+
//#region src/testing/assertions.ts
|
|
5
|
+
/**
|
|
6
|
+
* @voidhash/mimic-effect/testing - Assertion Helpers
|
|
7
|
+
*
|
|
8
|
+
* Internal assertion helpers used by the test suites.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Deep equality check that handles objects, arrays, and primitives.
|
|
12
|
+
*/
|
|
13
|
+
const isDeepEqual = (a, b) => {
|
|
14
|
+
if (a === b) return true;
|
|
15
|
+
if (a === null || b === null) return a === b;
|
|
16
|
+
if (a === void 0 || b === void 0) return a === b;
|
|
17
|
+
if (typeof a !== typeof b) return false;
|
|
18
|
+
if (typeof a === "number" && typeof b === "number") {
|
|
19
|
+
if (Number.isNaN(a) && Number.isNaN(b)) return true;
|
|
20
|
+
return a === b;
|
|
21
|
+
}
|
|
22
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
23
|
+
if (a.length !== b.length) return false;
|
|
24
|
+
for (let i = 0; i < a.length; i++) if (!isDeepEqual(a[i], b[i])) return false;
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
if (typeof a === "object" && typeof b === "object") {
|
|
28
|
+
const aObj = a;
|
|
29
|
+
const bObj = b;
|
|
30
|
+
const aKeys = Object.keys(aObj);
|
|
31
|
+
const bKeys = Object.keys(bObj);
|
|
32
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
33
|
+
for (const key of aKeys) {
|
|
34
|
+
if (!Object.prototype.hasOwnProperty.call(bObj, key)) return false;
|
|
35
|
+
if (!isDeepEqual(aObj[key], bObj[key])) return false;
|
|
36
|
+
}
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
return false;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Assert that two values are deeply equal.
|
|
43
|
+
*/
|
|
44
|
+
const assertEqual = (actual, expected, message) => Effect.gen(function* () {
|
|
45
|
+
if (!isDeepEqual(actual, expected)) yield* Effect.fail(new TestError({
|
|
46
|
+
message,
|
|
47
|
+
expected,
|
|
48
|
+
actual
|
|
49
|
+
}));
|
|
50
|
+
});
|
|
51
|
+
/**
|
|
52
|
+
* Assert that a condition is true.
|
|
53
|
+
*/
|
|
54
|
+
const assertTrue = (condition, message) => Effect.gen(function* () {
|
|
55
|
+
if (!condition) yield* Effect.fail(new TestError({ message }));
|
|
56
|
+
});
|
|
57
|
+
/**
|
|
58
|
+
* Assert that a value is undefined.
|
|
59
|
+
*/
|
|
60
|
+
const assertUndefined = (value, message) => Effect.gen(function* () {
|
|
61
|
+
if (value !== void 0) yield* Effect.fail(new TestError({
|
|
62
|
+
message,
|
|
63
|
+
expected: void 0,
|
|
64
|
+
actual: value
|
|
65
|
+
}));
|
|
66
|
+
});
|
|
67
|
+
/**
|
|
68
|
+
* Assert that a value is defined (not undefined).
|
|
69
|
+
*/
|
|
70
|
+
const assertDefined = (value, message) => Effect.gen(function* () {
|
|
71
|
+
if (value === void 0) yield* Effect.fail(new TestError({
|
|
72
|
+
message,
|
|
73
|
+
expected: "defined value",
|
|
74
|
+
actual: void 0
|
|
75
|
+
}));
|
|
76
|
+
return value;
|
|
77
|
+
});
|
|
78
|
+
/**
|
|
79
|
+
* Assert that an array has the expected length.
|
|
80
|
+
*/
|
|
81
|
+
const assertLength = (array, expectedLength, message) => Effect.gen(function* () {
|
|
82
|
+
if (array.length !== expectedLength) yield* Effect.fail(new TestError({
|
|
83
|
+
message,
|
|
84
|
+
expected: expectedLength,
|
|
85
|
+
actual: array.length
|
|
86
|
+
}));
|
|
87
|
+
});
|
|
88
|
+
/**
|
|
89
|
+
* Assert that an array is empty.
|
|
90
|
+
*/
|
|
91
|
+
const assertEmpty = (array, message) => assertLength(array, 0, message);
|
|
92
|
+
/**
|
|
93
|
+
* Assert that an array is sorted by a key.
|
|
94
|
+
*/
|
|
95
|
+
const assertSortedBy = (array, key, message) => Effect.gen(function* () {
|
|
96
|
+
for (let i = 1; i < array.length; i++) {
|
|
97
|
+
const prev = array[i - 1][key];
|
|
98
|
+
const curr = array[i][key];
|
|
99
|
+
if (prev > curr) yield* Effect.fail(new TestError({
|
|
100
|
+
message,
|
|
101
|
+
expected: `array sorted by ${String(key)}`,
|
|
102
|
+
actual: `element at index ${i - 1} (${prev}) > element at index ${i} (${curr})`
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
//#endregion
|
|
108
|
+
export { assertDefined, assertEmpty, assertEqual, assertLength, assertSortedBy, assertTrue, assertUndefined };
|
|
109
|
+
//# sourceMappingURL=assertions.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assertions.mjs","names":[],"sources":["../../src/testing/assertions.ts"],"sourcesContent":["/**\n * @voidhash/mimic-effect/testing - Assertion Helpers\n *\n * Internal assertion helpers used by the test suites.\n */\nimport { Effect } from \"effect\";\nimport { TestError } from \"./types\";\n\n// =============================================================================\n// Deep Equality\n// =============================================================================\n\n/**\n * Deep equality check that handles objects, arrays, and primitives.\n */\nexport const isDeepEqual = (a: unknown, b: unknown): boolean => {\n if (a === b) return true;\n\n if (a === null || b === null) return a === b;\n if (a === undefined || b === undefined) return a === b;\n\n if (typeof a !== typeof b) return false;\n\n if (typeof a === \"number\" && typeof b === \"number\") {\n if (Number.isNaN(a) && Number.isNaN(b)) return true;\n return a === b;\n }\n\n if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (!isDeepEqual(a[i], b[i])) return false;\n }\n return true;\n }\n\n if (typeof a === \"object\" && typeof b === \"object\") {\n const aObj = a as Record<string, unknown>;\n const bObj = b as Record<string, unknown>;\n const aKeys = Object.keys(aObj);\n const bKeys = Object.keys(bObj);\n\n if (aKeys.length !== bKeys.length) return false;\n\n for (const key of aKeys) {\n if (!Object.prototype.hasOwnProperty.call(bObj, key)) return false;\n if (!isDeepEqual(aObj[key], bObj[key])) return false;\n }\n\n return true;\n }\n\n return false;\n};\n\n// =============================================================================\n// Assertion Helpers\n// =============================================================================\n\n/**\n * Assert that two values are deeply equal.\n */\nexport const assertEqual = <T>(\n actual: T,\n expected: T,\n message: string\n): Effect.Effect<void, TestError> =>\n Effect.gen(function* () {\n if (!isDeepEqual(actual, expected)) {\n yield* Effect.fail(new TestError({ message, expected, actual }));\n }\n });\n\n/**\n * Assert that a condition is true.\n */\nexport const assertTrue = (\n condition: boolean,\n message: string\n): Effect.Effect<void, TestError> =>\n Effect.gen(function* () {\n if (!condition) {\n yield* Effect.fail(new TestError({ message }));\n }\n });\n\n/**\n * Assert that a condition is false.\n */\nexport const assertFalse = (\n condition: boolean,\n message: string\n): Effect.Effect<void, TestError> =>\n Effect.gen(function* () {\n if (condition) {\n yield* Effect.fail(new TestError({ message }));\n }\n });\n\n/**\n * Assert that a value is undefined.\n */\nexport const assertUndefined = (\n value: unknown,\n message: string\n): Effect.Effect<void, TestError> =>\n Effect.gen(function* () {\n if (value !== undefined) {\n yield* Effect.fail(\n new TestError({ message, expected: undefined, actual: value })\n );\n }\n });\n\n/**\n * Assert that a value is defined (not undefined).\n */\nexport const assertDefined = <T>(\n value: T | undefined,\n message: string\n): Effect.Effect<T, TestError> =>\n Effect.gen(function* () {\n if (value === undefined) {\n yield* Effect.fail(\n new TestError({ message, expected: \"defined value\", actual: undefined })\n );\n }\n return value as T;\n });\n\n/**\n * Assert that an array has the expected length.\n */\nexport const assertLength = <T>(\n array: T[],\n expectedLength: number,\n message: string\n): Effect.Effect<void, TestError> =>\n Effect.gen(function* () {\n if (array.length !== expectedLength) {\n yield* Effect.fail(\n new TestError({\n message,\n expected: expectedLength,\n actual: array.length,\n })\n );\n }\n });\n\n/**\n * Assert that an array is empty.\n */\nexport const assertEmpty = <T>(\n array: T[],\n message: string\n): Effect.Effect<void, TestError> => assertLength(array, 0, message);\n\n/**\n * Assert that an array is sorted by a key.\n */\nexport const assertSortedBy = <T>(\n array: T[],\n key: keyof T,\n message: string\n): Effect.Effect<void, TestError> =>\n Effect.gen(function* () {\n for (let i = 1; i < array.length; i++) {\n const prev = array[i - 1]![key];\n const curr = array[i]![key];\n if (prev > curr) {\n yield* Effect.fail(\n new TestError({\n message,\n expected: `array sorted by ${String(key)}`,\n actual: `element at index ${i - 1} (${prev}) > element at index ${i} (${curr})`,\n })\n );\n }\n }\n });\n"],"mappings":";;;;;;;;;;;;AAeA,MAAa,eAAe,GAAY,MAAwB;AAC9D,KAAI,MAAM,EAAG,QAAO;AAEpB,KAAI,MAAM,QAAQ,MAAM,KAAM,QAAO,MAAM;AAC3C,KAAI,MAAM,UAAa,MAAM,OAAW,QAAO,MAAM;AAErD,KAAI,OAAO,MAAM,OAAO,EAAG,QAAO;AAElC,KAAI,OAAO,MAAM,YAAY,OAAO,MAAM,UAAU;AAClD,MAAI,OAAO,MAAM,EAAE,IAAI,OAAO,MAAM,EAAE,CAAE,QAAO;AAC/C,SAAO,MAAM;;AAGf,KAAI,MAAM,QAAQ,EAAE,IAAI,MAAM,QAAQ,EAAE,EAAE;AACxC,MAAI,EAAE,WAAW,EAAE,OAAQ,QAAO;AAClC,OAAK,IAAI,IAAI,GAAG,IAAI,EAAE,QAAQ,IAC5B,KAAI,CAAC,YAAY,EAAE,IAAI,EAAE,GAAG,CAAE,QAAO;AAEvC,SAAO;;AAGT,KAAI,OAAO,MAAM,YAAY,OAAO,MAAM,UAAU;EAClD,MAAM,OAAO;EACb,MAAM,OAAO;EACb,MAAM,QAAQ,OAAO,KAAK,KAAK;EAC/B,MAAM,QAAQ,OAAO,KAAK,KAAK;AAE/B,MAAI,MAAM,WAAW,MAAM,OAAQ,QAAO;AAE1C,OAAK,MAAM,OAAO,OAAO;AACvB,OAAI,CAAC,OAAO,UAAU,eAAe,KAAK,MAAM,IAAI,CAAE,QAAO;AAC7D,OAAI,CAAC,YAAY,KAAK,MAAM,KAAK,KAAK,CAAE,QAAO;;AAGjD,SAAO;;AAGT,QAAO;;;;;AAUT,MAAa,eACX,QACA,UACA,YAEA,OAAO,IAAI,aAAa;AACtB,KAAI,CAAC,YAAY,QAAQ,SAAS,CAChC,QAAO,OAAO,KAAK,IAAI,UAAU;EAAE;EAAS;EAAU;EAAQ,CAAC,CAAC;EAElE;;;;AAKJ,MAAa,cACX,WACA,YAEA,OAAO,IAAI,aAAa;AACtB,KAAI,CAAC,UACH,QAAO,OAAO,KAAK,IAAI,UAAU,EAAE,SAAS,CAAC,CAAC;EAEhD;;;;AAkBJ,MAAa,mBACX,OACA,YAEA,OAAO,IAAI,aAAa;AACtB,KAAI,UAAU,OACZ,QAAO,OAAO,KACZ,IAAI,UAAU;EAAE;EAAS,UAAU;EAAW,QAAQ;EAAO,CAAC,CAC/D;EAEH;;;;AAKJ,MAAa,iBACX,OACA,YAEA,OAAO,IAAI,aAAa;AACtB,KAAI,UAAU,OACZ,QAAO,OAAO,KACZ,IAAI,UAAU;EAAE;EAAS,UAAU;EAAiB,QAAQ;EAAW,CAAC,CACzE;AAEH,QAAO;EACP;;;;AAKJ,MAAa,gBACX,OACA,gBACA,YAEA,OAAO,IAAI,aAAa;AACtB,KAAI,MAAM,WAAW,eACnB,QAAO,OAAO,KACZ,IAAI,UAAU;EACZ;EACA,UAAU;EACV,QAAQ,MAAM;EACf,CAAC,CACH;EAEH;;;;AAKJ,MAAa,eACX,OACA,YACmC,aAAa,OAAO,GAAG,QAAQ;;;;AAKpE,MAAa,kBACX,OACA,KACA,YAEA,OAAO,IAAI,aAAa;AACtB,MAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;EACrC,MAAM,OAAO,MAAM,IAAI,GAAI;EAC3B,MAAM,OAAO,MAAM,GAAI;AACvB,MAAI,OAAO,KACT,QAAO,OAAO,KACZ,IAAI,UAAU;GACZ;GACA,UAAU,mBAAmB,OAAO,IAAI;GACxC,QAAQ,oBAAoB,IAAI,EAAE,IAAI,KAAK,uBAAuB,EAAE,IAAI,KAAK;GAC9E,CAAC,CACH;;EAGL"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const require_types = require('./types.cjs');
|
|
2
|
+
const require_ColdStorageTestSuite = require('./ColdStorageTestSuite.cjs');
|
|
3
|
+
const require_HotStorageTestSuite = require('./HotStorageTestSuite.cjs');
|
|
4
|
+
const require_StorageIntegrationTestSuite = require('./StorageIntegrationTestSuite.cjs');
|
|
5
|
+
const require_FailingStorage = require('./FailingStorage.cjs');
|
|
6
|
+
|
|
7
|
+
exports.ColdStorageCategories = require_ColdStorageTestSuite.Categories;
|
|
8
|
+
exports.ColdStorageTestSuite = require_ColdStorageTestSuite.ColdStorageTestSuite;
|
|
9
|
+
exports.FailingStorage = require_FailingStorage.FailingStorage;
|
|
10
|
+
exports.HotStorageCategories = require_HotStorageTestSuite.Categories;
|
|
11
|
+
exports.HotStorageTestSuite = require_HotStorageTestSuite.HotStorageTestSuite;
|
|
12
|
+
exports.IntegrationCategories = require_StorageIntegrationTestSuite.Categories;
|
|
13
|
+
exports.StorageIntegrationTestSuite = require_StorageIntegrationTestSuite.StorageIntegrationTestSuite;
|
|
14
|
+
exports.TestError = require_types.TestError;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { FailedTest, StorageTestCase, TestError, TestResults } from "./types.cjs";
|
|
2
|
+
import { Categories, ColdStorageTestError, ColdStorageTestSuite } from "./ColdStorageTestSuite.cjs";
|
|
3
|
+
import { Categories as Categories$1, HotStorageTestError, HotStorageTestSuite } from "./HotStorageTestSuite.cjs";
|
|
4
|
+
import { Categories as Categories$2, StorageIntegrationTestSuite } from "./StorageIntegrationTestSuite.cjs";
|
|
5
|
+
import { FailingColdStorageConfig, FailingHotStorageConfig, FailingStorage } from "./FailingStorage.cjs";
|
|
6
|
+
export { Categories as ColdStorageCategories, type ColdStorageTestError, ColdStorageTestSuite, type FailedTest, type FailingColdStorageConfig, type FailingHotStorageConfig, FailingStorage, Categories$1 as HotStorageCategories, type HotStorageTestError, HotStorageTestSuite, Categories$2 as IntegrationCategories, StorageIntegrationTestSuite, type StorageTestCase, TestError, type TestResults };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { FailedTest, StorageTestCase, TestError, TestResults } from "./types.mjs";
|
|
2
|
+
import { Categories, ColdStorageTestError, ColdStorageTestSuite } from "./ColdStorageTestSuite.mjs";
|
|
3
|
+
import { Categories as Categories$1, HotStorageTestError, HotStorageTestSuite } from "./HotStorageTestSuite.mjs";
|
|
4
|
+
import { Categories as Categories$2, StorageIntegrationTestSuite } from "./StorageIntegrationTestSuite.mjs";
|
|
5
|
+
import { FailingColdStorageConfig, FailingHotStorageConfig, FailingStorage } from "./FailingStorage.mjs";
|
|
6
|
+
export { Categories as ColdStorageCategories, type ColdStorageTestError, ColdStorageTestSuite, type FailedTest, type FailingColdStorageConfig, type FailingHotStorageConfig, FailingStorage, Categories$1 as HotStorageCategories, type HotStorageTestError, HotStorageTestSuite, Categories$2 as IntegrationCategories, StorageIntegrationTestSuite, type StorageTestCase, TestError, type TestResults };
|