436 lines
17 KiB
TypeScript
436 lines
17 KiB
TypeScript
import test, { describe } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import crypto from "node:crypto";
|
|
|
|
import { REGISTRY } from "../../open-sse/config/providers/index.ts";
|
|
import { getExecutor, hasSpecializedExecutor } from "../../open-sse/executors/index.ts";
|
|
import { ZedHostedExecutor, __test__ } from "../../open-sse/executors/zed-hosted.ts";
|
|
import type { ExecutorLog } from "../../open-sse/executors/base.ts";
|
|
import {
|
|
createZedNativeAuthData,
|
|
encodeZedPrivateKeyVerifier,
|
|
decodeZedPrivateKeyVerifier,
|
|
decryptZedAccessToken,
|
|
parseZedCallbackPayload,
|
|
buildZedUserAuthHeader,
|
|
resolveZedOrganizationId,
|
|
mapZedModel,
|
|
clearZedCaches,
|
|
type ZedCredentials,
|
|
} from "../../open-sse/shared/zedAuth.ts";
|
|
|
|
const { normalizeZedProvider, unwrapZedLine } = __test__;
|
|
|
|
// ─── Registry ──────────────────────────────────────────────────────────────
|
|
|
|
describe("zed-hosted registry entry", () => {
|
|
test("registers under id zed-hosted, distinct from the zed IDE-import id", () => {
|
|
const entry = REGISTRY["zed-hosted"];
|
|
assert.ok(entry, "REGISTRY.zed-hosted must exist");
|
|
assert.equal(entry.id, "zed-hosted");
|
|
assert.equal(entry.executor, "zed-hosted");
|
|
assert.equal(entry.authType, "oauth");
|
|
assert.notEqual("zed-hosted", "zed", "must not collide with the existing zed IDE id");
|
|
});
|
|
|
|
test("models is empty — catalog is fetched live, never hardcoded", () => {
|
|
const entry = REGISTRY["zed-hosted"];
|
|
assert.deepEqual(entry.models, []);
|
|
assert.equal(typeof entry.modelsUrl, "string");
|
|
assert.ok(entry.modelsUrl.length > 0);
|
|
});
|
|
|
|
test("no embedded oauth client_id/client_secret literal (Hard Rule #11 N/A — no upstream secret)", () => {
|
|
const entry = REGISTRY["zed-hosted"];
|
|
assert.equal(entry.oauth, undefined);
|
|
});
|
|
|
|
test("executor is wired in the executors map", () => {
|
|
assert.ok(hasSpecializedExecutor("zed-hosted"));
|
|
assert.ok(getExecutor("zed-hosted") instanceof ZedHostedExecutor);
|
|
});
|
|
});
|
|
|
|
// ─── zedAuth: RSA keypair + native-app sign-in URL ─────────────────────────
|
|
|
|
describe("createZedNativeAuthData", () => {
|
|
test("generates a fresh keypair and a native_app_signin URL carrying the public key", () => {
|
|
const authData = createZedNativeAuthData();
|
|
assert.match(authData.authUrl, /^https:\/\/zed\.dev\/native_app_signin\?/);
|
|
const url = new URL(authData.authUrl);
|
|
assert.ok(url.searchParams.get("native_app_public_key"));
|
|
assert.equal(url.searchParams.get("native_app_port"), String(authData.nativeAppPort));
|
|
assert.ok(authData.privateKeyVerifier.startsWith("zed-rsa-pkcs1:"));
|
|
assert.ok(authData.systemId.length > 0);
|
|
});
|
|
|
|
test("two calls produce different keypairs (never reused across login attempts)", () => {
|
|
const first = createZedNativeAuthData();
|
|
const second = createZedNativeAuthData();
|
|
assert.notEqual(first.privateKeyVerifier, second.privateKeyVerifier);
|
|
});
|
|
|
|
test("honors a custom nativeAppPort", () => {
|
|
const authData = createZedNativeAuthData({}, { nativeAppPort: 12345 });
|
|
assert.equal(authData.nativeAppPort, 12345);
|
|
const url = new URL(authData.authUrl);
|
|
assert.equal(url.searchParams.get("native_app_port"), "12345");
|
|
});
|
|
});
|
|
|
|
describe("zed private key verifier encode/decode round-trip", () => {
|
|
test("round-trips an RSA PEM private key through the opaque verifier string", () => {
|
|
const { privateKey } = crypto.generateKeyPairSync("rsa", {
|
|
modulusLength: 2048,
|
|
publicKeyEncoding: { type: "pkcs1", format: "der" },
|
|
privateKeyEncoding: { type: "pkcs1", format: "pem" },
|
|
});
|
|
const verifier = encodeZedPrivateKeyVerifier(privateKey);
|
|
assert.ok(verifier.startsWith("zed-rsa-pkcs1:"));
|
|
const decoded = decodeZedPrivateKeyVerifier(verifier);
|
|
assert.equal(decoded, privateKey);
|
|
});
|
|
|
|
test("rejects a malformed/missing verifier", () => {
|
|
assert.throws(() => decodeZedPrivateKeyVerifier(""), /Missing Zed private key verifier/);
|
|
assert.throws(
|
|
() => decodeZedPrivateKeyVerifier("not-a-zed-verifier"),
|
|
/Missing Zed private key verifier/
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("RSA encrypt/decrypt round-trip (native-app callback token decryption)", () => {
|
|
test("decrypts an OAEP-encrypted access token using the matching private key", () => {
|
|
const { publicKey, privateKey } = crypto.generateKeyPairSync("rsa", {
|
|
modulusLength: 2048,
|
|
publicKeyEncoding: { type: "pkcs1", format: "pem" },
|
|
privateKeyEncoding: { type: "pkcs1", format: "pem" },
|
|
});
|
|
const plaintextToken = "zed_access_token_abc123";
|
|
const encrypted = crypto.publicEncrypt(
|
|
{ key: publicKey, padding: crypto.constants.RSA_PKCS1_OAEP_PADDING, oaepHash: "sha256" },
|
|
Buffer.from(plaintextToken, "utf8")
|
|
);
|
|
const verifier = encodeZedPrivateKeyVerifier(privateKey);
|
|
const decrypted = decryptZedAccessToken(encrypted.toString("base64url"), verifier);
|
|
assert.equal(decrypted, plaintextToken);
|
|
});
|
|
|
|
test("falls back to PKCS1 padding when OAEP fails, else throws a clear error", () => {
|
|
const { publicKey, privateKey } = crypto.generateKeyPairSync("rsa", {
|
|
modulusLength: 2048,
|
|
publicKeyEncoding: { type: "pkcs1", format: "pem" },
|
|
privateKeyEncoding: { type: "pkcs1", format: "pem" },
|
|
});
|
|
const plaintextToken = "zed_access_token_pkcs1";
|
|
const encrypted = crypto.publicEncrypt(
|
|
{ key: publicKey, padding: crypto.constants.RSA_PKCS1_PADDING },
|
|
Buffer.from(plaintextToken, "utf8")
|
|
);
|
|
const verifier = encodeZedPrivateKeyVerifier(privateKey);
|
|
const decrypted = decryptZedAccessToken(encrypted.toString("base64url"), verifier);
|
|
assert.equal(decrypted, plaintextToken);
|
|
});
|
|
|
|
test("throws a descriptive error when the ciphertext is structurally invalid for the key", () => {
|
|
// Node/OpenSSL's PKCS1 fallback is deliberately lenient (implicit-reject
|
|
// padding, a Bleichenbacher-oracle mitigation) — arbitrary garbage bytes at
|
|
// the modulus size still "succeed" with meaningless plaintext instead of
|
|
// throwing, on both padding modes. An empty ciphertext is the one shape
|
|
// that reliably fails both padding attempts (RSA "data too small"),
|
|
// exercising the combined try/catch → rethrow path.
|
|
const { privateKey } = crypto.generateKeyPairSync("rsa", {
|
|
modulusLength: 2048,
|
|
publicKeyEncoding: { type: "pkcs1", format: "pem" },
|
|
privateKeyEncoding: { type: "pkcs1", format: "pem" },
|
|
});
|
|
const verifier = encodeZedPrivateKeyVerifier(privateKey);
|
|
assert.throws(
|
|
() => decryptZedAccessToken(Buffer.alloc(0).toString("base64url"), verifier),
|
|
/Failed to decrypt Zed access token/
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("parseZedCallbackPayload", () => {
|
|
test("parses a native-app callback URL (127.0.0.1:<port>/?user_id=...&access_token=...)", () => {
|
|
const parsed = parseZedCallbackPayload(
|
|
"http://127.0.0.1:58443/?user_id=user_123&access_token=ZW5jcnlwdGVk"
|
|
);
|
|
assert.equal(parsed.userId, "user_123");
|
|
assert.equal(parsed.encryptedAccessToken, "ZW5jcnlwdGVk");
|
|
});
|
|
|
|
test("parses a bare query string (no scheme/host)", () => {
|
|
const parsed = parseZedCallbackPayload("?user_id=abc&access_token=xyz");
|
|
assert.equal(parsed.userId, "abc");
|
|
assert.equal(parsed.encryptedAccessToken, "xyz");
|
|
});
|
|
|
|
test("parses JSON input", () => {
|
|
const parsed = parseZedCallbackPayload(JSON.stringify({ user_id: "u1", access_token: "t1" }));
|
|
assert.equal(parsed.userId, "u1");
|
|
assert.equal(parsed.encryptedAccessToken, "t1");
|
|
});
|
|
|
|
test("rejects empty input", () => {
|
|
assert.throws(() => parseZedCallbackPayload(""), /Missing Zed callback URL/);
|
|
});
|
|
|
|
test("rejects a payload missing user_id or access_token", () => {
|
|
assert.throws(
|
|
() => parseZedCallbackPayload("http://127.0.0.1:1/?user_id=only"),
|
|
/must include user_id and access_token/
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("buildZedUserAuthHeader", () => {
|
|
test('builds the "<userId> <accessToken>" scheme (not Bearer)', () => {
|
|
const header = buildZedUserAuthHeader({
|
|
accessToken: "tok",
|
|
providerSpecificData: { userId: "u1" },
|
|
});
|
|
assert.equal(header, "u1 tok");
|
|
});
|
|
|
|
test("throws when userId or accessToken is missing", () => {
|
|
assert.throws(() => buildZedUserAuthHeader({ accessToken: "tok" }));
|
|
assert.throws(() => buildZedUserAuthHeader({ providerSpecificData: { userId: "u1" } }));
|
|
});
|
|
});
|
|
|
|
describe("resolveZedOrganizationId", () => {
|
|
test("prefers an explicit providerSpecificData.organizationId", () => {
|
|
const orgId = resolveZedOrganizationId({
|
|
providerSpecificData: { organizationId: "org-explicit" },
|
|
});
|
|
assert.equal(orgId, "org-explicit");
|
|
});
|
|
|
|
test("falls back to the personal organization from userInfo", () => {
|
|
const orgId = resolveZedOrganizationId(
|
|
{ providerSpecificData: {} },
|
|
{
|
|
organizations: [
|
|
{ id: "org-team", is_personal: false },
|
|
{ id: "org-personal", is_personal: true },
|
|
],
|
|
}
|
|
);
|
|
assert.equal(orgId, "org-personal");
|
|
});
|
|
|
|
test("falls back to the first organization when none is personal", () => {
|
|
const orgId = resolveZedOrganizationId(
|
|
{ providerSpecificData: {} },
|
|
{
|
|
organizations: [{ id: "org-first" }, { id: "org-second" }],
|
|
}
|
|
);
|
|
assert.equal(orgId, "org-first");
|
|
});
|
|
});
|
|
|
|
describe("mapZedModel", () => {
|
|
test("maps a raw Zed model into the normalized shape", () => {
|
|
const mapped = mapZedModel({
|
|
id: "claude-sonnet-5",
|
|
display_name: "Claude Sonnet 5",
|
|
provider: "anthropic",
|
|
max_token_count: 1000000,
|
|
max_output_tokens: 128000,
|
|
supports_tools: true,
|
|
supports_images: true,
|
|
supports_thinking: true,
|
|
});
|
|
assert.ok(mapped);
|
|
assert.equal(mapped?.id, "claude-sonnet-5");
|
|
assert.equal(mapped?.name, "Claude Sonnet 5");
|
|
assert.equal(mapped?.contextLength, 1000000);
|
|
assert.equal(mapped?.supportsTools, true);
|
|
});
|
|
|
|
test("returns null for a model with no id", () => {
|
|
assert.equal(mapZedModel({}), null);
|
|
});
|
|
});
|
|
|
|
// ─── Executor: provider-family inference ────────────────────────────────────
|
|
|
|
describe("normalizeZedProvider", () => {
|
|
test("maps explicit provider strings", () => {
|
|
assert.equal(normalizeZedProvider("anthropic", "any"), "Anthropic");
|
|
assert.equal(normalizeZedProvider("openai", "any"), "OpenAi");
|
|
assert.equal(normalizeZedProvider("open_ai", "any"), "OpenAi");
|
|
assert.equal(normalizeZedProvider("google", "any"), "Google");
|
|
assert.equal(normalizeZedProvider("gemini", "any"), "Google");
|
|
assert.equal(normalizeZedProvider("xai", "any"), "XAi");
|
|
assert.equal(normalizeZedProvider("x-ai", "any"), "XAi");
|
|
});
|
|
|
|
test("infers from the model id when provider is absent", () => {
|
|
assert.equal(normalizeZedProvider(null, "claude-sonnet-5"), "Anthropic");
|
|
assert.equal(normalizeZedProvider(null, "gemini-3.1-pro"), "Google");
|
|
assert.equal(normalizeZedProvider(null, "grok-4"), "XAi");
|
|
assert.equal(normalizeZedProvider(null, "gpt-5.5"), "OpenAi");
|
|
assert.equal(normalizeZedProvider(null, "some-unknown-model"), "OpenAi");
|
|
});
|
|
});
|
|
|
|
// ─── Executor: NDJSON line unwrapping ───────────────────────────────────────
|
|
|
|
describe("unwrapZedLine", () => {
|
|
test("parses an event line", () => {
|
|
const line = JSON.stringify({ event: { type: "message_start" } });
|
|
assert.deepEqual(unwrapZedLine(line), { event: { type: "message_start" } });
|
|
});
|
|
|
|
test("parses a status line", () => {
|
|
const line = JSON.stringify({ status: "stream_ended" });
|
|
assert.deepEqual(unwrapZedLine(line), { status: "stream_ended" });
|
|
});
|
|
|
|
test('recognizes the "[DONE]" sentinel, with or without an SSE "data:" prefix', () => {
|
|
assert.deepEqual(unwrapZedLine("[DONE]"), { done: true });
|
|
assert.deepEqual(unwrapZedLine("data: [DONE]"), { done: true });
|
|
});
|
|
|
|
test("returns null for a blank line", () => {
|
|
assert.equal(unwrapZedLine(""), null);
|
|
assert.equal(unwrapZedLine(" "), null);
|
|
});
|
|
|
|
test("returns null for unparsable JSON", () => {
|
|
assert.equal(unwrapZedLine("not json"), null);
|
|
});
|
|
});
|
|
|
|
// ─── Executor: resolveModel + zedLlmFetch (mocked fetch) ────────────────────
|
|
|
|
describe("ZedHostedExecutor.resolveModel + zedLlmFetch (mocked upstream)", () => {
|
|
const credentials = {
|
|
accessToken: "plaintext-access-token",
|
|
providerSpecificData: { userId: "u1", organizationId: "org-1" },
|
|
};
|
|
|
|
test("resolves the provider family from the live model catalog", async (t) => {
|
|
clearZedCaches();
|
|
const calls: string[] = [];
|
|
const originalFetch = globalThis.fetch;
|
|
t.after(() => {
|
|
globalThis.fetch = originalFetch;
|
|
clearZedCaches();
|
|
});
|
|
|
|
globalThis.fetch = (async (input: RequestInfo | URL) => {
|
|
const url = String(input);
|
|
calls.push(url);
|
|
if (url.includes("/client/llm_tokens")) {
|
|
return new Response(JSON.stringify({ token: "llm-token-abc" }), {
|
|
status: 200,
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
}
|
|
if (url.includes("/models")) {
|
|
return new Response(
|
|
JSON.stringify({
|
|
models: [
|
|
{ id: "claude-sonnet-5", provider: "anthropic", display_name: "Claude Sonnet 5" },
|
|
{ id: "gpt-5.5", provider: "openai", display_name: "GPT-5.5" },
|
|
],
|
|
}),
|
|
{ status: 200, headers: { "Content-Type": "application/json" } }
|
|
);
|
|
}
|
|
throw new Error(`Unexpected fetch: ${url}`);
|
|
}) as typeof fetch;
|
|
|
|
const executor = new ZedHostedExecutor();
|
|
const result = await executor.resolveModel(
|
|
"claude-sonnet-5",
|
|
credentials as ZedCredentials,
|
|
undefined,
|
|
undefined
|
|
);
|
|
assert.equal(result.provider, "Anthropic");
|
|
assert.ok(calls.some((u) => u.includes("/client/llm_tokens")));
|
|
assert.ok(calls.some((u) => u.includes("/models")));
|
|
});
|
|
|
|
test("falls back to model-id inference when the catalog fetch fails", async (t) => {
|
|
clearZedCaches();
|
|
const originalFetch = globalThis.fetch;
|
|
t.after(() => {
|
|
globalThis.fetch = originalFetch;
|
|
clearZedCaches();
|
|
});
|
|
globalThis.fetch = (async () => {
|
|
throw new Error("network down");
|
|
}) as typeof fetch;
|
|
|
|
const executor = new ZedHostedExecutor();
|
|
const warnCalls: string[] = [];
|
|
const result = await executor.resolveModel(
|
|
"gemini-3.1-pro",
|
|
credentials as ZedCredentials,
|
|
undefined,
|
|
{ warn: (_tag: string, msg: string) => warnCalls.push(msg) } as ExecutorLog
|
|
);
|
|
assert.equal(result.provider, "Google");
|
|
assert.ok(warnCalls.length > 0);
|
|
});
|
|
});
|
|
|
|
// ─── Executor: parseError ────────────────────────────────────────────────────
|
|
|
|
describe("ZedHostedExecutor.parseError", () => {
|
|
const executor = new ZedHostedExecutor();
|
|
|
|
test("surfaces a friendly message for trial_blocked", () => {
|
|
const response = new Response(null, { status: 402 });
|
|
const result = executor.parseError(
|
|
response,
|
|
JSON.stringify({ code: "trial_blocked", message: "trial exhausted" })
|
|
);
|
|
assert.equal(result.status, 402);
|
|
assert.match(result.message, /trial\/billing access/);
|
|
});
|
|
|
|
test("prefixes other error codes with Zed", () => {
|
|
const response = new Response(null, { status: 400 });
|
|
const result = executor.parseError(
|
|
response,
|
|
JSON.stringify({ code: "bad_request", message: "oops" })
|
|
);
|
|
assert.equal(result.message, "Zed bad_request: oops");
|
|
});
|
|
|
|
test("falls back to raw body text when there is no code", () => {
|
|
const response = new Response(null, { status: 500, statusText: "Internal Server Error" });
|
|
const result = executor.parseError(response, "boom");
|
|
assert.equal(result.message, "boom");
|
|
});
|
|
|
|
test("never leaks a stack trace (Hard Rule #12 — message stays upstream-text-only)", () => {
|
|
const response = new Response(null, { status: 500 });
|
|
const result = executor.parseError(response, "totally fine upstream text");
|
|
assert.ok(!result.message.includes(" at "));
|
|
});
|
|
});
|
|
|
|
// ─── Executor: no proactive refresh (long-lived native-app token) ──────────
|
|
|
|
describe("ZedHostedExecutor credential-refresh contract", () => {
|
|
test("needsRefresh is always false — no refresh flow exposed by Zed", () => {
|
|
const executor = new ZedHostedExecutor();
|
|
assert.equal(executor.needsRefresh(), false);
|
|
});
|
|
|
|
test("refreshCredentials resolves to null", async () => {
|
|
const executor = new ZedHostedExecutor();
|
|
assert.equal(await executor.refreshCredentials(), null);
|
|
});
|
|
});
|