mirror of
https://github.com/cline/cline.git
synced 2026-09-19 02:05:44 +08:00
test: split standalone vs extension tests; run standalone secrets migration in CI
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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<void>((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()
|
||||
})
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user