Ctrl K
ComunidadGuías…

test/image-tools.test.js

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"use strict";

// Pembuatan gambar: tool generate_image (Grok Imagine), deteksi dukungan per provider,
// sakelar Off, dan metadata kartu chat. Hermetik: fetch palsu, workspace sementara.

const assert = require("assert");
const fs = require("fs");
const os = require("os");
const path = require("path");
const createImageTools = require("../lib/image-tools");
const AiChat = require("../lib/ai-chat");
const ProviderFactory = require("../lib/provider-factory");
const I = require("../lib/agent-instructions");

const PNG = Buffer.from("89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d4944415478da63f8ffff3f0005fe02fea7d6a4b50000000049454e44ae426082", "hex");

function fakeFetch(handler) {
  const calls = [];
  const fn = async (url, init) => {
    const body = init && init.body ? JSON.parse(init.body) : null;
    calls.push({ url, method: (init && init.method) || "GET", body, headers: (init && init.headers) || {} });
    const r = await handler(url, body, calls.length);
    const status = r.status || 200;
    return {
      ok: status >= 200 && status < 300, status, statusText: "",
      json: async () => r.json, text: async () => JSON.stringify(r.json || { error: { message: r.error || "" } }),
      arrayBuffer: async () => r.bytes,
    };
  };
  fn.calls = calls;
  return fn;
}

(async () => {
  const root = fs.mkdtempSync(path.join(os.tmpdir(), "vrcloud-img-"));
  try {
    const auth = { token: "t", bases: ["https://api.example/v1"] };

    // Generate: model dari discovery, b64 disimpan, path default, ekstensi dari isi berkas.
    {
      const f = fakeFetch((url, body) => {
        if (/image-generation-models$/.test(url)) return { json: { models: [{ id: "grok-2-image" }, { id: "grok-imagine-image-2.0" }] } };
        assert.strictEqual(body.model, "grok-imagine-image-2.0", "best-ranked model is used first");
        assert.strictEqual(body.response_format, "b64_json");
        return { json: { data: [{ b64_json: PNG.toString("base64"), revised_prompt: "a red fox" }] } };
      });
      const tools = createImageTools({ workspace: root, getAuth: async () => auth, fetch: f });
      const r = await tools.generate_image.execute({ prompt: "A red fox, flat illustration", aspect_ratio: "16:9" });
      const im = r.structuredContent.images[0];
      assert.ok(/^generated\/images\/a-red-fox-flat-illustration-\d{8}-\d{6}\.png$/.test(im.path), im.path);
      assert.ok(fs.readFileSync(path.join(root, im.path)).equals(PNG));
      assert.strictEqual(im.url, "/preview/" + im.path);
      assert.strictEqual(r.structuredContent.path, im.path);
      assert.deepStrictEqual(r.structuredContent.paths, [im.path]);
      assert.ok(/Generated 1 image\(s\) with grok-imagine-image-2\.0/.test(r.content[0].text));
      assert.strictEqual(f.calls.find((c) => /generations$/.test(c.url)).body.aspect_ratio, "16:9");
    }

    // Path eksplisit (ekstensi diganti sesuai format), n variasi, URL yang diunduh.
    {
      const f = fakeFetch((url, body) => {
        if (/image-generation-models$/.test(url)) return { status: 404, error: "nope" };
        if (/generations$/.test(url)) return { json: { data: [{ url: "https://cdn/1" }, { url: "https://cdn/2" }] } };
        return { bytes: PNG };
      });
      const tools = createImageTools({ workspace: root, getAuth: async () => auth, fetch: f });
      const r = await tools.generate_image.execute({ prompt: "logo", path: "public/img/logo.jpg", n: 2 });
      assert.deepStrictEqual(r.structuredContent.paths, ["public/img/logo-1.png", "public/img/logo-2.png"]);
      assert.strictEqual(f.calls.find((c) => /generations$/.test(c.url)).body.model, createImageTools.DEFAULT_MODELS[0], "fallback model list when discovery fails");
    }

    // aspect_ratio ditolak -> kirim ulang tanpa; model tidak ada -> model berikutnya.
    {
      const f = fakeFetch((url, body) => {
        if (/image-generation-models$/.test(url)) return { json: { models: [{ id: "grok-imagine-image-2.0" }, { id: "grok-2-image" }] } };
        if (body.model === "grok-imagine-image-2.0") return { status: 400, error: "model not available for this team" };
        if (body.aspect_ratio) return { status: 400, error: "Unknown field aspect_ratio" };
        return { json: { data: [{ b64_json: PNG.toString("base64") }] } };
      });
      const tools = createImageTools({ workspace: root, getAuth: async () => auth, fetch: f });
      const r = await tools.generate_image.execute({ prompt: "x", aspect_ratio: "1:1", path: "a/b.png" });
      assert.strictEqual(r.structuredContent.model, "grok-2-image");
    }

    // Edit: gambar sumber dikirim sebagai data URI ke /images/edits.
    {
      fs.writeFileSync(path.join(root, "src.png"), PNG);
      const f = fakeFetch((url, body) => {
        if (/image-generation-models$/.test(url)) return { json: { models: [{ id: "grok-imagine-image-2.0" }] } };
        assert.ok(/\/images\/edits$/.test(url), "edit endpoint");
        assert.ok(/^data:image\/png;base64,/.test(body.image.url));
        return { json: { data: [{ b64_json: PNG.toString("base64") }] } };
      });
      const tools = createImageTools({ workspace: root, getAuth: async () => auth, fetch: f });
      const r = await tools.generate_image.execute({ prompt: "make it blue", source_images: ["src.png"], path: "out/blue.png" });
      assert.strictEqual(r.structuredContent.mode, "edit");
    }

    // Path di luar workspace ditolak; tanpa kredensial = pesan yang jelas; 401 tidak dicoba ulang ke base lain.
    {
      const f = fakeFetch(() => ({ json: { data: [{ b64_json: PNG.toString("base64") }] } }));
      const tools = createImageTools({ workspace: root, getAuth: async () => auth, fetch: f });
      for (const bad of ["../escape.png", "/etc/x.png", "a/../../x.png"]) {
        await assert.rejects(tools.generate_image.execute({ prompt: "x", path: bad, model: "m" }), /workspace/);
      }
      await assert.rejects(tools.generate_image.execute({ prompt: "x", source_images: ["../../secret.png"], model: "m" }), /workspace/);
      const none = createImageTools({ workspace: root, getAuth: async () => null, fetch: f });
      await assert.rejects(none.generate_image.execute({ prompt: "x" }), /sign in with Grok|XAI_API_KEY/);
      const f401 = fakeFetch(() => ({ status: 401, error: "bad token" }));
      const two = createImageTools({ workspace: root, getAuth: async () => ({ token: "t", bases: ["https://a/v1", "https://b/v1"] }), fetch: f401 });
      await assert.rejects(two.generate_image.execute({ prompt: "x", model: "m" }), /401/);
      assert.strictEqual(f401.calls.length, 1, "401 is final, not retried on the next base");
      const f403 = fakeFetch((url) => (/^https:\/\/a\//.test(url) ? { status: 403, error: "forbidden" } : { json: { data: [{ b64_json: PNG.toString("base64") }] } }));
      const fb = createImageTools({ workspace: root, getAuth: async () => ({ token: "t", bases: ["https://a/v1", "https://b/v1"] }), fetch: f403 });
      const ok = await fb.generate_image.execute({ prompt: "x", model: "m", path: "fb.png" });
      assert.strictEqual(ok.structuredContent.path, "fb.png", "403 falls through to the next base");
    }

    // Dukungan per provider + penyaringan tool + sakelar Off.
    {
      const chat = new AiChat({
        workspace: root, storeFile: path.join(root, "s.json"), configFile: path.join(root, "c.json"),
        providerFactory: { cursorStatus: () => ({ available: true, error: "" }), create: async () => { throw new Error("unused"); } },
        orchestration: "off",
        grokAuth: { status: () => ({ connected: true }), getAccessToken: async () => "tok", pending: null, connected: () => true },
        customTools: () => Object.assign({ read_only_x: { annotations: { readOnlyHint: true } } }, createImageTools({ workspace: root })),
      });
      const prevKey = process.env.XAI_API_KEY;
      delete process.env.XAI_API_KEY;
      try {
        chat.provider = "cursor";
        assert.deepStrictEqual(chat.imageSupport().via, "native");
        assert.ok(!chat.customToolSet().generate_image, "Cursor uses its native generateImage, not ours");
        chat.provider = "grok";
        assert.strictEqual(chat.imageSupport().supported, true);
        assert.ok(chat.customToolSet().generate_image, "Grok gets generate_image");
        const a = await chat.imageAuth();
        assert.strictEqual(a.token, "tok");
        assert.ok(a.bases.length >= 1 && typeof a.headers === "function");
        chat.provider = "anthropic";
        assert.strictEqual(chat.imageSupport().supported, false);
        assert.ok(/Claude/.test(chat.imageSupport().reason));
        assert.ok(!chat.customToolSet().generate_image, "Claude without XAI_API_KEY: no tool");
        process.env.XAI_API_KEY = "xai-test";
        assert.strictEqual(chat.imageSupport().via, "xai-key");
        assert.ok(chat.customToolSet().generate_image);
        assert.strictEqual((await chat.imageAuth()).token, "xai-test");
        delete process.env.XAI_API_KEY;

        chat.provider = "grok";
        await chat.setConfig({ imageTools: "off" });
        assert.strictEqual(chat.status().imageTools, "off");
        assert.ok(!chat.customToolSet().generate_image, "Off removes the tool");
        assert.deepStrictEqual(chat.imageRestrictions(), { noImage: true });
        await chat.setConfig({ imageTools: "auto" });
        assert.deepStrictEqual(chat.imageRestrictions(), {});
        await assert.rejects(chat.setConfig({ imageTools: "sometimes" }), /imageTools/);
        assert.ok(AiChat.isEvidenceTool("generate_image") && AiChat.isEvidenceTool("generateimage"));
      } finally {
        if (prevKey == null) delete process.env.XAI_API_KEY; else process.env.XAI_API_KEY = prevKey;
        await chat.close();
      }
    }

    // Off juga melarang generateImage bawaan Cursor di setiap anak tangga fallback.
    {
      const ladder = ProviderFactory.disallowedCandidates("agent", { noImage: true });
      assert.ok(ladder.every((rung) => rung.indexOf("generateImage") !== -1));
      const both = ProviderFactory.disallowedCandidates("agent", { noImage: true, noNativeTask: true });
      assert.ok(both.every((rung) => rung.indexOf("generateImage") !== -1 && rung.indexOf("task") !== -1));
      assert.ok(!ProviderFactory.filterCustomTools({ generate_image: {} }, { noImage: true }).generate_image);
      assert.ok(!ProviderFactory.filterCustomTools({ generate_image: {} }, { readOnly: true }).generate_image, "Ask mode cannot write images");
    }

    // Prompt: tiga keadaan.
    {
      const base = { workspace: root, mode: "agent", platform: "linux", userText: "x" };
      assert.ok(/Image generation is available \(generate_image tool, Grok Imagine\)/.test(I.agentPrompt(Object.assign({ images: { on: true, via: "xai" } }, base))));
      assert.ok(/generateImage tool/.test(I.agentPrompt(Object.assign({ images: { on: true, via: "native" } }, base))));
      assert.ok(/NOT available with the current model \(Claude/.test(I.agentPrompt(Object.assign({ images: { on: false, mode: "auto", reason: "Claude models cannot generate images" } }, base))));
      assert.ok(/turned OFF by the user/.test(I.agentPrompt(Object.assign({ images: { on: false, mode: "off" } }, base))));
      assert.ok(/Grok Imagine/.test(I.tool("generate_image")));
    }

    // Metadata kartu chat: generate_image (xAI) dan generateImage bawaan Cursor.
    {
      const chat = new AiChat({ workspace: root, storeFile: path.join(root, "s2.json"), configFile: path.join(root, "c2.json"),
        providerFactory: { cursorStatus: () => ({ available: true, error: "" }), create: async () => { throw new Error("unused"); } }, orchestration: "off" });
      const frames = [];
      const emit = (kind, val) => frames.push({ kind, val });
      chat.emitFromEvent({ type: "tool_call", call_id: "g1", name: "mcp", status: "completed",
        args: { toolName: "generate_image", args: { prompt: "fox" } },
        result: { status: "success", value: { structuredContent: { model: "m", mode: "generate", images: [{ path: "generated/images/fox.png", url: "/preview/generated/images/fox.png" }], path: "generated/images/fox.png", paths: ["generated/images/fox.png"] } } } }, emit, null);
      const t1 = frames.find((f) => f.kind === "tool").val;
      assert.deepStrictEqual(t1.meta.images, [{ url: "/preview/generated/images/fox.png", path: "generated/images/fox.png" }]);
      frames.length = 0;
      chat.emitFromEvent({ type: "tool_call", call_id: "g2", name: "generateImage", status: "completed",
        args: { prompt: "logo" }, result: { status: "success", value: { filePath: path.join(root, "assets", "my logo.png") } } }, emit, null);
      const t2 = frames.find((f) => f.kind === "tool").val;
      assert.deepStrictEqual(t2.meta.images, [{ url: "/preview/assets/my%20logo.png", path: "assets/my logo.png" }]);
      await chat.close();
    }

    console.log("image-tools: ok");
  } finally {
    fs.rmSync(root, { recursive: true, force: true });
  }
})().catch((error) => {
  console.error(error.stack || error);
  process.exitCode = 1;
});