diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 97bd8707f7..6fe1b63dcc 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -97,9 +97,17 @@ jobs: if: matrix.runner == 'ubuntu' && matrix.deps == 'true' run: | sudo apt-get update - sudo apt-get install -y dbus gnome-keyring libsecret-1-0 libsecret-tools xvfb + sudo apt-get install -y dbus gnome-keyring libsecret-1-0 libsecret-tools xvfb unzip echo "Provisioned libsecret and keyring" + - name: Build standalone (Linux - deps:true) + if: matrix.runner == 'ubuntu' && matrix.deps == 'true' + run: npm run compile-standalone + + - name: Build standalone (macOS) + if: matrix.runner == 'macos' + run: npm run compile-standalone + - name: Run E2E tests - Linux if: matrix.runner == 'ubuntu' env: diff --git a/src/test/e2e/secrets.test.ts b/src/test/e2e/secrets.test.ts index be2ea16ac3..762ddcb838 100644 --- a/src/test/e2e/secrets.test.ts +++ b/src/test/e2e/secrets.test.ts @@ -1,7 +1,5 @@ import { spawnSync } from "node:child_process" -import * as fs from "node:fs" import os from "node:os" -import * as path from "node:path" import { expect, test } from "@playwright/test" import { e2e } from "./utils/helpers" @@ -61,6 +59,8 @@ function winDelete(service: string, account: string): boolean { return res.status === 0 } +// Extension-host validation of OS keychain commands + e2e("Secrets - OS keychain get/store/delete", async () => { const platform = os.platform() const service = "cline" @@ -126,60 +126,7 @@ e2e("Secrets - OS keychain get/store/delete", async () => { expect(after).toBeUndefined() }) -e2e("Secrets migration - moves legacy secrets.json to OS keychain and removes file", async () => { - const platform = os.platform() - if (platform !== "darwin" && platform !== "linux") { - test.skip(true, "Migration test only runs on macOS/Linux where OS keychain is available in CI") - return - } - if (platform === "darwin" && !hasCommand("security")) { - test.skip(true, "security CLI not available") - return - } - if (platform === "linux" && !hasCommand("secret-tool")) { - test.skip(true, "secret-tool not available") - return - } - - // Prepare env for helpers to seed secrets.json before app launch - process.env.E2E_MIGRATION_SECRETS_JSON = JSON.stringify({ openRouterApiKey: "migrate-me" }) - - // Launch VS Code to trigger extension startup and migration - const { E2ETestHelper } = await import("./utils/helpers") - const executablePath = await (await import("@vscode/test-electron")).downloadAndUnzipVSCode("stable", undefined) - const { _electron } = await import("playwright") - const userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), "vsce-mig")) - const app = await _electron.launch({ - executablePath, - env: { ...process.env, CLINE_DIR: userDataDir, E2E_TEST: "true" }, - args: [ - "--disable-extensions", - "--skip-welcome", - "--skip-release-notes", - `--user-data-dir=${userDataDir}`, - `--install-extension=${path.join(E2ETestHelper.CODEBASE_ROOT_DIR, "dist", "e2e.vsix")}`, - `--extensionDevelopmentPath=${E2ETestHelper.CODEBASE_ROOT_DIR}`, - path.join(E2ETestHelper.E2E_TESTS_DIR, "fixtures", "workspace"), - ], - }) - await E2ETestHelper.waitUntil(() => app.windows().length > 0) - - // Verify key present in OS keychain - const service = "Cline: openRouterApiKey" - const account = "cline_openRouterApiKey" - const present = - platform === "darwin" - ? spawnSync("security", ["find-generic-password", "-s", service, "-a", account, "-w"], { stdio: "ignore" }).status === - 0 - : spawnSync("secret-tool", ["lookup", "service", service, "account", account], { stdio: "ignore" }).status === 0 - expect(present).toBeTruthy() - - // Verify legacy file removed - const secretsPath = path.join(userDataDir, "data", "secrets.json") - expect(fs.existsSync(secretsPath)).toBeFalsy() - - await app.close() -}) +// Standalone deps warning remains part of extension-host E2E (UI toast) test.describe("Standalone deps warning", () => { e2e("Toast presence matches EXPECT_DEPS env", async ({ page }) => { diff --git a/src/test/e2e/standalone-migration.test.ts b/src/test/e2e/standalone-migration.test.ts new file mode 100644 index 0000000000..3de061071f --- /dev/null +++ b/src/test/e2e/standalone-migration.test.ts @@ -0,0 +1,92 @@ +import { ChildProcess, spawn, spawnSync } from "node:child_process" +import * as fs from "node:fs" +import * as os from "node:os" +import * as path from "node:path" +import { expect, test } from "@playwright/test" + +function hasCommand(cmd: string): boolean { + if (process.platform === "win32") return true + const result = spawnSync("sh", ["-c", `command -v ${cmd}`], { stdio: "ignore" }) + return result.status === 0 +} + +test("Standalone migration moves secrets.json to OS keychain and removes file", async () => { + const platform = os.platform() + if (platform === "win32") { + test.skip(true, "Skip on Windows; covered by shell/E2E elsewhere") + return + } + if (platform === "darwin" && !hasCommand("security")) { + test.skip(true, "security CLI not available on macOS runner") + return + } + if (platform === "linux" && !hasCommand("secret-tool")) { + test.skip(true, "secret-tool not available on Linux runner") + return + } + + // Prepare isolated CLINE_DIR with legacy secrets.json + const userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), "cline-standalone-mig-")) + const dataDir = path.join(userDataDir, "data") + fs.mkdirSync(dataDir, { recursive: true }) + const secretsPath = path.join(dataDir, "secrets.json") + fs.writeFileSync(secretsPath, JSON.stringify({ openRouterApiKey: "migrate-me" }, null, 2)) + + // Ensure no preexisting key in OS keychain + const service = "Cline: openRouterApiKey" + const account = "cline_openRouterApiKey" + if (platform === "darwin") { + spawnSync("security", ["delete-generic-password", "-s", service, "-a", account], { stdio: "ignore" }) + } else if (platform === "linux") { + spawnSync("secret-tool", ["clear", "service", service, "account", account], { stdio: "ignore" }) + } + + // Start the standalone core service via helper script + const procEnv = { + ...process.env, + CLINE_DIR: userDataDir, + E2E_TEST: "true", + CLINE_ENVIRONMENT: "local", + } + + const server: ChildProcess = spawn( + process.platform === "win32" ? "npx.cmd" : "npx", + ["tsx", "scripts/test-standalone-core-api-server.ts"], + { stdio: "pipe", env: procEnv }, + ) + + // Wait until server prints that it's running, then poll for migration results + await new Promise((resolve, reject) => { + const timeout = setTimeout(() => resolve(), 3000) + server.stdout?.once("data", () => { + clearTimeout(timeout) + resolve() + }) + server.once("error", reject) + }) + + // Poll keychain up to ~10s for migration to complete + const start = Date.now() + let present = false + while (Date.now() - start < 10000) { + const status = + platform === "darwin" + ? spawnSync("security", ["find-generic-password", "-s", service, "-a", account, "-w"], { + stdio: "ignore", + }).status + : spawnSync("secret-tool", ["lookup", "service", service, "account", account], { stdio: "ignore" }).status + if (status === 0) { + present = true + break + } + await new Promise((r) => setTimeout(r, 300)) + } + + // Stop server + try { + server.kill("SIGINT") + } catch {} + + expect(present).toBeTruthy() + expect(fs.existsSync(secretsPath)).toBeFalsy() +}) diff --git a/src/test/e2e/utils/helpers.ts b/src/test/e2e/utils/helpers.ts index 0c3b7d72d3..e855a89158 100644 --- a/src/test/e2e/utils/helpers.ts +++ b/src/test/e2e/utils/helpers.ts @@ -240,30 +240,6 @@ export const e2e = test const executablePath = await downloadAndUnzipVSCode(channel, undefined, new SilentReporter()) await use(async (workspacePath: string) => { - // Prepare CLINE_DIR in the VS Code environment to isolate per-test data - // Optionally pre-create legacy secrets.json for migration tests - try { - const fs = await import("node:fs") - const pathMod = await import("node:path") - const clineDir = userDataDir // Use user data dir as CLINE_DIR for isolation - // Pre-create secrets.json if requested for migration test titles - const wantsMigration = - (process.env.E2E_MIGRATION_SECRETS_JSON || "").length > 0 && - testInfo.title.toLowerCase().includes("migration") - if (wantsMigration) { - const dataDir = pathMod.join(clineDir, "data") - fs.mkdirSync(dataDir, { recursive: true }) - const secretsPath = pathMod.join(dataDir, "secrets.json") - fs.writeFileSync( - secretsPath, - JSON.stringify(JSON.parse(process.env.E2E_MIGRATION_SECRETS_JSON as string), null, 2), - ) - } - } catch {} - - // Expose the chosen CLINE_DIR to the test process for assertions - process.env.LAST_E2E_CLINE_DIR = userDataDir - const app = await _electron.launch({ executablePath, env: { @@ -271,8 +247,6 @@ export const e2e = test TEMP_PROFILE: "true", E2E_TEST: "true", CLINE_ENVIRONMENT: "local", - // Ensure extension uses test-specific data directory - CLINE_DIR: userDataDir, GRPC_RECORDER_FILE_NAME: E2ETestHelper.generateTestFileName(testInfo.title, testInfo.project.name), // GRPC_RECORDER_ENABLED: "true", // GRPC_RECORDER_TESTS_FILTERS_ENABLED: "true"