309 lines
8.8 KiB
TypeScript
309 lines
8.8 KiB
TypeScript
/**
|
|
* Unit tests for the real pure helpers exported by src/core.ts.
|
|
* These are hermetic: no pi runtime and no network.
|
|
*/
|
|
|
|
import assert from "node:assert/strict";
|
|
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { describe, it } from "node:test";
|
|
|
|
import {
|
|
getApiKey,
|
|
getEnvironmentInfo,
|
|
mapFinishReason,
|
|
messagesToCC,
|
|
parseStreamEventLine,
|
|
textContent,
|
|
toJsonSchema,
|
|
toolsToJson,
|
|
} from "../src/core.ts";
|
|
|
|
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",
|
|
);
|
|
});
|
|
|
|
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" }));
|
|
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 });
|
|
}
|
|
});
|
|
|
|
it("ignores malformed auth files", () => {
|
|
const dir = mkdtempSync(join(tmpdir(), "cc-auth-bad-"));
|
|
try {
|
|
const bad = join(dir, "bad.json");
|
|
writeFileSync(bad, "not json");
|
|
assert.equal(getApiKey({ env: {}, authPaths: [bad] }), undefined);
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it("uses injected homeDir for default auth paths", () => {
|
|
const dir = mkdtempSync(join(tmpdir(), "cc-home-"));
|
|
try {
|
|
const authDir = join(dir, ".pi", "agent");
|
|
mkdirSync(authDir, { recursive: true });
|
|
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 });
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("textContent()", () => {
|
|
it("extracts and joins text blocks", () => {
|
|
assert.equal(
|
|
textContent({
|
|
content: [
|
|
{ type: "text", text: "hello" },
|
|
{ type: "image", data: "x" },
|
|
{ type: "text", text: "world" },
|
|
],
|
|
}),
|
|
"hello\nworld",
|
|
);
|
|
});
|
|
|
|
it("handles empty or missing content", () => {
|
|
assert.equal(textContent({ content: [] }), "");
|
|
assert.equal(textContent({}), "");
|
|
});
|
|
});
|
|
|
|
describe("getEnvironmentInfo()", () => {
|
|
it("returns platform, arch, and Node version", () => {
|
|
const info = getEnvironmentInfo();
|
|
assert.match(info, /^(darwin|linux|win32)-/);
|
|
assert.ok(info.includes("Node.js"));
|
|
});
|
|
});
|
|
|
|
describe("toJsonSchema()", () => {
|
|
it("converts scalar, enum, object, optional, array, and union schema shapes", () => {
|
|
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: "object",
|
|
properties: {
|
|
name: { kind: "string" },
|
|
tags: { kind: "array", items: { kind: "string" }, optional: true },
|
|
},
|
|
}),
|
|
{
|
|
type: "object",
|
|
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" },
|
|
);
|
|
});
|
|
|
|
it("preserves explicit required arrays and handles unknown values", () => {
|
|
assert.deepEqual(
|
|
toJsonSchema({
|
|
type: "object",
|
|
properties: { name: { type: "string" }, nickname: { type: "string" } },
|
|
required: ["name"],
|
|
}),
|
|
{
|
|
type: "object",
|
|
properties: { name: { type: "string" }, nickname: { type: "string" } },
|
|
required: ["name"],
|
|
},
|
|
);
|
|
assert.deepEqual(toJsonSchema(undefined), {});
|
|
assert.deepEqual(toJsonSchema({ kind: "wat" }), {});
|
|
});
|
|
});
|
|
|
|
describe("toolsToJson()", () => {
|
|
it("converts pi tools to Command Code tool JSON", () => {
|
|
assert.deepEqual(
|
|
toolsToJson([
|
|
{
|
|
name: "get_weather",
|
|
description: "Get weather",
|
|
parameters: {
|
|
kind: "object",
|
|
properties: { city: { kind: "string" } },
|
|
},
|
|
},
|
|
]),
|
|
[
|
|
{
|
|
type: "function",
|
|
name: "get_weather",
|
|
description: "Get weather",
|
|
input_schema: {
|
|
type: "object",
|
|
properties: { city: { type: "string" } },
|
|
required: ["city"],
|
|
},
|
|
},
|
|
],
|
|
);
|
|
});
|
|
|
|
it("returns an empty array for missing tools", () => {
|
|
assert.deepEqual(toolsToJson(), []);
|
|
});
|
|
});
|
|
|
|
describe("messagesToCC()", () => {
|
|
it("converts user, assistant, and tool result messages", () => {
|
|
const result = messagesToCC([
|
|
{ role: "user", content: "read /tmp/test" },
|
|
{
|
|
role: "assistant",
|
|
content: [
|
|
{ type: "thinking", thinking: "I will read" },
|
|
{ type: "text", text: "Sure" },
|
|
{
|
|
type: "toolCall",
|
|
id: "c1",
|
|
name: "read",
|
|
arguments: { path: "/tmp/test" },
|
|
},
|
|
],
|
|
},
|
|
{
|
|
role: "toolResult",
|
|
toolCallId: "c1",
|
|
toolName: "read",
|
|
isError: false,
|
|
content: [
|
|
{ type: "text", text: "hello" },
|
|
{ type: "text", text: "world" },
|
|
],
|
|
},
|
|
]);
|
|
|
|
assert.equal(objectAt(result, ["0", "role"]), "user");
|
|
assert.equal(objectAt(result, ["1", "role"]), "assistant");
|
|
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",
|
|
);
|
|
});
|
|
|
|
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", () => {
|
|
assert.deepEqual(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",
|
|
},
|
|
);
|
|
});
|
|
|
|
it("ignores comments, event labels, done markers, and malformed JSON", () => {
|
|
assert.equal(parseStreamEventLine(":"), undefined);
|
|
assert.equal(parseStreamEventLine("event: message"), undefined);
|
|
assert.equal(parseStreamEventLine("data: [DONE]"), undefined);
|
|
assert.equal(parseStreamEventLine("not-json"), undefined);
|
|
});
|
|
});
|
|
|
|
describe("mapFinishReason()", () => {
|
|
it("maps provider finish reasons to pi stop reasons", () => {
|
|
assert.equal(mapFinishReason("stop"), "stop");
|
|
assert.equal(mapFinishReason("tool-calls"), "toolUse");
|
|
assert.equal(mapFinishReason("max_tokens"), "length");
|
|
assert.equal(mapFinishReason("max_output_tokens"), "length");
|
|
});
|
|
});
|