Resolve "WIP: UTXO Selection and Storage"
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
|
||||
import { Encryption } from '~/src/utils/encryption';
|
||||
import { binToUtf8, utf8ToBin } from '@bitauth/libauth';
|
||||
|
||||
describe('Encryption', () => {
|
||||
let encryption: Encryption;
|
||||
const testKey = 'test-secret-key-123';
|
||||
|
||||
beforeEach(async () => {
|
||||
encryption = await Encryption.fromPassphrase(testKey);
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
it('should create instance with string key', async () => {
|
||||
const enc = await Encryption.fromPassphrase('test-key');
|
||||
expect(enc).toBeInstanceOf(Encryption);
|
||||
});
|
||||
|
||||
it('should create instance with Uint8Array key', async () => {
|
||||
const enc = await Encryption.fromBytes(utf8ToBin('test-key'));
|
||||
expect(enc).toBeInstanceOf(Encryption);
|
||||
});
|
||||
});
|
||||
|
||||
describe('encrypt', () => {
|
||||
it('should encrypt string data', async () => {
|
||||
const data = 'Hello, World!';
|
||||
const encrypted = await encryption.encrypt(data);
|
||||
expect(encrypted).toBeInstanceOf(Uint8Array);
|
||||
expect(encrypted.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should encrypt Uint8Array data', async () => {
|
||||
const data = utf8ToBin('Hello, World!');
|
||||
const encrypted = await encryption.encrypt(data);
|
||||
expect(encrypted).toBeInstanceOf(Uint8Array);
|
||||
expect(encrypted.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should produce different ciphertext for same input due to random IV', async () => {
|
||||
const data = 'Hello, World!';
|
||||
const encrypted1 = await encryption.encrypt(data);
|
||||
const encrypted2 = await encryption.encrypt(data);
|
||||
expect(Buffer.from(encrypted1).toString('hex')).not.toBe(
|
||||
Buffer.from(encrypted2).toString('hex'),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('decrypt', () => {
|
||||
it('should decrypt encrypted string data correctly', async () => {
|
||||
const originalData = 'Hello, World!';
|
||||
const encrypted = await encryption.encrypt(originalData);
|
||||
const decrypted = await encryption.decrypt(encrypted);
|
||||
expect(binToUtf8(decrypted)).toBe(originalData);
|
||||
});
|
||||
|
||||
it('should decrypt encrypted Uint8Array data correctly', async () => {
|
||||
const originalData = utf8ToBin('Hello, World!');
|
||||
const encrypted = await encryption.encrypt(originalData);
|
||||
const decrypted = await encryption.decrypt(encrypted);
|
||||
expect(decrypted).toEqual(originalData);
|
||||
});
|
||||
|
||||
it('should throw error when trying to decrypt string input', async () => {
|
||||
await expect(encryption.decrypt('invalid-input' as any)).rejects.toThrow(
|
||||
'Encrypted payload must be Uint8Array',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error when trying to decrypt invalid data', async () => {
|
||||
const invalidData = new Uint8Array([1, 2, 3, 4]);
|
||||
await expect(encryption.decrypt(invalidData)).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('cross-instance encryption', () => {
|
||||
it('should decrypt data encrypted with different instance using same key', async () => {
|
||||
const enc1 = await Encryption.fromPassphrase(testKey);
|
||||
const enc2 = await Encryption.fromPassphrase(testKey);
|
||||
|
||||
const originalData = 'Hello, World!';
|
||||
const encrypted = await enc1.encrypt(originalData);
|
||||
const decrypted = await enc2.decrypt(encrypted);
|
||||
|
||||
expect(binToUtf8(decrypted)).toBe(originalData);
|
||||
});
|
||||
});
|
||||
|
||||
describe('large data handling', () => {
|
||||
it('should handle large data correctly', async () => {
|
||||
const largeData = 'x'.repeat(1000000); // 1MB of data
|
||||
const encrypted = await encryption.encrypt(largeData);
|
||||
const decrypted = await encryption.decrypt(encrypted);
|
||||
expect(binToUtf8(decrypted)).toBe(largeData);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
//
|
||||
@@ -0,0 +1,251 @@
|
||||
import { describe, test, expect, vi, beforeEach } from 'vitest';
|
||||
import { EventEmitter } from '~/src/utils/event-emitter.js';
|
||||
|
||||
type TestEvents = {
|
||||
test: { data: string };
|
||||
count: { value: number };
|
||||
multiPayload: { first: string; second: number };
|
||||
};
|
||||
|
||||
describe('EventEmitter', () => {
|
||||
let emitter: EventEmitter<TestEvents>;
|
||||
|
||||
beforeEach(() => {
|
||||
emitter = new EventEmitter<TestEvents>();
|
||||
});
|
||||
|
||||
test('should register and trigger an event listener', () => {
|
||||
const mockCallback = vi.fn();
|
||||
emitter.on('test', mockCallback);
|
||||
|
||||
emitter.emit('test', { data: 'hello' });
|
||||
|
||||
expect(mockCallback).toHaveBeenCalledTimes(1);
|
||||
expect(mockCallback).toHaveBeenCalledWith({ data: 'hello' });
|
||||
});
|
||||
|
||||
test('should register multiple listeners for the same event', () => {
|
||||
const mockCallback1 = vi.fn();
|
||||
const mockCallback2 = vi.fn();
|
||||
|
||||
emitter.on('test', mockCallback1);
|
||||
emitter.on('test', mockCallback2);
|
||||
|
||||
emitter.emit('test', { data: 'hello' });
|
||||
|
||||
expect(mockCallback1).toHaveBeenCalledTimes(1);
|
||||
expect(mockCallback2).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('should remove a specific listener', () => {
|
||||
const mockCallback1 = vi.fn();
|
||||
const mockCallback2 = vi.fn();
|
||||
|
||||
emitter.on('test', mockCallback1);
|
||||
emitter.on('test', mockCallback2);
|
||||
|
||||
emitter.off('test', mockCallback1);
|
||||
emitter.emit('test', { data: 'hello' });
|
||||
|
||||
expect(mockCallback1).not.toHaveBeenCalled();
|
||||
expect(mockCallback2).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('should remove all listeners when removeAllListeners is called', () => {
|
||||
const mockCallback1 = vi.fn();
|
||||
const mockCallback2 = vi.fn();
|
||||
|
||||
emitter.on('test', mockCallback1);
|
||||
emitter.on('count', mockCallback2);
|
||||
|
||||
emitter.removeAllListeners();
|
||||
|
||||
emitter.emit('test', { data: 'hello' });
|
||||
emitter.emit('count', { value: 42 });
|
||||
|
||||
expect(mockCallback1).not.toHaveBeenCalled();
|
||||
expect(mockCallback2).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should call listener only once when using once', () => {
|
||||
const mockCallback = vi.fn();
|
||||
|
||||
emitter.once('test', mockCallback);
|
||||
|
||||
emitter.emit('test', { data: 'first' });
|
||||
emitter.emit('test', { data: 'second' });
|
||||
|
||||
expect(mockCallback).toHaveBeenCalledTimes(1);
|
||||
expect(mockCallback).toHaveBeenCalledWith({ data: 'first' });
|
||||
});
|
||||
|
||||
test('should handle multiple event types correctly', () => {
|
||||
const testCallback = vi.fn();
|
||||
const countCallback = vi.fn();
|
||||
|
||||
emitter.on('test', testCallback);
|
||||
emitter.on('count', countCallback);
|
||||
|
||||
emitter.emit('test', { data: 'test data' });
|
||||
emitter.emit('count', { value: 123 });
|
||||
|
||||
expect(testCallback).toHaveBeenCalledWith({ data: 'test data' });
|
||||
expect(countCallback).toHaveBeenCalledWith({ value: 123 });
|
||||
});
|
||||
|
||||
test('should debounce events correctly', async () => {
|
||||
vi.useFakeTimers();
|
||||
const mockCallback = vi.fn();
|
||||
|
||||
emitter.on('test', mockCallback, 100);
|
||||
|
||||
emitter.emit('test', { data: 'first' });
|
||||
emitter.emit('test', { data: 'second' });
|
||||
emitter.emit('test', { data: 'third' });
|
||||
|
||||
expect(mockCallback).not.toHaveBeenCalled();
|
||||
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
expect(mockCallback).toHaveBeenCalledTimes(1);
|
||||
expect(mockCallback).toHaveBeenCalledWith({ data: 'third' });
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
test('should handle complex payloads', () => {
|
||||
const mockCallback = vi.fn();
|
||||
|
||||
emitter.on('multiPayload', mockCallback);
|
||||
|
||||
const payload = { first: 'hello', second: 42 };
|
||||
emitter.emit('multiPayload', payload);
|
||||
|
||||
expect(mockCallback).toHaveBeenCalledWith(payload);
|
||||
});
|
||||
|
||||
test('should return true when event has listeners and false when it does not', () => {
|
||||
const mockCallback = vi.fn();
|
||||
|
||||
expect(emitter.emit('test', { data: 'hello' })).toBe(false);
|
||||
|
||||
emitter.on('test', mockCallback);
|
||||
|
||||
expect(emitter.emit('test', { data: 'hello' })).toBe(true);
|
||||
});
|
||||
|
||||
test('should handle debounced once events correctly', async () => {
|
||||
vi.useFakeTimers();
|
||||
const mockCallback = vi.fn();
|
||||
|
||||
emitter.once('test', mockCallback, 100);
|
||||
|
||||
emitter.emit('test', { data: 'first' });
|
||||
emitter.emit('test', { data: 'second' });
|
||||
emitter.emit('test', { data: 'third' });
|
||||
|
||||
expect(mockCallback).not.toHaveBeenCalled();
|
||||
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
expect(mockCallback).toHaveBeenCalledTimes(1);
|
||||
expect(mockCallback).toHaveBeenCalledWith({ data: 'third' });
|
||||
|
||||
// Emit again after debounce
|
||||
emitter.emit('test', { data: 'fourth' });
|
||||
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
expect(mockCallback).toHaveBeenCalledTimes(1);
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
test('waitFor should resolve when predicate is satisfied', async () => {
|
||||
const promise = emitter.waitFor(
|
||||
'test',
|
||||
(payload) => payload.data === 'success',
|
||||
);
|
||||
|
||||
emitter.emit('test', { data: 'not yet' });
|
||||
emitter.emit('test', { data: 'success' });
|
||||
|
||||
const result = await promise;
|
||||
expect(result).toEqual({ data: 'success' });
|
||||
});
|
||||
|
||||
test('waitFor should reject on timeout', async () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
const promise = emitter.waitFor(
|
||||
'test',
|
||||
(payload) => payload.data === 'success',
|
||||
1000,
|
||||
);
|
||||
|
||||
// Need to handle the rejection or wait for it
|
||||
const promiseResult = promise.catch((error) => {
|
||||
expect(error.message).toBe('Timeout waiting for event "test"');
|
||||
});
|
||||
|
||||
emitter.emit('test', { data: 'not yet' });
|
||||
|
||||
await vi.runAllTimersAsync();
|
||||
await promiseResult;
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
test('waitFor should clean up listeners after resolution', async () => {
|
||||
const promise = emitter.waitFor(
|
||||
'test',
|
||||
(payload) => payload.data === 'success',
|
||||
);
|
||||
|
||||
emitter.emit('test', { data: 'success' });
|
||||
await promise;
|
||||
|
||||
// The internal listener should be removed
|
||||
expect(emitter.emit('test', { data: 'another' })).toBe(false);
|
||||
});
|
||||
|
||||
test('waitFor should handle multiple concurrent waits', async () => {
|
||||
const promise1 = emitter.waitFor(
|
||||
'test',
|
||||
(payload) => payload.data === 'first',
|
||||
);
|
||||
const promise2 = emitter.waitFor(
|
||||
'test',
|
||||
(payload) => payload.data === 'second',
|
||||
);
|
||||
|
||||
emitter.emit('test', { data: 'second' });
|
||||
emitter.emit('test', { data: 'first' });
|
||||
|
||||
const [result1, result2] = await Promise.all([promise1, promise2]);
|
||||
|
||||
expect(result1).toEqual({ data: 'first' });
|
||||
expect(result2).toEqual({ data: 'second' });
|
||||
});
|
||||
|
||||
test('waitFor should clean up timeout on successful resolution', async () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
const promise = emitter.waitFor(
|
||||
'test',
|
||||
(payload) => payload.data === 'success',
|
||||
1000,
|
||||
);
|
||||
|
||||
// Emit success before timeout
|
||||
emitter.emit('test', { data: 'success' });
|
||||
|
||||
// Fast-forward time - should not cause rejection
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
const result = await promise;
|
||||
expect(result).toEqual({ data: 'success' });
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,97 @@
|
||||
import { bench, describe } from 'vitest';
|
||||
import { type Activity, Activities } from '~/src/wallet';
|
||||
import { StoreInMemory } from '~/src/storage'; // Adjust import path as needed
|
||||
|
||||
// Helper function to generate test activities
|
||||
function generateActivities(count: number) {
|
||||
const activities: Array<Activity> = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
activities.push({
|
||||
timestamp: BigInt(i),
|
||||
type: 'WalletName',
|
||||
id: `wallet${i % 1000}`, // Creates some overlap in IDs to test conflict resolution
|
||||
value: `Value ${i}`,
|
||||
});
|
||||
}
|
||||
return activities;
|
||||
}
|
||||
|
||||
describe('Activities Performance Benchmarks', () => {
|
||||
bench('Adding 100,000 activities sequentially', async () => {
|
||||
const store = new StoreInMemory();
|
||||
const activities = new Activities(store);
|
||||
const testActivities = generateActivities(100_000);
|
||||
|
||||
for (const activity of testActivities) {
|
||||
await activities.addActivity(activity);
|
||||
}
|
||||
});
|
||||
|
||||
bench('Adding 100,000 activities in batches of 1000', async () => {
|
||||
const store = new StoreInMemory();
|
||||
const activities = new Activities(store);
|
||||
const testActivities = generateActivities(100_000);
|
||||
const batchSize = 1000;
|
||||
|
||||
for (let i = 0; i < testActivities.length; i += batchSize) {
|
||||
const batch = testActivities.slice(i, i + batchSize);
|
||||
await Promise.all(
|
||||
batch.map((activity) => activities.addActivity(activity)),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
bench('Compiling snapshot after 100,000 activities', async () => {
|
||||
const store = new StoreInMemory();
|
||||
const activities = new Activities(store);
|
||||
const testActivities = generateActivities(100_000);
|
||||
|
||||
// First add all activities
|
||||
await Promise.all(
|
||||
testActivities.map((activity) => activities.addActivity(activity)),
|
||||
);
|
||||
|
||||
// Benchmark the snapshot compilation
|
||||
await activities.compileSnapshot();
|
||||
});
|
||||
|
||||
bench('Mixed operations benchmark', async () => {
|
||||
const store = new StoreInMemory();
|
||||
const activities = new Activities(store);
|
||||
|
||||
// Add activities in smaller batches with intermittent snapshot compilations
|
||||
const batchSize = 10000;
|
||||
const totalActivities = 100_000;
|
||||
const testActivities = generateActivities(totalActivities);
|
||||
|
||||
for (let i = 0; i < totalActivities; i += batchSize) {
|
||||
const batch = testActivities.slice(i, i + batchSize);
|
||||
await Promise.all(
|
||||
batch.map((activity) => activities.addActivity(activity)),
|
||||
);
|
||||
|
||||
// Compile snapshot every 100,000 activities
|
||||
if ((i + batchSize) % 100000 === 0) {
|
||||
await activities.compileSnapshot();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
bench('Activity conflict resolution performance', async () => {
|
||||
const store = new StoreInMemory();
|
||||
const activities = new Activities(store);
|
||||
|
||||
// Generate activities with lots of conflicts (same id/type)
|
||||
const testActivities = Array.from({ length: 100_000 }, (_, i) => ({
|
||||
timestamp: BigInt(i),
|
||||
type: 'WalletName',
|
||||
id: `wallet${i % 100}`, // Only 100 unique wallets, forcing lots of conflicts
|
||||
value: `Value ${i}`,
|
||||
}));
|
||||
|
||||
await Promise.all(
|
||||
testActivities.map((activity) => activities.addActivity(activity)),
|
||||
);
|
||||
await activities.compileSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,285 @@
|
||||
import { describe, test, expect, beforeEach } from 'vitest';
|
||||
import { Activities } from '~/src/wallet';
|
||||
import { StoreInMemory } from '~/src/storage';
|
||||
|
||||
describe('Activities', () => {
|
||||
let store: StoreInMemory;
|
||||
let activities: Activities;
|
||||
|
||||
beforeEach(() => {
|
||||
store = new StoreInMemory();
|
||||
activities = new Activities(store);
|
||||
});
|
||||
|
||||
test('should add activity with auto-generated timestamp', async () => {
|
||||
const activity = {
|
||||
type: 'WalletName',
|
||||
id: 'wallet1',
|
||||
value: 'Test Wallet',
|
||||
};
|
||||
|
||||
await activities.addActivity(activity);
|
||||
await activities.compileSnapshot();
|
||||
|
||||
const storedActivity = activities.getActivity('WalletName', 'wallet1');
|
||||
expect(storedActivity).toBeDefined();
|
||||
expect(storedActivity?.timestamp).toBeDefined();
|
||||
expect(storedActivity?.value).toBe('Test Wallet');
|
||||
});
|
||||
|
||||
test('should preserve provided timestamp', async () => {
|
||||
const timestamp = BigInt(1234567890000);
|
||||
const activity = {
|
||||
timestamp,
|
||||
type: 'WalletName',
|
||||
id: 'wallet1',
|
||||
value: 'Test Wallet',
|
||||
};
|
||||
|
||||
await activities.addActivity(activity);
|
||||
const snapshot = await activities.compileSnapshot();
|
||||
|
||||
const storedActivity = activities.getActivity('WalletName', 'wallet1');
|
||||
expect(storedActivity?.timestamp).toBe(timestamp);
|
||||
});
|
||||
|
||||
test('should update existing activity with newer timestamp', async () => {
|
||||
const oldTimestamp = BigInt(1000);
|
||||
const newTimestamp = BigInt(2000);
|
||||
|
||||
await activities.addActivity({
|
||||
timestamp: oldTimestamp,
|
||||
type: 'WalletName',
|
||||
id: 'wallet1',
|
||||
value: 'Old Name',
|
||||
});
|
||||
|
||||
await activities.addActivity({
|
||||
timestamp: newTimestamp,
|
||||
type: 'WalletName',
|
||||
id: 'wallet1',
|
||||
value: 'New Name',
|
||||
});
|
||||
|
||||
await activities.compileSnapshot();
|
||||
const activity = activities.getActivity('WalletName', 'wallet1');
|
||||
|
||||
expect(activity?.value).toBe('New Name');
|
||||
expect(activity?.timestamp).toBe(newTimestamp);
|
||||
});
|
||||
|
||||
test('should honor first-seen policy', async () => {
|
||||
activities.registerPolicy('TransactionFirstSeen', (existing, newer) => {
|
||||
return existing || newer;
|
||||
});
|
||||
|
||||
const oldTimestamp = BigInt(1000);
|
||||
const newTimestamp = BigInt(2000);
|
||||
|
||||
await activities.addActivity({
|
||||
timestamp: oldTimestamp,
|
||||
type: 'TransactionFirstSeen',
|
||||
id: 'tx1',
|
||||
value: 'First Value',
|
||||
});
|
||||
|
||||
await activities.addActivity({
|
||||
timestamp: newTimestamp,
|
||||
type: 'TransactionFirstSeen',
|
||||
id: 'tx1',
|
||||
value: 'Second Value',
|
||||
});
|
||||
|
||||
const snapshot = await activities.compileSnapshot();
|
||||
const activity = activities.getActivity('TransactionFirstSeen', 'tx1');
|
||||
|
||||
expect(activity?.value).toBe('First Value');
|
||||
expect(activity?.timestamp).toBe(oldTimestamp);
|
||||
});
|
||||
|
||||
test('should handle custom policy with comparison function', async () => {
|
||||
// Policy that keeps activity with highest value
|
||||
activities.registerPolicy('CustomType', (existing, newer) => {
|
||||
if (!existing) return newer;
|
||||
return existing.value > newer.value ? existing : newer;
|
||||
});
|
||||
|
||||
await activities.addActivity({
|
||||
timestamp: BigInt(1000),
|
||||
type: 'CustomType',
|
||||
id: 'custom1',
|
||||
value: 50,
|
||||
});
|
||||
|
||||
await activities.addActivity({
|
||||
timestamp: BigInt(2000),
|
||||
type: 'CustomType',
|
||||
id: 'custom1',
|
||||
value: 30,
|
||||
});
|
||||
|
||||
const snapshot = await activities.compileSnapshot();
|
||||
const activity = activities.getActivity('CustomType', 'custom1');
|
||||
|
||||
expect(activity?.value).toBe(50);
|
||||
});
|
||||
|
||||
test('should process multiple activities in chronological order', async () => {
|
||||
const testActivities = [
|
||||
{
|
||||
timestamp: BigInt(3000),
|
||||
type: 'WalletName',
|
||||
id: 'wallet1',
|
||||
value: 'Name 3',
|
||||
},
|
||||
{
|
||||
timestamp: BigInt(1000),
|
||||
type: 'WalletName',
|
||||
id: 'wallet1',
|
||||
value: 'Name 1',
|
||||
},
|
||||
{
|
||||
timestamp: BigInt(2000),
|
||||
type: 'WalletName',
|
||||
id: 'wallet1',
|
||||
value: 'Name 2',
|
||||
},
|
||||
];
|
||||
|
||||
for (const activity of testActivities) {
|
||||
await activities.addActivity(activity);
|
||||
}
|
||||
|
||||
await activities.compileSnapshot();
|
||||
const finalActivity = activities.getActivity('WalletName', 'wallet1');
|
||||
|
||||
expect(finalActivity?.value).toBe('Name 3');
|
||||
});
|
||||
|
||||
test('should handle multiple activity types independently', async () => {
|
||||
await activities.addActivity({
|
||||
timestamp: BigInt(1000),
|
||||
type: 'WalletName',
|
||||
id: 'wallet1',
|
||||
value: 'My Wallet',
|
||||
});
|
||||
|
||||
await activities.addActivity({
|
||||
timestamp: BigInt(2000),
|
||||
type: 'WalletDescription',
|
||||
id: 'wallet1',
|
||||
value: 'My Description',
|
||||
});
|
||||
|
||||
await activities.compileSnapshot();
|
||||
|
||||
expect(activities.getActivity('WalletName', 'wallet1')?.value).toBe(
|
||||
'My Wallet',
|
||||
);
|
||||
expect(activities.getActivity('WalletDescription', 'wallet1')?.value).toBe(
|
||||
'My Description',
|
||||
);
|
||||
});
|
||||
|
||||
test('should update snapshot immediately when activity is added', async () => {
|
||||
const activity = {
|
||||
timestamp: BigInt(1000),
|
||||
type: 'WalletName',
|
||||
id: 'wallet1',
|
||||
value: 'Test Wallet',
|
||||
};
|
||||
|
||||
await activities.addActivity(activity);
|
||||
|
||||
// Check snapshot without calling compileSnapshot
|
||||
const storedActivity = activities.getActivity('WalletName', 'wallet1');
|
||||
expect(storedActivity?.value).toBe('Test Wallet');
|
||||
});
|
||||
|
||||
test('should handle missing timestamp gracefully', async () => {
|
||||
const activity = {
|
||||
type: 'WalletName',
|
||||
id: 'wallet1',
|
||||
value: 'Test Wallet',
|
||||
};
|
||||
|
||||
await activities.addActivity(activity);
|
||||
await activities.compileSnapshot();
|
||||
const storedActivity = activities.getActivity('WalletName', 'wallet1');
|
||||
|
||||
expect(storedActivity).toBeDefined();
|
||||
expect(storedActivity?.timestamp).toBeDefined();
|
||||
expect(typeof storedActivity?.timestamp).toBe('bigint');
|
||||
});
|
||||
|
||||
test('should handle store errors gracefully', async () => {
|
||||
// Simulate store failure
|
||||
store.set = async () => {
|
||||
throw new Error('Store error');
|
||||
};
|
||||
|
||||
await expect(
|
||||
activities.addActivity({
|
||||
type: 'WalletName',
|
||||
id: 'wallet1',
|
||||
value: 'Test',
|
||||
}),
|
||||
).rejects.toThrow('Store error');
|
||||
});
|
||||
|
||||
test('should handle concurrent activity additions', async () => {
|
||||
const promises: Promise<void>[] = []; // Add explicit type annotation
|
||||
for (let i = 0; i < 10; i++) {
|
||||
promises.push(
|
||||
activities.addActivity({
|
||||
timestamp: BigInt(i),
|
||||
type: 'WalletName',
|
||||
id: 'wallet1',
|
||||
value: `Value ${i}`,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
await activities.compileSnapshot();
|
||||
|
||||
// Should have the last value
|
||||
expect(activities.getActivity('WalletName', 'wallet1')?.value).toBe(
|
||||
'Value 9',
|
||||
);
|
||||
});
|
||||
|
||||
test('should cleanup old activities from store when newer versions are added', async () => {
|
||||
// Add first activity
|
||||
await activities.addActivity({
|
||||
timestamp: BigInt(1000),
|
||||
type: 'WalletName',
|
||||
id: 'wallet1',
|
||||
value: 'Old Name',
|
||||
});
|
||||
|
||||
expect(Object.keys(await store.all()).length).toBe(1);
|
||||
|
||||
// Add newer version - should trigger cleanup of old version
|
||||
await activities.addActivity({
|
||||
timestamp: BigInt(2000),
|
||||
type: 'WalletName',
|
||||
id: 'wallet1',
|
||||
value: 'New Name',
|
||||
});
|
||||
|
||||
// Should still be 1 as old activity was culled
|
||||
expect(Object.keys(await store.all()).length).toBe(1);
|
||||
|
||||
// Add different type
|
||||
await activities.addActivity({
|
||||
timestamp: BigInt(1500),
|
||||
type: 'WalletDescription',
|
||||
id: 'wallet1',
|
||||
value: 'Description',
|
||||
});
|
||||
|
||||
// Should now have 2 activities
|
||||
expect(Object.keys(await store.all()).length).toBe(2);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user