diff --git a/tests/translator/bugs-gemini-cursor-commandcode.test.js b/tests/translator/bugs-gemini-cursor-commandcode.test.js index 4b74c60e..8483bfc1 100644 --- a/tests/translator/bugs-gemini-cursor-commandcode.test.js +++ b/tests/translator/bugs-gemini-cursor-commandcode.test.js @@ -62,15 +62,18 @@ describe("OpenAI → CommandCode", () => { expect(Object.keys(call.input).length, "arguments silently dropped to {}").toBeGreaterThan(0); }); - // openai-to-commandcode.js:41-42 — image becomes "[image omitted]" - // KNOWN BUG - it.fails("image content is preserved", () => { + it("image content is preserved as native CommandCode image blocks", () => { const out = O2CC({ messages: [{ role: "user", content: [ { type: "text", text: "look" }, { type: "image_url", image_url: { url: "data:image/png;base64,BBBB" } }, ] }], }); - expect(JSON.stringify(out), "image omitted").toContain("BBBB"); + expect(JSON.stringify(out)).toContain("BBBB"); + expect(JSON.stringify(out)).not.toContain("[image omitted]"); + expect(out.params.messages[0].content).toEqual([ + { type: "text", text: "look" }, + { type: "image", image: "data:image/png;base64,BBBB", mimeType: "image/png" }, + ]); }); }); diff --git a/tests/translator/thinking-unified.test.js b/tests/translator/thinking-unified.test.js index fd8f9d28..f04b18bd 100644 --- a/tests/translator/thinking-unified.test.js +++ b/tests/translator/thinking-unified.test.js @@ -250,6 +250,29 @@ describe("applyThinking per provider format", () => { const out = apply("gemini-cli", "gemini-3.5-flash-lite", { reasoning_effort: "medium" }, "gemini-cli"); expect(out.generationConfig.thinkingConfig.thinkingLevel).toBe("medium"); }); + it("commandcode envelope writes params.reasoning_effort, not wrapper fields", () => { + const out = apply("commandcode", "deepseek/deepseek-v4.1-flash", { + params: { model: "deepseek/deepseek-v4.1-flash", messages: [] }, + reasoning_effort: "high", + }, "commandcode"); + expect(out.params.reasoning_effort).toBe("high"); + expect(out.reasoning_effort).toBeUndefined(); + expect(out.thinking).toBeUndefined(); + }); + it("commandcode preserves low effort instead of remapping to high", () => { + const out = apply("commandcode", "deepseek/deepseek-v4.1-flash", { + params: { messages: [] }, + reasoning_effort: "low", + }, "commandcode"); + expect(out.params.reasoning_effort).toBe("low"); + }); + it("commandcode preserves max effort", () => { + const out = apply("commandcode", "deepseek/deepseek-v4.1-flash", { + params: { messages: [] }, + reasoning_effort: "max", + }, "commandcode"); + expect(out.params.reasoning_effort).toBe("max"); + }); }); describe("extractReasoningText (response shapes)", () => { diff --git a/tests/unit/capabilities.test.js b/tests/unit/capabilities.test.js index 3482a2c8..cde8e9b4 100644 --- a/tests/unit/capabilities.test.js +++ b/tests/unit/capabilities.test.js @@ -73,4 +73,26 @@ describe("getCapabilitiesForModel", () => { maxOutput: 128000, }); }); + + it("CommandCode v4.1-flash is vision + effort capable", () => { + expect(getCapabilitiesForModel("commandcode", "deepseek/deepseek-v4.1-flash")).toMatchObject({ + vision: true, + reasoning: true, + thinkingFormat: "commandcode", + thinkingEffortSupported: true, + }); + }); + + it("CommandCode MiniMax-M3 is vision capable", () => { + expect(getCapabilitiesForModel("commandcode", "MiniMaxAI/MiniMax-M3").vision).toBe(true); + }); + + it("CommandCode text-only DeepSeek V4 Flash stays non-vision", () => { + expect(getCapabilitiesForModel("commandcode", "deepseek/deepseek-v4-flash").vision).toBe(false); + expect(getCapabilitiesForModel("commandcode", "deepseek/deepseek-v4-flash")).toMatchObject({ + reasoning: true, + thinkingFormat: "commandcode", + thinkingEffortSupported: true, + }); + }); }); diff --git a/tests/unit/commandcode-usage.test.js b/tests/unit/commandcode-usage.test.js new file mode 100644 index 00000000..5f38dc47 --- /dev/null +++ b/tests/unit/commandcode-usage.test.js @@ -0,0 +1,135 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; + +vi.mock("../../open-sse/utils/proxyFetch.js", () => ({ + proxyAwareFetch: vi.fn(), +})); + +import { proxyAwareFetch } from "../../open-sse/utils/proxyFetch.js"; +import { getUsageForProvider } from "../../open-sse/services/usage.js"; +import { + USAGE_SUPPORTED_PROVIDERS, + USAGE_APIKEY_PROVIDERS, +} from "../../src/shared/constants/providers.js"; +import { parseQuotaData } from "../../src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.js"; + +const BASE = "https://api.commandcode.ai"; + +function jsonResponse(body, status = 200) { + return new Response(JSON.stringify(body), { + status, + headers: { "Content-Type": "application/json" }, + }); +} + +const WHOAMI = { + user: { name: "Hieu", email: "hieu@example.com" }, + org: { id: "org_1", name: "personal" }, +}; +const CREDITS = { + credits: { monthlyCredits: 12.5, purchasedCredits: 1, freeCredits: 0.5 }, + windowLimits: { + fiveHour: { used: 2, cap: 10, resetAt: Date.now() + 3_600_000, exceeded: false }, + weekly: { used: 20, cap: 70, resetAt: Date.now() + 86_400_000, exceeded: false }, + }, +}; +const SUBS = { + data: { + planId: "individual-goat", + currentPeriodStart: "2026-09-01T00:00:00.000Z", + currentPeriodEnd: "2026-10-01T00:00:00.000Z", + }, +}; + +function mockHappyPath() { + proxyAwareFetch.mockImplementation(async (url) => { + const u = String(url); + if (u.includes("/alpha/whoami")) return jsonResponse(WHOAMI); + if (u.includes("/alpha/billing/credits")) return jsonResponse(CREDITS); + if (u.includes("/alpha/billing/subscriptions")) return jsonResponse(SUBS); + return jsonResponse({ error: "unexpected " + u }, 404); + }); +} + +describe("commandcode registry usage flags", () => { + it("is listed for apikey quota dashboard", () => { + expect(USAGE_SUPPORTED_PROVIDERS).toContain("commandcode"); + expect(USAGE_APIKEY_PROVIDERS).toContain("commandcode"); + }); +}); + +describe("getUsageForProvider(commandcode)", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("returns a message when apiKey is missing", async () => { + const usage = await getUsageForProvider({ provider: "commandcode" }); + expect(usage.message).toMatch(/api key/i); + expect(proxyAwareFetch).not.toHaveBeenCalled(); + }); + + it("GETs whoami, credits, and subscriptions with Bearer apiKey", async () => { + mockHappyPath(); + const usage = await getUsageForProvider({ + provider: "commandcode", + apiKey: "user_test", + }); + + expect(usage.message).toBeUndefined(); + expect(usage.plan).toBe("GOAT"); + const urls = proxyAwareFetch.mock.calls.map(([url]) => String(url)); + expect(urls.some((u) => u.startsWith(`${BASE}/alpha/whoami`))).toBe(true); + expect(urls.some((u) => u.includes("/alpha/billing/credits") && u.includes("orgId=org_1"))).toBe(true); + expect(urls.some((u) => u.includes("/alpha/billing/subscriptions") && u.includes("orgId=org_1"))).toBe(true); + expect(proxyAwareFetch.mock.calls[0][1].headers.Authorization).toBe("Bearer user_test"); + }); + + it("maps remaining credits vs plan cap and rate windows", async () => { + mockHappyPath(); + const usage = await getUsageForProvider({ + provider: "commandcode", + apiKey: "user_test", + }); + + // remaining = 12.5 + 1 + 0.5 = 14; cap GOAT = 70; used = 56 + expect(usage.quotas.Credits).toMatchObject({ + used: 56, + total: 70, + unlimited: false, + }); + expect(usage.quotas["Session (5h)"]).toMatchObject({ + used: 2, + total: 10, + unlimited: false, + }); + expect(usage.quotas.Weekly).toMatchObject({ + used: 20, + total: 70, + }); + expect(new Date(usage.quotas.Credits.resetAt).toISOString()).toBe("2026-10-01T00:00:00.000Z"); + }); + + it("returns an auth message on 401", async () => { + proxyAwareFetch.mockResolvedValueOnce(jsonResponse({ error: "unauthorized" }, 401)); + const usage = await getUsageForProvider({ + provider: "commandcode", + apiKey: "bad", + }); + expect(usage.message).toMatch(/auth|key|login/i); + }); +}); + +describe("parseQuotaData(commandcode)", () => { + it("forwards used/total/resetAt for the dashboard table", () => { + const rows = parseQuotaData("commandcode", { + plan: "GOAT", + quotas: { + Credits: { used: 56, total: 70, resetAt: "2026-10-01T00:00:00.000Z" }, + "Session (5h)": { used: 2, total: 10, resetAt: "2026-09-16T10:00:00.000Z" }, + }, + }); + expect(rows).toHaveLength(2); + expect(rows[0]).toMatchObject({ name: "Credits", used: 56, total: 70 }); + expect(rows[1]).toMatchObject({ name: "Session (5h)", used: 2, total: 10 }); + }); +}); diff --git a/tests/unit/openai-to-commandcode.test.js b/tests/unit/openai-to-commandcode.test.js index 7f12dc85..0a441dd7 100644 --- a/tests/unit/openai-to-commandcode.test.js +++ b/tests/unit/openai-to-commandcode.test.js @@ -179,3 +179,57 @@ describe("openaiToCommandCodeRequest — tools schema conversion", () => { expect(out.params.tools).toBeUndefined(); }); }); + +describe("openaiToCommandCodeRequest — native image blocks", () => { + const PNG_B64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=="; + const DATA_URI = `data:image/png;base64,${PNG_B64}`; + + it("maps OpenAI image_url data URI to CommandCode {type:image,image,mimeType}", () => { + const out = openaiToCommandCodeRequest(MODEL, { + messages: [{ + role: "user", + content: [ + { type: "text", text: "what color?" }, + { type: "image_url", image_url: { url: DATA_URI } }, + ], + }], + }, true); + + expect(out.params.messages[0].content).toEqual([ + { type: "text", text: "what color?" }, + { type: "image", image: DATA_URI, mimeType: "image/png" }, + ]); + }); + + it("maps Claude/OpenAI base64 image source to a data-URI image block", () => { + const out = openaiToCommandCodeRequest(MODEL, { + messages: [{ + role: "user", + content: [ + { type: "image", source: { type: "base64", media_type: "image/png", data: PNG_B64 } }, + ], + }], + }, true); + + expect(out.params.messages[0].content).toEqual([ + { type: "image", image: DATA_URI, mimeType: "image/png" }, + ]); + }); + + it("does not stub dropped images as [image omitted]", () => { + const out = openaiToCommandCodeRequest(MODEL, { + messages: [{ + role: "user", + content: [ + { type: "text", text: "see this" }, + { type: "image_url", image_url: { url: DATA_URI } }, + ], + }], + }, true); + + const texts = out.params.messages[0].content + .filter((b) => b.type === "text") + .map((b) => b.text); + expect(texts).not.toContain("[image omitted]"); + }); +}); diff --git a/tests/unit/prefetch-images.test.js b/tests/unit/prefetch-images.test.js index 2289697b..0d4c0db3 100644 --- a/tests/unit/prefetch-images.test.js +++ b/tests/unit/prefetch-images.test.js @@ -55,4 +55,21 @@ describe("prefetchRemoteImages", () => { expect(n).toBe(1); expect(body.messages[0].content[0].source.type).toBe("base64"); }); + + it("openai source -> commandcode target: converts remote URL to base64", async () => { + const body = { messages: [{ role: "user", content: [{ type: "image_url", image_url: { url: "https://x/a.png" } }] }] }; + const n = await prefetchRemoteImages(body, FORMATS.OPENAI, FORMATS.COMMANDCODE); + expect(n).toBe(1); + expect(body.messages[0].content[0].image_url.url.startsWith("data:image/png;base64,")).toBe(true); + expect(fetchImageAsBase64).toHaveBeenCalled(); + }); + + it("claude source -> commandcode target: source.url -> base64", async () => { + const body = { messages: [{ role: "user", content: [ + { type: "image", source: { type: "url", url: "https://x/a.png" } }, + ] }] }; + const n = await prefetchRemoteImages(body, FORMATS.CLAUDE, FORMATS.COMMANDCODE); + expect(n).toBe(1); + expect(body.messages[0].content[0].source.type).toBe("base64"); + }); }); diff --git a/tests/unit/usage-dispatch.test.js b/tests/unit/usage-dispatch.test.js index 84a5f469..ead63892 100644 --- a/tests/unit/usage-dispatch.test.js +++ b/tests/unit/usage-dispatch.test.js @@ -16,7 +16,7 @@ const SUPPORTED = [ "github", "gemini-cli", "antigravity", "claude", "codex", "kiro", "qoder", "iflow", "ollama", "glm", "glm-cn", "minimax", "minimax-cn", "vercel-ai-gateway", "grok-cli", "kimi", - "deepseek", "opencode-go", "zed", + "deepseek", "opencode-go", "zed", "commandcode", ]; describe("usage dispatch", () => {