mirror of https://github.com/openclaw/openclaw.git
121 lines
3.9 KiB
TypeScript
121 lines
3.9 KiB
TypeScript
import type { ImageContent } from "@mariozechner/pi-ai";
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { MAX_IMAGE_BYTES } from "../media/constants.js";
|
|
import { buildCliArgs, loadPromptRefImages } from "./cli-runner/helpers.js";
|
|
import * as promptImageUtils from "./pi-embedded-runner/run/images.js";
|
|
import type { SandboxFsBridge } from "./sandbox/fs-bridge.js";
|
|
import * as toolImages from "./tool-images.js";
|
|
|
|
describe("loadPromptRefImages", () => {
|
|
beforeEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it("returns empty results when the prompt has no image refs", async () => {
|
|
const loadImageFromRefSpy = vi.spyOn(promptImageUtils, "loadImageFromRef");
|
|
const sanitizeImageBlocksSpy = vi.spyOn(toolImages, "sanitizeImageBlocks");
|
|
|
|
await expect(
|
|
loadPromptRefImages({
|
|
prompt: "just text",
|
|
workspaceDir: "/workspace",
|
|
}),
|
|
).resolves.toEqual([]);
|
|
|
|
expect(loadImageFromRefSpy).not.toHaveBeenCalled();
|
|
expect(sanitizeImageBlocksSpy).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("passes the max-byte guardrail through load and sanitize", async () => {
|
|
const loadedImage: ImageContent = {
|
|
type: "image",
|
|
data: "c29tZS1pbWFnZQ==",
|
|
mimeType: "image/png",
|
|
};
|
|
const sanitizedImage: ImageContent = {
|
|
type: "image",
|
|
data: "c2FuaXRpemVkLWltYWdl",
|
|
mimeType: "image/jpeg",
|
|
};
|
|
const sandbox = {
|
|
root: "/sandbox",
|
|
bridge: {} as SandboxFsBridge,
|
|
};
|
|
|
|
const loadImageFromRefSpy = vi
|
|
.spyOn(promptImageUtils, "loadImageFromRef")
|
|
.mockResolvedValueOnce(loadedImage);
|
|
const sanitizeImageBlocksSpy = vi
|
|
.spyOn(toolImages, "sanitizeImageBlocks")
|
|
.mockResolvedValueOnce({ images: [sanitizedImage], dropped: 0 });
|
|
|
|
const result = await loadPromptRefImages({
|
|
prompt: "Look at /tmp/photo.png",
|
|
workspaceDir: "/workspace",
|
|
workspaceOnly: true,
|
|
sandbox,
|
|
});
|
|
|
|
const [ref, workspaceDir, options] = loadImageFromRefSpy.mock.calls[0] ?? [];
|
|
expect(ref).toMatchObject({ resolved: "/tmp/photo.png", type: "path" });
|
|
expect(workspaceDir).toBe("/workspace");
|
|
expect(options).toEqual({
|
|
maxBytes: MAX_IMAGE_BYTES,
|
|
workspaceOnly: true,
|
|
sandbox,
|
|
});
|
|
expect(sanitizeImageBlocksSpy).toHaveBeenCalledWith([loadedImage], "prompt:images", {
|
|
maxBytes: MAX_IMAGE_BYTES,
|
|
});
|
|
expect(result).toEqual([sanitizedImage]);
|
|
});
|
|
|
|
it("dedupes repeated refs and skips failed loads before sanitizing", async () => {
|
|
const loadedImage: ImageContent = {
|
|
type: "image",
|
|
data: "b25lLWltYWdl",
|
|
mimeType: "image/png",
|
|
};
|
|
|
|
const loadImageFromRefSpy = vi
|
|
.spyOn(promptImageUtils, "loadImageFromRef")
|
|
.mockResolvedValueOnce(loadedImage)
|
|
.mockResolvedValueOnce(null);
|
|
const sanitizeImageBlocksSpy = vi
|
|
.spyOn(toolImages, "sanitizeImageBlocks")
|
|
.mockResolvedValueOnce({ images: [loadedImage], dropped: 0 });
|
|
|
|
const result = await loadPromptRefImages({
|
|
prompt: "Compare /tmp/a.png with /tmp/a.png and /tmp/b.png",
|
|
workspaceDir: "/workspace",
|
|
});
|
|
|
|
expect(loadImageFromRefSpy).toHaveBeenCalledTimes(2);
|
|
expect(
|
|
loadImageFromRefSpy.mock.calls.map(
|
|
(call) => (call[0] as { resolved?: string } | undefined)?.resolved,
|
|
),
|
|
).toEqual(["/tmp/a.png", "/tmp/b.png"]);
|
|
expect(sanitizeImageBlocksSpy).toHaveBeenCalledWith([loadedImage], "prompt:images", {
|
|
maxBytes: MAX_IMAGE_BYTES,
|
|
});
|
|
expect(result).toEqual([loadedImage]);
|
|
});
|
|
});
|
|
|
|
describe("buildCliArgs", () => {
|
|
it("keeps passing model overrides on resumed CLI sessions", () => {
|
|
expect(
|
|
buildCliArgs({
|
|
backend: {
|
|
command: "codex",
|
|
modelArg: "--model",
|
|
},
|
|
baseArgs: ["exec", "resume", "thread-123"],
|
|
modelId: "gpt-5.4",
|
|
useResume: true,
|
|
}),
|
|
).toEqual(["exec", "resume", "thread-123", "--model", "gpt-5.4"]);
|
|
});
|
|
});
|