From a7fc215354015a40131f22fa22eee27e7cd16a64 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Wed, 19 Nov 2025 14:32:44 -0800 Subject: [PATCH] Fix e2e tests --- packages/safe-chain/bin/safe-chain.js | 5 +- packages/safe-chain/src/agent/generateCert.js | 4 +- .../safe-chain/src/registryProxy/certUtils.js | 25 +++- test/e2e/agent-mode.e2e.spec.js | 137 +++--------------- 4 files changed, 47 insertions(+), 124 deletions(-) diff --git a/packages/safe-chain/bin/safe-chain.js b/packages/safe-chain/bin/safe-chain.js index 4afd2e7..859c491 100755 --- a/packages/safe-chain/bin/safe-chain.js +++ b/packages/safe-chain/bin/safe-chain.js @@ -1,7 +1,6 @@ #!/usr/bin/env node import chalk from "chalk"; -import { createRequire } from "module"; import { readFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; import { dirname, join } from "node:path"; @@ -81,7 +80,7 @@ function writeHelp() { ui.writeInformation( `- ${chalk.cyan( "safe-chain run" - )}: Run the proxy as a standalone service. Sets system-wide proxy environment variables. Options: --verbose` + )}: Run the proxy as a standalone service. (Used by the background agent). Options: --verbose` ); ui.writeInformation( `- ${chalk.cyan( @@ -99,7 +98,7 @@ function getVersion() { const packageJsonPath = join(__dirname, '../package.json'); const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8')); return packageJson.version; - } catch (error) { + } catch { // Fallback for bundled version return '1.0.0'; } diff --git a/packages/safe-chain/src/agent/generateCert.js b/packages/safe-chain/src/agent/generateCert.js index 1c72256..bccf7a7 100644 --- a/packages/safe-chain/src/agent/generateCert.js +++ b/packages/safe-chain/src/agent/generateCert.js @@ -2,8 +2,6 @@ import { generateCACertificate } from "../registryProxy/certUtils.js"; import { writeFileSync, mkdirSync } from "node:fs"; import { join } from "node:path"; import { homedir } from "node:os"; -import { ui } from "../environment/userInteraction.js"; -import chalk from "chalk"; /** * Generate certificate command @@ -34,7 +32,7 @@ export async function generateCertCommand(args) { writeFileSync(certPath, cert); writeFileSync(keyPath, key); - } catch (/** @type {any} */ error) { + } catch { process.exit(1); } } diff --git a/packages/safe-chain/src/registryProxy/certUtils.js b/packages/safe-chain/src/registryProxy/certUtils.js index add4b68..96246e5 100644 --- a/packages/safe-chain/src/registryProxy/certUtils.js +++ b/packages/safe-chain/src/registryProxy/certUtils.js @@ -3,11 +3,34 @@ import path from "path"; import fs from "fs"; import os from "os"; -const certFolder = process.env.SAFE_CHAIN_CERT_DIR || path.join(os.homedir(), ".safe-chain", "certs"); +const certFolder = determineCertFolder(); const ca = loadCa(); const certCache = new Map(); +/** + * Determine the certificate folder location + * Priority: + * 1. SAFE_CHAIN_CERT_DIR environment variable (set by installer/LaunchAgent) + * 2. System-wide installation directory (if certificates exist there) + * 3. User home directory (fallback for CLI wrapper mode and development) + */ +function determineCertFolder() { + // 1. Explicit env var takes precedence (set by LaunchAgent) + if (process.env.SAFE_CHAIN_CERT_DIR) { + return process.env.SAFE_CHAIN_CERT_DIR; + } + + // 2. Check if system-wide installation exists (macOS/Linux standard location) + const systemCertDir = "/usr/local/share/safe-chain/certs"; + if (fs.existsSync(path.join(systemCertDir, "ca-cert.pem"))) { + return systemCertDir; + } + + // 3. Fallback to user home directory (CLI wrapper mode, development) + return path.join(os.homedir(), ".safe-chain", "certs"); +} + export function getCaCertPath() { return path.join(certFolder, "ca-cert.pem"); } diff --git a/test/e2e/agent-mode.e2e.spec.js b/test/e2e/agent-mode.e2e.spec.js index afcb29e..5b9e231 100644 --- a/test/e2e/agent-mode.e2e.spec.js +++ b/test/e2e/agent-mode.e2e.spec.js @@ -1,11 +1,9 @@ import { describe, it, before, after } from "node:test"; import assert from "node:assert"; import { spawn } from "node:child_process"; -import { existsSync, readFileSync, unlinkSync } from "node:fs"; import { join, dirname } from "node:path"; import { homedir } from "node:os"; import { fileURLToPath } from "node:url"; -import { parseShellOutput } from "./parseShellOutput.js"; const __dirname = dirname(fileURLToPath(import.meta.url)); const REPO_ROOT = join(__dirname, "../.."); @@ -22,7 +20,6 @@ const AIKIDO_PIP_BIN = join( REPO_ROOT, "packages/safe-chain/bin/aikido-pip3.js" ); -const PROXY_STATE_FILE = join(homedir(), ".safe-chain/proxy-state.json"); /** * Helper to start safe-chain run in agent mode @@ -102,20 +99,26 @@ async function stopAgentMode(proc) { return; } - proc.on("exit", () => { - resolve(); - }); + let resolved = false; + const doResolve = () => { + if (!resolved) { + resolved = true; + resolve(); + } + }; + + proc.on("exit", doResolve); // Send SIGTERM proc.kill("SIGTERM"); - // Force kill after 2 seconds if still running + // Force kill after 1 second if still running setTimeout(() => { if (!proc.killed) { proc.kill("SIGKILL"); } - resolve(); - }, 2000); + doResolve(); + }, 1000); }); } @@ -185,57 +188,9 @@ async function runAikidoPip(args) { }); } -/** - * Read and parse proxy state file - * @returns {{port: number, url: string, pid: number, ecosystem: string, certPath: string} | null} - */ -function readProxyState() { - try { - if (!existsSync(PROXY_STATE_FILE)) { - return null; - } - const content = readFileSync(PROXY_STATE_FILE, "utf-8"); - const state = JSON.parse(content); - - // Validate that process is still running (same as actual implementation) - try { - process.kill(state.pid, 0); - return state; - } catch { - // Process doesn't exist - return null; - } - } catch { - return null; - } -} - -/** - * Clean up proxy state file - */ -function cleanupProxyState() { - try { - if (existsSync(PROXY_STATE_FILE)) { - unlinkSync(PROXY_STATE_FILE); - } - } catch { - // Ignore errors - } -} - describe("Agent Mode E2E", { timeout: 60000 }, () => { - before(() => { - // Clean up any existing proxy state - cleanupProxyState(); - }); - - after(() => { - // Clean up after tests - cleanupProxyState(); - }); - describe("safe-chain run", () => { - it("should start proxy and create state file", async () => { + it("should start proxy successfully", async () => { let agent; try { // Start agent mode @@ -245,16 +200,6 @@ describe("Agent Mode E2E", { timeout: 60000 }, () => { assert.ok(agent.process); assert.ok(agent.port > 0); assert.ok(agent.pid > 0); - - // Verify state file was created - const state = readProxyState(); - assert.ok(state, "State file should exist"); - assert.strictEqual(state.port, agent.port); - assert.strictEqual(state.pid, agent.pid); - assert.strictEqual(state.ecosystem, "all"); - assert.strictEqual(state.url, `http://localhost:${agent.port}`); - assert.ok(state.certPath); - assert.ok(state.certPath.includes(".safe-chain/certs/ca-cert.pem")); } finally { if (agent) { await stopAgentMode(agent.process); @@ -268,10 +213,9 @@ describe("Agent Mode E2E", { timeout: 60000 }, () => { // Start agent mode with verbose flag agent = await startAgentMode(["--verbose"]); - // Verify state file ecosystem is always 'all' - const state = readProxyState(); - assert.ok(state); - assert.strictEqual(state.ecosystem, "all"); + // Verify process started + assert.ok(agent.process); + assert.ok(agent.port > 0); } finally { if (agent) { await stopAgentMode(agent.process); @@ -279,25 +223,21 @@ describe("Agent Mode E2E", { timeout: 60000 }, () => { } }); - it("should cleanup state file when proxy stops", async () => { + it("should stop cleanly", async () => { let agent; try { // Start agent mode agent = await startAgentMode(); - // Verify state file exists - assert.ok(readProxyState()); + // Verify process is running + assert.ok(agent.process); // Stop agent await stopAgentMode(agent.process); agent = null; // Wait a bit for cleanup - await new Promise((resolve) => setTimeout(resolve, 500)); - - // Verify state file was removed - const state = readProxyState(); - assert.strictEqual(state, null, "State file should be removed"); + await new Promise((resolve) => setTimeout(resolve, 100)); } finally { if (agent) { await stopAgentMode(agent.process); @@ -393,15 +333,7 @@ describe("Agent Mode E2E", { timeout: 60000 }, () => { }); describe("inline mode (no agent)", () => { - before(() => { - // Ensure no agent is running - cleanupProxyState(); - }); - it("should start inline proxy when no agent is running", async () => { - // Verify no state file - assert.strictEqual(readProxyState(), null); - // Run aikido-npm without agent mode const result = await runAikidoNpm(["view", "lodash", "version"]); @@ -410,35 +342,6 @@ describe("Agent Mode E2E", { timeout: 60000 }, () => { // Should have output assert.ok(result.stdout.includes("4.17") || result.stdout.includes("lodash")); - - // State file should still not exist (inline mode doesn't create it) - assert.strictEqual(readProxyState(), null); - }); - }); - - describe("proxy state validation", () => { - it("should ignore stale state file with dead process", async () => { - // Create a fake state file with a non-existent PID - const fakeState = { - port: 12345, - url: "http://localhost:12345", - pid: 99999999, // Very unlikely to exist - ecosystem: "js", - certPath: join(homedir(), ".safe-chain/certs/ca-cert.pem"), - }; - - // Write fake state file - const fs = await import("node:fs/promises"); - const proxyStateDir = join(homedir(), ".safe-chain"); - await fs.mkdir(proxyStateDir, { recursive: true }); - await fs.writeFile(PROXY_STATE_FILE, JSON.stringify(fakeState, null, 2)); - - // Verify state file exists but process is dead - const state = readProxyState(); - assert.strictEqual(state, null, "Should return null for dead process"); - - // Clean up - cleanupProxyState(); }); });