- 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
152 lines
5.5 KiB
JavaScript
152 lines
5.5 KiB
JavaScript
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);
|