// @vitest-environment node // // Node env: @solana/web3.js's VersionedTransaction.deserialize fails its // `instanceof Uint8Array` check under jsdom. @vitest-environment is per-file, // so this is a separate file from injected-connector.test.ts (jsdom). import { describe, it, expect, beforeEach, vi } from 'vitest' import { assertSameMessage } from '@meshconnect/uwc-types' import { InjectedConnector } from './injected-connector' // Mock all service dependencies — same pattern as injected-connector.test.ts vi.mock('./services/storage-service') vi.mock('./services/ethereum/ethereum-wallet-service') vi.mock('./services/tron/tron-wallet-service') vi.mock('./services/connection-manager') vi.mock('./services/signature-service') vi.mock('./services/transaction-service') vi.mock('@meshconnect/uwc-bridge-child', () => ({ BridgeChild: vi.fn(), MAX_POLL_MS: 5000 })) // SolanaWalletService needs fine-grained control — we mock at the module level // and override getConnectedAdapter per test. vi.mock('./services/solana/solana-wallet-service') // Build a minimal valid serialized VersionedTransaction for deserialize tests. // We import the real web3.js so the bytes are structurally sound. import { Keypair, SystemProgram, Transaction, TransactionMessage, VersionedTransaction } from '@solana/web3.js' const payer = Keypair.generate() const recipient = Keypair.generate().publicKey function buildSerializedV0Tx(): Uint8Array { const msg = new TransactionMessage({ payerKey: payer.publicKey, recentBlockhash: '11111111111111111111111111111111', instructions: [ SystemProgram.transfer({ fromPubkey: payer.publicKey, toPubkey: recipient, lamports: 1 }) ] }).compileToV0Message() return new VersionedTransaction(msg).serialize() } // Signed bytes simulated by signing a copy of the tx with the payer key. function buildSignedV0Bytes(): Uint8Array { const msg = new TransactionMessage({ payerKey: payer.publicKey, recentBlockhash: '11111111111111111111111111111111', instructions: [ SystemProgram.transfer({ fromPubkey: payer.publicKey, toPubkey: recipient, lamports: 1 }) ] }).compileToV0Message() const tx = new VersionedTransaction(msg) tx.sign([payer]) return tx.serialize() } // Same message shape as the builders above but a different transfer amount, so // its message bytes differ — models a wallet that tampered with the request. function buildSignedV0BytesWith(lamports: number): Uint8Array { const msg = new TransactionMessage({ payerKey: payer.publicKey, recentBlockhash: '11111111111111111111111111111111', instructions: [ SystemProgram.transfer({ fromPubkey: payer.publicKey, toPubkey: recipient, lamports }) ] }).compileToV0Message() const tx = new VersionedTransaction(msg) tx.sign([payer]) return tx.serialize() } describe('InjectedConnector.signSolanaTransactionBytes', () => { let connector: InjectedConnector // References to the auto-mocked service instances created during construction. let mockConnManager: ReturnType let mockSolService: ReturnType beforeEach(async () => { vi.clearAllMocks() connector = new InjectedConnector() const { ConnectionManager } = await import('./services/connection-manager') const { SolanaWalletService } = await import('./services/solana/solana-wallet-service') mockConnManager = (vi.mocked(ConnectionManager).mock.results[0] as any) .value mockSolService = (vi.mocked(SolanaWalletService).mock.results[0] as any) .value // Default: active Solana connection mockConnManager.getCurrentNetworkId.mockReturnValue( 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp' ) }) // ── Guard errors ────────────────────────────────────────────────────────── it('throws when the active network is not solana', async () => { mockConnManager.getCurrentNetworkId.mockReturnValue('eip155:1') await expect( connector.signSolanaTransactionBytes(buildSerializedV0Tx()) ).rejects.toThrow( 'signSolanaTransactionBytes requires an active Solana connection' ) }) it('throws when getCurrentNetworkId returns null', async () => { mockConnManager.getCurrentNetworkId.mockReturnValue(null) await expect( connector.signSolanaTransactionBytes(buildSerializedV0Tx()) ).rejects.toThrow( 'signSolanaTransactionBytes requires an active Solana connection' ) }) it('throws when no Solana adapter is connected', async () => { mockSolService.getConnectedAdapter.mockReturnValue(null) await expect( connector.signSolanaTransactionBytes(buildSerializedV0Tx()) ).rejects.toThrow('No connected Solana adapter') }) it('throws when adapter lacks signTransaction and wallet getter is absent', async () => { // Adapter with no .wallet, no .signTransaction — unsupported wallet mockSolService.getConnectedAdapter.mockReturnValue({ signAndSendTransaction: vi.fn() // signTransaction intentionally absent }) await expect( connector.signSolanaTransactionBytes(buildSerializedV0Tx()) ).rejects.toThrow('Connected wallet does not support signTransaction') }) // ── Direct Wallet Standard feature path ────────────────────────────────── it('calls the solana:signTransaction feature directly and returns signedTransaction bytes', async () => { const signedBytes = buildSignedV0Bytes() const featureSignTx = vi .fn() .mockResolvedValue([{ signedTransaction: signedBytes }]) const mockAccount = { address: payer.publicKey.toBase58() } const mockWallet = { accounts: [mockAccount], features: { 'solana:signTransaction': { signTransaction: featureSignTx } } } // Adapter shaped as StandardWalletAdapter — exposes .wallet getter const mockAdapter = { wallet: mockWallet, signTransaction: vi.fn(), // present but must NOT be called signAndSendTransaction: vi.fn() // must NEVER be called } mockSolService.getConnectedAdapter.mockReturnValue(mockAdapter) const serialized = buildSerializedV0Tx() const result = await connector.signSolanaTransactionBytes(serialized) expect(result).toBe(signedBytes) expect(featureSignTx).toHaveBeenCalledOnce() expect(featureSignTx).toHaveBeenCalledWith({ account: mockAccount, transaction: serialized }) }) it('SECURITY: rejects a wallet that returns a different transaction (tampered message)', async () => { // Asked to sign lamports:1, the wallet returns a signed lamports:999 tx. const tampered = buildSignedV0BytesWith(999) const featureSignTx = vi .fn() .mockResolvedValue([{ signedTransaction: tampered }]) const mockAccount = { address: payer.publicKey.toBase58() } const mockAdapter = { wallet: { accounts: [mockAccount], features: { 'solana:signTransaction': { signTransaction: featureSignTx } } }, signTransaction: vi.fn() } mockSolService.getConnectedAdapter.mockReturnValue(mockAdapter) await expect( connector.signSolanaTransactionBytes(buildSerializedV0Tx()) ).rejects.toThrow('Signed transaction does not match the request') }) it('passes the raw bytes, not a deserialized object, to the feature', async () => { const signedBytes = buildSignedV0Bytes() const featureSignTx = vi .fn() .mockResolvedValue([{ signedTransaction: signedBytes }]) const serialized = buildSerializedV0Tx() mockSolService.getConnectedAdapter.mockReturnValue({ wallet: { accounts: [{ address: 'any' }], features: { 'solana:signTransaction': { signTransaction: featureSignTx } } }, signTransaction: vi.fn(), signAndSendTransaction: vi.fn() }) await connector.signSolanaTransactionBytes(serialized) // The argument passed to the feature must be the exact same Uint8Array reference const callArg = featureSignTx.mock.calls[0][0] expect(callArg.transaction).toBe(serialized) }) // ── INVARIANT: sign-only never broadcasts ───────────────────────────────── it('INVARIANT: signAndSendTransaction is never called on the direct-feature path', async () => { const signedBytes = buildSignedV0Bytes() const signAndSend = vi.fn() mockSolService.getConnectedAdapter.mockReturnValue({ wallet: { accounts: [{ address: 'any' }], features: { 'solana:signTransaction': { signTransaction: vi .fn() .mockResolvedValue([{ signedTransaction: signedBytes }]) } } }, signTransaction: vi.fn(), signAndSendTransaction: signAndSend }) await connector.signSolanaTransactionBytes(buildSerializedV0Tx()) expect(signAndSend).not.toHaveBeenCalled() }) it('surfaces a wallet rejection on the feature path as a typed "rejected" error', async () => { // The Link UI needs to tell "user cancelled" apart from a real failure, so a // rejection must arrive as WalletConnectorError{ type: 'rejected' } — not a // generic Error. mockSolService.getConnectedAdapter.mockReturnValue({ wallet: { accounts: [{ address: 'any' }], features: { 'solana:signTransaction': { signTransaction: vi .fn() .mockRejectedValue(new Error('user rejected')) } } }, signTransaction: vi.fn(), signAndSendTransaction: vi.fn() }) await expect( connector.signSolanaTransactionBytes(buildSerializedV0Tx()) ).rejects.toMatchObject({ type: 'rejected', message: 'user rejected' }) }) it('falls back to direct feature path even when wallet.accounts is empty (zero-account edge)', async () => { // accounts.length === 0 → falls through to the adapter fallback const signedBytes = buildSignedV0Bytes() const adapterSignTx = vi .fn() .mockResolvedValue(VersionedTransaction.deserialize(signedBytes)) mockSolService.getConnectedAdapter.mockReturnValue({ wallet: { accounts: [], // empty → skip feature path features: { 'solana:signTransaction': { signTransaction: vi.fn() // must NOT be called } } }, signTransaction: adapterSignTx, signAndSendTransaction: vi.fn() }) const result = await connector.signSolanaTransactionBytes( buildSerializedV0Tx() ) expect(adapterSignTx).toHaveBeenCalledOnce() expect(result).toBeInstanceOf(Uint8Array) expect(result.length).toBeGreaterThan(0) }) // ── Adapter fallback path ───────────────────────────────────────────────── it('falls back to adapter.signTransaction when wallet getter is absent', async () => { const signedBytes = buildSignedV0Bytes() const adapterSignTx = vi .fn() .mockResolvedValue(VersionedTransaction.deserialize(signedBytes)) // No .wallet property at all — undefined mockSolService.getConnectedAdapter.mockReturnValue({ wallet: undefined, signTransaction: adapterSignTx, signAndSendTransaction: vi.fn() }) const result = await connector.signSolanaTransactionBytes( buildSerializedV0Tx() ) expect(adapterSignTx).toHaveBeenCalledOnce() // Deserialized → re-serialized; returned bytes must be a non-empty Uint8Array expect(result).toBeInstanceOf(Uint8Array) expect(result.length).toBeGreaterThan(0) }) it('falls back when wallet lacks the solana:signTransaction feature key', async () => { const signedBytes = buildSignedV0Bytes() const adapterSignTx = vi .fn() .mockResolvedValue(VersionedTransaction.deserialize(signedBytes)) mockSolService.getConnectedAdapter.mockReturnValue({ wallet: { accounts: [{ address: 'any' }], features: {} // key absent }, signTransaction: adapterSignTx, signAndSendTransaction: vi.fn() }) await connector.signSolanaTransactionBytes(buildSerializedV0Tx()) expect(adapterSignTx).toHaveBeenCalledOnce() }) it('INVARIANT: signAndSendTransaction is never called on the adapter fallback path', async () => { const signedBytes = buildSignedV0Bytes() const signAndSend = vi.fn() mockSolService.getConnectedAdapter.mockReturnValue({ wallet: undefined, signTransaction: vi .fn() .mockResolvedValue(VersionedTransaction.deserialize(signedBytes)), signAndSendTransaction: signAndSend }) await connector.signSolanaTransactionBytes(buildSerializedV0Tx()) expect(signAndSend).not.toHaveBeenCalled() }) it('surfaces an adapter-fallback failure as a typed "unknown" error, preserving the message', async () => { // A non-rejection failure (e.g. locked wallet) stays type 'unknown' but keeps // the original message so it is still actionable in logs. mockSolService.getConnectedAdapter.mockReturnValue({ wallet: undefined, signTransaction: vi.fn().mockRejectedValue(new Error('wallet locked')), signAndSendTransaction: vi.fn() }) await expect( connector.signSolanaTransactionBytes(buildSerializedV0Tx()) ).rejects.toMatchObject({ type: 'unknown', message: 'wallet locked' }) }) it('deserializes the input bytes before passing to adapter.signTransaction', async () => { const signedBytes = buildSignedV0Bytes() const adapterSignTx = vi .fn() .mockResolvedValue(VersionedTransaction.deserialize(signedBytes)) mockSolService.getConnectedAdapter.mockReturnValue({ wallet: undefined, signTransaction: adapterSignTx, signAndSendTransaction: vi.fn() }) await connector.signSolanaTransactionBytes(buildSerializedV0Tx()) const arg = adapterSignTx.mock.calls[0][0] // adapter receives a VersionedTransaction, not raw bytes expect(arg).toBeInstanceOf(VersionedTransaction) }) it('SECURITY (Path 3): rejects a tampered tx from a legacy adapter', async () => { // Adapter returns a signed tx with a different message (lamports 999). const adapterSignTx = vi .fn() .mockResolvedValue( VersionedTransaction.deserialize(buildSignedV0BytesWith(999)) ) mockSolService.getConnectedAdapter.mockReturnValue({ wallet: undefined, signTransaction: adapterSignTx, signAndSendTransaction: vi.fn() }) await expect( connector.signSolanaTransactionBytes(buildSerializedV0Tx()) ).rejects.toThrow('Signed transaction does not match the request') }) // ── Bridge path (Comlink-proxied adapter) ──────────────────────────────── it('BRIDGE PATH: calls adapter.signSolanaTransactionBytes and returns its bytes without touching wallet.features', async () => { const bridgeBytes = buildSignedV0Bytes() const bridgeFn = vi.fn().mockResolvedValue(bridgeBytes) // wallet.features is present but must NOT be touched — assert via spy const featureSignTx = vi.fn() const mockAdapter = { // Bridge advertises the customFunction; detection awaits this list. customFunctions: ['signSolanaTransactionBytes'], // Extended bridge property — present on the Comlink-proxied adapter signSolanaTransactionBytes: bridgeFn, // Direct path artifacts — must remain untouched wallet: { accounts: [{ address: 'AAA' }], features: { 'solana:signTransaction': { signTransaction: featureSignTx } } }, signTransaction: vi.fn(), signAndSendTransaction: vi.fn() } mockSolService.getConnectedAdapter.mockReturnValue(mockAdapter) mockConnManager.getCurrentAccount.mockReturnValue('AAA') const serialized = buildSerializedV0Tx() const result = await connector.signSolanaTransactionBytes(serialized) expect(result).toBe(bridgeBytes) expect(bridgeFn).toHaveBeenCalledOnce() // The connected account is threaded to the parent so it can't fall back to // a drifted publicKey. expect(bridgeFn).toHaveBeenCalledWith(serialized, 'AAA') // The Wallet Standard feature path must NOT have been used expect(featureSignTx).not.toHaveBeenCalled() }) it('BRIDGE PATH: threads the connected account even when it differs from accounts[0]', async () => { const bridgeFn = vi.fn().mockResolvedValue(buildSignedV0Bytes()) mockSolService.getConnectedAdapter.mockReturnValue({ customFunctions: ['signSolanaTransactionBytes'], signSolanaTransactionBytes: bridgeFn, wallet: undefined, signTransaction: vi.fn(), signAndSendTransaction: vi.fn() }) mockConnManager.getCurrentAccount.mockReturnValue('CONNECTED_ACCT') const serialized = buildSerializedV0Tx() await connector.signSolanaTransactionBytes(serialized) expect(bridgeFn).toHaveBeenCalledWith(serialized, 'CONNECTED_ACCT') }) it('BRIDGE PATH INVARIANT: signAndSendTransaction is never called when the bridge wrapper handles signing', async () => { const signAndSend = vi.fn() const mockAdapter = { customFunctions: ['signSolanaTransactionBytes'], signSolanaTransactionBytes: vi .fn() .mockResolvedValue(buildSignedV0Bytes()), wallet: undefined, signTransaction: vi.fn(), signAndSendTransaction: signAndSend } mockSolService.getConnectedAdapter.mockReturnValue(mockAdapter) await connector.signSolanaTransactionBytes(buildSerializedV0Tx()) expect(signAndSend).not.toHaveBeenCalled() }) it('BRIDGE PATH: returns the exact Uint8Array the bridge wrapper resolves with', async () => { // Ensures no re-serialization happens on the bridge path — bytes pass through as-is const expectedBytes = buildSignedV0Bytes() const mockAdapter = { customFunctions: ['signSolanaTransactionBytes'], signSolanaTransactionBytes: vi.fn().mockResolvedValue(expectedBytes), wallet: undefined, signTransaction: vi.fn(), signAndSendTransaction: vi.fn() } mockSolService.getConnectedAdapter.mockReturnValue(mockAdapter) const result = await connector.signSolanaTransactionBytes( buildSerializedV0Tx() ) expect(result).toBe(expectedBytes) }) it('BRIDGE PATH SECURITY: rejects tampered bytes returned by the parent', async () => { // The wallet signs on the parent (client page); the child (iframe) verifies // here. A parent that returns a different-message tx must be rejected. const tampered = buildSignedV0BytesWith(999) const mockAdapter = { customFunctions: ['signSolanaTransactionBytes'], signSolanaTransactionBytes: vi.fn().mockResolvedValue(tampered), wallet: undefined, signTransaction: vi.fn(), signAndSendTransaction: vi.fn() } mockSolService.getConnectedAdapter.mockReturnValue(mockAdapter) await expect( connector.signSolanaTransactionBytes(buildSerializedV0Tx()) ).rejects.toThrow('Signed transaction does not match the request') }) // Closes the version-coupling break: an older bridge-parent without // `signSolanaTransactionBytes` in customFunctions must NOT trigger the bridge // path (Comlink would otherwise present every property as truthy and we'd // call a method that doesn't exist on the parent). it('BRIDGE COMPAT: falls back to direct path when bridge customFunctions lacks signSolanaTransactionBytes (old bridge-parent)', async () => { const signedBytes = buildSignedV0Bytes() const bridgeFn = vi.fn() const featureSignTx = vi .fn() .mockResolvedValue([{ signedTransaction: signedBytes }]) const mockAdapter = { // Old bridge: only the previous customFunctions, NOT signSolanaTransactionBytes customFunctions: [ 'sendSerializedTransaction', 'signSerializedTransaction' ], // Sync truthy check would falsely match this on a real Comlink proxy signSolanaTransactionBytes: bridgeFn, wallet: { accounts: [{ address: 'AAA' }], features: { 'solana:signTransaction': { signTransaction: featureSignTx } } }, signTransaction: vi.fn(), signAndSendTransaction: vi.fn() } mockSolService.getConnectedAdapter.mockReturnValue(mockAdapter) const result = await connector.signSolanaTransactionBytes( buildSerializedV0Tx() ) expect(result).toBe(signedBytes) expect(bridgeFn).not.toHaveBeenCalled() expect(featureSignTx).toHaveBeenCalledOnce() }) // Gap #18: bridge-path error propagation. Comlink flattens the parent's error // to a plain Error, so we re-classify by message — a parent-side rejection must // still reach the caller as type 'rejected'. it('BRIDGE PATH: a wallet rejection on the parent surfaces as a typed "rejected" error', async () => { const mockAdapter = { customFunctions: ['signSolanaTransactionBytes'], signSolanaTransactionBytes: vi .fn() .mockRejectedValue(new Error('user rejected on the parent')), wallet: undefined, signTransaction: vi.fn(), signAndSendTransaction: vi.fn() } mockSolService.getConnectedAdapter.mockReturnValue(mockAdapter) await expect( connector.signSolanaTransactionBytes(buildSerializedV0Tx()) ).rejects.toMatchObject({ type: 'rejected' }) }) // ── Multi-account direct path ────────────────────────────────────────────── it('MULTI-ACCOUNT: picks the account matching getCurrentAccount(), not accounts[0]', async () => { const signedBytes = buildSignedV0Bytes() const featureSignTx = vi .fn() .mockResolvedValue([{ signedTransaction: signedBytes }]) const accountAAA = { address: 'AAA' } const accountBBB = { address: 'BBB' } const mockAdapter = { // No bridge wrapper — must use direct feature path wallet: { accounts: [accountAAA, accountBBB], features: { 'solana:signTransaction': { signTransaction: featureSignTx } } }, signTransaction: vi.fn(), signAndSendTransaction: vi.fn() } mockSolService.getConnectedAdapter.mockReturnValue(mockAdapter) // Simulate user connected as second account mockConnManager.getCurrentAccount.mockReturnValue('BBB') await connector.signSolanaTransactionBytes(buildSerializedV0Tx()) const callArg = featureSignTx.mock.calls[0][0] expect(callArg.account).toBe(accountBBB) expect(callArg.account).not.toBe(accountAAA) }) it('MULTI-ACCOUNT: throws instead of guessing when no account is selected and several exist', async () => { // Signing with the wrong key yields a useless signature for a payment, so we // refuse rather than fall back to accounts[0]. const featureSignTx = vi.fn() const mockAdapter = { wallet: { accounts: [{ address: 'AAA' }, { address: 'BBB' }], features: { 'solana:signTransaction': { signTransaction: featureSignTx } } }, signTransaction: vi.fn(), signAndSendTransaction: vi.fn() } mockSolService.getConnectedAdapter.mockReturnValue(mockAdapter) mockConnManager.getCurrentAccount.mockReturnValue(null) await expect( connector.signSolanaTransactionBytes(buildSerializedV0Tx()) ).rejects.toThrow('Cannot determine which Solana account to sign with') expect(featureSignTx).not.toHaveBeenCalled() }) it('MULTI-ACCOUNT: throws when the selected account is not in the wallet (never signs with a different account)', async () => { const featureSignTx = vi.fn() const mockAdapter = { wallet: { accounts: [{ address: 'AAA' }, { address: 'BBB' }], features: { 'solana:signTransaction': { signTransaction: featureSignTx } } }, signTransaction: vi.fn(), signAndSendTransaction: vi.fn() } mockSolService.getConnectedAdapter.mockReturnValue(mockAdapter) // Address that doesn't match any wallet account mockConnManager.getCurrentAccount.mockReturnValue('ZZZZ_NOT_IN_WALLET') await expect( connector.signSolanaTransactionBytes(buildSerializedV0Tx()) ).rejects.toThrow('Connected Solana account not found in the wallet') expect(featureSignTx).not.toHaveBeenCalled() }) it('SINGLE-ACCOUNT: signs with the only account when none is explicitly selected', async () => { const signedBytes = buildSignedV0Bytes() const featureSignTx = vi .fn() .mockResolvedValue([{ signedTransaction: signedBytes }]) const onlyAccount = { address: 'AAA' } mockSolService.getConnectedAdapter.mockReturnValue({ wallet: { accounts: [onlyAccount], features: { 'solana:signTransaction': { signTransaction: featureSignTx } } }, signTransaction: vi.fn(), signAndSendTransaction: vi.fn() }) mockConnManager.getCurrentAccount.mockReturnValue(null) await connector.signSolanaTransactionBytes(buildSerializedV0Tx()) expect(featureSignTx.mock.calls[0][0].account).toBe(onlyAccount) }) }) describe('reserialize is byte-stable (Path 1 + Path 3 check safety)', () => { // Paths 1 (bridge legacy) and 3 (direct legacy) deserialize → sign → // re-serialize, then assertSameMessage runs on the result. These prove the // round-trip preserves the message, so a legit sign isn't wrongly rejected. it('preserves the message through deserialize → user-sign → serialize (v0)', () => { const feePayer = Keypair.generate() const user = Keypair.generate() const msg = new TransactionMessage({ payerKey: feePayer.publicKey, recentBlockhash: '11111111111111111111111111111111', instructions: [ SystemProgram.transfer({ fromPubkey: user.publicKey, // makes `user` a required signer (slot 1) toPubkey: recipient, lamports: 1 }) ] }).compileToV0Message() // Relay (Alchemy) signs slot 0; the user slot is still empty — this is what // the FE receives from /build. const built = new VersionedTransaction(msg) built.sign([feePayer]) const input = built.serialize() // Bridge legacy fallback: deserialize → user signs slot 1 → re-serialize. const round = VersionedTransaction.deserialize(input) round.sign([user]) const reserialized = round.serialize() // The canonical check must accept the re-serialized output. expect(() => assertSameMessage(input, reserialized)).not.toThrow() }) it('preserves the message through deserialize → user-sign → serialize (legacy)', () => { const feePayer = Keypair.generate() const user = Keypair.generate() const tx = new Transaction() tx.recentBlockhash = '11111111111111111111111111111111' tx.feePayer = feePayer.publicKey tx.add( SystemProgram.transfer({ fromPubkey: user.publicKey, toPubkey: recipient, lamports: 1 }) ) tx.partialSign(feePayer) // slot 0 signed; user slot empty const input = new Uint8Array(tx.serialize({ requireAllSignatures: false })) const round = Transaction.from(input) round.partialSign(user) const reserialized = new Uint8Array( round.serialize({ requireAllSignatures: false }) ) expect(() => assertSameMessage(input, reserialized)).not.toThrow() }) })