@freqhole/api-client 0.1.32 → 0.2.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.
@@ -1,443 +0,0 @@
1
- // tests for storage/identity.ts and storage/webLocks.ts
2
- //
3
- // these tests run in node (tsx) which has no real IndexedDB or navigator.locks.
4
- // they stub those globals with minimal in-memory implementations to exercise the
5
- // policy logic without a browser environment.
6
-
7
- import type { IdentityStore, P2PIdentity } from "../storage/identity.js";
8
- import {
9
- resolveIdentity,
10
- persistIdentity,
11
- } from "../storage/identity.js";
12
- import { acquireNodeLeadership, LOCK_NAME } from "../storage/webLocks.js";
13
-
14
- // ---------------------------------------------------------------------------
15
- // helpers
16
- // ---------------------------------------------------------------------------
17
-
18
- function makeIdentity(nodeId = "node-abc"): P2PIdentity {
19
- return {
20
- id: "p2p_identity",
21
- secret_key: new Uint8Array([1, 2, 3]),
22
- node_id: nodeId,
23
- created_at: 1000,
24
- };
25
- }
26
-
27
- function makeLocalStore(
28
- initial: P2PIdentity | null = null,
29
- ): IdentityStore & { stored: P2PIdentity | null } {
30
- let stored: P2PIdentity | null = initial;
31
- return {
32
- get stored() {
33
- return stored;
34
- },
35
- async get() {
36
- return stored;
37
- },
38
- async set(identity) {
39
- stored = identity;
40
- },
41
- };
42
- }
43
-
44
- type StoreName = string;
45
-
46
- /** minimal fake IDB: one database, configurable stores and records */
47
- function stubIndexedDB(opts: {
48
- databases: Array<{ name: string; version: number }>;
49
- stores: Record<string, Record<string, unknown>>;
50
- }) {
51
- const dbData = opts.stores;
52
-
53
- const fakeIDB = {
54
- databases: async () => opts.databases,
55
- open: (name: string) => {
56
- const storeNames = Object.keys(dbData);
57
- const req: Partial<IDBOpenDBRequest> & {
58
- onsuccess: ((ev: Event) => void) | null;
59
- onerror: ((ev: Event) => void) | null;
60
- onupgradeneeded: ((ev: IDBVersionChangeEvent) => void) | null;
61
- transaction: IDBTransaction | null;
62
- result: IDBDatabase;
63
- } = {
64
- onsuccess: null,
65
- onerror: null,
66
- onupgradeneeded: null,
67
- transaction: null,
68
- result: undefined as unknown as IDBDatabase,
69
- };
70
-
71
- const fakeDB: Partial<IDBDatabase> = {
72
- objectStoreNames: {
73
- contains: (storeName: StoreName) =>
74
- storeNames.includes(storeName),
75
- } as DOMStringList,
76
- close() {},
77
- transaction(storeName: string | string[], _mode?: IDBTransactionMode) {
78
- const sName = Array.isArray(storeName) ? storeName[0] : storeName;
79
- const store = dbData[sName] ?? {};
80
- const os: Partial<IDBObjectStore> = {
81
- get(key: IDBValidKey) {
82
- const getReq: Partial<IDBRequest> & {
83
- onsuccess: ((ev: Event) => void) | null;
84
- onerror: ((ev: Event) => void) | null;
85
- result: unknown;
86
- error: DOMException | null;
87
- } = {
88
- onsuccess: null,
89
- onerror: null,
90
- result: store[key as string] ?? undefined,
91
- error: null,
92
- };
93
- // fire async so caller can set handlers first
94
- queueMicrotask(() => getReq.onsuccess?.({} as Event));
95
- return getReq as IDBRequest;
96
- },
97
- put(value: unknown, key?: IDBValidKey) {
98
- if (key !== undefined) store[key as string] = value;
99
- const putReq: Partial<IDBRequest> & {
100
- onsuccess: ((ev: Event) => void) | null;
101
- onerror: ((ev: Event) => void) | null;
102
- result: unknown;
103
- error: DOMException | null;
104
- } = {
105
- onsuccess: null,
106
- onerror: null,
107
- result: key,
108
- error: null,
109
- };
110
- queueMicrotask(() => putReq.onsuccess?.({} as Event));
111
- return putReq as IDBRequest;
112
- },
113
- };
114
- const tx: Partial<IDBTransaction> = {
115
- objectStore: () => os as IDBObjectStore,
116
- };
117
- return tx as IDBTransaction;
118
- },
119
- };
120
-
121
- req.result = fakeDB as IDBDatabase;
122
-
123
- if (!opts.databases.some((d) => d.name === name)) {
124
- // db does not exist - fire upgradeneeded then let abort handler run
125
- queueMicrotask(() => {
126
- if (req.onupgradeneeded) {
127
- const ev = {
128
- target: { transaction: null },
129
- } as unknown as IDBVersionChangeEvent;
130
- req.onupgradeneeded(ev);
131
- }
132
- req.onerror?.({} as Event);
133
- });
134
- } else {
135
- queueMicrotask(() => req.onsuccess?.({} as Event));
136
- }
137
-
138
- return req as IDBOpenDBRequest;
139
- },
140
- deleteDatabase: (_name: string) => ({} as IDBOpenDBRequest),
141
- };
142
-
143
- return fakeIDB;
144
- }
145
-
146
- // ---------------------------------------------------------------------------
147
- // test runner helpers (matches existing test/ conventions)
148
- // ---------------------------------------------------------------------------
149
-
150
- export async function runStorageTests(): Promise<{
151
- passed: number;
152
- failed: number;
153
- }> {
154
- let passed = 0;
155
- let failed = 0;
156
-
157
- async function test(name: string, fn: () => void | Promise<void>) {
158
- try {
159
- await fn();
160
- console.log(`✓ ${name}`);
161
- passed++;
162
- } catch (err) {
163
- console.log(
164
- `✗ ${name}: ${err instanceof Error ? err.message : String(err)}`,
165
- );
166
- failed++;
167
- }
168
- }
169
-
170
- function assert(condition: boolean, msg: string) {
171
- if (!condition) throw new Error(msg);
172
- }
173
-
174
- console.log("running storage tests...\n");
175
-
176
- // ---------------------------------------------------------------------------
177
- // identity - freqhole_app absent
178
- // ---------------------------------------------------------------------------
179
-
180
- await test(
181
- "resolveIdentity - returns local identity when freqhole_app absent",
182
- async () => {
183
- const identity = makeIdentity("node-local");
184
- const local = makeLocalStore(identity);
185
-
186
- // stub indexedDB with no databases
187
- const fakeIDB = stubIndexedDB({ databases: [], stores: {} });
188
- const origIDB = (globalThis as Record<string, unknown>).indexedDB;
189
- (globalThis as Record<string, unknown>).indexedDB = fakeIDB;
190
- try {
191
- const result = await resolveIdentity(local);
192
- assert(result?.node_id === "node-local", "expected local identity");
193
- } finally {
194
- (globalThis as Record<string, unknown>).indexedDB = origIDB;
195
- }
196
- },
197
- );
198
-
199
- await test(
200
- "persistIdentity - writes to local store when freqhole_app absent",
201
- async () => {
202
- const identity = makeIdentity("node-new");
203
- const local = makeLocalStore(null);
204
-
205
- const fakeIDB = stubIndexedDB({ databases: [], stores: {} });
206
- const origIDB = (globalThis as Record<string, unknown>).indexedDB;
207
- (globalThis as Record<string, unknown>).indexedDB = fakeIDB;
208
- try {
209
- await persistIdentity(identity, local);
210
- assert(local.stored?.node_id === "node-new", "expected identity in local store");
211
- } finally {
212
- (globalThis as Record<string, unknown>).indexedDB = origIDB;
213
- }
214
- },
215
- );
216
-
217
- // ---------------------------------------------------------------------------
218
- // identity - freqhole_app present
219
- // ---------------------------------------------------------------------------
220
-
221
- await test(
222
- "resolveIdentity - prefers spume identity when freqhole_app has one",
223
- async () => {
224
- const spumeIdentity = makeIdentity("node-spume");
225
- const localIdentity = makeIdentity("node-local");
226
- const local = makeLocalStore(localIdentity);
227
-
228
- const fakeIDB = stubIndexedDB({
229
- databases: [{ name: "freqhole_app", version: 1 }],
230
- stores: {
231
- app_state: { p2p_identity: spumeIdentity },
232
- },
233
- });
234
- const origIDB = (globalThis as Record<string, unknown>).indexedDB;
235
- (globalThis as Record<string, unknown>).indexedDB = fakeIDB;
236
- try {
237
- const result = await resolveIdentity(local);
238
- assert(result?.node_id === "node-spume", "expected spume identity to win");
239
- } finally {
240
- (globalThis as Record<string, unknown>).indexedDB = origIDB;
241
- }
242
- },
243
- );
244
-
245
- await test(
246
- "resolveIdentity - falls back to local when freqhole_app has no identity",
247
- async () => {
248
- const localIdentity = makeIdentity("node-local");
249
- const local = makeLocalStore(localIdentity);
250
-
251
- const fakeIDB = stubIndexedDB({
252
- databases: [{ name: "freqhole_app", version: 1 }],
253
- stores: { app_state: {} }, // store exists but no identity
254
- });
255
- const origIDB = (globalThis as Record<string, unknown>).indexedDB;
256
- (globalThis as Record<string, unknown>).indexedDB = fakeIDB;
257
- try {
258
- const result = await resolveIdentity(local);
259
- assert(result?.node_id === "node-local", "expected local fallback");
260
- } finally {
261
- (globalThis as Record<string, unknown>).indexedDB = origIDB;
262
- }
263
- },
264
- );
265
-
266
- await test(
267
- "persistIdentity - writes to freqhole_app when it exists",
268
- async () => {
269
- const identity = makeIdentity("node-new");
270
- const local = makeLocalStore(null);
271
- const storeData: Record<string, unknown> = {};
272
-
273
- const fakeIDB = stubIndexedDB({
274
- databases: [{ name: "freqhole_app", version: 1 }],
275
- stores: { app_state: storeData },
276
- });
277
- const origIDB = (globalThis as Record<string, unknown>).indexedDB;
278
- (globalThis as Record<string, unknown>).indexedDB = fakeIDB;
279
- try {
280
- await persistIdentity(identity, local);
281
- assert(
282
- (storeData["p2p_identity"] as P2PIdentity | undefined)?.node_id === "node-new",
283
- "expected identity written to freqhole_app",
284
- );
285
- assert(local.stored === null, "expected local store untouched");
286
- } finally {
287
- (globalThis as Record<string, unknown>).indexedDB = origIDB;
288
- }
289
- },
290
- );
291
-
292
- // ---------------------------------------------------------------------------
293
- // webLocks - unsupported fallback
294
- // ---------------------------------------------------------------------------
295
-
296
- // navigator is read-only on globalThis in Node.js; use defineProperty to stub it
297
- function stubNavigator(value: unknown): () => void {
298
- const descriptor = Object.getOwnPropertyDescriptor(globalThis, "navigator");
299
- Object.defineProperty(globalThis, "navigator", {
300
- configurable: true,
301
- writable: true,
302
- value,
303
- });
304
- return () => {
305
- if (descriptor) {
306
- Object.defineProperty(globalThis, "navigator", descriptor);
307
- } else {
308
- // was not originally defined - remove our stub
309
- Object.defineProperty(globalThis, "navigator", {
310
- configurable: true,
311
- writable: true,
312
- value: undefined,
313
- });
314
- }
315
- };
316
- }
317
-
318
- await test(
319
- "acquireNodeLeadership - calls onAcquired immediately when navigator.locks absent",
320
- async () => {
321
- const restore = stubNavigator(undefined);
322
- try {
323
- const states: string[] = [];
324
- let acquired = false;
325
-
326
- const cancel = acquireNodeLeadership({
327
- onAcquired: () => {
328
- acquired = true;
329
- },
330
- onStateChange: (s) => states.push(s),
331
- });
332
-
333
- // give the microtask queue a tick
334
- await Promise.resolve();
335
-
336
- assert(states[0] === "unsupported", "expected unsupported state");
337
- assert(acquired, "expected onAcquired to be called");
338
- cancel(); // no-op
339
- } finally {
340
- restore();
341
- }
342
- },
343
- );
344
-
345
- await test(
346
- "acquireNodeLeadership - exports LOCK_NAME constant",
347
- () => {
348
- assert(LOCK_NAME === "freqhole-iroh-node", "wrong lock name");
349
- },
350
- );
351
-
352
- // ---------------------------------------------------------------------------
353
- // webLocks - navigator.locks stub (granted immediately via ifAvailable)
354
- // ---------------------------------------------------------------------------
355
-
356
- await test(
357
- "acquireNodeLeadership - calls onAcquired when lock granted",
358
- async () => {
359
- const states: string[] = [];
360
- let acquired = false;
361
-
362
- // stub navigator.locks where ifAvailable returns a non-null lock
363
- const restore = stubNavigator({
364
- locks: {
365
- request: async (
366
- _name: string,
367
- opts: { ifAvailable?: boolean },
368
- cb: (lock: Lock | null) => Promise<void>,
369
- ) => {
370
- if (opts.ifAvailable) {
371
- // grant immediately
372
- await cb({ name: _name, mode: "exclusive" } as Lock);
373
- }
374
- },
375
- },
376
- });
377
-
378
- try {
379
- const cancel = acquireNodeLeadership({
380
- onAcquired: () => {
381
- acquired = true;
382
- },
383
- onStateChange: (s) => states.push(s),
384
- });
385
-
386
- // the lock request is fire-and-forget; wait a few microtasks
387
- await new Promise((r) => setTimeout(r, 10));
388
- cancel();
389
-
390
- assert(states.includes("leader"), "expected leader state");
391
- assert(acquired, "expected onAcquired to be called");
392
- } finally {
393
- restore();
394
- }
395
- },
396
- );
397
-
398
- await test(
399
- "acquireNodeLeadership - reports waiting when lock not available",
400
- async () => {
401
- const states: string[] = [];
402
-
403
- // stub: ifAvailable returns null (lock held elsewhere), queued request never resolves
404
- const restore = stubNavigator({
405
- locks: {
406
- request: async (
407
- _name: string,
408
- opts: { ifAvailable?: boolean; signal?: AbortSignal },
409
- cb: (lock: Lock | null) => Promise<void>,
410
- ) => {
411
- if (opts.ifAvailable) {
412
- // lock held - return null
413
- await cb(null);
414
- } else {
415
- // wait forever (or until signal aborts)
416
- await new Promise<void>((_, reject) => {
417
- opts.signal?.addEventListener("abort", () =>
418
- reject(new DOMException("aborted", "AbortError")),
419
- );
420
- });
421
- }
422
- },
423
- },
424
- });
425
-
426
- try {
427
- const cancel = acquireNodeLeadership({
428
- onAcquired: () => {},
429
- onStateChange: (s) => states.push(s),
430
- });
431
-
432
- await new Promise((r) => setTimeout(r, 10));
433
- assert(states.includes("waiting"), "expected waiting state");
434
- cancel(); // trigger abort
435
- await new Promise((r) => setTimeout(r, 10));
436
- } finally {
437
- restore();
438
- }
439
- },
440
- );
441
-
442
- return { passed, failed };
443
- }