OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-wallet-desktop commits / 5f72259

add test infrastructure setup (phase 1).
- install brittle and memfs testing dependencies - add test scripts to package.json (test, test:unit, test:integration, test:regression, test:watch) - create test directory structure (unit, integration, regression, mocks, fixtures) - add test/setup.js with bare-node-runtime compatibility - add test/helpers.js with shared test utilities - add test/fixtures/mnemonics.json with valid/invalid test mnemonics - add test/fixtures/addresses.json with Bitcoin and Ethereum test addresses - add test/mocks/wdk-mock.cjs with full WDK mock implementation - add test/mocks/fs-mock.cjs with in-memory filesystem using memfs ready for phase 2: writing unit tests for lib/ modules. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
5f722597dc8301380750b3b285a7cfdf3cb7afa6
by plankatron <fleeky@gmail.com> · 2026-01-29T02:01:29.000Z

Changed files (7) +648 -1

package.json +10 -1
……@@ -7,7 +7,12 @@
77 "#node:*": "bare:*"
88 },
99 "scripts": {
10 "postinstall": "bash scripts/setup-pear-compat.sh"
10 "postinstall": "bash scripts/setup-pear-compat.sh",
11 "test": "brittle test/**/*.test.js",
12 "test:unit": "brittle test/unit/**/*.test.js",
13 "test:integration": "brittle test/integration/**/*.test.js",
14 "test:regression": "brittle test/regression/**/*.test.js",
15 "test:watch": "brittle --watch test/**/*.test.js"
1116 },
1217 "pear": {
1318 "name": "hyper-wallet-desktop",
……@@ -35,6 +40,10 @@
3540 }
3641 }
3742 },
43 "devDependencies": {
44 "brittle": "^3.5.0",
45 "memfs": "^4.6.0"
46 },
3847 "dependencies": {
3948 "@noble/hashes": "^1.8.0",
4049 "@tetherto/wdk": "^1.0.0-beta.5",
test/fixtures/addresses.json +91 -0
……@@ -0,0 +1,91 @@
1{
2 "bitcoin": {
3 "mainnet": {
4 "p2pkh": [
5 "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa",
6 "1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2",
7 "12cbQLTFMXRnSzktFkuoG3eHoMeFtpTu3S"
8 ],
9 "p2sh": [
10 "3J98t1WpEZ73CNmYviecrnyiWrnqRhWNLy",
11 "342ftSRCvFHfCeFFBuz4xwbeqnDw6BGUey",
12 "3QJmV3qfvL9SuYo34YihAf3sRCW3qSinyC"
13 ],
14 "bech32": [
15 "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4",
16 "bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3"
17 ],
18 "bech32m": [
19 "bc1p0xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqzk5jj0"
20 ]
21 },
22 "testnet": {
23 "p2pkh": [
24 "mipcBbFg9gMiCh81Kj8tqqdgoZub1ZJRfn",
25 "mqbJWn9kx5h4FzKPJ3WpN2WzLPBRPb8Y8k",
26 "n3ZddxzLvAY9o7184TB4c6FJasAybsw4HZ"
27 ],
28 "p2sh": [
29 "2MzQwSSnBHWHqSAqtTVQ6v47XtaisrJa1Vc",
30 "2N5muMepJizJE1gR7FbHsiVdC2W8i3UUUgF",
31 "2MsFDzHRUAMpjHxKyoEHU3aMCMsVtXMsfu8"
32 ],
33 "bech32": [
34 "tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx",
35 "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7"
36 ]
37 },
38 "testnet4": {
39 "p2pkh": [
40 "n3ZddxzLvAY9o7184TB4c6FJasAybsw4HZ"
41 ],
42 "bech32": [
43 "tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx"
44 ]
45 },
46 "invalid": [
47 "1InvalidChecksum123",
48 "bc1invalidbech32",
49 "notanaddress",
50 "3InvalidP2SH",
51 "",
52 "1"
53 ]
54 },
55 "ethereum": {
56 "mainnet": {
57 "valid": [
58 "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0",
59 "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed",
60 "0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1",
61 "0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF"
62 ]
63 },
64 "testnet": {
65 "sepolia": [
66 "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0",
67 "0x1234567890123456789012345678901234567890"
68 ]
69 },
70 "invalid": [
71 "0x123",
72 "742d35Cc6634C0532925a3b844Bc9e7595f0bEb0",
73 "0xINVALIDHEXCHARS",
74 "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0TOOLONG",
75 "",
76 "0x"
77 ]
78 },
79 "descriptions": {
80 "bitcoin": {
81 "p2pkh": "Pay-to-Public-Key-Hash (starts with 1 or m/n)",
82 "p2sh": "Pay-to-Script-Hash (starts with 3 or 2)",
83 "bech32": "SegWit v0 (starts with bc1 or tb1)",
84 "bech32m": "SegWit v1+ (starts with bc1p or tb1p)"
85 },
86 "ethereum": {
87 "format": "40 hexadecimal characters with 0x prefix",
88 "eip55": "EIP-55 checksum not validated (Pear limitation)"
89 }
90 }
91}
test/fixtures/mnemonics.json +23 -0
……@@ -0,0 +1,23 @@
1{
2 "identity": {
3 "valid24": "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art",
4 "invalid24": "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon invalid",
5 "short": "abandon abandon abandon",
6 "empty": "",
7 "malformed": "not a valid mnemonic phrase at all just random words here"
8 },
9 "wallet": {
10 "valid12": "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
11 "invalid12": "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon invalid",
12 "valid12_alt": "zone zone zone zone zone zone zone zone zone zone zone zone",
13 "short": "abandon abandon",
14 "empty": "",
15 "malformed": "invalid mnemonic phrase"
16 },
17 "descriptions": {
18 "valid24": "Valid 24-word BIP-39 mnemonic for identity (keet-identity-key)",
19 "invalid24": "Invalid 24-word mnemonic (last word has invalid checksum)",
20 "valid12": "Valid 12-word BIP-39 mnemonic for wallet",
21 "invalid12": "Invalid 12-word mnemonic (last word has invalid checksum)"
22 }
23}
test/helpers.js +139 -0
……@@ -0,0 +1,139 @@
1// test/helpers.js - Shared Test Utilities
2
3const crypto = require('hypercore-crypto')
4const b4a = require('b4a')
5
6/**
7 * Generate random hex token for testing
8 * @param {number} bytes - Number of bytes (default: 32)
9 * @returns {string} - Hex string
10 */
11function generateTestToken(bytes = 32) {
12 return b4a.toString(crypto.randomBytes(bytes), 'hex')
13}
14
15/**
16 * Generate test mnemonic (NOT cryptographically secure - testing only!)
17 * @param {number} words - Number of words (12 or 24)
18 * @returns {string} - Space-separated mnemonic
19 */
20function generateTestMnemonic(words = 12) {
21 // Use deterministic test mnemonics from fixtures
22 const testWords = [
23 'abandon', 'ability', 'able', 'about', 'above', 'absent',
24 'absorb', 'abstract', 'absurd', 'abuse', 'access', 'accident',
25 'account', 'accuse', 'achieve', 'acid', 'acoustic', 'acquire',
26 'across', 'act', 'action', 'actor', 'actress', 'actual'
27 ]
28
29 return testWords.slice(0, words).join(' ')
30}
31
32/**
33 * Wait for async condition to be true
34 * @param {Function} condition - Async function returning boolean
35 * @param {number} timeout - Max wait time in ms (default: 5000)
36 * @param {number} interval - Check interval in ms (default: 100)
37 * @returns {Promise<void>}
38 */
39async function waitFor(condition, timeout = 5000, interval = 100) {
40 const startTime = Date.now()
41
42 while (Date.now() - startTime < timeout) {
43 if (await condition()) {
44 return
45 }
46 await new Promise(resolve => setTimeout(resolve, interval))
47 }
48
49 throw new Error(`Timeout waiting for condition after ${timeout}ms`)
50}
51
52/**
53 * Assert function throws with expected error
54 * @param {Function} fn - Function that should throw
55 * @param {string|RegExp} expectedError - Expected error message or pattern
56 * @returns {Promise<void>}
57 */
58async function assertThrows(fn, expectedError) {
59 let threw = false
60 let error = null
61
62 try {
63 await fn()
64 } catch (err) {
65 threw = true
66 error = err
67 }
68
69 if (!threw) {
70 throw new Error('Expected function to throw but it did not')
71 }
72
73 if (expectedError instanceof RegExp) {
74 if (!expectedError.test(error.message)) {
75 throw new Error(`Expected error matching ${expectedError} but got: ${error.message}`)
76 }
77 } else if (typeof expectedError === 'string') {
78 if (!error.message.includes(expectedError)) {
79 throw new Error(`Expected error containing "${expectedError}" but got: ${error.message}`)
80 }
81 }
82}
83
84/**
85 * Create temporary test directory
86 * @returns {string} - Path to temp directory
87 */
88function createTempDir() {
89 const os = require('bare-os')
90 const path = require('bare-path')
91 const fs = require('bare-fs')
92
93 const tmpDir = path.join(os.tmpdir(), `hyper-wallet-test-${Date.now()}`)
94 fs.mkdirSync(tmpDir, { recursive: true, mode: 0o700 })
95 return tmpDir
96}
97
98/**
99 * Clean up temporary test directory
100 * @param {string} dir - Directory to remove
101 */
102function cleanupTempDir(dir) {
103 const fs = require('bare-fs')
104 const path = require('bare-path')
105
106 if (fs.existsSync(dir)) {
107 const files = fs.readdirSync(dir)
108 for (const file of files) {
109 const filePath = path.join(dir, file)
110 const stat = fs.statSync(filePath)
111
112 if (stat.isDirectory()) {
113 cleanupTempDir(filePath)
114 } else {
115 fs.unlinkSync(filePath)
116 }
117 }
118 fs.rmdirSync(dir)
119 }
120}
121
122/**
123 * Sleep for specified milliseconds
124 * @param {number} ms - Milliseconds to sleep
125 * @returns {Promise<void>}
126 */
127function sleep(ms) {
128 return new Promise(resolve => setTimeout(resolve, ms))
129}
130
131module.exports = {
132 generateTestToken,
133 generateTestMnemonic,
134 waitFor,
135 assertThrows,
136 createTempDir,
137 cleanupTempDir,
138 sleep
139}
test/mocks/fs-mock.cjs +127 -0
……@@ -0,0 +1,127 @@
1// test/mocks/fs-mock.cjs - In-Memory Filesystem Mock for Testing
2
3const { Volume } = require('memfs')
4
5/**
6 * Create mock filesystem with initial structure
7 * @param {Object} initialFiles - Initial file structure (optional)
8 * @returns {Object} - Mock fs object with standard fs methods
9 */
10function createMockFS(initialFiles = {}) {
11 // Create default hyper-wallet directory structure
12 const defaultStructure = {
13 '/home/user/.hyper-wallet/': null,
14 '/tmp/': null,
15 ...initialFiles
16 }
17
18 const vol = Volume.fromJSON(defaultStructure)
19
20 return {
21 // File read/write operations
22 readFileSync: vol.readFileSync.bind(vol),
23 writeFileSync: vol.writeFileSync.bind(vol),
24 appendFileSync: vol.appendFileSync.bind(vol),
25 readFile: vol.readFile.bind(vol),
26 writeFile: vol.writeFile.bind(vol),
27 appendFile: vol.appendFile.bind(vol),
28
29 // File existence and stats
30 existsSync: vol.existsSync.bind(vol),
31 statSync: vol.statSync.bind(vol),
32 lstatSync: vol.lstatSync.bind(vol),
33 stat: vol.stat.bind(vol),
34 lstat: vol.lstat.bind(vol),
35
36 // Directory operations
37 mkdirSync: vol.mkdirSync.bind(vol),
38 rmdirSync: vol.rmdirSync.bind(vol),
39 readdirSync: vol.readdirSync.bind(vol),
40 mkdir: vol.mkdir.bind(vol),
41 rmdir: vol.rmdir.bind(vol),
42 readdir: vol.readdir.bind(vol),
43
44 // File manipulation
45 unlinkSync: vol.unlinkSync.bind(vol),
46 renameSync: vol.renameSync.bind(vol),
47 copyFileSync: vol.copyFileSync.bind(vol),
48 unlink: vol.unlink.bind(vol),
49 rename: vol.rename.bind(vol),
50 copyFile: vol.copyFile.bind(vol),
51
52 // Permissions (mocked - memfs doesn't enforce but tracks)
53 chmodSync: vol.chmodSync.bind(vol),
54 chownSync: vol.chownSync.bind(vol),
55 chmod: vol.chmod.bind(vol),
56 chown: vol.chown.bind(vol),
57
58 // Access the underlying Volume for advanced operations
59 __vol: vol,
60
61 // Helper: Reset filesystem to initial state
62 reset() {
63 vol.reset()
64 const newVol = Volume.fromJSON(defaultStructure)
65 Object.assign(vol, newVol)
66 },
67
68 // Helper: Dump current filesystem state (for debugging)
69 dump() {
70 return vol.toJSON()
71 }
72 }
73}
74
75/**
76 * Create mock fs with wallet files already present
77 * @param {Object} options - Configuration
78 * @param {string} options.identityMnemonic - Identity mnemonic content
79 * @param {string} options.walletMnemonic - Wallet mnemonic content
80 * @param {string} options.token - API token content
81 * @returns {Object} - Mock fs object
82 */
83function createMockFSWithWallet(options = {}) {
84 const {
85 identityMnemonic = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art',
86 walletMnemonic = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about',
87 token = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'
88 } = options
89
90 const initialFiles = {
91 '/home/user/.hyper-wallet/.identity-mnemonic': identityMnemonic,
92 '/home/user/.hyper-wallet/.wallet-mnemonic': walletMnemonic,
93 '/home/user/.hyper-wallet/.token': token,
94 '/home/user/.hyper-wallet/devices.json': JSON.stringify([])
95 }
96
97 return createMockFS(initialFiles)
98}
99
100/**
101 * Mock fs.promises API
102 * @param {Object} mockFS - Mock fs object from createMockFS
103 * @returns {Object} - Promises-based fs API
104 */
105function createMockFSPromises(mockFS) {
106 return {
107 readFile: (...args) => Promise.resolve(mockFS.readFileSync(...args)),
108 writeFile: (...args) => { mockFS.writeFileSync(...args); return Promise.resolve() },
109 appendFile: (...args) => { mockFS.appendFileSync(...args); return Promise.resolve() },
110 unlink: (...args) => { mockFS.unlinkSync(...args); return Promise.resolve() },
111 mkdir: (...args) => { mockFS.mkdirSync(...args); return Promise.resolve() },
112 rmdir: (...args) => { mockFS.rmdirSync(...args); return Promise.resolve() },
113 readdir: (...args) => Promise.resolve(mockFS.readdirSync(...args)),
114 stat: (...args) => Promise.resolve(mockFS.statSync(...args)),
115 lstat: (...args) => Promise.resolve(mockFS.lstatSync(...args)),
116 rename: (...args) => { mockFS.renameSync(...args); return Promise.resolve() },
117 copyFile: (...args) => { mockFS.copyFileSync(...args); return Promise.resolve() },
118 chmod: (...args) => { mockFS.chmodSync(...args); return Promise.resolve() },
119 chown: (...args) => { mockFS.chownSync(...args); return Promise.resolve() }
120 }
121}
122
123module.exports = {
124 createMockFS,
125 createMockFSWithWallet,
126 createMockFSPromises
127}
test/mocks/wdk-mock.cjs +231 -0
……@@ -0,0 +1,231 @@
1// test/mocks/wdk-mock.cjs - WDK Mock Factory for Testing
2
3const crypto = require('hypercore-crypto')
4const b4a = require('b4a')
5
6/**
7 * Mock WDK class that simulates @tetherto/wdk behavior
8 */
9class MockWDK {
10 constructor(mnemonic, options = {}) {
11 this.mnemonic = mnemonic
12 this.wallets = {}
13 this.options = options
14 }
15
16 /**
17 * Register wallet manager (chainable)
18 * @param {string} chain - Chain name (bitcoin, ethereum)
19 * @param {Function} Manager - Wallet manager class
20 * @param {Object} config - Configuration
21 * @returns {MockWDK} - This instance for chaining
22 */
23 registerWallet(chain, Manager, config) {
24 this.wallets[chain] = {
25 Manager,
26 config,
27 accounts: new Map()
28 }
29 return this
30 }
31
32 /**
33 * Get account for chain at index
34 * @param {string} chain - Chain name
35 * @param {number} accountIndex - BIP44 account index
36 * @returns {Promise<MockAccount>}
37 */
38 async getAccount(chain, accountIndex = 0) {
39 const wallet = this.wallets[chain]
40 if (!wallet) {
41 throw new Error(`Wallet not registered for chain: ${chain}`)
42 }
43
44 const key = `${chain}:${accountIndex}`
45 if (!wallet.accounts.has(key)) {
46 wallet.accounts.set(key, new MockAccount(chain, accountIndex, this.mnemonic))
47 }
48
49 return wallet.accounts.get(key)
50 }
51
52 /**
53 * Generate random seed phrase (12 words)
54 * @returns {string} - BIP-39 mnemonic
55 */
56 static getRandomSeedPhrase() {
57 // Generate deterministic test mnemonic from random bytes
58 const entropy = crypto.randomBytes(16) // 128 bits for 12 words
59 const checksum = entropy[0] % 16 // Simple checksum for testing
60
61 const words = [
62 'abandon', 'ability', 'able', 'about', 'above', 'absent',
63 'absorb', 'abstract', 'absurd', 'abuse', 'access', 'about'
64 ]
65
66 return words.join(' ')
67 }
68}
69
70/**
71 * Mock Account class that simulates WDK account operations
72 */
73class MockAccount {
74 constructor(chain, accountIndex, mnemonic) {
75 this.chain = chain
76 this.accountIndex = accountIndex
77 this.mnemonic = mnemonic
78
79 // Mock balances
80 this.balance = BigInt(chain === 'bitcoin' ? 100000000 : 1000000000000000000) // 1 BTC or 1 ETH
81 this.tokenBalances = new Map()
82
83 // Default token balances
84 this.tokenBalances.set('0x7169D38820dfd117C3FA1f22a697dBA58d90BA06', BigInt(500000000)) // 500 USDT (6 decimals)
85 this.tokenBalances.set('0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238', BigInt(250000000)) // 250 USDC (6 decimals)
86 }
87
88 /**
89 * Get account balance
90 * @returns {Promise<BigInt>}
91 */
92 async getBalance() {
93 return this.balance
94 }
95
96 /**
97 * Get address for account
98 * @param {number} addressIndex - BIP44 address index
99 * @returns {Promise<string>}
100 */
101 async getAddress(addressIndex = 0) {
102 if (this.chain === 'bitcoin') {
103 // Mock Bitcoin testnet address
104 return `tb1q${b4a.toString(crypto.randomBytes(32), 'hex').slice(0, 38)}`
105 } else if (this.chain === 'ethereum') {
106 // Mock Ethereum address
107 return `0x${b4a.toString(crypto.randomBytes(20), 'hex')}`
108 }
109 throw new Error(`Unsupported chain: ${this.chain}`)
110 }
111
112 /**
113 * Get token balance (ERC-20)
114 * @param {string} contractAddress - Token contract address
115 * @returns {Promise<BigInt>}
116 */
117 async getTokenBalance(contractAddress) {
118 return this.tokenBalances.get(contractAddress) || BigInt(0)
119 }
120
121 /**
122 * Send transaction
123 * @param {Object} params - Transaction parameters
124 * @returns {Promise<Object>} - Transaction result with hash
125 */
126 async sendTransaction(params) {
127 const { to, value } = params
128
129 // Validate balance
130 if (value > this.balance) {
131 throw new Error('Insufficient balance')
132 }
133
134 // Deduct from balance
135 this.balance -= value
136
137 // Return mock transaction
138 return {
139 hash: `0x${b4a.toString(crypto.randomBytes(32), 'hex')}`,
140 from: await this.getAddress(),
141 to,
142 value: value.toString(),
143 blockNumber: Math.floor(Math.random() * 1000000),
144 timestamp: Date.now()
145 }
146 }
147
148 /**
149 * Quote transaction fee
150 * @param {Object} params - Transaction parameters
151 * @returns {Promise<Object>} - Fee estimate
152 */
153 async quoteSendTransaction(params) {
154 const { value } = params
155
156 // Mock fee calculation (0.1% of amount or minimum)
157 const calculatedFee = value / BigInt(1000)
158 const minFee = this.chain === 'bitcoin' ? BigInt(1000) : BigInt(21000) // 1000 sats or 21000 gwei
159
160 const fee = calculatedFee > minFee ? calculatedFee : minFee
161
162 return {
163 fee,
164 feeRate: this.chain === 'bitcoin' ? '10 sat/vB' : '50 gwei',
165 estimatedConfirmation: '10 minutes'
166 }
167 }
168
169 /**
170 * Transfer ERC-20 token
171 * @param {Object} params - Transfer parameters
172 * @returns {Promise<Object>} - Transfer result
173 */
174 async transfer(params) {
175 const { token, recipient, amount } = params
176
177 // Validate token balance
178 const tokenBalance = await this.getTokenBalance(token)
179 if (amount > tokenBalance) {
180 throw new Error('Insufficient token balance')
181 }
182
183 // Deduct from token balance
184 this.tokenBalances.set(token, tokenBalance - amount)
185
186 return {
187 hash: `0x${b4a.toString(crypto.randomBytes(32), 'hex')}`,
188 from: await this.getAddress(),
189 to: recipient,
190 token,
191 amount: amount.toString(),
192 blockNumber: Math.floor(Math.random() * 1000000),
193 timestamp: Date.now()
194 }
195 }
196
197 /**
198 * Quote token transfer fee
199 * @param {Object} params - Transfer parameters
200 * @returns {Promise<Object>} - Fee estimate
201 */
202 async quoteTransfer(params) {
203 // ERC-20 transfer costs more gas than ETH transfer
204 const fee = BigInt(65000) // ~65k gas units
205
206 return {
207 fee,
208 feeRate: '50 gwei',
209 estimatedConfirmation: '10 minutes'
210 }
211 }
212}
213
214/**
215 * Mock WalletManager classes (Bitcoin, EVM)
216 */
217class MockWalletManagerBtc {
218 static default = MockWalletManagerBtc
219}
220
221class MockWalletManagerEvm {
222 static default = MockWalletManagerEvm
223}
224
225module.exports = {
226 default: MockWDK,
227 MockWDK,
228 MockAccount,
229 WalletManagerBtc: MockWalletManagerBtc,
230 WalletManagerEvm: MockWalletManagerEvm
231}
test/setup.js +27 -0
……@@ -0,0 +1,27 @@
1// test/setup.js - Test Environment Setup
2// CRITICAL: Load bare-node-runtime FIRST for Node.js compatibility in Bare
3require('bare-node-runtime/global')
4
5// Global test configuration
6global.TEST_TIMEOUT = 30000 // 30 seconds default timeout
7global.TEST_RETRY_ATTEMPTS = 3 // Retry flaky tests
8
9// Suppress console noise in tests (unless DEBUG=1)
10if (!process.env.DEBUG) {
11 const originalConsole = { ...console }
12 global.console = {
13 ...originalConsole,
14 log: () => {},
15 info: () => {},
16 debug: () => {},
17 warn: () => {},
18 // Keep error for test failures
19 error: originalConsole.error
20 }
21}
22
23// Export test environment info
24module.exports = {
25 timeout: global.TEST_TIMEOUT,
26 retries: global.TEST_RETRY_ATTEMPTS
27}