mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-02 16:58:52 +01:00
Deliver native Kimi Code setup, exact models, scoped credentials, verified packages and runtime packs. Preserve current upstream release attestations and restore verified same-run remote retry dispatch without weakening process ownership. Co-Authored-By: Paperclip <noreply@paperclip.ing>
323 lines
17 KiB
TypeScript
323 lines
17 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { assertValidAdapterLoginCapability } from "@paperclipai/adapter-utils";
|
|
import { listServerAdapters, requireServerAdapter } from "./registry.js";
|
|
import * as executionTarget from "@paperclipai/adapter-utils/execution-target";
|
|
import { BUILTIN_ADAPTER_TYPES } from "./builtin-adapter-types.js";
|
|
import { QUALIFIED_ACPX_PROFILES, KIMI_DISTRIBUTION_CLOSURE_SHA256 } from "../vendor/paperclip-runner/index.js";
|
|
|
|
const { probeInstallation, probeGrokInstallation, probePiInstallation, probeKimiInstallation, probeKimiModels, probeKimiRemote, verifyApiKey } = vi.hoisted(() => ({
|
|
verifyApiKey: vi.fn(),
|
|
probeInstallation: vi.fn(),
|
|
probeGrokInstallation: vi.fn(),
|
|
probePiInstallation: vi.fn(),
|
|
probeKimiInstallation: vi.fn(),
|
|
probeKimiModels: vi.fn(),
|
|
probeKimiRemote: vi.fn(),
|
|
}));
|
|
vi.mock("../services/ai-api-key-test.js", () => ({ validateAiApiKey: verifyApiKey }));
|
|
vi.mock("../services/kimi-connection-probe.js", () => ({ probeKimiExecutionTarget: probeKimiRemote }));
|
|
vi.mock("../vendor/paperclip-runner/live/index.js", () => ({
|
|
probeAcpxClaudeInstallation: probeInstallation,
|
|
probeAcpxGrokInstallation: probeGrokInstallation,
|
|
probeAcpxPiInstallation: probePiInstallation,
|
|
probeAcpxKimiInstallation: probeKimiInstallation,
|
|
probeKimiModels,
|
|
probeAcpxCursorInstallation: vi.fn(async () => undefined),
|
|
}));
|
|
|
|
// The registry registers a login capability for the two built-in interactive
|
|
// adapters. The test checks the scalar values and the presence of the required
|
|
// callbacks. It also runs the shared validator, so the built-in capabilities
|
|
// obey the same fail-closed contract as an external adapter.
|
|
|
|
describe("built-in adapter login capabilities", () => {
|
|
it("registers the Codex device-login capability", () => {
|
|
const capability = requireServerAdapter("codex_local").loginCapability;
|
|
expect(capability).toBeDefined();
|
|
if (!capability) return;
|
|
expect(capability.panelMode).toBe("displayed_code");
|
|
expect(capability.timeoutPolicy).toBe("caller_bounded");
|
|
expect(capability.completionClaim).toBeUndefined();
|
|
expect(typeof capability.getCommand).toBe("function");
|
|
expect(typeof capability.parsePrompt).toBe("function");
|
|
expect(() => assertValidAdapterLoginCapability(capability, "codex_local")).not.toThrow();
|
|
});
|
|
|
|
it("registers the Grok device-login capability", () => {
|
|
const capability = requireServerAdapter("grok_local").loginCapability;
|
|
expect(capability).toBeDefined();
|
|
if (!capability) return;
|
|
expect(capability.panelMode).toBe("displayed_code");
|
|
expect(capability.timeoutPolicy).toBe("caller_bounded");
|
|
expect(capability.completionClaim).toBeUndefined();
|
|
expect(typeof capability.getCommand).toBe("function");
|
|
expect(typeof capability.parsePrompt).toBe("function");
|
|
expect(() => assertValidAdapterLoginCapability(capability, "grok_local")).not.toThrow();
|
|
});
|
|
|
|
it("registers the Claude setup-token capability", () => {
|
|
const capability = requireServerAdapter("claude_local").loginCapability;
|
|
expect(capability).toBeDefined();
|
|
if (!capability) return;
|
|
expect(capability.panelMode).toBe("submitted_browser_code");
|
|
expect(capability.timeoutPolicy).toBe("fixed");
|
|
expect(capability.completionClaim).toBe("storedSessionId");
|
|
expect(typeof capability.getCommand).toBe("function");
|
|
expect(typeof capability.parsePrompt).toBe("function");
|
|
expect(typeof capability.captureCredential).toBe("function");
|
|
expect(() => assertValidAdapterLoginCapability(capability, "claude_local")).not.toThrow();
|
|
});
|
|
});
|
|
|
|
describe("built-in runtime connection tool delivery", () => {
|
|
const expectedStrategies = new Map([
|
|
["acpx_local", "environment"],
|
|
["claude_local", "native_mcp"],
|
|
["codex_local", "native_mcp"],
|
|
["cursor_cloud", "invocation_context"],
|
|
["cursor", "environment"],
|
|
["gemini_local", "environment"],
|
|
["grok_local", "environment"],
|
|
["hermes_gateway", "invocation_context"],
|
|
["hermes_local", "environment"],
|
|
["kimi_local", "environment"],
|
|
["openclaw_gateway", "invocation_context"],
|
|
["opencode_local", "environment"],
|
|
["paperclip_runner", "environment"],
|
|
["pi_local", "environment"],
|
|
["process", "environment"],
|
|
["http", "invocation_context"],
|
|
] as const);
|
|
|
|
it("requires every built-in adapter to declare its expected delivery strategy", () => {
|
|
const builtIns = listServerAdapters().filter((adapter) => BUILTIN_ADAPTER_TYPES.has(adapter.type));
|
|
expect(new Set(builtIns.map((adapter) => adapter.type))).toEqual(BUILTIN_ADAPTER_TYPES);
|
|
expect(new Map(builtIns.map((adapter) => [adapter.type, adapter.runtimeToolDelivery]))).toEqual(
|
|
expectedStrategies,
|
|
);
|
|
});
|
|
|
|
it.each([...expectedStrategies])("delivers %s runtime tools through %s", (type, strategy) => {
|
|
expect(requireServerAdapter(type).runtimeToolDelivery).toBe(strategy);
|
|
});
|
|
});
|
|
|
|
|
|
describe("native ACPX environment checks", () => {
|
|
beforeEach(() => {
|
|
verifyApiKey.mockReset().mockResolvedValue(undefined);
|
|
probeInstallation.mockReset().mockResolvedValue(undefined);
|
|
probeGrokInstallation.mockReset().mockResolvedValue(undefined);
|
|
probePiInstallation.mockReset().mockResolvedValue(undefined);
|
|
probeKimiInstallation.mockReset().mockResolvedValue(undefined);
|
|
probeKimiModels.mockReset().mockResolvedValue(undefined);
|
|
probeKimiRemote.mockReset().mockResolvedValue(undefined);
|
|
});
|
|
afterEach(() => { vi.restoreAllMocks(); vi.unstubAllEnvs(); });
|
|
|
|
const context = {
|
|
companyId: "company-test",
|
|
adapterType: "paperclip_runner",
|
|
config: { provider: "acpx", acpxAgent: "claude", model: "claude-sonnet-5" },
|
|
};
|
|
|
|
it("requires an explicit Kimi credential and probes its own installed runtime during qualification", async () => {
|
|
const model = "explicit-kimi-model";
|
|
authorizeKimi(model);
|
|
const config = { provider: "acpx", acpxAgent: "kimi", model };
|
|
const missing = await requireServerAdapter("paperclip_runner").testEnvironment!({ ...context, config });
|
|
expect(missing.status).toBe("fail");
|
|
expect(missing.checks[0].code).toBe("KIMI_AUTH_REQUIRED");
|
|
expect(probeKimiInstallation).not.toHaveBeenCalled();
|
|
|
|
const ready = await requireServerAdapter("paperclip_runner").testEnvironment!({ ...context, config: { ...config, env: { KIMI_API_KEY: "fixture-key" } } });
|
|
expect(ready.status).toBe("pass");
|
|
expect(probeKimiRemote).toHaveBeenCalledExactlyOnceWith("fixture-key", undefined, model, { companyId: context.companyId });
|
|
expect(probeKimiInstallation).not.toHaveBeenCalled();
|
|
expect(probeKimiModels).not.toHaveBeenCalled();
|
|
expect(probeInstallation).not.toHaveBeenCalled();
|
|
expect(verifyApiKey).not.toHaveBeenCalled();
|
|
expect(ready.checks[0]).toMatchObject({ code: "kimi_metadata_verified", message: expect.stringContaining("No model prompt was sent") });
|
|
});
|
|
|
|
it("keeps a rejected Kimi catalog credential out of the ready lifecycle", async () => {
|
|
const model = "explicit-kimi-model";
|
|
authorizeKimi(model);
|
|
probeKimiRemote.mockRejectedValue(Object.assign(new Error("The Kimi Code credential was rejected by the model catalog."), { code: "KIMI_AUTH_REQUIRED" }));
|
|
const result = await requireServerAdapter("paperclip_runner").testEnvironment!({ ...context,
|
|
config: { provider: "acpx", acpxAgent: "kimi", model, env: { KIMI_API_KEY: "fixture-key" } },
|
|
});
|
|
expect(result.status).toBe("fail");
|
|
expect(result.checks[0].message).toContain("credential was rejected");
|
|
});
|
|
|
|
it("requires the selected remote Kimi runtime and authenticated catalog instead of a host installation", async () => {
|
|
const model = "explicit-kimi-model";
|
|
authorizeKimi(model);
|
|
const runner = { execute: vi.fn() };
|
|
const target = { kind: "remote" as const, transport: "sandbox" as const, remoteCwd: "/workspace", providerKey: "test", runner };
|
|
const result = await requireServerAdapter("paperclip_runner").testEnvironment!({ ...context,
|
|
config: { provider: "acpx", acpxAgent: "kimi", model, env: { KIMI_API_KEY: "fixture-key" } },
|
|
executionTarget: target,
|
|
});
|
|
expect(result.status).toBe("pass");
|
|
expect(result.checks[0]).toMatchObject({ code: "kimi_metadata_verified" });
|
|
expect(probeKimiRemote).toHaveBeenCalledExactlyOnceWith("fixture-key", target, model, { companyId: context.companyId });
|
|
expect(runner.execute).not.toHaveBeenCalled();
|
|
expect(probeKimiInstallation).not.toHaveBeenCalled();
|
|
expect(probeKimiModels).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("verifies a native Claude key without the global CLI or a model prompt", async () => {
|
|
const result = await requireServerAdapter("paperclip_runner").testEnvironment!({ ...context,
|
|
config: { ...context.config, env: { ANTHROPIC_API_KEY: "test-key" } },
|
|
});
|
|
expect(result.status).toBe("pass");
|
|
expect(verifyApiKey).toHaveBeenCalledExactlyOnceWith("anthropic", "test-key");
|
|
expect(result.checks.map(check => check.code)).toEqual(["acpx_runtime_ready", "ai_connection_api_key_reverified"]);
|
|
});
|
|
|
|
it.each(["ai_connection_api_key_rejected", "ai_connection_verification_failed"])("fails setup on %s", async code => {
|
|
verifyApiKey.mockRejectedValue({ details: { code } });
|
|
const result = await requireServerAdapter("paperclip_runner").testEnvironment!({ ...context,
|
|
config: { ...context.config, env: { ANTHROPIC_API_KEY: "test-key" } },
|
|
});
|
|
expect(result.status).toBe("fail");
|
|
expect(result.checks.at(-1)).toMatchObject({ code, level: "error" });
|
|
});
|
|
|
|
it.each(["ANTHROPIC_BASE_URL", "ANTHROPIC_AUTH_TOKEN", "CLAUDE_CODE_USE_BEDROCK", "CLAUDE_CODE_USE_VERTEX", "CLAUDE_CODE_USE_FOUNDRY"])("preserves the supplementary probe for custom routing via %s", async name => {
|
|
const result = await requireServerAdapter("paperclip_runner").testEnvironment!({ ...context,
|
|
config: { ...context.config, env: { ANTHROPIC_API_KEY: "test-key", [name]: "configured" } },
|
|
});
|
|
expect(verifyApiKey).not.toHaveBeenCalled();
|
|
expect(result.checks.some(check => check.code === "ai_connection_api_key_reverified")).toBe(false);
|
|
});
|
|
|
|
it("reports unsupported local platforms before a successful CLI login can mask them", async () => {
|
|
probeInstallation.mockRejectedValue(new Error("ACPX Claude requires a supported runtime platform"));
|
|
const result = await requireServerAdapter("paperclip_runner").testEnvironment!(context);
|
|
expect(result.status).toBe("fail");
|
|
expect(result.checks).toEqual([expect.objectContaining({
|
|
code: "acpx_runtime_unavailable",
|
|
level: "error",
|
|
})]);
|
|
});
|
|
|
|
it.each([
|
|
["claude", "claude-opus-5-5", "aarch64", "warn"],
|
|
["claude", "claude-opus-5-5", "arm64", "warn"],
|
|
["claude", "claude-opus-5-5", "armv7l", "fail"],
|
|
["grok", "grok-4.7", "aarch64", "fail"],
|
|
["pi", "openrouter/deepseek/deepseek-v4-flash-0731", "aarch64", "fail"],
|
|
])("checks the selected remote %s architecture %s without widening other providers", async (agent, model, architecture, status) => {
|
|
const result = await requireServerAdapter("paperclip_runner").testEnvironment!({ ...context,
|
|
config: { provider: "acpx", acpxAgent: agent, model },
|
|
executionTarget: { kind: "remote", transport: "sandbox", remoteCwd: "/workspace", providerKey: "test",
|
|
runner: { execute: vi.fn().mockResolvedValue({ exitCode: 0, timedOut: false, stdout: `Linux\n${architecture}\n` }) } },
|
|
});
|
|
expect(result.status).toBe(status);
|
|
expect(result.checks[0].code).toBe(status === "warn" ? "acpx_remote_runtime_unverified" : "acpx_runtime_unavailable");
|
|
expect(probeInstallation).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("identifies an incomplete installation and gives the operator a repair", async () => {
|
|
probeInstallation.mockRejectedValue(Object.assign(new Error("The Claude ACP runtime package is missing. Reinstall or rebuild Paperclip."), { code: "ACPX_RUNTIME_PACKAGE_MISSING" }));
|
|
const result = await requireServerAdapter("paperclip_runner").testEnvironment!(context);
|
|
expect(result.status).toBe("fail");
|
|
expect(result.checks[0]).toMatchObject({ code: "acpx_runtime_package_missing", message: expect.stringContaining("Reinstall or rebuild") });
|
|
});
|
|
|
|
it("requires a successful installed runtime probe", async () => {
|
|
const result = await requireServerAdapter("paperclip_runner").testEnvironment!(context);
|
|
expect(result.status).toBe("pass");
|
|
expect(probeInstallation).toHaveBeenCalledWith(context.config.model);
|
|
});
|
|
|
|
it.each([true, false])("checks Grok's own installation readiness (%s)", async (ready) => {
|
|
if (!ready) probeGrokInstallation.mockRejectedValueOnce(new Error("Grok executable digest mismatch"));
|
|
const result = await requireServerAdapter("paperclip_runner").testEnvironment!({
|
|
...context,
|
|
config: { provider: "acpx", acpxAgent: "grok", model: "grok-4.7" },
|
|
});
|
|
expect(result.status).toBe(ready ? "pass" : "fail");
|
|
expect(probeGrokInstallation).toHaveBeenCalledWith("grok-4.7");
|
|
expect(probeInstallation).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it.each([true, false])("checks qualified Pi's own installation readiness (%s)", async (ready) => {
|
|
if (!ready) probePiInstallation.mockRejectedValueOnce(new Error("Pi installation integrity mismatch"));
|
|
const model = "openrouter/deepseek/deepseek-v4-flash-0731";
|
|
const result = await requireServerAdapter("paperclip_runner").testEnvironment!({
|
|
...context,
|
|
config: { provider: "acpx", acpxAgent: "pi", model },
|
|
});
|
|
expect(result.status).toBe(ready ? "pass" : "fail");
|
|
expect(probePiInstallation).toHaveBeenCalledWith(model);
|
|
expect(probeInstallation).not.toHaveBeenCalled();
|
|
expect(probeGrokInstallation).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("does not use the host platform to reject a remote environment", async () => {
|
|
vi.spyOn(process, "platform", "get").mockReturnValue("darwin");
|
|
const result = await requireServerAdapter("paperclip_runner").testEnvironment!({
|
|
...context,
|
|
executionTarget: {
|
|
kind: "remote", transport: "sandbox", remoteCwd: "/workspace", providerKey: "test",
|
|
runner: { execute: vi.fn().mockResolvedValue({ exitCode: 0, timedOut: false, stdout: "Linux\nx86_64\n" }) },
|
|
},
|
|
});
|
|
expect(result.status).toBe("warn");
|
|
expect(result.checks[0].code).toBe("acpx_remote_runtime_unverified");
|
|
expect(probeInstallation).not.toHaveBeenCalled();
|
|
});
|
|
|
|
const sshTarget = {
|
|
kind: "remote" as const, transport: "ssh" as const, remoteCwd: "/workspace",
|
|
spec: {
|
|
host: "example.test", port: 22, username: "tester", remoteCwd: "/workspace",
|
|
remoteWorkspacePath: "/workspace", privateKey: null, knownHosts: null, strictHostKeyChecking: true,
|
|
},
|
|
};
|
|
|
|
it.each([
|
|
["Linux\nx86_64\n", "warn"],
|
|
["Darwin\nx86_64\n", "warn"],
|
|
["Darwin\narm64\n", "warn"],
|
|
["Linux\naarch64\n", "warn"],
|
|
["Linux\narm64\n", "warn"],
|
|
["Linux\narmv7l\n", "fail"],
|
|
["", "fail"],
|
|
])("qualifies the SSH platform from its own uname output %j", async (stdout, status) => {
|
|
vi.spyOn(process, "platform", "get").mockReturnValue("linux");
|
|
vi.spyOn(process, "arch", "get").mockReturnValue("x64");
|
|
const probe = vi.spyOn(executionTarget, "runAdapterExecutionTargetShellCommand").mockResolvedValue({
|
|
exitCode: 0, timedOut: false, stdout, stderr: "", signal: null, pid: null, startedAt: new Date(0).toISOString(),
|
|
});
|
|
const result = await requireServerAdapter("paperclip_runner").testEnvironment!({ ...context, executionTarget: sshTarget });
|
|
expect(result.status).toBe(status);
|
|
expect(probe).toHaveBeenCalledWith(expect.any(String), sshTarget, "uname -s && uname -m", {
|
|
cwd: "/workspace", env: {}, timeoutSec: 15,
|
|
});
|
|
});
|
|
|
|
it.each(["timeout", "exit", "exception"])("does not qualify an SSH target after a probe %s", async (failure) => {
|
|
const probe = vi.spyOn(executionTarget, "runAdapterExecutionTargetShellCommand");
|
|
if (failure === "exception") probe.mockRejectedValue(new Error("connection unavailable"));
|
|
else probe.mockResolvedValue({
|
|
exitCode: failure === "exit" ? 1 : 0, timedOut: failure === "timeout",
|
|
stdout: "Linux\nx86_64\n", stderr: "", signal: null, pid: null, startedAt: new Date(0).toISOString(),
|
|
});
|
|
const result = await requireServerAdapter("paperclip_runner").testEnvironment!({ ...context, executionTarget: sshTarget });
|
|
expect(result.status).toBe("fail");
|
|
expect(result.checks[0].code).toBe("acpx_runtime_unavailable");
|
|
});
|
|
});
|
|
|
|
function authorizeKimi(model: string) {
|
|
vi.stubEnv("PAPERCLIP_ACPX_CANDIDATE_ADMISSION", JSON.stringify({ schema: "paperclip.acpx-candidate-admission.v1",
|
|
companyId: "company-test", agent: "kimi", model, profileDigest: QUALIFIED_ACPX_PROFILES.kimi.commandDigest,
|
|
platform: "linux", architecture: "x64", nativeClosureDigest: `sha256:${KIMI_DISTRIBUTION_CLOSURE_SHA256["linux-x64"]}`,
|
|
expiresAt: Date.now() + 60_000 }));
|
|
}
|