chore: add CI workflow (typecheck + prettier format check), tsconfig, and prettier formatting

This commit is contained in:
Patrick Wozniak
2026-05-05 13:56:41 +02:00
parent be6d17ea56
commit e9853eb4a4
18 changed files with 1488 additions and 250 deletions
+14 -3
View File
@@ -65,7 +65,15 @@ export async function collectEvents(
return await Promise.race([
collect(),
new Promise<AssistantMessageEvent[]>((_, reject) => {
setTimeout(() => reject(new Error(`Timed out collecting stream events after ${timeoutMs}ms`)), timeoutMs);
setTimeout(
() =>
reject(
new Error(
`Timed out collecting stream events after ${timeoutMs}ms`,
),
),
timeoutMs,
);
}),
]);
}
@@ -94,7 +102,9 @@ export interface TestDepsResult {
calculatedUsages: Usage[];
}
export function createTestDeps(overrides: Partial<CoreDependencies> = {}): TestDepsResult {
export function createTestDeps(
overrides: Partial<CoreDependencies> = {},
): TestDepsResult {
const calculatedUsages: Usage[] = [];
const streamCommandCode = createStreamCommandCode({
createStream: createTestEventStream,
@@ -197,7 +207,8 @@ export async function startMockCommandCodeServer(): Promise<MockCommandCodeServe
if (!ended) closedBeforeEnd = true;
});
const chunks = plan.chunks ?? (plan.events ?? []).map((event) => `${event}\n`);
const chunks =
plan.chunks ?? (plan.events ?? []).map((event) => `${event}\n`);
const delays = plan.delays ?? chunks.map(() => 0);
let index = 0;
+18 -7
View File
@@ -34,12 +34,17 @@ describe("streamCommandCode — abort behavior", () => {
controller.abort();
const { streamCommandCode } = createTestDeps({ apiBase: server.baseUrl() });
const events = await collectEvents(streamCommandCode(makeModel(), makeContext(), {
apiKey: "mock-key",
signal: controller.signal,
}));
const events = await collectEvents(
streamCommandCode(makeModel(), makeContext(), {
apiKey: "mock-key",
signal: controller.signal,
}),
);
assert.deepEqual(events.map((event) => event.type), ["start", "error"]);
assert.deepEqual(
events.map((event) => event.type),
["start", "error"],
);
const error = events.at(-1);
assert.equal(error?.type, "error");
if (error?.type !== "error") throw new Error("expected error");
@@ -65,13 +70,19 @@ describe("streamCommandCode — abort behavior", () => {
setTimeout(() => controller.abort(), 50);
const events = await collectEvents(stream, 2_000);
assert.ok(events.some((event) => event.type === "text_delta"), "stream should process data before abort");
assert.ok(
events.some((event) => event.type === "text_delta"),
"stream should process data before abort",
);
const error = events.at(-1);
assert.equal(error?.type, "error");
if (error?.type !== "error") throw new Error("expected error");
assert.equal(error.reason, "aborted");
assert.equal(error.error.errorMessage, "Request aborted");
await new Promise((resolve) => setTimeout(resolve, 50));
assert.ok(server.responseClosedBeforeEnd(), "abort should close the hanging upstream response");
assert.ok(
server.responseClosedBeforeEnd(),
"abort should close the hanging upstream response",
);
});
});
+252
View File
@@ -0,0 +1,252 @@
/**
* Tests for the Command Code OAuth / browser auth flow.
*
* Tests the local callback server (src/auth-server.ts) and the OAuth
* integration functions (src/oauth.ts).
*/
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { startAuthServer, type AuthCallback } from "../src/auth-server.ts";
import { getApiKey, login, refreshToken } from "../src/oauth.ts";
describe("startAuthServer()", () => {
it("starts on a random port and accepts a valid callback POST", async () => {
const { server, port, waitForCallback } = await startAuthServer();
const callbackData: AuthCallback = {
apiKey: "user_testKey123",
state: "test-state-token",
userId: "user_123",
userName: "Test User",
keyName: "test-key",
};
// Simulate the Command Code Studio posting the API key back
const response = await fetch(`http://127.0.0.1:${port}/callback`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Origin: "https://commandcode.ai",
},
body: JSON.stringify(callbackData),
});
assert.equal(response.status, 200);
const body = (await response.json()) as { success: boolean };
assert.equal(body.success, true);
const result = await waitForCallback;
assert.equal(result.apiKey, "user_testKey123");
assert.equal(result.state, "test-state-token");
assert.equal(result.userId, "user_123");
assert.equal(result.userName, "Test User");
assert.equal(result.keyName, "test-key");
// Server should close after successful callback
await new Promise((resolve) => server.on("close", resolve));
});
it("rejects when the callback indicates access_denied", async () => {
const { server, port, waitForCallback } = await startAuthServer();
const response = await fetch(`http://127.0.0.1:${port}/callback`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Origin: "https://commandcode.ai",
},
body: JSON.stringify({
error: "access_denied",
error_description: "User cancelled",
}),
});
assert.equal(response.status, 200);
await assert.rejects(() => waitForCallback, /User cancelled/);
await new Promise((resolve) => server.on("close", resolve));
});
it("returns 400 for missing required fields", async () => {
const { server, port } = await startAuthServer();
const response = await fetch(`http://127.0.0.1:${port}/callback`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Origin: "https://commandcode.ai",
},
body: JSON.stringify({ apiKey: "key", state: "s" }),
});
assert.equal(response.status, 400);
await new Promise((resolve) => setTimeout(resolve, 100));
server.close();
});
it("handles CORS preflight OPTIONS request", async () => {
const { server, port } = await startAuthServer();
const response = await fetch(`http://127.0.0.1:${port}/callback`, {
method: "OPTIONS",
headers: { Origin: "https://commandcode.ai" },
});
assert.equal(response.status, 204);
await new Promise((resolve) => setTimeout(resolve, 100));
server.close();
});
it("returns 404 for non-callback paths", async () => {
const { server, port } = await startAuthServer();
const response = await fetch(`http://127.0.0.1:${port}/other`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: "{}",
});
assert.equal(response.status, 404);
await new Promise((resolve) => setTimeout(resolve, 100));
server.close();
});
it("returns 405 for GET on /callback", async () => {
const { server, port } = await startAuthServer();
const response = await fetch(`http://127.0.0.1:${port}/callback`, {
method: "GET",
headers: { Origin: "https://commandcode.ai" },
});
assert.equal(response.status, 405);
await new Promise((resolve) => setTimeout(resolve, 100));
server.close();
});
});
describe("OAuth functions", () => {
it("getApiKey returns the access token", () => {
const creds = {
refresh: "refresh-key",
access: "access-key",
expires: Date.now() + 3600000,
};
assert.equal(getApiKey(creds), "access-key");
});
it("refreshToken returns updated far-future expiry", async () => {
const creds = {
refresh: "my-api-key",
access: "my-api-key",
expires: Date.now() - 1000, // already expired
};
const result = await refreshToken(creds);
assert.equal(result.access, "my-api-key");
assert.equal(result.refresh, "my-api-key");
assert.ok(result.expires > Date.now(), "expiry should be in the future");
});
});
describe("login()", () => {
it("completes the full browser login flow via the local server", async () => {
let authUrl = "";
const callbacks = {
onAuth(params: { url: string }) {
authUrl = params.url;
},
onPrompt(params: { message: string }): Promise<string> {
throw new Error("onPrompt should not be called in browser flow");
},
};
// Start login in the background
const loginPromise = login(callbacks);
// Verify the auth URL was passed to callbacks
assert.match(
authUrl,
/^https:\/\/commandcode\.ai\/studio\/auth\/cli\?callback=http:\/\/localhost:\d+\/callback&state=/,
);
// Extract port and state from the URL
const url = new URL(authUrl);
const port = parseInt(
url.searchParams.get("callback")?.match(/localhost:(\d+)/)?.[1] ?? "0",
);
const state = url.searchParams.get("state") ?? "";
assert.ok(port > 0, "auth server should be on a non-zero port");
assert.ok(state.length > 0, "state token should not be empty");
// Simulate the Command Code Studio posting the API key back
const response = await fetch(`http://127.0.0.1:${port}/callback`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Origin: "https://commandcode.ai",
},
body: JSON.stringify({
apiKey: "user_browserApiKey",
state,
userId: "user_456",
userName: "Browser User",
keyName: "browser-key",
}),
});
assert.equal(response.status, 200);
const result = await loginPromise;
assert.equal(result.access, "user_browserApiKey");
assert.equal(result.refresh, "user_browserApiKey");
assert.ok(
result.expires > Date.now(),
"expiry should be far in the future",
);
});
it("rejects on state token mismatch", async () => {
let authUrl = "";
const callbacks = {
onAuth(params: { url: string }) {
authUrl = params.url;
},
onPrompt(params: { message: string }): Promise<string> {
throw new Error("should not prompt");
},
};
const loginPromise = login(callbacks);
const url = new URL(authUrl);
const port = parseInt(
url.searchParams.get("callback")?.match(/localhost:(\d+)/)?.[1] ?? "0",
);
// Post back with a wrong state token
await fetch(`http://127.0.0.1:${port}/callback`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Origin: "https://commandcode.ai",
},
body: JSON.stringify({
apiKey: "user_badState",
state: "wrong-state-token",
userId: "user_789",
userName: "Attacker",
keyName: "evil-key",
}),
});
await assert.rejects(() => loginPromise, /State token mismatch/);
});
});
+97 -31
View File
@@ -6,7 +6,15 @@
import assert from "node:assert/strict";
import { spawn, spawnSync } from "node:child_process";
import { accessSync, constants, existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import {
accessSync,
constants,
existsSync,
mkdirSync,
mkdtempSync,
rmSync,
writeFileSync,
} from "node:fs";
import { createServer } from "node:http";
import { homedir, tmpdir } from "node:os";
import { delimiter, dirname, join, resolve } from "node:path";
@@ -59,7 +67,12 @@ const server = createServer((req, res) => {
}
requestCount += 1;
lastRequestHeaders = Object.fromEntries(Object.entries(req.headers).map(([key, value]) => [key, Array.isArray(value) ? value.join(", ") : value ?? ""]));
lastRequestHeaders = Object.fromEntries(
Object.entries(req.headers).map(([key, value]) => [
key,
Array.isArray(value) ? value.join(", ") : (value ?? ""),
]),
);
let body = "";
req.on("data", (chunk) => {
@@ -76,8 +89,12 @@ const server = createServer((req, res) => {
"Content-Type": "text/plain; charset=utf-8",
"Transfer-Encoding": "chunked",
});
res.write(`${JSON.stringify({ type: "text-delta", text: "mock-pi-ok" })}\n`);
res.write(`${JSON.stringify({ type: "finish", finishReason: "stop", totalUsage: { inputTokens: 1, outputTokens: 1 } })}\n`);
res.write(
`${JSON.stringify({ type: "text-delta", text: "mock-pi-ok" })}\n`,
);
res.write(
`${JSON.stringify({ type: "finish", finishReason: "stop", totalUsage: { inputTokens: 1, outputTokens: 1 } })}\n`,
);
res.end();
});
});
@@ -88,9 +105,11 @@ const port = typeof address === "object" && address ? address.port : 0;
const apiBase = `http://127.0.0.1:${port}`;
function hasLivePiAuth() {
return !!process.env.COMMANDCODE_API_KEY ||
return (
!!process.env.COMMANDCODE_API_KEY ||
existsSync(join(homedir(), ".commandcode", "auth.json")) ||
existsSync(join(homedir(), ".pi", "agent", "auth.json"));
existsSync(join(homedir(), ".pi", "agent", "auth.json"))
);
}
let tempHome;
@@ -105,7 +124,10 @@ if (hasLivePiAuth()) {
console.log("[pi-local] live pi auth not found; using mock auth fallback");
tempHome = mkdtempSync(join(tmpdir(), "pi-cc-home-"));
mkdirSync(join(tempHome, ".commandcode"), { recursive: true });
writeFileSync(join(tempHome, ".commandcode", "auth.json"), JSON.stringify({ apiKey: "mock-key" }));
writeFileSync(
join(tempHome, ".commandcode", "auth.json"),
JSON.stringify({ apiKey: "mock-key" }),
);
env.HOME = tempHome;
env.USERPROFILE = tempHome;
env.COMMANDCODE_API_KEY = "mock-key";
@@ -122,7 +144,11 @@ function runPi(args, timeoutMs = 30_000) {
let stderr = "";
const timer = setTimeout(() => {
child.kill();
resolve({ code: -1, stdout, stderr: `${stderr}\nTIMEOUT after ${timeoutMs}ms` });
resolve({
code: -1,
stdout,
stderr: `${stderr}\nTIMEOUT after ${timeoutMs}ms`,
});
}, timeoutMs);
child.stdout.on("data", (chunk) => {
stdout += chunk.toString("utf-8");
@@ -138,16 +164,24 @@ function runPi(args, timeoutMs = 30_000) {
}
async function runRpcQuery(timeoutMs = 30_000) {
const child = spawn(PI_BIN, [
"--mode", "rpc",
"-e", EXT_PATH,
"--provider", "commandcode",
"--model", TEST_MODEL,
], {
cwd: PROJECT_DIR,
env,
stdio: ["pipe", "pipe", "pipe"],
});
const child = spawn(
PI_BIN,
[
"--mode",
"rpc",
"-e",
EXT_PATH,
"--provider",
"commandcode",
"--model",
TEST_MODEL,
],
{
cwd: PROJECT_DIR,
env,
stdio: ["pipe", "pipe", "pipe"],
},
);
let stdout = "";
let stderr = "";
@@ -174,7 +208,9 @@ async function runRpcQuery(timeoutMs = 30_000) {
resolve(ok);
};
child.stdin.write(`${JSON.stringify({ id: "prompt-1", type: "prompt", message: "say mock token" })}\n`);
child.stdin.write(
`${JSON.stringify({ id: "prompt-1", type: "prompt", message: "say mock token" })}\n`,
);
child.stdout.on("data", (chunk) => {
const text = chunk.toString("utf-8");
@@ -188,13 +224,23 @@ async function runRpcQuery(timeoutMs = 30_000) {
try {
const event = JSON.parse(trimmed);
events.push(event);
if (event.type === "response" && event.id === "prompt-1" && event.success === true) {
if (
event.type === "response" &&
event.id === "prompt-1" &&
event.success === true
) {
sawPromptAccepted = true;
}
if (event.type === "message_update" && event.assistantMessageEvent?.type === "text_delta") {
if (
event.type === "message_update" &&
event.assistantMessageEvent?.type === "text_delta"
) {
sawTextDelta = true;
}
if (event.type === "message_end" && event.message?.role === "assistant") {
if (
event.type === "message_end" &&
event.message?.role === "assistant"
) {
sawAssistantMessage = true;
finish(true);
}
@@ -212,7 +258,15 @@ async function runRpcQuery(timeoutMs = 30_000) {
});
const ok = await done;
return { ok, stdout, stderr, events, sawPromptAccepted, sawAssistantMessage, sawTextDelta };
return {
ok,
stdout,
stderr,
events,
sawPromptAccepted,
sawAssistantMessage,
sawTextDelta,
};
}
try {
@@ -224,17 +278,25 @@ try {
console.log("[pi-local] print mode through real extension and mock API");
requestCount = 0;
const print = await runPi([
"-e", EXT_PATH,
"-p", "say mock token",
"--provider", "commandcode",
"--model", TEST_MODEL,
], 30_000);
const print = await runPi(
[
"-e",
EXT_PATH,
"-p",
"say mock token",
"--provider",
"commandcode",
"--model",
TEST_MODEL,
],
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 "),
typeof lastRequestHeaders.authorization === "string" &&
lastRequestHeaders.authorization.startsWith("Bearer "),
"should send a bearer Authorization header",
);
assert.equal(lastRequestBody?.params?.model, TEST_MODEL);
@@ -245,7 +307,11 @@ try {
assert.equal(
rpc.ok,
true,
JSON.stringify({ stderr: rpc.stderr, stdout: rpc.stdout, events: rpc.events.slice(-10) }, null, 2),
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);
+101 -19
View File
@@ -24,18 +24,40 @@ import { objectAt } from "./helpers.ts";
describe("getApiKey()", () => {
it("uses COMMANDCODE_API_KEY from provided env", () => {
assert.equal(getApiKey({ env: { COMMANDCODE_API_KEY: "env-key" }, authPaths: [] }), "env-key");
assert.equal(
getApiKey({ env: { COMMANDCODE_API_KEY: "env-key" }, authPaths: [] }),
"env-key",
);
});
it("reads apiKey and commandcode fields from explicit auth paths", () => {
it("reads apiKey, commandcode, and pi OAuth credential fields from explicit auth paths", () => {
const dir = mkdtempSync(join(tmpdir(), "cc-auth-"));
try {
const first = join(dir, "first.json");
const second = join(dir, "second.json");
const oauth = join(dir, "oauth.json");
writeFileSync(first, JSON.stringify({ apiKey: "file-key" }));
writeFileSync(second, JSON.stringify({ commandcode: "fallback-key" }));
assert.equal(getApiKey({ env: {}, authPaths: [first, second] }), "file-key");
writeFileSync(
oauth,
JSON.stringify({
commandcode: {
type: "oauth",
access: "oauth-access-key",
refresh: "oauth-refresh-key",
expires: Date.now() + 3600000,
},
}),
);
assert.equal(
getApiKey({ env: {}, authPaths: [first, second] }),
"file-key",
);
assert.equal(getApiKey({ env: {}, authPaths: [second] }), "fallback-key");
assert.equal(
getApiKey({ env: {}, authPaths: [oauth] }),
"oauth-access-key",
);
} finally {
rmSync(dir, { recursive: true, force: true });
}
@@ -57,7 +79,10 @@ describe("getApiKey()", () => {
try {
const authDir = join(dir, ".pi", "agent");
mkdirSync(authDir, { recursive: true });
writeFileSync(join(authDir, "auth.json"), JSON.stringify({ commandcode: "pi-key" }));
writeFileSync(
join(authDir, "auth.json"),
JSON.stringify({ commandcode: "pi-key" }),
);
assert.equal(getApiKey({ env: {}, homeDir: () => dir }), "pi-key");
} finally {
rmSync(dir, { recursive: true, force: true });
@@ -68,7 +93,13 @@ describe("getApiKey()", () => {
describe("textContent()", () => {
it("extracts and joins text blocks", () => {
assert.equal(
textContent({ content: [{ type: "text", text: "hello" }, { type: "image", data: "x" }, { type: "text", text: "world" }] }),
textContent({
content: [
{ type: "text", text: "hello" },
{ type: "image", data: "x" },
{ type: "text", text: "world" },
],
}),
"hello\nworld",
);
});
@@ -92,10 +123,13 @@ describe("toJsonSchema()", () => {
assert.deepEqual(toJsonSchema({ kind: "string" }), { type: "string" });
assert.deepEqual(toJsonSchema({ kind: "Number" }), { type: "number" });
assert.deepEqual(toJsonSchema({ kind: "boolean" }), { type: "boolean" });
assert.deepEqual(toJsonSchema({ kind: "string", enum: ["left", "right"] }), {
type: "string",
enum: ["left", "right"],
});
assert.deepEqual(
toJsonSchema({ kind: "string", enum: ["left", "right"] }),
{
type: "string",
enum: ["left", "right"],
},
);
assert.deepEqual(
toJsonSchema({
kind: "object",
@@ -106,12 +140,21 @@ describe("toJsonSchema()", () => {
}),
{
type: "object",
properties: { name: { type: "string" }, tags: { type: "array", items: { type: "string" } } },
properties: {
name: { type: "string" },
tags: { type: "array", items: { type: "string" } },
},
required: ["name"],
},
);
assert.deepEqual(toJsonSchema({ kind: "optional", wrapped: { kind: "string" } }), { type: "string" });
assert.deepEqual(toJsonSchema({ kind: "union", variants: [{}, { kind: "number" }] }), { type: "number" });
assert.deepEqual(
toJsonSchema({ kind: "optional", wrapped: { kind: "string" } }),
{ type: "string" },
);
assert.deepEqual(
toJsonSchema({ kind: "union", variants: [{}, { kind: "number" }] }),
{ type: "number" },
);
});
it("preserves explicit required arrays and handles unknown values", () => {
@@ -174,7 +217,12 @@ describe("messagesToCC()", () => {
content: [
{ type: "thinking", thinking: "I will read" },
{ type: "text", text: "Sure" },
{ type: "toolCall", id: "c1", name: "read", arguments: { path: "/tmp/test" } },
{
type: "toolCall",
id: "c1",
name: "read",
arguments: { path: "/tmp/test" },
},
],
},
{
@@ -182,7 +230,10 @@ describe("messagesToCC()", () => {
toolCallId: "c1",
toolName: "read",
isError: false,
content: [{ type: "text", text: "hello" }, { type: "text", text: "world" }],
content: [
{ type: "text", text: "hello" },
{ type: "text", text: "world" },
],
},
]);
@@ -191,7 +242,32 @@ describe("messagesToCC()", () => {
assert.equal(objectAt(result, ["1", "content", "0", "type"]), "reasoning");
assert.equal(objectAt(result, ["1", "content", "2", "type"]), "tool-call");
assert.equal(objectAt(result, ["2", "role"]), "tool");
assert.equal(objectAt(result, ["2", "content", "0", "output", "value"]), "hello\nworld");
assert.equal(
objectAt(result, ["2", "content", "0", "output", "value"]),
"hello\nworld",
);
});
it("drops orphaned tool calls that have no matching tool result", () => {
const result = messagesToCC([
{ role: "user", content: "edit a file" },
{
role: "assistant",
content: [
{ type: "text", text: "I will edit it" },
{
type: "toolCall",
id: "missing-result",
name: "edit",
arguments: { path: "x" },
},
],
},
]);
assert.equal(objectAt(result, ["1", "role"]), "assistant");
assert.equal(objectAt(result, ["1", "content", "0", "type"]), "text");
assert.equal(objectAt(result, ["1", "content", "1"]), undefined);
});
it("handles empty conversations", () => {
@@ -201,11 +277,17 @@ describe("messagesToCC()", () => {
describe("parseStreamEventLine()", () => {
it("parses plain JSON and SSE data lines", () => {
assert.deepEqual(parseStreamEventLine('{"type":"text-delta","text":"x"}'), { type: "text-delta", text: "x" });
assert.deepEqual(parseStreamEventLine('data: {"type":"finish","finishReason":"stop"}'), {
type: "finish",
finishReason: "stop",
assert.deepEqual(parseStreamEventLine('{"type":"text-delta","text":"x"}'), {
type: "text-delta",
text: "x",
});
assert.deepEqual(
parseStreamEventLine('data: {"type":"finish","finishReason":"stop"}'),
{
type: "finish",
finishReason: "stop",
},
);
});
it("ignores comments, event labels, done markers, and malformed JSON", () => {
+134 -56
View File
@@ -47,9 +47,11 @@ const RPC_START_TIMEOUT = 15_000;
const RPC_QUERY_TIMEOUT = 60_000;
function hasCommandCodeAuth() {
return !!process.env.COMMANDCODE_API_KEY ||
return (
!!process.env.COMMANDCODE_API_KEY ||
existsSync(join(homedir(), ".commandcode", "auth.json")) ||
existsSync(join(homedir(), ".pi", "agent", "auth.json"));
existsSync(join(homedir(), ".pi", "agent", "auth.json"))
);
}
const HAS_AUTH = hasCommandCodeAuth();
@@ -76,7 +78,9 @@ function kill(child) {
async function runPrintMode() {
if (!HAS_AUTH) {
console.log("[smoke] SKIP — Command Code auth not found, skipping print mode test\n");
console.log(
"[smoke] SKIP — Command Code auth not found, skipping print mode test\n",
);
skipped++;
return;
}
@@ -87,23 +91,37 @@ async function runPrintMode() {
}
console.log(`[smoke] Running pi in print mode with extension: ${EXT_PATH}`);
console.log(`[smoke] ${PI_BIN} -e ${EXT_PATH} -p "say hi" --provider commandcode --model ${TEST_MODEL}\n`);
console.log(
`[smoke] ${PI_BIN} -e ${EXT_PATH} -p "say hi" --provider commandcode --model ${TEST_MODEL}\n`,
);
const child = spawn(PI_BIN, [
"-e", EXT_PATH,
"-p", "say hi in one word",
"--provider", "commandcode",
"--model", TEST_MODEL,
], {
env: { ...process.env },
stdio: ["ignore", "pipe", "pipe"],
});
const child = spawn(
PI_BIN,
[
"-e",
EXT_PATH,
"-p",
"say hi in one word",
"--provider",
"commandcode",
"--model",
TEST_MODEL,
],
{
env: { ...process.env },
stdio: ["ignore", "pipe", "pipe"],
},
);
let stdout = "";
let stderr = "";
child.stdout.on("data", (d) => { stdout += d.toString(); });
child.stderr.on("data", (d) => { stderr += d.toString(); });
child.stdout.on("data", (d) => {
stdout += d.toString();
});
child.stderr.on("data", (d) => {
stderr += d.toString();
});
const done = new Promise((resolve) => {
const timer = setTimeout(() => {
@@ -115,11 +133,17 @@ async function runPrintMode() {
child.on("close", (code) => {
clearTimeout(timer);
if (code === 0) {
console.log("[smoke] PASS — extension loaded and agent ran without crash");
console.log(`[smoke] stdout (last 300 chars): ${stdout.slice(-300).trim()}`);
console.log(
"[smoke] PASS — extension loaded and agent ran without crash",
);
console.log(
`[smoke] stdout (last 300 chars): ${stdout.slice(-300).trim()}`,
);
} else {
console.log(`[smoke] FAIL — exit code ${code}`);
console.log(`[smoke] stderr (last 500 chars): ${stderr.slice(-500).trim()}`);
console.log(
`[smoke] stderr (last 500 chars): ${stderr.slice(-500).trim()}`,
);
}
resolve(code === 0);
});
@@ -136,7 +160,9 @@ async function runPrintMode() {
async function runListModels() {
if (!HAS_AUTH) {
console.log("[smoke] SKIP — Command Code auth not found, skipping model list test\n");
console.log(
"[smoke] SKIP — Command Code auth not found, skipping model list test\n",
);
skipped++;
return;
}
@@ -146,19 +172,20 @@ async function runListModels() {
return;
}
console.log(`[smoke] Checking that models are discoverable via pi --list-models\n`);
console.log(
`[smoke] Checking that models are discoverable via pi --list-models\n`,
);
const child = spawn(PI_BIN, [
"-e", EXT_PATH,
"--list-models",
], {
const child = spawn(PI_BIN, ["-e", EXT_PATH, "--list-models"], {
env: { ...process.env },
stdio: ["ignore", "pipe", "pipe"],
});
let stdout = "";
child.stdout.on("data", (d) => { stdout += d.toString(); });
child.stdout.on("data", (d) => {
stdout += d.toString();
});
const done = new Promise((resolve) => {
const timer = setTimeout(() => {
@@ -172,8 +199,12 @@ async function runListModels() {
if (code === 0 && stdout.includes("commandcode")) {
console.log("[smoke] PASS — commandcode provider models are listed");
} else {
console.log("[smoke] FAIL — commandcode models not found or error listing");
console.log(`[smoke] stdout (last 500 chars): ${stdout.slice(-500).trim()}`);
console.log(
"[smoke] FAIL — commandcode models not found or error listing",
);
console.log(
`[smoke] stdout (last 500 chars): ${stdout.slice(-500).trim()}`,
);
}
resolve(code === 0 && stdout.includes("commandcode"));
});
@@ -190,12 +221,16 @@ async function runListModels() {
async function runRpcStartup() {
if (!HAS_AUTH) {
console.log("[smoke] SKIP — Command Code auth not found, skipping RPC startup test\n");
console.log(
"[smoke] SKIP — Command Code auth not found, skipping RPC startup test\n",
);
skipped++;
return;
}
if (!HAS_PI) {
console.log("[smoke] SKIP — pi is not on PATH, skipping RPC startup test\n");
console.log(
"[smoke] SKIP — pi is not on PATH, skipping RPC startup test\n",
);
skipped++;
return;
}
@@ -203,10 +238,7 @@ async function runRpcStartup() {
console.log(`[smoke] Testing RPC mode startup with extension\n`);
console.log(`[smoke] ${PI_BIN} --mode rpc -e ${EXT_PATH}\n`);
const child = spawn(PI_BIN, [
"--mode", "rpc",
"-e", EXT_PATH,
], {
const child = spawn(PI_BIN, ["--mode", "rpc", "-e", EXT_PATH], {
env: { ...process.env },
stdio: ["pipe", "pipe", "pipe"],
});
@@ -226,13 +258,20 @@ async function runRpcStartup() {
try {
const msg = JSON.parse(trimmed);
events.push(msg);
if (msg.type === "response" && msg.id === "state-1" && msg.command === "get_state" && msg.success === true) {
if (
msg.type === "response" &&
msg.id === "state-1" &&
msg.command === "get_state" &&
msg.success === true
) {
sawStateResponse = true;
console.log("[smoke] RPC received get_state response");
}
if (msg.type === "error" || msg.type === "fatal") {
sawError = true;
console.error(`[smoke] RPC error: ${JSON.stringify(msg).slice(0, 300)}`);
console.error(
`[smoke] RPC error: ${JSON.stringify(msg).slice(0, 300)}`,
);
}
} catch {
// ignore non-JSON
@@ -241,7 +280,9 @@ async function runRpcStartup() {
});
const result = new Promise((resolve) => {
child.stdin.write(JSON.stringify({ id: "state-1", type: "get_state" }) + "\n");
child.stdin.write(
JSON.stringify({ id: "state-1", type: "get_state" }) + "\n",
);
const timer = setTimeout(async () => {
if (sawStateResponse) {
@@ -252,14 +293,18 @@ async function runRpcStartup() {
resolve(false);
}
// Send quit
try { child.stdin.write(JSON.stringify({ type: "quit" }) + "\n"); } catch {}
try {
child.stdin.write(JSON.stringify({ type: "quit" }) + "\n");
} catch {}
kill(child);
}, RPC_START_TIMEOUT);
child.on("close", (code) => {
clearTimeout(timer);
if (!sawStateResponse && !sawError) {
console.log(`[smoke] FAIL — pi exited with code ${code} before get_state response`);
console.log(
`[smoke] FAIL — pi exited with code ${code} before get_state response`,
);
resolve(false);
}
});
@@ -276,7 +321,9 @@ async function runRpcStartup() {
async function runRpcQuery() {
if (!HAS_AUTH) {
console.log("[smoke] SKIP — Command Code auth not found, skipping RPC prompt test\n");
console.log(
"[smoke] SKIP — Command Code auth not found, skipping RPC prompt test\n",
);
skipped++;
return;
}
@@ -287,17 +334,27 @@ async function runRpcQuery() {
}
console.log(`[smoke] Testing RPC prompt flow\n`);
console.log(`[smoke] pi --mode rpc -e ${EXT_PATH} → prompt "say hi" → expect response\n`);
console.log(
`[smoke] pi --mode rpc -e ${EXT_PATH} → prompt "say hi" → expect response\n`,
);
const child = spawn(PI_BIN, [
"--mode", "rpc",
"-e", EXT_PATH,
"--provider", "commandcode",
"--model", TEST_MODEL,
], {
env: { ...process.env },
stdio: ["pipe", "pipe", "pipe"],
});
const child = spawn(
PI_BIN,
[
"--mode",
"rpc",
"-e",
EXT_PATH,
"--provider",
"commandcode",
"--model",
TEST_MODEL,
],
{
env: { ...process.env },
stdio: ["pipe", "pipe", "pipe"],
},
);
let sawPromptAccepted = false;
let sawAssistantMessage = false;
@@ -312,12 +369,19 @@ async function runRpcQuery() {
if (!trimmed) continue;
try {
const msg = JSON.parse(trimmed);
if (msg.type === "response" && msg.id === "prompt-1" && msg.command === "prompt" && msg.success === true) {
if (
msg.type === "response" &&
msg.id === "prompt-1" &&
msg.command === "prompt" &&
msg.success === true
) {
sawPromptAccepted = true;
}
if (msg.type === "message_end" && msg.message?.role === "assistant") {
sawAssistantMessage = true;
console.log("[smoke] PASS — received assistant message_end in RPC mode");
console.log(
"[smoke] PASS — received assistant message_end in RPC mode",
);
}
} catch {
// ignore
@@ -326,7 +390,13 @@ async function runRpcQuery() {
});
const result = new Promise((resolve) => {
child.stdin.write(JSON.stringify({ id: "prompt-1", type: "prompt", message: "say hi in one word" }) + "\n");
child.stdin.write(
JSON.stringify({
id: "prompt-1",
type: "prompt",
message: "say hi in one word",
}) + "\n",
);
console.log("[smoke] Sent RPC prompt");
const timer = setTimeout(() => {
@@ -334,10 +404,14 @@ async function runRpcQuery() {
console.log("[smoke] PASS — full RPC prompt/response cycle works");
resolve(true);
} else {
console.log("[smoke] WARN — no assistant message_end received (may still be streaming)");
console.log(
"[smoke] WARN — no assistant message_end received (may still be streaming)",
);
resolve(false);
}
try { child.stdin.write(JSON.stringify({ type: "quit" }) + "\n"); } catch {}
try {
child.stdin.write(JSON.stringify({ type: "quit" }) + "\n");
} catch {}
kill(child);
}, RPC_QUERY_TIMEOUT);
@@ -362,7 +436,9 @@ async function runRpcQuery() {
console.log("=".repeat(60));
console.log(" pi-commandcode-provider Integration Smoke Test");
console.log("=".repeat(60));
console.log(` Auth: ${HAS_AUTH ? "✓ found" : "✗ not found (tests will be skipped)"}`);
console.log(
` Auth: ${HAS_AUTH ? "✓ found" : "✗ not found (tests will be skipped)"}`,
);
console.log(` Extension: ${EXT_PATH}`);
console.log("=".repeat(60));
console.log("");
@@ -374,7 +450,9 @@ await runRpcQuery();
console.log("");
console.log("=".repeat(60));
console.log(` SUITE RESULT: ${passed} passed, ${failed} failed, ${skipped} skipped`);
console.log(
` SUITE RESULT: ${passed} passed, ${failed} failed, ${skipped} skipped`,
);
console.log("=".repeat(60));
process.exit(failed > 0 ? 1 : 0);
+158 -46
View File
@@ -37,8 +37,14 @@ function eventTypes(events: readonly AssistantMessageEvent[]): string[] {
describe("streamCommandCode — auth", () => {
it("emits a missing-key error without touching the network", async () => {
const { streamCommandCode } = createTestDeps({ apiBase: server.baseUrl(), env: {}, authPaths: [] });
const stream = streamCommandCode(makeModel(), makeContext(), { apiKey: "" });
const { streamCommandCode } = createTestDeps({
apiBase: server.baseUrl(),
env: {},
authPaths: [],
});
const stream = streamCommandCode(makeModel(), makeContext(), {
apiKey: "",
});
const events = await collectEvents(stream);
assert.deepEqual(eventTypes(events), ["error"]);
@@ -53,11 +59,19 @@ describe("streamCommandCode — auth", () => {
type: "success",
events: [JSON.stringify({ type: "finish", finishReason: "stop" })],
});
const { streamCommandCode } = createTestDeps({ apiBase: server.baseUrl(), env: { COMMANDCODE_API_KEY: "env-key" } });
const { streamCommandCode } = createTestDeps({
apiBase: server.baseUrl(),
env: { COMMANDCODE_API_KEY: "env-key" },
});
await collectEvents(streamCommandCode(makeModel(), makeContext(), { apiKey: "option-key" }));
await collectEvents(
streamCommandCode(makeModel(), makeContext(), { apiKey: "option-key" }),
);
assert.equal(server.lastRequestHeaders().authorization, "Bearer option-key");
assert.equal(
server.lastRequestHeaders().authorization,
"Bearer option-key",
);
});
});
@@ -79,17 +93,33 @@ describe("streamCommandCode — successful streams", () => {
}),
],
});
const { streamCommandCode, calculatedUsages } = createTestDeps({ apiBase: server.baseUrl() });
const { streamCommandCode, calculatedUsages } = createTestDeps({
apiBase: server.baseUrl(),
});
const events = await collectEvents(streamCommandCode(makeModel(), makeContext(), { apiKey: "mock-key" }));
const events = await collectEvents(
streamCommandCode(makeModel(), makeContext(), { apiKey: "mock-key" }),
);
assert.deepEqual(eventTypes(events), ["start", "text_start", "text_delta", "text_delta", "text_end", "done"]);
assert.deepEqual(eventTypes(events), [
"start",
"text_start",
"text_delta",
"text_delta",
"text_end",
"done",
]);
const done = events.at(-1);
assert.equal(done?.type, "done");
if (done?.type !== "done") throw new Error("expected done");
assert.equal(done.reason, "stop");
assert.equal(done.message.content[0]?.type, "text");
assert.equal(done.message.content[0]?.type === "text" ? done.message.content[0].text : "", "Hello");
assert.equal(
done.message.content[0]?.type === "text"
? done.message.content[0].text
: "",
"Hello",
);
assert.equal(done.message.usage.totalTokens, 11);
assert.equal(calculatedUsages.length, 1);
});
@@ -105,11 +135,17 @@ describe("streamCommandCode — successful streams", () => {
});
const { streamCommandCode } = createTestDeps({ apiBase: server.baseUrl() });
const events = await collectEvents(streamCommandCode(makeModel(), makeContext(), { apiKey: "mock-key" }), 500);
const events = await collectEvents(
streamCommandCode(makeModel(), makeContext(), { apiKey: "mock-key" }),
500,
);
assert.equal(events.at(-1)?.type, "done");
await new Promise((resolve) => setTimeout(resolve, 50));
assert.ok(server.responseClosedBeforeEnd(), "client should cancel the still-open response body");
assert.ok(
server.responseClosedBeforeEnd(),
"client should cancel the still-open response body",
);
});
it("emits reasoning and tool-call blocks in order", async () => {
@@ -119,13 +155,20 @@ describe("streamCommandCode — successful streams", () => {
JSON.stringify({ type: "reasoning-delta", text: "think" }),
JSON.stringify({ type: "reasoning-end" }),
JSON.stringify({ type: "text-delta", text: "Using tool" }),
JSON.stringify({ type: "tool-call", toolCallId: "call_1", toolName: "read_file", input: { path: "/tmp/x" } }),
JSON.stringify({
type: "tool-call",
toolCallId: "call_1",
toolName: "read_file",
input: JSON.stringify({ path: "/tmp/x" }),
}),
JSON.stringify({ type: "finish", finishReason: "tool-calls" }),
],
});
const { streamCommandCode } = createTestDeps({ apiBase: server.baseUrl() });
const events = await collectEvents(streamCommandCode(makeModel(), makeContext(), { apiKey: "mock-key" }));
const events = await collectEvents(
streamCommandCode(makeModel(), makeContext(), { apiKey: "mock-key" }),
);
assert.deepEqual(eventTypes(events), [
"start",
@@ -142,9 +185,15 @@ describe("streamCommandCode — successful streams", () => {
const done = events.at(-1);
if (done?.type !== "done") throw new Error("expected done");
assert.equal(done.reason, "toolUse");
assert.deepEqual(done.message.content.map((content) => content.type), ["thinking", "text", "toolCall"]);
assert.deepEqual(
done.message.content.map((content) => content.type),
["thinking", "text", "toolCall"],
);
const toolCall = done.message.content[2];
assert.equal(toolCall?.type === "toolCall" ? toolCall.name : "", "read_file");
assert.equal(
toolCall?.type === "toolCall" ? toolCall.name : "",
"read_file",
);
});
it("flushes reasoning if finish arrives without reasoning-end", async () => {
@@ -157,7 +206,9 @@ describe("streamCommandCode — successful streams", () => {
});
const { streamCommandCode } = createTestDeps({ apiBase: server.baseUrl() });
const events = await collectEvents(streamCommandCode(makeModel(), makeContext(), { apiKey: "mock-key" }));
const events = await collectEvents(
streamCommandCode(makeModel(), makeContext(), { apiKey: "mock-key" }),
);
const done = events.at(-1);
if (done?.type !== "done") throw new Error("expected done");
@@ -167,67 +218,109 @@ describe("streamCommandCode — successful streams", () => {
describe("streamCommandCode — request serialization", () => {
it("sends the expected request body and default headers", async () => {
server.mockResponse({ type: "success", events: [JSON.stringify({ type: "finish", finishReason: "stop" })] });
server.mockResponse({
type: "success",
events: [JSON.stringify({ type: "finish", finishReason: "stop" })],
});
const { streamCommandCode } = createTestDeps({ apiBase: server.baseUrl() });
const context = makeContext({
messages: [
{ role: "user", content: "first" },
{ role: "assistant", content: [{ type: "text", text: "first response" }] },
{
role: "assistant",
content: [{ type: "text", text: "first response" }],
},
{ role: "user", content: "second" },
],
tools: [
{
name: "get_weather",
description: "Get weather",
parameters: { kind: "object", properties: { city: { kind: "string" } } },
parameters: {
kind: "object",
properties: { city: { kind: "string" } },
},
},
],
});
await collectEvents(streamCommandCode(makeModel(), context, { apiKey: "mock-key", maxTokens: 500 }));
await collectEvents(
streamCommandCode(makeModel(), context, {
apiKey: "mock-key",
maxTokens: 500,
}),
);
const body = server.lastRequestBody();
assert.equal(objectAt(body, ["config", "workingDir"]), "/repo");
assert.equal(objectAt(body, ["config", "date"]), "2026-05-05");
assert.equal(objectAt(body, ["params", "model"]), "deepseek/deepseek-v4-flash");
assert.equal(
objectAt(body, ["params", "model"]),
"deepseek/deepseek-v4-flash",
);
assert.equal(objectAt(body, ["params", "stream"]), true);
assert.equal(objectAt(body, ["params", "max_tokens"]), 500);
assert.equal(objectAt(body, ["params", "system"]), "You are a test assistant.");
assert.equal(objectAt(body, ["params", "messages", "1", "content", "0", "text"]), "first response");
assert.equal(objectAt(body, ["params", "tools", "0", "name"]), "get_weather");
assert.equal(
objectAt(body, ["params", "system"]),
"You are a test assistant.",
);
assert.equal(
objectAt(body, ["params", "messages", "1", "content", "0", "text"]),
"first response",
);
assert.equal(
objectAt(body, ["params", "tools", "0", "name"]),
"get_weather",
);
const headers = server.lastRequestHeaders();
assert.equal(headers.authorization, "Bearer mock-key");
assert.equal(headers["x-command-code-version"], "0.24.1");
assert.equal(headers["x-session-id"], "00000000-0000-4000-8000-000000000000");
assert.equal(
headers["x-session-id"],
"00000000-0000-4000-8000-000000000000",
);
});
it("caps maxTokens and passes custom headers", async () => {
server.mockResponse({ type: "success", events: [JSON.stringify({ type: "finish", finishReason: "stop" })] });
server.mockResponse({
type: "success",
events: [JSON.stringify({ type: "finish", finishReason: "stop" })],
});
const { streamCommandCode } = createTestDeps({ apiBase: server.baseUrl() });
await collectEvents(streamCommandCode(makeModel({ maxTokens: 500_000 }), makeContext(), {
apiKey: "mock-key",
maxTokens: 500_000,
headers: { "x-custom": "value" },
}));
await collectEvents(
streamCommandCode(makeModel({ maxTokens: 500_000 }), makeContext(), {
apiKey: "mock-key",
maxTokens: 500_000,
headers: { "x-custom": "value" },
}),
);
assert.equal(objectAt(server.lastRequestBody(), ["params", "max_tokens"]), 200_000);
assert.equal(
objectAt(server.lastRequestBody(), ["params", "max_tokens"]),
200_000,
);
assert.equal(server.lastRequestHeaders()["x-custom"], "value");
});
it("runs onPayload and onResponse hooks", async () => {
server.mockResponse({ type: "success", events: [JSON.stringify({ type: "finish", finishReason: "stop" })] });
server.mockResponse({
type: "success",
events: [JSON.stringify({ type: "finish", finishReason: "stop" })],
});
const { streamCommandCode } = createTestDeps({ apiBase: server.baseUrl() });
let responseStatus = 0;
await collectEvents(streamCommandCode(makeModel(), makeContext(), {
apiKey: "mock-key",
onPayload: () => ({ replaced: true }),
onResponse: (response) => {
responseStatus = response.status;
},
}));
await collectEvents(
streamCommandCode(makeModel(), makeContext(), {
apiKey: "mock-key",
onPayload: () => ({ replaced: true }),
onResponse: (response) => {
responseStatus = response.status;
},
}),
);
assert.equal(objectAt(server.lastRequestBody(), ["replaced"]), true);
assert.equal(responseStatus, 200);
@@ -239,7 +332,9 @@ describe("streamCommandCode — upstream errors and malformed streams", () => {
server.mockResponse({ type: "error", status: 429, body: "rate limited" });
const { streamCommandCode } = createTestDeps({ apiBase: server.baseUrl() });
const events = await collectEvents(streamCommandCode(makeModel(), makeContext(), { apiKey: "mock-key" }));
const events = await collectEvents(
streamCommandCode(makeModel(), makeContext(), { apiKey: "mock-key" }),
);
assert.deepEqual(eventTypes(events), ["start", "error"]);
const error = events.at(-1);
@@ -251,11 +346,18 @@ describe("streamCommandCode — upstream errors and malformed streams", () => {
it("emits error for provider error events", async () => {
server.mockResponse({
type: "success",
events: [JSON.stringify({ type: "error", error: { message: "provider failed" } })],
events: [
JSON.stringify({
type: "error",
error: { message: "provider failed" },
}),
],
});
const { streamCommandCode } = createTestDeps({ apiBase: server.baseUrl() });
const events = await collectEvents(streamCommandCode(makeModel(), makeContext(), { apiKey: "mock-key" }));
const events = await collectEvents(
streamCommandCode(makeModel(), makeContext(), { apiKey: "mock-key" }),
);
const error = events.at(-1);
assert.equal(error?.type, "error");
@@ -265,7 +367,10 @@ describe("streamCommandCode — upstream errors and malformed streams", () => {
it("handles SSE lines, malformed lines, split chunks, and final line without newline", async () => {
const textEvent = `data: ${JSON.stringify({ type: "text-delta", text: "split" })}\n`;
const finishEvent = JSON.stringify({ type: "finish", finishReason: "max_tokens" });
const finishEvent = JSON.stringify({
type: "finish",
finishReason: "max_tokens",
});
server.mockResponse({
type: "success",
chunks: [
@@ -279,11 +384,18 @@ describe("streamCommandCode — upstream errors and malformed streams", () => {
});
const { streamCommandCode } = createTestDeps({ apiBase: server.baseUrl() });
const events = await collectEvents(streamCommandCode(makeModel(), makeContext(), { apiKey: "mock-key" }));
const events = await collectEvents(
streamCommandCode(makeModel(), makeContext(), { apiKey: "mock-key" }),
);
const done = events.at(-1);
if (done?.type !== "done") throw new Error("expected done");
assert.equal(done.reason, "length");
assert.equal(done.message.content[0]?.type === "text" ? done.message.content[0].text : "", "split");
assert.equal(
done.message.content[0]?.type === "text"
? done.message.content[0].text
: "",
"split",
);
});
});