From d71367a8f375c417df9ce23bfcd9a5e80e7ae30e Mon Sep 17 00:00:00 2001 From: Anson Date: Sat, 23 Aug 2025 00:55:13 +0100 Subject: [PATCH 1/3] feat(e2e): add script to test published packages with e2e tests --- e2e/package.json | 7 +- package.json | 1 + test-e2e-published.mjs | 391 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 396 insertions(+), 3 deletions(-) create mode 100644 test-e2e-published.mjs diff --git a/e2e/package.json b/e2e/package.json index 60d429ad3..27157b565 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -21,6 +21,7 @@ "build:esm": "mkdir -p dist-esm && bun build src/index.ts --outdir dist-esm --format esm --target node --external '@lit-protocol/*' && mv dist-esm/index.js dist/index.mjs && rm -rf dist-esm", "prepublishOnly": "bun run build", "test": "bun test", + "test:e2e": "dotenvx run --env-file=../.env -- bun test ./src/e2e.spec.ts --timeout 50000000 -t", "test:watch": "bun test --watch" }, "keywords": [ @@ -43,9 +44,9 @@ "typescript": "^5.0.0" }, "peerDependencies": { - "@lit-protocol/auth": "*", - "@lit-protocol/lit-client": "*", - "@lit-protocol/networks": "*" + "@lit-protocol/auth": "8.0.0-prealpha-886.4", + "@lit-protocol/lit-client": "8.0.0-prealpha-886.4", + "@lit-protocol/networks": "8.0.0-prealpha-886.4" }, "engines": { "node": ">=18.0.0" diff --git a/package.json b/package.json index ae85b044b..22e316879 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "unlink-all": "for dir in packages/*/; do echo \"Unlinking in $dir\"; (cd \"$dir\" && bun unlink) || { echo \"ERROR: Failed to unlink in $dir\"; exit 1; }; done", "auth-services": "cd packages/auth-services && bun run start", "test:e2e": "bun test ./e2e/src/e2e.spec.ts -t", + "test:e2e:published": "node test-e2e-published.mjs", "artillery:init": "bun run ./e2e/artillery/src/init.ts", "artillery:balance-status": "LOG_LEVEL=silent bun run ./e2e/artillery/src/balance-status.ts", "artillery:pkp-sign": "DEBUG_HTTP=true LOG_LEVEL=silent dotenvx run --env-file=.env -- sh -c 'artillery run ./e2e/artillery/configs/pkp-sign.yml ${ARTILLERY_KEY:+--record --key $ARTILLERY_KEY}'", diff --git a/test-e2e-published.mjs b/test-e2e-published.mjs new file mode 100644 index 000000000..542cb40d2 --- /dev/null +++ b/test-e2e-published.mjs @@ -0,0 +1,391 @@ +#!/usr/bin/env node + +/** + * E2E Published Package Test Script + * + * This script replaces peer dependency versions in ./e2e/package.json with a specified version + * and then runs the e2e tests in the naga-local network environment. + * + * Usage: bun run test:e2e:published + * Example: bun run test:e2e:published 8.0.0-prealpha-886.4 + */ + +import { readFile, writeFile, rm } from 'fs/promises'; +import { spawn } from 'child_process'; + +import path from 'path'; +import { existsSync } from 'fs'; + +// Configuration +const E2E_PACKAGE_JSON_PATH = './e2e/package.json'; +const E2E_DIRECTORY = './e2e'; + +// Global state to track if cleanup is needed +let needsCleanup = false; +let isCleaningUp = false; +let currentTestProcess = null; + +/** + * Main execution function + */ +async function main() { + const version = process.argv[2]; + + if (!version) { + console.error('โŒ Error: Version number is required'); + console.log('Usage: bun run test:e2e:published '); + console.log('Example: bun run test:e2e:published 8.0.0-prealpha-886.4'); + process.exit(1); + } + + console.log( + `๐Ÿš€ Starting E2E published package test with version: ${version}` + ); + + let testsPassed = false; + + try { + // Step 1: Update peer dependencies in e2e/package.json + await updatePeerDependencies(version); + needsCleanup = true; // Mark that cleanup is needed after this point + + // Step 2: Install dependencies to ensure we're using published packages + await installDependencies(); + + // Step 3: Verify installed package versions + await verifyInstalledVersions(version); + + // Step 4: Run e2e tests in naga-local network + await runE2ETests(); + + testsPassed = true; + console.log('โœ… E2E published package test completed successfully!'); + } catch (error) { + console.error('โŒ E2E published package test failed:', error.message); + testsPassed = false; + } finally { + // Step 3: Always cleanup regardless of success or failure + if (needsCleanup && !isCleaningUp) { + try { + await cleanup(); + } catch (cleanupError) { + console.error('โš ๏ธ Cleanup failed:', cleanupError.message); + } + } + } + + if (!testsPassed) { + process.exit(1); + } +} + +/** + * Updates peer dependencies in e2e/package.json + * @param {string} version - The version to replace "*" with + */ +async function updatePeerDependencies(version) { + console.log('๐Ÿ“ Updating peer dependencies in e2e/package.json...'); + + try { + // Read the current package.json + const packageJsonContent = await readFile(E2E_PACKAGE_JSON_PATH, 'utf8'); + const packageJson = JSON.parse(packageJsonContent); + + // Replace all "*" versions in peerDependencies with the specified version + if (packageJson.peerDependencies) { + for (const [depName, depVersion] of Object.entries( + packageJson.peerDependencies + )) { + if (depVersion === '*') { + packageJson.peerDependencies[depName] = version; + console.log(` โœ“ Updated ${depName}: "*" โ†’ "${version}"`); + } + } + } + + // Write the updated package.json + await writeFile( + E2E_PACKAGE_JSON_PATH, + JSON.stringify(packageJson, null, 2) + '\n' + ); + console.log('โœ… Peer dependencies updated successfully'); + } catch (error) { + throw new Error(`Failed to update peer dependencies: ${error.message}`); + } +} + +/** + * Installs dependencies to ensure we're using published packages + */ +async function installDependencies() { + console.log('๐Ÿ“ฆ Installing dependencies from npm...'); + + return new Promise((resolve, reject) => { + const command = 'bun'; + const args = ['install']; + + console.log(` Running: ${command} ${args.join(' ')}`); + console.log('๐Ÿ“‹ Install Output:'); + console.log('โ”€'.repeat(60)); + + const installProcess = spawn(command, args, { + cwd: E2E_DIRECTORY, + stdio: 'inherit', + }); + + currentTestProcess = installProcess; // Track for signal handling + + installProcess.on('close', (code) => { + currentTestProcess = null; + console.log('โ”€'.repeat(60)); + if (code === 0) { + console.log('โœ… Dependencies installed successfully'); + resolve(); + } else { + reject( + new Error(`Dependency installation failed with exit code ${code}`) + ); + } + }); + + installProcess.on('error', (error) => { + currentTestProcess = null; + reject( + new Error(`Failed to start dependency installation: ${error.message}`) + ); + }); + }); +} + +/** + * Verifies that the correct package versions are installed + */ +async function verifyInstalledVersions(expectedVersion) { + console.log('๐Ÿ” Verifying installed package versions...'); + + try { + const nodeModulesPath = path.join(E2E_DIRECTORY, 'node_modules'); + const packagesToCheck = [ + '@lit-protocol/auth', + '@lit-protocol/lit-client', + '@lit-protocol/networks', + ]; + + for (const packageName of packagesToCheck) { + const packageJsonPath = path.join( + nodeModulesPath, + packageName, + 'package.json' + ); + + if (existsSync(packageJsonPath)) { + const packageContent = await readFile(packageJsonPath, 'utf8'); + const packageJson = JSON.parse(packageContent); + const installedVersion = packageJson.version; + + if (installedVersion === expectedVersion) { + console.log(` โœ… ${packageName}: ${installedVersion} โœ“`); + } else { + console.log( + ` โŒ ${packageName}: expected ${expectedVersion}, got ${installedVersion}` + ); + throw new Error( + `Version mismatch for ${packageName}: expected ${expectedVersion}, got ${installedVersion}` + ); + } + } else { + throw new Error(`Package ${packageName} not found in node_modules`); + } + } + + console.log('โœ… All package versions verified successfully'); + } catch (error) { + throw new Error(`Failed to verify package versions: ${error.message}`); + } +} + +/** + * Runs the e2e tests in the naga-local network environment + */ +async function runE2ETests() { + console.log('๐Ÿงช Running E2E tests in naga-local network...'); + + return new Promise((resolve, reject) => { + const command = 'bun'; + const args = ['run', 'test:e2e', 'all', '--timeout', '50000000']; + + console.log(` Running: NETWORK=naga-local ${command} ${args.join(' ')}`); + console.log('๐Ÿ“‹ Test Output (real-time):'); + console.log('โ”€'.repeat(60)); + + const testProcess = spawn(command, args, { + cwd: E2E_DIRECTORY, + env: { ...process.env, NETWORK: 'naga-local' }, + stdio: 'inherit', // This enables real-time output + }); + + currentTestProcess = testProcess; // Store reference for signal handling + + testProcess.on('close', (code) => { + currentTestProcess = null; // Clear reference + console.log('โ”€'.repeat(60)); + if (code === 0) { + console.log('โœ… E2E tests completed successfully'); + resolve(); + } else { + reject(new Error(`E2E tests failed with exit code ${code}`)); + } + }); + + testProcess.on('error', (error) => { + currentTestProcess = null; // Clear reference + reject(new Error(`Failed to start E2E tests: ${error.message}`)); + }); + }); +} + +/** + * Reverts peer dependencies back to "*" in e2e/package.json + */ +async function revertPeerDependencies() { + console.log('๐Ÿ”„ Reverting peer dependencies back to "*"...'); + + try { + // Read the current package.json + const packageJsonContent = await readFile(E2E_PACKAGE_JSON_PATH, 'utf8'); + const packageJson = JSON.parse(packageJsonContent); + + // Revert all @lit-protocol peer dependencies back to "*" + if (packageJson.peerDependencies) { + for (const [depName, depVersion] of Object.entries( + packageJson.peerDependencies + )) { + if (depName.startsWith('@lit-protocol/') && depVersion !== '*') { + packageJson.peerDependencies[depName] = '*'; + console.log(` โœ“ Reverted ${depName}: "${depVersion}" โ†’ "*"`); + } + } + } + + // Write the reverted package.json + await writeFile( + E2E_PACKAGE_JSON_PATH, + JSON.stringify(packageJson, null, 2) + '\n' + ); + console.log('โœ… Peer dependencies reverted successfully'); + } catch (error) { + throw new Error(`Failed to revert peer dependencies: ${error.message}`); + } +} + +/** + * Cleans up files and directories created during the test + */ +async function cleanup() { + if (isCleaningUp) return; // Prevent multiple cleanup attempts + isCleaningUp = true; + + console.log('๐Ÿงน Starting cleanup...'); + + // Step 1: Remove node_modules in e2e directory + const nodeModulesPath = path.join(E2E_DIRECTORY, 'node_modules'); + if (existsSync(nodeModulesPath)) { + console.log(' ๐Ÿ—‘๏ธ Removing ./e2e/node_modules...'); + await rm(nodeModulesPath, { recursive: true, force: true }); + console.log(' โœ“ node_modules removed'); + } else { + console.log(' โ„น๏ธ node_modules not found, skipping'); + } + + // Step 2: Remove bun.lock in e2e directory + const bunLockPath = path.join(E2E_DIRECTORY, 'bun.lock'); + if (existsSync(bunLockPath)) { + console.log(' ๐Ÿ—‘๏ธ Removing ./e2e/bun.lock...'); + await rm(bunLockPath, { force: true }); + console.log(' โœ“ bun.lock removed'); + } else { + console.log(' โ„น๏ธ bun.lock not found, skipping'); + } + + // Step 3: Revert peer dependencies back to "*" + await revertPeerDependencies(); + + console.log('โœ… Cleanup completed successfully'); +} + +/** + * Handles graceful shutdown when the process is interrupted + */ +async function handleGracefulShutdown(signal) { + // Prevent multiple signal handlers from running + if (isCleaningUp) { + console.log(`\nโณ Already cleaning up, ignoring ${signal}...`); + return; + } + + console.log(`\nโš ๏ธ Received ${signal}. Performing cleanup before exit...`); + + // Kill the test process if it's running + if (currentTestProcess && !currentTestProcess.killed) { + console.log('๐Ÿ›‘ Terminating running test process...'); + currentTestProcess.kill('SIGTERM'); + // Give it a moment to terminate gracefully + await new Promise((resolve) => setTimeout(resolve, 1000)); + if (!currentTestProcess.killed) { + currentTestProcess.kill('SIGKILL'); + } + } + + if (needsCleanup && !isCleaningUp) { + try { + await cleanup(); + } catch (cleanupError) { + console.error('โŒ Cleanup failed during shutdown:', cleanupError.message); + } + } + + console.log('๐Ÿ‘‹ Cleanup completed. Exiting...'); + process.exit(0); +} + +// Register signal handlers for graceful shutdown +process.on('SIGINT', handleGracefulShutdown.bind(null, 'SIGINT')); // Ctrl+C +process.on('SIGTERM', handleGracefulShutdown.bind(null, 'SIGTERM')); // Termination signal +process.on('SIGHUP', handleGracefulShutdown.bind(null, 'SIGHUP')); // Hang up signal + +// Handle uncaught exceptions and unhandled rejections +process.on('uncaughtException', async (error) => { + console.error('๐Ÿ’ฅ Uncaught exception:', error); + if (needsCleanup && !isCleaningUp) { + try { + await cleanup(); + } catch (cleanupError) { + console.error( + 'โŒ Cleanup failed during exception handling:', + cleanupError.message + ); + } + } + process.exit(1); +}); + +process.on('unhandledRejection', async (reason, promise) => { + console.error('๐Ÿ’ฅ Unhandled rejection at:', promise, 'reason:', reason); + if (needsCleanup && !isCleaningUp) { + try { + await cleanup(); + } catch (cleanupError) { + console.error( + 'โŒ Cleanup failed during rejection handling:', + cleanupError.message + ); + } + } + process.exit(1); +}); + +// Run the script +main().catch((error) => { + console.error('๐Ÿ’ฅ Unexpected error:', error); + process.exit(1); +}); From 7872cf433a386e0bc1a1e8a044ef316a2258a35d Mon Sep 17 00:00:00 2001 From: Anson Date: Sat, 23 Aug 2025 01:00:06 +0100 Subject: [PATCH 2/3] chore(e2e): update peerDependencies to use wildcard versions for @lit-protocol packages --- e2e/package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/e2e/package.json b/e2e/package.json index 27157b565..50503715f 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -44,9 +44,9 @@ "typescript": "^5.0.0" }, "peerDependencies": { - "@lit-protocol/auth": "8.0.0-prealpha-886.4", - "@lit-protocol/lit-client": "8.0.0-prealpha-886.4", - "@lit-protocol/networks": "8.0.0-prealpha-886.4" + "@lit-protocol/auth": "*", + "@lit-protocol/lit-client": "*", + "@lit-protocol/networks": "*" }, "engines": { "node": ">=18.0.0" From 1fb70618ac35d67c0a0d36463003570562628e48 Mon Sep 17 00:00:00 2001 From: Anson Date: Sat, 23 Aug 2025 01:08:09 +0100 Subject: [PATCH 3/3] feat(e2e): implement graceful shutdown handling in test process --- test-e2e-published.mjs | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/test-e2e-published.mjs b/test-e2e-published.mjs index 542cb40d2..c530a1600 100644 --- a/test-e2e-published.mjs +++ b/test-e2e-published.mjs @@ -24,6 +24,7 @@ const E2E_DIRECTORY = './e2e'; let needsCleanup = false; let isCleaningUp = false; let currentTestProcess = null; +let isShuttingDown = false; /** * Main execution function @@ -318,26 +319,37 @@ async function cleanup() { */ async function handleGracefulShutdown(signal) { // Prevent multiple signal handlers from running - if (isCleaningUp) { - console.log(`\nโณ Already cleaning up, ignoring ${signal}...`); + if (isShuttingDown) { + console.log(`\nโณ Already shutting down, ignoring ${signal}...`); return; } + isShuttingDown = true; console.log(`\nโš ๏ธ Received ${signal}. Performing cleanup before exit...`); // Kill the test process if it's running if (currentTestProcess && !currentTestProcess.killed) { console.log('๐Ÿ›‘ Terminating running test process...'); - currentTestProcess.kill('SIGTERM'); - // Give it a moment to terminate gracefully - await new Promise((resolve) => setTimeout(resolve, 1000)); - if (!currentTestProcess.killed) { - currentTestProcess.kill('SIGKILL'); + try { + currentTestProcess.kill('SIGTERM'); + // Give it a moment to terminate gracefully + await new Promise((resolve) => setTimeout(resolve, 2000)); + if (!currentTestProcess.killed) { + currentTestProcess.kill('SIGKILL'); + } + } catch (error) { + console.log('โš ๏ธ Error terminating test process:', error.message); } } + // Only cleanup if we haven't already started if (needsCleanup && !isCleaningUp) { try { + // Temporarily ignore signals during cleanup to prevent interruption + process.removeAllListeners('SIGINT'); + process.removeAllListeners('SIGTERM'); + process.removeAllListeners('SIGHUP'); + await cleanup(); } catch (cleanupError) { console.error('โŒ Cleanup failed during shutdown:', cleanupError.message);