Files
pi-commandcode-provider/tests/test-pi-local.mjs
T
Patrick Wozniak 0700d9b61d fix(core): register the custom api in the pi-ai compat registry
pi routes the main chat through the registered provider, but sibling
extensions that call streamSimple from @earendil-works/pi-ai/compat with
the active Command Code model resolve model.api through the compat
api-registry, which only knows built-in APIs. On plain pi that failed
with "No API provider registered for api: commandcode-custom".

Register commandcode-custom there and delegate to the transport router.
The registry resolves no credentials for extension providers, so fall
back to the configured Command Code key when the caller passes none.

Closes #68

(cherry picked from commit 7e9659e672771c6a9223b95938e50fb2a051a0a7)
2026-09-01 23:29:30 +02:00

902 lines
27 KiB
JavaScript

#!/usr/bin/env node
/**
* Local end-to-end test: loads the real extension through the pi CLI while the
* Command Code API is replaced by a deterministic local mock server.
*/
import assert from "node:assert/strict"
import { spawn, spawnSync } from "node:child_process"
import { accessSync, constants, mkdtempSync, rmSync, mkdirSync, writeFileSync } from "node:fs"
import { createServer } from "node:http"
import { tmpdir } from "node:os"
import { delimiter, dirname, join, resolve } from "node:path"
import { fileURLToPath } from "node:url"
const __dirname = dirname(fileURLToPath(import.meta.url))
const PROJECT_DIR = resolve(__dirname, "..")
const EXT_PATH = resolve(PROJECT_DIR, "index.ts")
const COMPAT_CALLER_EXT_PATH = resolve(
PROJECT_DIR,
"tests",
"fixtures",
"compat-caller-extension.ts",
)
const TEST_MODEL = "gpt-5.4"
const CLAUDE_TEST_MODEL = "claude-sonnet-4-6"
function findPiBinary() {
if (process.env.PI_BIN) return process.env.PI_BIN
const localBin = resolve(PROJECT_DIR, "node_modules", ".bin")
const candidates = (process.env.PATH ?? "")
.split(delimiter)
.map((entry) => resolve(entry, "pi"))
.filter((candidate) => !candidate.startsWith(localBin))
for (const candidate of candidates) {
try {
accessSync(candidate, constants.X_OK)
return candidate
} catch {
// Try next PATH entry.
}
}
return undefined
}
const PI_BIN = findPiBinary()
if (!PI_BIN) {
console.log("[pi-local] SKIP — pi is not on PATH")
process.exit(0)
}
const piCheck = spawnSync(PI_BIN, ["--help"], { stdio: "ignore" })
if (piCheck.error) {
console.log(`[pi-local] SKIP — pi failed to start: ${piCheck.error.message}`)
process.exit(0)
}
let requestCount = 0
let modelListRequestCount = 0
let lastRequestBody
let lastRequestHeaders = {}
let overflowMode = false
let overflowRequestCount = 0
let modelsDelayMs = 0
let includeRefreshedModel = false
function modelCatalog() {
const data = [
{
id: TEST_MODEL,
object: "model",
created: 1779824324,
owned_by: "command-code",
name: "GPT 5.4",
context_length: 1_000_000,
},
{
id: CLAUDE_TEST_MODEL,
object: "model",
created: 1779824324,
owned_by: "command-code",
name: "Claude Sonnet 4.6",
context_length: 200_000,
},
{
id: "cc-second-model",
object: "model",
created: 1779824324,
owned_by: "command-code",
name: "Qwen 3.7 Max",
context_length: 1_000_000,
},
]
if (includeRefreshedModel) {
data.push({
id: "cc-refreshed-model",
object: "model",
created: 1779824324,
owned_by: "command-code",
name: "Refreshed Model",
context_length: 200_000,
})
}
return { object: "list", data }
}
const server = createServer((req, res) => {
if (req.method === "GET" && req.url === "/provider/v1/models") {
modelListRequestCount += 1
const respond = () => {
if (res.destroyed) return
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" })
res.end(JSON.stringify(modelCatalog()))
}
if (modelsDelayMs > 0) setTimeout(respond, modelsDelayMs)
else respond()
return
}
const isOpenAIRequest = req.method === "POST" && req.url === "/provider/v1/chat/completions"
const isAnthropicRequest = req.method === "POST" && req.url === "/provider/v1/messages"
if (!isOpenAIRequest && !isAnthropicRequest) {
res.writeHead(404)
res.end("Not found")
return
}
requestCount += 1
if (overflowMode) overflowRequestCount += 1
lastRequestHeaders = Object.fromEntries(
Object.entries(req.headers).map(([key, value]) => [
key,
Array.isArray(value) ? value.join(", ") : (value ?? ""),
]),
)
let body = ""
req.on("data", (chunk) => {
body += chunk.toString("utf-8")
})
req.on("end", () => {
try {
lastRequestBody = JSON.parse(body)
} catch {
lastRequestBody = undefined
}
if (overflowMode && overflowRequestCount === 2) {
res.writeHead(400, { "Content-Type": "application/json; charset=utf-8" })
res.end(
JSON.stringify({
error: {
message: "Input exceeds context limit",
type: "invalid_request_error",
code: "context_length_exceeded",
},
}),
)
return
}
res.writeHead(200, {
"Content-Type": "text/event-stream; charset=utf-8",
"Transfer-Encoding": "chunked",
})
const text = overflowMode
? overflowRequestCount === 1
? "overflow-initial"
: overflowRequestCount === 3
? "compaction-summary"
: "overflow-recovered"
: "mock-pi-ok"
if (isAnthropicRequest) {
res.write(
`event: message_start\ndata: ${JSON.stringify({ type: "message_start", message: { id: "mock", type: "message", role: "assistant", content: [], model: CLAUDE_TEST_MODEL, stop_reason: null, stop_sequence: null, usage: { input_tokens: 1, output_tokens: 0 } } })}\n\n`,
)
res.write(
`event: content_block_start\ndata: ${JSON.stringify({ type: "content_block_start", index: 0, content_block: { type: "text", text: "" } })}\n\n`,
)
res.write(
`event: content_block_delta\ndata: ${JSON.stringify({ type: "content_block_delta", index: 0, delta: { type: "text_delta", text } })}\n\n`,
)
res.write(
`event: content_block_stop\ndata: ${JSON.stringify({ type: "content_block_stop", index: 0 })}\n\n`,
)
res.write(
`event: message_delta\ndata: ${JSON.stringify({ type: "message_delta", delta: { stop_reason: "end_turn", stop_sequence: null }, usage: { output_tokens: 1 } })}\n\n`,
)
res.end(`event: message_stop\ndata: ${JSON.stringify({ type: "message_stop" })}\n\n`)
return
}
res.write(
`data: ${JSON.stringify({ id: "mock", object: "chat.completion.chunk", choices: [{ index: 0, delta: { role: "assistant", content: text }, finish_reason: null }] })}\n\n`,
)
res.write(
`data: ${JSON.stringify({ id: "mock", object: "chat.completion.chunk", choices: [{ index: 0, delta: {}, finish_reason: "stop" }] })}\n\n`,
)
res.write(
`data: ${JSON.stringify({ id: "mock", object: "chat.completion.chunk", choices: [], usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 } })}\n\n`,
)
res.end("data: [DONE]\n\n")
})
})
await new Promise((resolve) => server.listen(0, resolve))
const address = server.address()
const port = typeof address === "object" && address ? address.port : 0
const apiBase = `http://127.0.0.1:${port}`
const tempHome = mkdtempSync(join(tmpdir(), "pi-cc-home-"))
const agentDir = join(tempHome, "custom-pi-agent")
mkdirSync(agentDir, { recursive: true })
writeFileSync(
join(agentDir, "settings.json"),
JSON.stringify({ compaction: { enabled: true, reserveTokens: 10, keepRecentTokens: 10 } }),
)
const env = {
...process.env,
HOME: tempHome,
USERPROFILE: tempHome,
PI_CODING_AGENT_DIR: agentDir,
PI_CODING_AGENT_SESSION_DIR: join(tempHome, "sessions"),
COMMANDCODE_API_BASE: `${apiBase}/provider/v1`,
COMMAND_CODE_API_KEY: "mock-key",
CMD_ZDR: "1",
COMMANDCODE_MODELS_URL: `${apiBase}/provider/v1/models`,
}
function runPi(args, timeoutMs = 30_000) {
return new Promise((resolve) => {
const child = spawn(PI_BIN, args, {
cwd: PROJECT_DIR,
env,
stdio: ["ignore", "pipe", "pipe"],
})
let stdout = ""
let stderr = ""
const timer = setTimeout(() => {
child.kill()
resolve({
code: -1,
stdout,
stderr: `${stderr}\nTIMEOUT after ${timeoutMs}ms`,
})
}, timeoutMs)
child.stdout.on("data", (chunk) => {
stdout += chunk.toString("utf-8")
})
child.stderr.on("data", (chunk) => {
stderr += chunk.toString("utf-8")
})
child.on("close", (code) => {
clearTimeout(timer)
resolve({ code, stdout, stderr })
})
})
}
async function runRpcQuery(
timeoutMs = 30_000,
promptMessage = "say mock token",
extraArgs = [],
promptFields = {},
) {
const child = spawn(
PI_BIN,
[
"--no-extensions",
"--mode",
"rpc",
"-e",
EXT_PATH,
"--provider",
"commandcode",
"--model",
TEST_MODEL,
...extraArgs,
],
{
cwd: PROJECT_DIR,
env,
stdio: ["pipe", "pipe", "pipe"],
},
)
let stdout = ""
let stderr = ""
let buffer = ""
let sawPromptAccepted = false
let sawAssistantMessage = false
let sawTextDelta = false
const events = []
const done = new Promise((resolve) => {
const timer = setTimeout(() => {
child.kill()
resolve(false)
}, timeoutMs)
const finish = (ok) => {
clearTimeout(timer)
try {
child.stdin.write(`${JSON.stringify({ type: "quit" })}\n`)
} catch {
// ignore shutdown race
}
child.kill()
resolve(ok)
}
child.stdin.write(
`${JSON.stringify({
id: "prompt-1",
type: "prompt",
message: promptMessage,
...promptFields,
})}\n`,
)
child.stdout.on("data", (chunk) => {
const text = chunk.toString("utf-8")
stdout += text
buffer += text
const lines = buffer.split("\n")
buffer = lines.pop() ?? ""
for (const line of lines) {
const trimmed = line.trim()
if (!trimmed) continue
try {
const event = JSON.parse(trimmed)
events.push(event)
if (event.type === "response" && event.id === "prompt-1" && event.success === true) {
sawPromptAccepted = true
}
if (
event.type === "message_update" &&
event.assistantMessageEvent?.type === "text_delta"
) {
sawTextDelta = true
}
if (event.type === "message_end" && event.message?.role === "assistant") {
sawAssistantMessage = true
finish(true)
}
} catch {
// ignore non-JSON output
}
}
})
child.stderr.on("data", (chunk) => {
stderr += chunk.toString("utf-8")
})
child.on("close", () => {
if (!sawAssistantMessage) finish(false)
})
})
const ok = await done
return {
ok,
stdout,
stderr,
events,
sawPromptAccepted,
sawAssistantMessage,
sawTextDelta,
}
}
async function runRpcExtensionCommands(timeoutMs = 30_000) {
const child = spawn(
PI_BIN,
[
"--no-extensions",
"--mode",
"rpc",
"-e",
EXT_PATH,
"--provider",
"commandcode",
"--model",
TEST_MODEL,
],
{
cwd: PROJECT_DIR,
env,
stdio: ["pipe", "pipe", "pipe"],
},
)
let buffer = ""
let stderr = ""
const events = []
const waiters = []
const publish = (event) => {
events.push(event)
for (let index = waiters.length - 1; index >= 0; index -= 1) {
const waiter = waiters[index]
if (!waiter.predicate(event)) continue
waiters.splice(index, 1)
clearTimeout(waiter.timer)
waiter.resolve(event)
}
}
child.stdout.on("data", (chunk) => {
buffer += chunk.toString("utf-8")
const lines = buffer.split("\n")
buffer = lines.pop() ?? ""
for (const line of lines) {
if (!line.trim()) continue
try {
publish(JSON.parse(line))
} catch {
// Ignore non-JSON output.
}
}
})
child.stderr.on("data", (chunk) => {
stderr += chunk.toString("utf-8")
})
const waitFor = (predicate) =>
new Promise((resolve, reject) => {
const existing = events.find(predicate)
if (existing) {
resolve(existing)
return
}
const timer = setTimeout(() => {
const index = waiters.findIndex((waiter) => waiter.timer === timer)
if (index >= 0) waiters.splice(index, 1)
reject(new Error(`RPC event timeout. stderr: ${stderr.slice(-500)}`))
}, timeoutMs)
waiters.push({ predicate, resolve, timer })
})
const send = (value) => child.stdin.write(`${JSON.stringify(value)}\n`)
try {
send({ id: "commands", type: "get_commands" })
const commandsResponse = await waitFor(
(event) => event.type === "response" && event.id === "commands",
)
const commandNames = commandsResponse.data?.commands?.map((command) => command.name) ?? []
send({ id: "status-before", type: "prompt", message: "/commandcode-status" })
await waitFor(
(event) => event.type === "response" && event.id === "status-before" && event.success,
)
const statusBefore = await waitFor(
(event) =>
event.type === "extension_ui_request" &&
event.method === "notify" &&
typeof event.message === "string" &&
event.message.includes("model count: 3"),
)
includeRefreshedModel = true
send({ id: "refresh", type: "prompt", message: "/commandcode-refresh" })
await waitFor((event) => event.type === "response" && event.id === "refresh" && event.success)
const refreshNotification = await waitFor(
(event) =>
event.type === "extension_ui_request" &&
event.method === "notify" &&
typeof event.message === "string" &&
event.message.includes("4 models from live"),
)
send({ id: "status-after", type: "prompt", message: "/commandcode-status" })
await waitFor(
(event) => event.type === "response" && event.id === "status-after" && event.success,
)
const statusAfter = await waitFor(
(event) =>
event.type === "extension_ui_request" &&
event.method === "notify" &&
typeof event.message === "string" &&
event.message.includes("model count: 4"),
)
return {
commandNames,
statusBefore: statusBefore.message,
refreshNotification: refreshNotification.message,
statusAfter: statusAfter.message,
stderr,
}
} finally {
child.kill()
}
}
async function runRpcCompatCall(timeoutMs = 30_000) {
const child = spawn(
PI_BIN,
[
"--no-extensions",
"--mode",
"rpc",
"-e",
EXT_PATH,
"-e",
COMPAT_CALLER_EXT_PATH,
"--provider",
"commandcode",
"--model",
TEST_MODEL,
],
{
cwd: PROJECT_DIR,
env,
stdio: ["pipe", "pipe", "pipe"],
},
)
let buffer = ""
let stderr = ""
const notification = new Promise((resolve, reject) => {
const timer = setTimeout(
() => reject(new Error(`compat-call timeout. stderr: ${stderr.slice(-500)}`)),
timeoutMs,
)
child.stdout.on("data", (chunk) => {
buffer += chunk.toString("utf-8")
const lines = buffer.split("\n")
buffer = lines.pop() ?? ""
for (const line of lines) {
if (!line.trim()) continue
let event
try {
event = JSON.parse(line)
} catch {
continue
}
if (
event.type === "extension_ui_request" &&
event.method === "notify" &&
typeof event.message === "string" &&
event.message.startsWith("compat-call")
) {
clearTimeout(timer)
resolve(event.message)
}
}
})
child.stderr.on("data", (chunk) => {
stderr += chunk.toString("utf-8")
})
})
try {
child.stdin.write(
`${JSON.stringify({ id: "compat", type: "prompt", message: "/compat-call" })}\n`,
)
return { message: await notification, stderr }
} finally {
child.kill()
}
}
async function runRpcOverflowRecovery(timeoutMs = 60_000) {
const child = spawn(
PI_BIN,
[
"--no-extensions",
"--mode",
"rpc",
"-e",
EXT_PATH,
"--provider",
"commandcode",
"--model",
TEST_MODEL,
],
{
cwd: PROJECT_DIR,
env,
stdio: ["pipe", "pipe", "pipe"],
},
)
let buffer = ""
let stderr = ""
const events = []
let firstSettled = false
let recovered = false
const result = new Promise((resolve) => {
const timer = setTimeout(() => {
child.kill()
resolve({ ok: false })
}, timeoutMs)
const finish = (ok) => {
clearTimeout(timer)
child.kill()
resolve({ ok })
}
child.stdout.on("data", (chunk) => {
buffer += chunk.toString("utf-8")
const lines = buffer.split("\n")
buffer = lines.pop() ?? ""
for (const line of lines) {
if (!line.trim()) continue
let event
try {
event = JSON.parse(line)
} catch {
continue
}
events.push(event)
if (event.type === "agent_settled" && !firstSettled) {
firstSettled = true
child.stdin.write(
`${JSON.stringify({ id: "overflow-prompt", type: "prompt", message: "trigger overflow recovery" })}\n`,
)
}
if (
event.type === "compaction_end" &&
event.reason === "overflow" &&
event.willRetry === true
) {
recovered = true
}
if (recovered && event.type === "agent_settled") finish(true)
}
})
child.stderr.on("data", (chunk) => {
stderr += chunk.toString("utf-8")
})
child.stdin.write(
`${JSON.stringify({ id: "initial-prompt", type: "prompt", message: "initial turn" })}\n`,
)
})
const outcome = await result
return {
...outcome,
requests: overflowRequestCount,
sawNormalizedOverflow: events.some(
(event) =>
event.type === "message_end" &&
event.message?.role === "assistant" &&
typeof event.message.errorMessage === "string" &&
event.message.errorMessage.startsWith("context_length_exceeded:"),
),
sawCompactionRetry: events.some(
(event) => event.type === "compaction_end" && event.reason === "overflow" && event.willRetry,
),
stderrHasSecrets: /mock-key|user_secret|api_key/i.test(stderr),
}
}
try {
console.log("[pi-local] first offline start without a cache")
const onlineModelsUrl = env.COMMANDCODE_MODELS_URL
env.COMMANDCODE_MODELS_URL = "http://127.0.0.1:1/provider/v1/models"
const modelsCachePath = join(env.PI_CODING_AGENT_DIR, "commandcode-models.json")
rmSync(modelsCachePath, { force: true })
const firstOfflineList = await runPi(
["--no-extensions", "-e", EXT_PATH, "--list-models", "commandcode"],
20_000,
)
assert.equal(firstOfflineList.code, 0, firstOfflineList.stderr)
assert.doesNotMatch(firstOfflineList.stderr, /Failed to load extension/)
assert.match(
firstOfflineList.stdout || firstOfflineList.stderr,
/No models matching|No models available/,
)
assert.match(firstOfflineList.stderr, /no valid cached catalog/)
assert.match(firstOfflineList.stderr, /until \/commandcode-refresh succeeds/)
assert.throws(() => accessSync(modelsCachePath, constants.R_OK), /ENOENT|no such file/i)
// A fresh process re-runs the extension entrypoint, which is the same path /reload uses.
console.log("[pi-local] recover models after empty offline start")
env.COMMANDCODE_MODELS_URL = onlineModelsUrl
modelListRequestCount = 0
const recoveryList = await runPi(
["--no-extensions", "-e", EXT_PATH, "--list-models", "commandcode"],
20_000,
)
assert.equal(recoveryList.code, 0, recoveryList.stderr)
const recoveryOutput = recoveryList.stdout || recoveryList.stderr
assert.match(recoveryOutput, /gpt-5\.4/)
assert.match(recoveryOutput, /cc-second-model/)
assert.doesNotMatch(recoveryList.stderr, /no valid cached catalog/)
assert.doesNotMatch(recoveryList.stderr, /Failed to load extension/)
assert.equal(modelListRequestCount, 1)
assert.doesNotThrow(() => accessSync(modelsCachePath, constants.R_OK))
console.log("[pi-local] list models through real extension")
modelListRequestCount = 0
const list = await runPi(["--no-extensions", "-e", EXT_PATH, "--list-models"], 20_000)
assert.equal(list.code, 0, list.stderr)
const listOutput = list.stdout || list.stderr
assert.match(listOutput, /commandcode/)
assert.match(listOutput, /gpt-5\.4/)
assert.match(listOutput, /cc-second-model/)
assert.equal(modelListRequestCount, 1)
assert.doesNotThrow(() => accessSync(modelsCachePath, constants.R_OK))
console.log("[pi-local] list cached models while model discovery is offline")
env.COMMANDCODE_MODELS_URL = "http://127.0.0.1:1/provider/v1/models"
const offlineList = await runPi(
["--no-extensions", "-e", EXT_PATH, "--list-models", "commandcode"],
20_000,
)
assert.equal(offlineList.code, 0, offlineList.stderr)
const offlineListOutput = offlineList.stdout || offlineList.stderr
assert.match(offlineListOutput, /gpt-5\.4/)
assert.match(offlineListOutput, /cc-second-model/)
assert.match(offlineList.stderr, /Using the cached catalog/)
console.log("[pi-local] use a cached model while model discovery is offline")
requestCount = 0
const offlinePrint = await runPi(
[
"--no-extensions",
"-e",
EXT_PATH,
"-p",
"say mock token",
"--provider",
"commandcode",
"--model",
TEST_MODEL,
],
30_000,
)
assert.equal(offlinePrint.code, 0, offlinePrint.stderr)
assert.match(offlinePrint.stdout, /mock-pi-ok/)
assert.match(offlinePrint.stderr, /Using the cached catalog/)
assert.equal(requestCount, 1)
env.COMMANDCODE_MODELS_URL = onlineModelsUrl
console.log("[pi-local] discovery timeout through real extension")
rmSync(modelsCachePath, { force: true })
modelsDelayMs = 5_000
env.COMMANDCODE_MODELS_TIMEOUT_MS = "50"
const timeoutStartedAt = Date.now()
const timedOutList = await runPi(
["--no-extensions", "-e", EXT_PATH, "--list-models", "commandcode"],
5_000,
)
const timeoutElapsedMs = Date.now() - timeoutStartedAt
assert.equal(timedOutList.code, 0, timedOutList.stderr)
assert.ok(timeoutElapsedMs < 2_000, `model discovery took ${timeoutElapsedMs}ms`)
assert.match(timedOutList.stderr, /timed out after 50ms/i)
modelsDelayMs = 0
delete env.COMMANDCODE_MODELS_TIMEOUT_MS
console.log("[pi-local] print mode with reasoning and tool schemas")
requestCount = 0
const print = await runPi(
[
"--no-extensions",
"-e",
EXT_PATH,
"-p",
"say mock token",
"--provider",
"commandcode",
"--model",
TEST_MODEL,
"--thinking",
"high",
],
30_000,
)
assert.equal(print.code, 0, print.stderr)
assert.match(print.stdout, /mock-pi-ok/)
assert.equal(requestCount, 1)
assert.ok(
typeof lastRequestHeaders.authorization === "string" &&
lastRequestHeaders.authorization.startsWith("Bearer "),
"should send a bearer Authorization header",
)
assert.equal(lastRequestHeaders["x-cmd-zdr"], "1")
assert.equal(lastRequestBody?.model, TEST_MODEL)
assert.equal(lastRequestBody?.reasoning_effort, "high")
const sentTools = lastRequestBody?.tools
assert.ok(Array.isArray(sentTools) && sentTools.length > 0)
const editTool = sentTools.find((tool) => tool.function?.name === "edit")
assert.equal(editTool?.function?.parameters?.type, "object")
assert.equal(editTool?.function?.parameters?.properties?.edits?.type, "array")
assert.equal(editTool?.function?.parameters?.properties?.edits?.items?.type, "object")
assert.equal(
editTool?.function?.parameters?.properties?.edits?.items?.properties?.oldText?.type,
"string",
)
console.log("[pi-local] Claude request through Anthropic Messages endpoint")
requestCount = 0
const claudePrint = await runPi(
[
"--no-extensions",
"-e",
EXT_PATH,
"-p",
"say mock token",
"--provider",
"commandcode",
"--model",
CLAUDE_TEST_MODEL,
"--thinking",
"high",
],
30_000,
)
assert.equal(claudePrint.code, 0, claudePrint.stderr)
assert.match(claudePrint.stdout, /mock-pi-ok/)
assert.equal(requestCount, 1)
assert.equal(lastRequestBody?.model, CLAUDE_TEST_MODEL)
assert.equal(lastRequestBody?.thinking?.type, "adaptive")
assert.deepEqual(lastRequestBody?.output_config, { effort: "high" })
assert.equal(lastRequestHeaders["x-api-key"], "mock-key")
assert.equal(lastRequestHeaders["x-cmd-zdr"], "1")
console.log("[pi-local] runtime commands through real RPC extension lifecycle")
includeRefreshedModel = false
const runtimeCommands = await runRpcExtensionCommands()
assert.ok(runtimeCommands.commandNames.includes("commandcode-refresh"))
assert.ok(runtimeCommands.commandNames.includes("commandcode-status"))
assert.match(runtimeCommands.statusBefore, /source: live/)
assert.match(runtimeCommands.refreshNotification, /4 models from live/)
assert.match(runtimeCommands.statusAfter, /model count: 4/)
assert.doesNotMatch(
`${runtimeCommands.statusBefore}\n${runtimeCommands.statusAfter}\n${runtimeCommands.stderr}`,
/mock-key/,
)
console.log("[pi-local] RPC prompt through real extension and mock API")
requestCount = 0
const rpc = await runRpcQuery()
assert.equal(
rpc.ok,
true,
JSON.stringify(
{ stderr: rpc.stderr, stdout: rpc.stdout, events: rpc.events.slice(-10) },
null,
2,
),
)
assert.equal(rpc.sawPromptAccepted, true)
assert.equal(rpc.sawAssistantMessage, true)
assert.equal(rpc.sawTextDelta, true)
assert.equal(requestCount, 1)
console.log("[pi-local] forward image input through the documented provider schema")
requestCount = 0
const imageRpc = await runRpcQuery(10_000, "describe image", [], {
images: [
{
type: "image",
data: "iVBORw0KGgo=",
mimeType: "image/png",
},
],
})
assert.equal(imageRpc.ok, true, imageRpc.stderr)
assert.equal(requestCount, 1)
const imageContent = lastRequestBody?.messages?.find(
(message) => message.role === "user",
)?.content
assert.ok(Array.isArray(imageContent), JSON.stringify(lastRequestBody?.messages))
assert.ok(
imageContent.some((part) => part.type === "image_url"),
JSON.stringify(imageContent),
)
console.log("[pi-local] sibling extension streams through the pi-ai compat registry")
requestCount = 0
const compatCall = await runRpcCompatCall()
assert.equal(compatCall.message, "compat-call ok: mock-pi-ok", compatCall.stderr)
assert.equal(requestCount, 1)
assert.equal(lastRequestBody?.model, TEST_MODEL)
assert.ok(
typeof lastRequestHeaders.authorization === "string" &&
lastRequestHeaders.authorization.startsWith("Bearer "),
"compat call should send a bearer Authorization header",
)
console.log("[pi-local] verify overflow normalization and compaction recovery")
overflowMode = true
overflowRequestCount = 0
const overflowRpc = await runRpcOverflowRecovery()
assert.equal(overflowRpc.ok, true)
assert.ok(overflowRpc.requests >= 4)
assert.equal(overflowRpc.sawCompactionRetry, true, JSON.stringify(overflowRpc))
assert.equal(overflowRpc.stderrHasSecrets, false)
overflowMode = false
console.log("[pi-local] PASS")
} finally {
await new Promise((resolve) => server.close(resolve))
rmSync(tempHome, { recursive: true, force: true })
}