test: split standalone vs extension tests; run standalone secrets migration in CI

This commit is contained in:
kvyb
2025-09-23 09:11:09 +08:00
parent 479d139823
commit cafe3d4702
4 changed files with 104 additions and 83 deletions
+9 -1
View File
@@ -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:
+3 -56
View File
@@ -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 }) => {
+92
View File
@@ -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()
})
-26
View File
@@ -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"