Luigit
repositories / pi-ext

pi-ext

bugabingas pi extensions

owned by admin

extensions/klaus/__tests__/protocol.test.ts

Raw
import type { Context, Model, Tool } from "@earendil-works/pi-ai";
import { Type } from "typebox";
import { describe, expect, it } from "vitest";
import {
	contextFingerprint,
	createAssistant,
	emptyUsage,
	isKlausModelId,
	klausSelector,
	MODEL_SELECTORS,
	modelFacingContent,
	requiredConstrainedTool,
	toKlausTools,
} from "../src/protocol";

const model: Model<string> = {
	id: "claude-sonnet-5",
	name: "Claude Sonnet 5",
	api: "klaus-agent-sdk",
	provider: "klaus",
	baseUrl: "https://klaus.invalid",
	reasoning: true,
	input: ["text", "image"],
	cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
	contextWindow: 200_000,
	maxTokens: 64_000,
};

const tool = (
	name: string,
	constrainedSampling?: Tool["constrainedSampling"],
): Tool => ({
	name,
	description: `Description for ${name}`,
	parameters: Type.Object(
		{ value: Type.String({ minLength: 1 }) },
		{ additionalProperties: false },
	),
	constrainedSampling,
});

const assistant = (overrides: Partial<Context["messages"][number]> = {}) => ({
	role: "assistant" as const,
	content: [
		{
			type: "thinking" as const,
			thinking: "private",
			thinkingSignature: "sig",
		},
		{ type: "text" as const, text: "visible" },
		{
			type: "toolCall" as const,
			id: "call-1",
			name: "echo",
			arguments: { value: "hello" },
		},
	],
	api: "test",
	provider: "test",
	model: "test",
	usage: emptyUsage(),
	stopReason: "stop" as const,
	timestamp: 1,
	...overrides,
});

describe("Klaus model protocol", () => {
	it("accepts every current Claude catalog id shape, aliases included", () => {
		for (const id of Object.keys(MODEL_SELECTORS)) {
			expect(isKlausModelId(id)).toBe(true);
		}
		for (const id of [
			"claude-opus-5-5",
			"claude-fable-5-1",
			"claude-sonnet-4",
			"claude-haiku-4-5",
		]) {
			expect(isKlausModelId(id)).toBe(true);
		}
		for (const id of [
			"",
			"default",
			"claude-sonnet-5-latest",
			"claude-opus-4-5-20251101",
			"CLAUDE-SONNET-5",
			"claude-sonnet-5_",
			"claude--5",
			"__proto__",
		]) {
			expect(isKlausModelId(id)).toBe(false);
		}
	});

	it("maps known ids to short selectors and passes unknown ids through", () => {
		expect(klausSelector("claude-opus-5")).toBe("opus");
		expect(klausSelector("claude-fable-5-1")).toBe("fable");
		expect(klausSelector("claude-opus-5-5")).toBe("claude-opus-5-5");
	});

	it("projects Pi tools without changing names, descriptions, order, or schemas", () => {
		const tools = [tool("z_tool"), tool("A tool")];
		const projected = toKlausTools(tools);

		expect(projected).toEqual([
			{
				name: "z_tool",
				description: "Description for z_tool",
				inputSchema: tools[0]?.parameters,
			},
			{
				name: "A tool",
				description: "Description for A tool",
				inputSchema: tools[1]?.parameters,
			},
		]);
		expect(toKlausTools(undefined)).toEqual([]);
	});

	it("rejects only the first required JSON-schema constrained tool", () => {
		expect(
			requiredConstrainedTool([
				tool("plain"),
				tool("preferred", { type: "json_schema", strict: "prefer" }),
				tool("grammar", {
					type: "grammar",
					variants: { openai_regex: ".*" },
				}),
				tool("required", { type: "json_schema", strict: "require" }),
				tool("later", { type: "json_schema", strict: "require" }),
			]),
		).toBe("required");
		expect(requiredConstrainedTool(undefined)).toBeUndefined();
	});

	it("creates independent zeroed usage objects", () => {
		const first = emptyUsage();
		const second = emptyUsage();
		first.input = 9;
		first.cost.total = 4;

		expect(second).toEqual({
			input: 0,
			output: 0,
			cacheRead: 0,
			cacheWrite: 0,
			totalTokens: 0,
			cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
		});
	});

	it("creates a pending assistant owned by the selected Klaus model", () => {
		const before = Date.now();
		const message = createAssistant(model);

		expect(message).toMatchObject({
			role: "assistant",
			content: [],
			api: "klaus-agent-sdk",
			provider: "klaus",
			model: "claude-sonnet-5",
			stopReason: "pending",
			usage: emptyUsage(),
		});
		expect(message.timestamp).toBeGreaterThanOrEqual(before);
		expect(message.timestamp).toBeLessThanOrEqual(Date.now());
	});

	it("forwards only model-facing text and image fields", () => {
		expect(
			modelFacingContent([
				{ type: "text", text: "hello" },
				{ type: "image", data: "base64", mimeType: "image/webp" },
			]),
		).toEqual([
			{ type: "text", text: "hello" },
			{ type: "image", data: "base64", mimeType: "image/webp" },
		]);
	});
});

describe("Klaus compatibility fingerprints", () => {
	const base = (): Context => ({
		systemPrompt: "system",
		messages: [
			{ role: "user", content: "question", timestamp: 1 },
			assistant(),
			{
				role: "toolResult",
				toolCallId: "call-1",
				toolName: "echo",
				content: [{ type: "text", text: "hello" }],
				isError: false,
				timestamp: 2,
				details: { private: "ignored" },
			},
		],
		tools: [tool("echo")],
	});

	it("is stable and always emits a SHA-256 hex digest", () => {
		const context = base();
		const first = contextFingerprint(context, "sonnet\0cwd");

		expect(first).toMatch(/^[a-f0-9]{64}$/);
		expect(contextFingerprint(structuredClone(context), "sonnet\0cwd")).toBe(
			first,
		);
	});

	it("ignores volatile accounting, timestamps, private reasoning, and tool details", () => {
		const first = base();
		const second = structuredClone(first);
		const user = second.messages[0];
		const answer = second.messages[1];
		const result = second.messages[2];
		if (
			user?.role !== "user" ||
			answer?.role !== "assistant" ||
			result?.role !== "toolResult"
		) {
			throw new Error("Broken fingerprint fixture.");
		}
		user.timestamp = 999;
		answer.timestamp = 999;
		answer.usage.input = 42;
		answer.usage.cost.total = 99;
		const thinking = answer.content.find((item) => item.type === "thinking");
		if (thinking?.type === "thinking") {
			thinking.thinking = "different private thought";
			thinking.thinkingSignature = "different signature";
		}
		result.timestamp = 999;
		result.details = { private: "different" };

		expect(contextFingerprint(first, "sonnet\0cwd")).toBe(
			contextFingerprint(second, "sonnet\0cwd"),
		);
	});

	it.each([
		["identity", (context: Context) => context, "opus\0cwd"],
		[
			"system prompt",
			(context: Context) => ({ ...context, systemPrompt: "other" }),
			"sonnet\0cwd",
		],
		[
			"user content",
			(context: Context) => ({
				...context,
				messages: context.messages.map((message, index) =>
					index === 0 && message.role === "user"
						? { ...message, content: "other" }
						: message,
				),
			}),
			"sonnet\0cwd",
		],
		[
			"assistant text",
			(context: Context) => ({
				...context,
				messages: context.messages.map((message, index) =>
					index === 1 && message.role === "assistant"
						? assistant({
								content: message.content.map((item) =>
									item.type === "text" ? { ...item, text: "other" } : item,
								),
							})
						: message,
				),
			}),
			"sonnet\0cwd",
		],
		[
			"tool arguments",
			(context: Context) => ({
				...context,
				messages: context.messages.map((message, index) =>
					index === 1 && message.role === "assistant"
						? assistant({
								content: message.content.map((item) =>
									item.type === "toolCall"
										? { ...item, arguments: { value: "other" } }
										: item,
								),
							})
						: message,
				),
			}),
			"sonnet\0cwd",
		],
		[
			"tool result error state",
			(context: Context) => ({
				...context,
				messages: context.messages.map((message) =>
					message.role === "toolResult"
						? { ...message, isError: true }
						: message,
				),
			}),
			"sonnet\0cwd",
		],
		[
			"active schema",
			(context: Context) => ({ ...context, tools: [tool("different")] }),
			"sonnet\0cwd",
		],
	] as const)("changes when %s changes", (_label, mutate, identity) => {
		const context = base();
		expect(contextFingerprint(mutate(context), identity)).not.toBe(
			contextFingerprint(context, "sonnet\0cwd"),
		);
	});

	it("distinguishes missing and empty message content without mutating context", () => {
		const context = base();
		const original = structuredClone(context);
		contextFingerprint(context);

		expect(context).toEqual(original);
		expect(contextFingerprint({ messages: [] })).not.toBe(
			contextFingerprint({
				messages: [{ role: "user", content: "", timestamp: 1 }],
			}),
		);
	});
});