feat(tests): add isolated pi launchers

This commit is contained in:
Patrick Wozniak
2026-08-07 09:55:02 +02:00
parent 690c640d5b
commit b100d553c1
9 changed files with 346 additions and 4 deletions
+1
View File
@@ -2,6 +2,7 @@
## Unreleased ## Unreleased
- Add repository commands for testing the current checkout either in a logged-out, automatically cleaned-up pi environment or with existing credentials and only Command Code models enabled.
- Use the host-provided `pi-ai` and `pi-coding-agent` core packages instead of installing private runtime copies, including for local and out-of-store development checkouts. - Use the host-provided `pi-ai` and `pi-coding-agent` core packages instead of installing private runtime copies, including for local and out-of-store development checkouts.
## 0.4.4 - 2026-08-03 ## 0.4.4 - 2026-08-03
+22
View File
@@ -21,9 +21,31 @@ npm run test:models
npm run test:oauth npm run test:oauth
npm run test:abort npm run test:abort
npm run test:stream npm run test:stream
npm run test:pi-isolated
npm run test:pi-authenticated
npm run test:pi-local npm run test:pi-local
``` ```
Start pi with only the current checkout installed in temporary config and session directories, without inheriting Command Code credentials:
```sh
npm run pi:isolated
```
Run `/login` inside pi. The temporary credential and sessions are deleted when pi exits.
To use the current checkout with your existing pi credentials and expose only Command Code models in the model picker:
```sh
npm run pi:authenticated
```
Both launchers default to `commandcode/gpt-5.6-luna`. Pass additional pi arguments after `--` to override defaults or change startup behavior:
```sh
npm run pi:authenticated -- --model claude-sonnet-4-6
```
Before opening a PR, run: Before opening a PR, run:
```sh ```sh
+16
View File
@@ -147,6 +147,22 @@ omp plugin uninstall pi-commandcode-provider
## Development ## Development
Start an isolated pi instance with only the current checkout installed and no existing Command Code credentials:
```sh
npm run pi:isolated
```
Run `/login` inside pi. Temporary credentials, configuration, and sessions are deleted when pi exits.
Start the current checkout with your existing pi credentials and only Command Code models in the model picker:
```sh
npm run pi:authenticated
```
Both commands accept additional pi arguments after `--`, for example `npm run pi:authenticated -- --model claude-sonnet-4-6`.
See [CONTRIBUTING.md](CONTRIBUTING.md) for local setup and tests. See [RELEASE.md](RELEASE.md) for the release process. See [CONTRIBUTING.md](CONTRIBUTING.md) for local setup and tests. See [RELEASE.md](RELEASE.md) for the release process.
## License ## License
+5 -3
View File
@@ -16,10 +16,11 @@ import { AssistantMessageEventStream } from "@earendil-works/pi-ai"
import { getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent" import { getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent"
import { join } from "node:path" import { join } from "node:path"
import { getApiKey as getStoredApiKey } from "./src/converters.ts"
import { COMMAND_CODE_CLI_VERSION, createStreamCommandCode, DEFAULT_API_BASE } from "./src/core.ts" import { COMMAND_CODE_CLI_VERSION, createStreamCommandCode, DEFAULT_API_BASE } from "./src/core.ts"
import { calculateCommandCodeCost } from "./src/cost.ts" import { calculateCommandCodeCost } from "./src/cost.ts"
import { DEFAULT_MODELS_URL, loadCommandCodeModels } from "./src/models.ts" import { DEFAULT_MODELS_URL, loadCommandCodeModels } from "./src/models.ts"
import { getApiKey, login, refreshToken } from "./src/oauth.ts" import { getApiKey as getOAuthApiKey, login, refreshToken } from "./src/oauth.ts"
const API_BASE = process.env.COMMANDCODE_API_BASE ?? DEFAULT_API_BASE const API_BASE = process.env.COMMANDCODE_API_BASE ?? DEFAULT_API_BASE
const MODELS_URL = process.env.COMMANDCODE_MODELS_URL ?? DEFAULT_MODELS_URL const MODELS_URL = process.env.COMMANDCODE_MODELS_URL ?? DEFAULT_MODELS_URL
@@ -82,6 +83,7 @@ const streamCommandCode = createStreamCommandCode({
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export default async function (pi: ExtensionAPI) { export default async function (pi: ExtensionAPI) {
const storedApiKey = getStoredApiKey()
const { models, warning } = await loadCommandCodeModels({ const { models, warning } = await loadCommandCodeModels({
url: MODELS_URL, url: MODELS_URL,
cachePath: MODELS_CACHE_PATH, cachePath: MODELS_CACHE_PATH,
@@ -92,7 +94,7 @@ export default async function (pi: ExtensionAPI) {
pi.registerProvider("commandcode", { pi.registerProvider("commandcode", {
name: "Command Code", name: "Command Code",
baseUrl: API_BASE, baseUrl: API_BASE,
apiKey: "$COMMANDCODE_API_KEY", apiKey: storedApiKey,
authHeader: true, authHeader: true,
api: "commandcode-custom", api: "commandcode-custom",
streamSimple: streamCommandCode, streamSimple: streamCommandCode,
@@ -104,7 +106,7 @@ export default async function (pi: ExtensionAPI) {
name: "Command Code", name: "Command Code",
login, login,
refreshToken, refreshToken,
getApiKey, getApiKey: getOAuthApiKey,
}, },
models: models.map((model) => ({ models: models.map((model) => ({
id: model.id, id: model.id,
+6 -1
View File
@@ -21,6 +21,7 @@
"files": [ "files": [
"index.ts", "index.ts",
"src/", "src/",
"scripts/",
"README.md", "README.md",
"CHANGELOG.md", "CHANGELOG.md",
"CONTRIBUTING.md", "CONTRIBUTING.md",
@@ -28,10 +29,12 @@
"LICENSE" "LICENSE"
], ],
"scripts": { "scripts": {
"test": "npm run typecheck && tsx tests/test-package-manifest.ts && tsx tests/test-pure-functions.ts && tsx tests/test-models.ts && tsx tests/test-pricing.ts && tsx tests/test-cost.ts && tsx tests/test-oauth.ts && tsx tests/test-abort.ts && tsx tests/test-stream.ts && tsx tests/test-retry.ts && node tests/test-pi-local.mjs && node tests/test-omp-compat.mjs", "test": "npm run typecheck && tsx tests/test-package-manifest.ts && tsx tests/test-pure-functions.ts && tsx tests/test-models.ts && tsx tests/test-pricing.ts && tsx tests/test-cost.ts && tsx tests/test-oauth.ts && tsx tests/test-abort.ts && tsx tests/test-stream.ts && tsx tests/test-retry.ts && node tests/test-pi-isolated.mjs && node tests/test-pi-authenticated.mjs && node tests/test-pi-local.mjs && node tests/test-omp-compat.mjs",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"format:check": "prettier --check '**/*.{ts,mjs,json,md}'", "format:check": "prettier --check '**/*.{ts,mjs,json,md}'",
"format": "prettier --write '**/*.{ts,mjs,json,md}'", "format": "prettier --write '**/*.{ts,mjs,json,md}'",
"pi:isolated": "node scripts/pi-isolated.mjs",
"pi:authenticated": "node scripts/pi-authenticated.mjs",
"test:unit": "tsx tests/test-pure-functions.ts", "test:unit": "tsx tests/test-pure-functions.ts",
"test:models": "tsx tests/test-models.ts", "test:models": "tsx tests/test-models.ts",
"test:pricing": "tsx tests/test-pricing.ts", "test:pricing": "tsx tests/test-pricing.ts",
@@ -39,6 +42,8 @@
"test:abort": "tsx tests/test-abort.ts", "test:abort": "tsx tests/test-abort.ts",
"test:stream": "tsx tests/test-stream.ts", "test:stream": "tsx tests/test-stream.ts",
"test:retry": "tsx tests/test-retry.ts", "test:retry": "tsx tests/test-retry.ts",
"test:pi-isolated": "node tests/test-pi-isolated.mjs",
"test:pi-authenticated": "node tests/test-pi-authenticated.mjs",
"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:cost": "tsx tests/test-cost.ts" "test:cost": "tsx tests/test-cost.ts"
+49
View File
@@ -0,0 +1,49 @@
#!/usr/bin/env node
import { spawn } from "node:child_process"
import { dirname, resolve } from "node:path"
import { fileURLToPath } from "node:url"
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..")
const extensionPath = resolve(repoRoot, "index.ts")
const env = {
...process.env,
PI_SKIP_VERSION_CHECK: "1",
}
delete env.COMMANDCODE_API_KEY
const child = spawn(
"pi",
[
"--no-extensions",
"--extension",
extensionPath,
"--provider",
"commandcode",
"--model",
"gpt-5.6-luna",
"--models",
"commandcode/*",
...process.argv.slice(2),
],
{
cwd: repoRoot,
env,
stdio: "inherit",
},
)
for (const signal of ["SIGINT", "SIGTERM"]) {
process.on(signal, () => child.kill(signal))
}
child.once("error", (error) => {
console.error(`Could not start pi: ${error.message}`)
process.exitCode = 1
})
child.once("exit", (status, signal) => {
if (signal) process.kill(process.pid, signal)
process.exitCode = status ?? 1
})
+78
View File
@@ -0,0 +1,78 @@
#!/usr/bin/env node
import { spawn } from "node:child_process"
import { mkdir, mkdtemp, rm } from "node:fs/promises"
import { tmpdir } from "node:os"
import { dirname, join, resolve } from "node:path"
import { fileURLToPath } from "node:url"
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..")
const testRoot = await mkdtemp(join(tmpdir(), "pi-commandcode-isolated-"))
const agentDir = join(testRoot, "agent")
const sessionDir = join(testRoot, "sessions")
await mkdir(agentDir, { mode: 0o700 })
await mkdir(sessionDir, { mode: 0o700 })
const env = {
...process.env,
HOME: testRoot,
USERPROFILE: testRoot,
PI_CODING_AGENT_DIR: agentDir,
PI_CODING_AGENT_SESSION_DIR: sessionDir,
PI_SKIP_VERSION_CHECK: "1",
}
delete env.COMMANDCODE_API_KEY
let activeChild
let receivedSignal
function forwardSignal(signal) {
receivedSignal = signal
activeChild?.kill(signal)
}
const forwardSigint = () => forwardSignal("SIGINT")
const forwardSigterm = () => forwardSignal("SIGTERM")
process.on("SIGINT", forwardSigint)
process.on("SIGTERM", forwardSigterm)
function runPi(args) {
return new Promise((resolveRun, rejectRun) => {
const child = spawn("pi", args, { cwd: repoRoot, env, stdio: "inherit" })
activeChild = child
child.once("error", rejectRun)
child.once("exit", (status, signal) => {
activeChild = undefined
resolveRun({ status, signal })
})
})
}
let result
try {
console.error("Installing the current checkout into an isolated pi environment...")
const install = await runPi(["install", repoRoot, "--no-approve"])
if (install.status !== 0 || install.signal) {
result = install
} else {
console.error("Starting pi. Temporary auth and sessions will be removed on exit.")
result = await runPi([
"--no-approve",
"--provider",
"commandcode",
"--model",
"gpt-5.6-luna",
...process.argv.slice(2),
])
}
} finally {
process.removeListener("SIGINT", forwardSigint)
process.removeListener("SIGTERM", forwardSigterm)
await rm(testRoot, { recursive: true, force: true })
console.error("Removed the isolated pi environment.")
}
const signal = receivedSignal ?? result?.signal
if (signal) process.kill(process.pid, signal)
process.exitCode = result?.status ?? 1
+73
View File
@@ -0,0 +1,73 @@
import assert from "node:assert/strict"
import { spawnSync } from "node:child_process"
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"
import { tmpdir } from "node:os"
import { delimiter, dirname, join, resolve } from "node:path"
import { fileURLToPath } from "node:url"
import { describe, it } from "node:test"
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..")
const launcher = join(repoRoot, "scripts", "pi-authenticated.mjs")
function runLauncher() {
const fakeBin = mkdtempSync(join(tmpdir(), "pi-commandcode-fake-bin-"))
const logPath = join(fakeBin, "call.json")
const fakePi = join(fakeBin, "pi")
writeFileSync(
fakePi,
`#!/bin/sh
node - "$@" <<'NODE'
const { writeFileSync } = require("node:fs")
writeFileSync(process.env.FAKE_PI_LOG, JSON.stringify({
args: process.argv.slice(2),
agentDir: process.env.PI_CODING_AGENT_DIR ?? null,
apiKey: process.env.COMMANDCODE_API_KEY ?? null,
skipVersionCheck: process.env.PI_SKIP_VERSION_CHECK,
}))
NODE
`,
{ mode: 0o700 },
)
try {
const result = spawnSync(process.execPath, [launcher, "--thinking", "high"], {
cwd: repoRoot,
env: {
...process.env,
PATH: `${fakeBin}${delimiter}${process.env.PATH ?? ""}`,
FAKE_PI_LOG: logPath,
PI_CODING_AGENT_DIR: "/existing/pi-agent",
COMMANDCODE_API_KEY: "existing-key",
},
encoding: "utf8",
})
return { result, call: JSON.parse(readFileSync(logPath, "utf8")) }
} finally {
rmSync(fakeBin, { recursive: true, force: true })
}
}
describe("authenticated pi launcher", () => {
it("loads only the checkout extension and leaves auth resolution to existing files", () => {
const { result, call } = runLauncher()
assert.equal(result.status, 0)
assert.deepEqual(call.args, [
"--no-extensions",
"--extension",
join(repoRoot, "index.ts"),
"--provider",
"commandcode",
"--model",
"gpt-5.6-luna",
"--models",
"commandcode/*",
"--thinking",
"high",
])
assert.equal(call.agentDir, "/existing/pi-agent")
assert.equal(call.apiKey, null)
assert.equal(call.skipVersionCheck, "1")
})
})
+96
View File
@@ -0,0 +1,96 @@
import assert from "node:assert/strict"
import { spawnSync } from "node:child_process"
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"
import { tmpdir } from "node:os"
import { delimiter, dirname, join, resolve } from "node:path"
import { fileURLToPath } from "node:url"
import { describe, it } from "node:test"
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..")
const launcher = join(repoRoot, "scripts", "pi-isolated.mjs")
function runLauncher({ exitStatus = 0 } = {}) {
const fakeBin = mkdtempSync(join(tmpdir(), "pi-commandcode-fake-bin-"))
const logPath = join(fakeBin, "calls.jsonl")
const fakePi = join(fakeBin, "pi")
writeFileSync(
fakePi,
`#!/bin/sh
node - "$@" <<'NODE'
const { appendFileSync } = require("node:fs")
appendFileSync(process.env.FAKE_PI_LOG, JSON.stringify({
args: process.argv.slice(2),
agentDir: process.env.PI_CODING_AGENT_DIR,
sessionDir: process.env.PI_CODING_AGENT_SESSION_DIR,
skipVersionCheck: process.env.PI_SKIP_VERSION_CHECK,
home: process.env.HOME,
userProfile: process.env.USERPROFILE,
inheritedApiKey: process.env.COMMANDCODE_API_KEY ?? null,
}) + "\\n")
NODE
if [ "$1" = "install" ]; then exit 0; fi
exit ${exitStatus}
`,
{ mode: 0o700 },
)
try {
const result = spawnSync(process.execPath, [launcher, "--model", "claude-sonnet-5"], {
cwd: repoRoot,
env: {
...process.env,
PATH: `${fakeBin}${delimiter}${process.env.PATH ?? ""}`,
FAKE_PI_LOG: logPath,
COMMANDCODE_API_KEY: "must-not-leak",
},
encoding: "utf8",
})
const calls = readFileSync(logPath, "utf8")
.trim()
.split("\n")
.map((line) => JSON.parse(line))
return { result, calls }
} finally {
rmSync(fakeBin, { recursive: true, force: true })
}
}
describe("isolated pi launcher", () => {
it("installs the current checkout, forwards arguments, and removes its environment", () => {
const { result, calls } = runLauncher()
assert.equal(result.status, 0)
assert.equal(calls.length, 2)
assert.deepEqual(calls[0].args, ["install", repoRoot, "--no-approve"])
assert.deepEqual(calls[1].args, [
"--no-approve",
"--provider",
"commandcode",
"--model",
"gpt-5.6-luna",
"--model",
"claude-sonnet-5",
])
const [install, launch] = calls
assert.equal(install.agentDir, launch.agentDir)
assert.equal(install.sessionDir, launch.sessionDir)
assert.equal(launch.skipVersionCheck, "1")
assert.equal(launch.inheritedApiKey, null)
assert.ok(launch.agentDir.includes("pi-commandcode-isolated-"))
assert.equal(launch.home, dirname(launch.agentDir))
assert.equal(launch.userProfile, dirname(launch.agentDir))
assert.equal(dirname(launch.agentDir), dirname(launch.sessionDir))
assert.equal(existsSync(dirname(launch.agentDir)), false)
assert.match(result.stderr, /Removed the isolated pi environment/)
})
it("returns the pi exit status and still removes its environment", () => {
const { result, calls } = runLauncher({ exitStatus: 7 })
assert.equal(result.status, 7)
assert.equal(calls.length, 2)
assert.equal(existsSync(dirname(calls[1].agentDir)), false)
})
})