feat: multi-wallet architecture with localStorage state, EIP-712 verification, cross-tab WS leadership
- wallets.js: WalletManager state management with chain validation - verifier.js: WalletVerifier EIP-712 signing via window.ethereum - stream.js: WalletStreamManager with BroadcastChannel leader election, backoff + jitter - AGENTS.md: updated with coding guidelines
This commit is contained in:
151
test/test_stream.js
Normal file
151
test/test_stream.js
Normal file
@ -0,0 +1,151 @@
|
||||
import { WalletManager } from '../wallets.js';
|
||||
import { WalletStreamManager } from '../stream.js';
|
||||
|
||||
// --- 1. Environment Mocks (Storage & Timers) ---
|
||||
const mockStorage = {};
|
||||
globalThis.localStorage = {
|
||||
getItem: (key) => mockStorage[key] || null,
|
||||
setItem: (key, value) => { mockStorage[key] = String(value); },
|
||||
clear: () => { Object.keys(mockStorage).forEach(k => delete mockStorage[k]); }
|
||||
};
|
||||
|
||||
// Speed up time so our backoff tests don't take 30 actual seconds to run
|
||||
const originalSetTimeout = globalThis.setTimeout;
|
||||
globalThis.setTimeout = (callback, delay) => {
|
||||
return originalSetTimeout(callback, 1); // Force all retries to happen instantly
|
||||
};
|
||||
|
||||
// Mock location for stream.js WS_URL evaluation
|
||||
globalThis.location = {
|
||||
protocol: 'http:',
|
||||
host: 'localhost:3000'
|
||||
};
|
||||
|
||||
// --- 2. The Mock WebSocket Server Mirror ---
|
||||
const mockInstances = [];
|
||||
let lastSentPayload = null;
|
||||
let connectionCount = 0;
|
||||
|
||||
class MockWebSocket {
|
||||
constructor(url) {
|
||||
this.url = url;
|
||||
this.readyState = 0; // CONNECTING
|
||||
connectionCount++;
|
||||
mockInstances.push(this);
|
||||
|
||||
originalSetTimeout(() => {
|
||||
this.readyState = 1; // OPEN
|
||||
if (this.onopen) this.onopen();
|
||||
}, 2);
|
||||
}
|
||||
|
||||
send(data) {
|
||||
lastSentPayload = JSON.parse(data);
|
||||
}
|
||||
|
||||
close() {
|
||||
this.readyState = 3; // CLOSED
|
||||
if (this.onclose) this.onclose({ code: 1000, reason: "Normal closure" });
|
||||
}
|
||||
|
||||
// Helper method for our test runner to mimic the server pushing data to client
|
||||
simulateServerPush(msgObject) {
|
||||
if (this.onmessage) {
|
||||
this.onmessage({ data: JSON.stringify(msgObject) });
|
||||
}
|
||||
}
|
||||
|
||||
// Helper method to simulate a sudden dirty network disconnect
|
||||
simulateNetworkDrop() {
|
||||
this.readyState = 3;
|
||||
if (this.onclose) this.onclose({ code: 1006, reason: "Abnormal Drop" });
|
||||
}
|
||||
}
|
||||
globalThis.WebSocket = MockWebSocket;
|
||||
|
||||
// --- 3. Test Runner Execution ---
|
||||
function assert(condition, message) {
|
||||
if (!condition) throw new Error(`❌ FAIL: ${message}`);
|
||||
console.log(`✅ PASS: ${message}`);
|
||||
}
|
||||
|
||||
async function runStreamTests() {
|
||||
console.log("🚀 Starting WalletStreamManager Real-Time Test Suite...\n");
|
||||
localStorage.clear();
|
||||
|
||||
// Seed two dummy wallets to simulate an existing local state
|
||||
const wm = new WalletManager();
|
||||
wm.addWallet("0x04f728C520C438A000f7A5E9d904F0e725FFAEFE", "base", "Base Acc");
|
||||
wm.addWallet("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", "bitcoin", "Satoshi Acc");
|
||||
|
||||
const stream = new WalletStreamManager(wm);
|
||||
stream.connect();
|
||||
|
||||
// Wait a few milliseconds for our mocked async connection handshakes to settle
|
||||
await new Promise(r => originalSetTimeout(r, 10));
|
||||
|
||||
// --- TEST 1: Multiplexed On-Open Subscription ---
|
||||
assert(connectionCount === 1, "WebSocket established exactly one connection");
|
||||
assert(lastSentPayload !== null, "Server received subscription frame on startup");
|
||||
assert(lastSentPayload.action === "subscribe", "Payload correctly set action to 'subscribe'");
|
||||
assert(lastSentPayload.subscriptions.length === 2, "Multiplexed frame packed all stored wallets successfully");
|
||||
|
||||
// --- TEST 2: Real-time Message Callback Routing ---
|
||||
let balanceUpdatedCalled = false;
|
||||
stream.on('balance_update', (msg) => {
|
||||
if (msg.address === "0x04f728C520C438A000f7A5E9d904F0e725FFAEFE") {
|
||||
balanceUpdatedCalled = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Access the underlying mock instance to simulate the server pushing data
|
||||
const activeSocketInstance = stream._ws;
|
||||
activeSocketInstance.simulateServerPush({
|
||||
type: "balance_update",
|
||||
address: "0x04f728C520C438A000f7A5E9d904F0e725FFAEFE",
|
||||
chain: "base",
|
||||
balance: "50000000000"
|
||||
});
|
||||
|
||||
assert(balanceUpdatedCalled === true, "Incoming server events route seamlessly to their respective listeners");
|
||||
|
||||
// --- TEST 3: Callback Error Isolation Safeguard ---
|
||||
let secondaryListenerExecuted = false;
|
||||
stream.on('transfer', () => {
|
||||
throw new Error("CRASHING INTRUDER: I am a broken UI component callback!");
|
||||
});
|
||||
stream.on('transfer', () => {
|
||||
secondaryListenerExecuted = true;
|
||||
});
|
||||
|
||||
// Push a transfer event. The first listener throws an exception, but shouldn't halt execution.
|
||||
try {
|
||||
activeSocketInstance.simulateServerPush({ type: "transfer", tx_hash: "0xabc" });
|
||||
assert(secondaryListenerExecuted === true, "ERROR ISOLATION: Second event handler fired successfully despite preceding listener throwing a hard error");
|
||||
} catch (e) {
|
||||
assert(false, "The StreamManager leaked a callback exception and crashed the event loop");
|
||||
}
|
||||
|
||||
// --- TEST 4: Dynamic Delta Streaming Tracker Updates ---
|
||||
const dynamicWallet = { address: "H7vjR5vP6dfYuxUzoZ76yvBpx6Z3zM7p9qE1vRkWk3xp", chain: "solana" };
|
||||
stream.subscribeToNewWallet(dynamicWallet);
|
||||
|
||||
assert(lastSentPayload.action === "subscribe_wallet", "Dynamic frame used accurate 'subscribe_wallet' verb");
|
||||
assert(lastSentPayload.wallet?.address === dynamicWallet.address, "Dynamic delta subscription dispatched for runtime updates without connection cycling");
|
||||
|
||||
// --- TEST 5: Resilient Exponential Backoff Retry Loop ---
|
||||
console.log("\n🕵️♂️ Triggering a sudden network failure drop scenario...");
|
||||
const preDropCount = connectionCount;
|
||||
|
||||
stream._ws.simulateNetworkDrop(); // Crash the current open pipe
|
||||
|
||||
await new Promise(r => originalSetTimeout(r, 15)); // Wait for fast-forward timers to run the retry loops
|
||||
|
||||
assert(connectionCount > preDropCount, "RESILIENCE: Stream Manager successfully caught connection drop and automatically invoked reconnection logic");
|
||||
|
||||
// Cleanup
|
||||
stream.disconnect();
|
||||
console.log("\n🎉 All core real-time streaming transport client tests passed!");
|
||||
}
|
||||
|
||||
runStreamTests().catch(console.error);
|
||||
Reference in New Issue
Block a user