test(e2e): add live account profiles
This commit is contained in:
@@ -127,6 +127,7 @@ export default async function (pi: ExtensionAPI) {
|
|||||||
timeoutMs: modelsTimeoutMs,
|
timeoutMs: modelsTimeoutMs,
|
||||||
}),
|
}),
|
||||||
createProviderConfig: (models) => createProviderConfig(models, apiBase, transport.stream),
|
createProviderConfig: (models) => createProviderConfig(models, apiBase, transport.stream),
|
||||||
|
getTransport: transport.getTransport,
|
||||||
})
|
})
|
||||||
|
|
||||||
await runtime.initialize()
|
await runtime.initialize()
|
||||||
|
|||||||
@@ -51,6 +51,9 @@
|
|||||||
"test:pi-local": "node tests/test-pi-local.mjs",
|
"test:pi-local": "node tests/test-pi-local.mjs",
|
||||||
"test:smoke": "node tests/test-smoke.mjs",
|
"test:smoke": "node tests/test-smoke.mjs",
|
||||||
"test:e2e:live": "node tests/test-live-e2e.mjs",
|
"test:e2e:live": "node tests/test-live-e2e.mjs",
|
||||||
|
"test:e2e:live:go": "node scripts/live-e2e-profile.mjs go",
|
||||||
|
"test:e2e:live:provider": "node scripts/live-e2e-profile.mjs provider",
|
||||||
|
"test:e2e:live:all": "node scripts/live-e2e-profile.mjs go provider",
|
||||||
"test:cost": "tsx tests/test-cost.ts"
|
"test:cost": "tsx tests/test-cost.ts"
|
||||||
},
|
},
|
||||||
"pi": {
|
"pi": {
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { spawn } from "node:child_process"
|
||||||
|
import { readFile } from "node:fs/promises"
|
||||||
|
import { dirname, resolve } from "node:path"
|
||||||
|
import { fileURLToPath } from "node:url"
|
||||||
|
|
||||||
|
const projectDir = resolve(dirname(fileURLToPath(import.meta.url)), "..")
|
||||||
|
const liveTest = resolve(projectDir, "tests", "test-live-e2e.mjs")
|
||||||
|
const profiles = process.argv.slice(2)
|
||||||
|
|
||||||
|
if (
|
||||||
|
profiles.length === 0 ||
|
||||||
|
profiles.some((profile) => profile !== "go" && profile !== "provider")
|
||||||
|
) {
|
||||||
|
console.error("Usage: node scripts/live-e2e-profile.mjs <go|provider> [go|provider]")
|
||||||
|
process.exit(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function credentialFor(profile) {
|
||||||
|
const prefix = profile === "go" ? "COMMANDCODE_E2E_GO" : "COMMANDCODE_E2E_PROVIDER"
|
||||||
|
const direct = process.env[`${prefix}_API_KEY`]?.trim()
|
||||||
|
const file = process.env[`${prefix}_API_KEY_FILE`]
|
||||||
|
|
||||||
|
if (direct && file)
|
||||||
|
throw new Error(`${prefix}_API_KEY and ${prefix}_API_KEY_FILE are mutually exclusive`)
|
||||||
|
if (direct) return direct
|
||||||
|
if (file) {
|
||||||
|
const credential = (await readFile(file, "utf-8")).trim()
|
||||||
|
if (credential) return credential
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Set ${prefix}_API_KEY_FILE (recommended) or ${prefix}_API_KEY`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function runProfile(profile, apiKey) {
|
||||||
|
const modelVariable =
|
||||||
|
profile === "go" ? "COMMANDCODE_E2E_GO_MODEL" : "COMMANDCODE_E2E_PROVIDER_MODEL"
|
||||||
|
const model = process.env[modelVariable] ?? "deepseek/deepseek-v4-flash"
|
||||||
|
const env = {
|
||||||
|
...process.env,
|
||||||
|
COMMANDCODE_API_KEY: apiKey,
|
||||||
|
COMMANDCODE_E2E_MODEL: model,
|
||||||
|
COMMANDCODE_E2E_PROFILE: profile,
|
||||||
|
}
|
||||||
|
delete env.COMMANDCODE_E2E_GO_API_KEY
|
||||||
|
delete env.COMMANDCODE_E2E_PROVIDER_API_KEY
|
||||||
|
|
||||||
|
return new Promise((resolveRun, reject) => {
|
||||||
|
console.log(`[live-e2e:${profile}] model ${model}`)
|
||||||
|
const child = spawn(process.execPath, [liveTest], {
|
||||||
|
cwd: projectDir,
|
||||||
|
env,
|
||||||
|
stdio: "inherit",
|
||||||
|
})
|
||||||
|
child.on("error", reject)
|
||||||
|
child.on("close", (code, signal) => {
|
||||||
|
if (code === 0) {
|
||||||
|
resolveRun()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
reject(new Error(`[live-e2e:${profile}] failed (${signal ?? `exit ${code}`})`))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const profile of profiles) {
|
||||||
|
await runProfile(profile, await credentialFor(profile))
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error instanceof Error ? error.message : String(error))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
+10
-5
@@ -28,11 +28,13 @@ export interface CommandCodeRuntimeOptions<TProviderConfig> {
|
|||||||
cachePath: string
|
cachePath: string
|
||||||
loadModels: () => Promise<LoadCommandCodeModelsResult>
|
loadModels: () => Promise<LoadCommandCodeModelsResult>
|
||||||
createProviderConfig: (models: readonly CommandCodeModel[]) => TProviderConfig
|
createProviderConfig: (models: readonly CommandCodeModel[]) => TProviderConfig
|
||||||
|
getTransport?: () => "unknown" | "provider" | "generate"
|
||||||
now?: () => number
|
now?: () => number
|
||||||
logWarning?: (message: string) => void
|
logWarning?: (message: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CommandCodeRuntimeStatus {
|
export interface CommandCodeRuntimeStatus {
|
||||||
|
transport: "unknown" | "provider" | "generate"
|
||||||
source: LoadCommandCodeModelsResult["source"]
|
source: LoadCommandCodeModelsResult["source"]
|
||||||
modelCount: number
|
modelCount: number
|
||||||
lastSuccess?: number
|
lastSuccess?: number
|
||||||
@@ -86,6 +88,7 @@ function formatTimestamp(timestamp: number | undefined): string {
|
|||||||
|
|
||||||
export function formatCommandCodeStatus(status: CommandCodeRuntimeStatus): string {
|
export function formatCommandCodeStatus(status: CommandCodeRuntimeStatus): string {
|
||||||
const lines = [
|
const lines = [
|
||||||
|
`transport: ${status.transport}`,
|
||||||
`source: ${status.source}`,
|
`source: ${status.source}`,
|
||||||
`model count: ${status.modelCount}`,
|
`model count: ${status.modelCount}`,
|
||||||
`last success: ${formatTimestamp(status.lastSuccess)}`,
|
`last success: ${formatTimestamp(status.lastSuccess)}`,
|
||||||
@@ -113,6 +116,7 @@ export class CommandCodeRuntime<TProviderConfig, TContext extends CommandCodeCom
|
|||||||
this.now = options.now ?? Date.now
|
this.now = options.now ?? Date.now
|
||||||
this.logWarning = options.logWarning ?? ((message) => console.warn(`[commandcode] ${message}`))
|
this.logWarning = options.logWarning ?? ((message) => console.warn(`[commandcode] ${message}`))
|
||||||
const initialStatus: CommandCodeRuntimeStatus = {
|
const initialStatus: CommandCodeRuntimeStatus = {
|
||||||
|
transport: "unknown",
|
||||||
source: "empty",
|
source: "empty",
|
||||||
modelCount: 0,
|
modelCount: 0,
|
||||||
cachePath: options.cachePath,
|
cachePath: options.cachePath,
|
||||||
@@ -123,7 +127,10 @@ export class CommandCodeRuntime<TProviderConfig, TContext extends CommandCodeCom
|
|||||||
}
|
}
|
||||||
|
|
||||||
getStatus(): CommandCodeRuntimeStatus {
|
getStatus(): CommandCodeRuntimeStatus {
|
||||||
return { ...this.status }
|
return {
|
||||||
|
...this.status,
|
||||||
|
transport: this.options.getTransport?.() ?? "unknown",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async initialize(): Promise<void> {
|
async initialize(): Promise<void> {
|
||||||
@@ -259,10 +266,8 @@ export class CommandCodeRuntime<TProviderConfig, TContext extends CommandCodeCom
|
|||||||
this.pi.registerCommand("commandcode-status", {
|
this.pi.registerCommand("commandcode-status", {
|
||||||
description: "Show redacted Command Code provider diagnostics",
|
description: "Show redacted Command Code provider diagnostics",
|
||||||
handler: async (_args, ctx) => {
|
handler: async (_args, ctx) => {
|
||||||
ctx.ui.notify(
|
const status = this.getStatus()
|
||||||
formatCommandCodeStatus(this.status),
|
ctx.ui.notify(formatCommandCodeStatus(status), status.warning ? "warning" : "info")
|
||||||
this.status.warning ? "warning" : "info",
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ import { fileURLToPath } from "node:url"
|
|||||||
const projectDir = resolve(dirname(fileURLToPath(import.meta.url)), "..")
|
const projectDir = resolve(dirname(fileURLToPath(import.meta.url)), "..")
|
||||||
const extensionPath = join(projectDir, "index.ts")
|
const extensionPath = join(projectDir, "index.ts")
|
||||||
const testModel = process.env.COMMANDCODE_E2E_MODEL ?? "deepseek/deepseek-v4-flash"
|
const testModel = process.env.COMMANDCODE_E2E_MODEL ?? "deepseek/deepseek-v4-flash"
|
||||||
|
const testProfile = process.env.COMMANDCODE_E2E_PROFILE
|
||||||
|
const expectedTransport =
|
||||||
|
testProfile === "go" ? "generate" : testProfile === "provider" ? "provider" : undefined
|
||||||
const marker = "commandcode-live-e2e-ok"
|
const marker = "commandcode-live-e2e-ok"
|
||||||
|
|
||||||
function findPiBinary() {
|
function findPiBinary() {
|
||||||
@@ -57,9 +60,18 @@ if (!piBin || !hasAuthMetadata()) {
|
|||||||
process.exit(0)
|
process.exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const profileAgentDir = testProfile
|
||||||
|
? mkdtempSync(join(tmpdir(), `pi-commandcode-live-${testProfile}-agent-`))
|
||||||
|
: undefined
|
||||||
|
|
||||||
function safeEnv(overrides = {}) {
|
function safeEnv(overrides = {}) {
|
||||||
const env = { ...process.env, PI_SKIP_VERSION_CHECK: "1", ...overrides }
|
const env = { ...process.env, PI_SKIP_VERSION_CHECK: "1", ...overrides }
|
||||||
|
if (testProfile && profileAgentDir) {
|
||||||
|
env.PI_CODING_AGENT_DIR = profileAgentDir
|
||||||
|
env.COMMANDCODE_MODELS_CACHE = join(profileAgentDir, "commandcode-models.json")
|
||||||
|
} else {
|
||||||
delete env.COMMANDCODE_API_KEY
|
delete env.COMMANDCODE_API_KEY
|
||||||
|
}
|
||||||
return env
|
return env
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,12 +240,22 @@ try {
|
|||||||
|
|
||||||
return { firstThinkingDeltas, secondThinkingDeltas, stderr: getStderr() }
|
return { firstThinkingDeltas, secondThinkingDeltas, stderr: getStderr() }
|
||||||
})
|
})
|
||||||
|
if (testProfile !== "provider") {
|
||||||
assert.ok(multiTurn.firstThinkingDeltas > 0, "first turn should stream reasoning")
|
assert.ok(multiTurn.firstThinkingDeltas > 0, "first turn should stream reasoning")
|
||||||
assert.ok(multiTurn.secondThinkingDeltas > 0, "follow-up turn should stream fresh reasoning")
|
assert.ok(multiTurn.secondThinkingDeltas > 0, "follow-up turn should stream fresh reasoning")
|
||||||
|
}
|
||||||
assert.doesNotMatch(multiTurn.stderr, /Bearer\s+\S+/i)
|
assert.doesNotMatch(multiTurn.stderr, /Bearer\s+\S+/i)
|
||||||
|
|
||||||
console.log("[live-e2e] live runtime refresh/status commands")
|
console.log("[live-e2e] live runtime refresh/status commands")
|
||||||
const runtime = await runRpc(extensionPath, async ({ send, waitFor, getStderr }) => {
|
const runtime = await runRpc(extensionPath, async ({ send, waitFor, getStderr }) => {
|
||||||
|
if (expectedTransport) {
|
||||||
|
send({ id: "transport-probe", type: "prompt", message: `Reply exactly: ${marker}` })
|
||||||
|
await waitFor(
|
||||||
|
(event) => event.type === "response" && event.id === "transport-probe" && event.success,
|
||||||
|
)
|
||||||
|
await waitFor((event) => event.type === "agent_settled")
|
||||||
|
}
|
||||||
|
|
||||||
send({ id: "commands", type: "get_commands" })
|
send({ id: "commands", type: "get_commands" })
|
||||||
const commands = await waitFor(
|
const commands = await waitFor(
|
||||||
(event) => event.type === "response" && event.id === "commands" && event.success,
|
(event) => event.type === "response" && event.id === "commands" && event.success,
|
||||||
@@ -264,6 +286,7 @@ try {
|
|||||||
assert.ok(runtime.names.includes("commandcode-refresh"))
|
assert.ok(runtime.names.includes("commandcode-refresh"))
|
||||||
assert.ok(runtime.names.includes("commandcode-status"))
|
assert.ok(runtime.names.includes("commandcode-status"))
|
||||||
assert.match(runtime.refresh, /model catalog (?:refreshed|unchanged)/)
|
assert.match(runtime.refresh, /model catalog (?:refreshed|unchanged)/)
|
||||||
|
if (expectedTransport) assert.match(runtime.status, new RegExp(`transport: ${expectedTransport}`))
|
||||||
assert.match(runtime.status, /source: (?:live|cache)/)
|
assert.match(runtime.status, /source: (?:live|cache)/)
|
||||||
assert.match(runtime.status, /model count: [1-9][0-9]*/)
|
assert.match(runtime.status, /model count: [1-9][0-9]*/)
|
||||||
assert.doesNotMatch(`${runtime.refresh}\n${runtime.status}\n${runtime.stderr}`, /Bearer\s+\S+/i)
|
assert.doesNotMatch(`${runtime.refresh}\n${runtime.status}\n${runtime.stderr}`, /Bearer\s+\S+/i)
|
||||||
@@ -296,6 +319,7 @@ try {
|
|||||||
assert.match(toolResult.stdout, new RegExp(marker))
|
assert.match(toolResult.stdout, new RegExp(marker))
|
||||||
assert.equal(readFileSync(targetPath, "utf-8"), marker)
|
assert.equal(readFileSync(targetPath, "utf-8"), marker)
|
||||||
|
|
||||||
|
if (testProfile !== "provider") {
|
||||||
console.log("[live-e2e] image rejection through real RPC host")
|
console.log("[live-e2e] image rejection through real RPC host")
|
||||||
const image = await runRpc(extensionPath, async ({ send, waitFor, events }) => {
|
const image = await runRpc(extensionPath, async ({ send, waitFor, events }) => {
|
||||||
send({
|
send({
|
||||||
@@ -320,6 +344,7 @@ try {
|
|||||||
/does not support image content/i.test(event.message?.errorMessage ?? ""),
|
/does not support image content/i.test(event.message?.errorMessage ?? ""),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
console.log("[live-e2e] packed artifact with existing authentication")
|
console.log("[live-e2e] packed artifact with existing authentication")
|
||||||
const packDir = join(tempRoot, "pack")
|
const packDir = join(tempRoot, "pack")
|
||||||
@@ -361,4 +386,5 @@ try {
|
|||||||
console.log("[live-e2e] PASS")
|
console.log("[live-e2e] PASS")
|
||||||
} finally {
|
} finally {
|
||||||
rmSync(tempRoot, { recursive: true, force: true })
|
rmSync(tempRoot, { recursive: true, force: true })
|
||||||
|
if (profileAgentDir) rmSync(profileAgentDir, { recursive: true, force: true })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ describe("Command Code runtime", () => {
|
|||||||
cachePath: "/tmp/commandcode-models.json",
|
cachePath: "/tmp/commandcode-models.json",
|
||||||
loadModels: () => firstLoad.promise,
|
loadModels: () => firstLoad.promise,
|
||||||
createProviderConfig: (models) => ({ models }),
|
createProviderConfig: (models) => ({ models }),
|
||||||
|
getTransport: () => "provider",
|
||||||
now: () => now,
|
now: () => now,
|
||||||
logWarning: () => {},
|
logWarning: () => {},
|
||||||
})
|
})
|
||||||
@@ -115,6 +116,7 @@ describe("Command Code runtime", () => {
|
|||||||
assert.ok(statusCommand)
|
assert.ok(statusCommand)
|
||||||
await statusCommand("", context)
|
await statusCommand("", context)
|
||||||
const statusMessage = context.notifications.at(-1)?.message ?? ""
|
const statusMessage = context.notifications.at(-1)?.message ?? ""
|
||||||
|
assert.match(statusMessage, /transport: provider/)
|
||||||
assert.match(statusMessage, /source: live/)
|
assert.match(statusMessage, /source: live/)
|
||||||
assert.match(statusMessage, /model count: 1/)
|
assert.match(statusMessage, /model count: 1/)
|
||||||
assert.match(statusMessage, /last success:/)
|
assert.match(statusMessage, /last success:/)
|
||||||
|
|||||||
Reference in New Issue
Block a user