Files
dione/test/test_stream.js
Dione a61e0b0457 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
2026-06-06 12:16:41 +00:00

152 lines
5.5 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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);