mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 03:42:43 +00:00
refactor(tests): dedupe tool, projector, and delivery fixtures
This commit is contained in:
@@ -64,6 +64,21 @@ function stubMinimaxOkFetch() {
|
||||
return fetch;
|
||||
}
|
||||
|
||||
function stubMinimaxFetch(baseResp: { status_code: number; status_msg: string }, content = "ok") {
|
||||
const fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: "OK",
|
||||
headers: new Headers(),
|
||||
json: async () => ({
|
||||
content,
|
||||
base_resp: baseResp,
|
||||
}),
|
||||
});
|
||||
global.fetch = withFetchPreconnect(fetch);
|
||||
return fetch;
|
||||
}
|
||||
|
||||
function stubOpenAiCompletionsOkFetch(text = "ok") {
|
||||
const fetch = vi.fn().mockResolvedValue(
|
||||
new Response(
|
||||
@@ -120,6 +135,13 @@ function createMinimaxImageConfig(): OpenClawConfig {
|
||||
};
|
||||
}
|
||||
|
||||
function createDefaultImageFallbackExpectation(primary: string) {
|
||||
return {
|
||||
primary,
|
||||
fallbacks: ["openai/gpt-5-mini", "anthropic/claude-opus-4-5"],
|
||||
};
|
||||
}
|
||||
|
||||
function makeModelDefinition(id: string, input: Array<"text" | "image">): ModelDefinitionConfig {
|
||||
return {
|
||||
id,
|
||||
@@ -156,6 +178,36 @@ function requireImageTool<T>(tool: T | null | undefined): T {
|
||||
return tool;
|
||||
}
|
||||
|
||||
function createRequiredImageTool(args: Parameters<typeof createImageTool>[0]) {
|
||||
return requireImageTool(createImageTool(args));
|
||||
}
|
||||
|
||||
type ImageToolInstance = ReturnType<typeof createRequiredImageTool>;
|
||||
|
||||
async function withTempSandboxState(
|
||||
run: (ctx: { stateDir: string; agentDir: string; sandboxRoot: string }) => Promise<void>,
|
||||
) {
|
||||
const stateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-image-sandbox-"));
|
||||
const agentDir = path.join(stateDir, "agent");
|
||||
const sandboxRoot = path.join(stateDir, "sandbox");
|
||||
await fs.mkdir(agentDir, { recursive: true });
|
||||
await fs.mkdir(sandboxRoot, { recursive: true });
|
||||
try {
|
||||
await run({ stateDir, agentDir, sandboxRoot });
|
||||
} finally {
|
||||
await fs.rm(stateDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
async function withMinimaxImageToolFromTempAgentDir(
|
||||
run: (tool: ImageToolInstance) => Promise<void>,
|
||||
) {
|
||||
await withTempAgentDir(async (agentDir) => {
|
||||
const cfg = createMinimaxImageConfig();
|
||||
await run(createRequiredImageTool({ config: cfg, agentDir }));
|
||||
});
|
||||
}
|
||||
|
||||
function findSchemaUnionKeywords(schema: unknown, path = "root"): string[] {
|
||||
if (!schema || typeof schema !== "object") {
|
||||
return [];
|
||||
@@ -214,10 +266,9 @@ describe("image tool implicit imageModel config", () => {
|
||||
const cfg: OpenClawConfig = {
|
||||
agents: { defaults: { model: { primary: "minimax/MiniMax-M2.5" } } },
|
||||
};
|
||||
expect(resolveImageModelConfigForTool({ cfg, agentDir })).toEqual({
|
||||
primary: "minimax/MiniMax-VL-01",
|
||||
fallbacks: ["openai/gpt-5-mini", "anthropic/claude-opus-4-5"],
|
||||
});
|
||||
expect(resolveImageModelConfigForTool({ cfg, agentDir })).toEqual(
|
||||
createDefaultImageFallbackExpectation("minimax/MiniMax-VL-01"),
|
||||
);
|
||||
expect(createImageTool({ config: cfg, agentDir })).not.toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -230,10 +281,9 @@ describe("image tool implicit imageModel config", () => {
|
||||
const cfg: OpenClawConfig = {
|
||||
agents: { defaults: { model: { primary: "zai/glm-4.7" } } },
|
||||
};
|
||||
expect(resolveImageModelConfigForTool({ cfg, agentDir })).toEqual({
|
||||
primary: "zai/glm-4.6v",
|
||||
fallbacks: ["openai/gpt-5-mini", "anthropic/claude-opus-4-5"],
|
||||
});
|
||||
expect(resolveImageModelConfigForTool({ cfg, agentDir })).toEqual(
|
||||
createDefaultImageFallbackExpectation("zai/glm-4.6v"),
|
||||
);
|
||||
expect(createImageTool({ config: cfg, agentDir })).not.toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -383,11 +433,7 @@ describe("image tool implicit imageModel config", () => {
|
||||
});
|
||||
|
||||
it("exposes an Anthropic-safe image schema without union keywords", async () => {
|
||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-image-"));
|
||||
try {
|
||||
const cfg = createMinimaxImageConfig();
|
||||
const tool = requireImageTool(createImageTool({ config: cfg, agentDir }));
|
||||
|
||||
await withMinimaxImageToolFromTempAgentDir(async (tool) => {
|
||||
const violations = findSchemaUnionKeywords(tool.parameters, "image.parameters");
|
||||
expect(violations).toEqual([]);
|
||||
|
||||
@@ -403,17 +449,11 @@ describe("image tool implicit imageModel config", () => {
|
||||
expect(imageSchema?.type).toBe("string");
|
||||
expect(imagesSchema?.type).toBe("array");
|
||||
expect(imageItems?.type).toBe("string");
|
||||
} finally {
|
||||
await fs.rm(agentDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps an Anthropic-safe image schema snapshot", async () => {
|
||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-image-"));
|
||||
try {
|
||||
const cfg = createMinimaxImageConfig();
|
||||
const tool = requireImageTool(createImageTool({ config: cfg, agentDir }));
|
||||
|
||||
await withMinimaxImageToolFromTempAgentDir(async (tool) => {
|
||||
expect(JSON.parse(JSON.stringify(tool.parameters))).toEqual({
|
||||
type: "object",
|
||||
properties: {
|
||||
@@ -429,19 +469,16 @@ describe("image tool implicit imageModel config", () => {
|
||||
maxImages: { type: "number" },
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
await fs.rm(agentDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("allows workspace images outside default local media roots", async () => {
|
||||
await withTempWorkspacePng(async ({ workspaceDir, imagePath }) => {
|
||||
const fetch = stubMinimaxOkFetch();
|
||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-image-"));
|
||||
try {
|
||||
await withTempAgentDir(async (agentDir) => {
|
||||
const cfg = createMinimaxImageConfig();
|
||||
|
||||
const withoutWorkspace = requireImageTool(createImageTool({ config: cfg, agentDir }));
|
||||
const withoutWorkspace = createRequiredImageTool({ config: cfg, agentDir });
|
||||
await expect(
|
||||
withoutWorkspace.execute("t0", {
|
||||
prompt: "Describe the image.",
|
||||
@@ -449,34 +486,27 @@ describe("image tool implicit imageModel config", () => {
|
||||
}),
|
||||
).rejects.toThrow(/Local media path is not under an allowed directory/i);
|
||||
|
||||
const withWorkspace = requireImageTool(
|
||||
createImageTool({ config: cfg, agentDir, workspaceDir }),
|
||||
);
|
||||
const withWorkspace = createRequiredImageTool({ config: cfg, agentDir, workspaceDir });
|
||||
|
||||
await expectImageToolExecOk(withWorkspace, imagePath);
|
||||
|
||||
expect(fetch).toHaveBeenCalledTimes(1);
|
||||
} finally {
|
||||
await fs.rm(agentDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("respects fsPolicy.workspaceOnly for non-sandbox image paths", async () => {
|
||||
await withTempWorkspacePng(async ({ workspaceDir, imagePath }) => {
|
||||
const fetch = stubMinimaxOkFetch();
|
||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-image-"));
|
||||
try {
|
||||
await withTempAgentDir(async (agentDir) => {
|
||||
const cfg = createMinimaxImageConfig();
|
||||
|
||||
const tool = requireImageTool(
|
||||
createImageTool({
|
||||
config: cfg,
|
||||
agentDir,
|
||||
workspaceDir,
|
||||
fsPolicy: { workspaceOnly: true },
|
||||
}),
|
||||
);
|
||||
const tool = createRequiredImageTool({
|
||||
config: cfg,
|
||||
agentDir,
|
||||
workspaceDir,
|
||||
fsPolicy: { workspaceOnly: true },
|
||||
});
|
||||
|
||||
// File inside workspace is allowed.
|
||||
await expectImageToolExecOk(tool, imagePath);
|
||||
@@ -493,17 +523,14 @@ describe("image tool implicit imageModel config", () => {
|
||||
} finally {
|
||||
await fs.rm(outsideDir, { recursive: true, force: true });
|
||||
}
|
||||
} finally {
|
||||
await fs.rm(agentDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("allows workspace images via createOpenClawCodingTools default workspace root", async () => {
|
||||
await withTempWorkspacePng(async ({ imagePath }) => {
|
||||
const fetch = stubMinimaxOkFetch();
|
||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-image-"));
|
||||
try {
|
||||
await withTempAgentDir(async (agentDir) => {
|
||||
const cfg = createMinimaxImageConfig();
|
||||
|
||||
const tools = createOpenClawCodingTools({ config: cfg, agentDir });
|
||||
@@ -512,52 +539,44 @@ describe("image tool implicit imageModel config", () => {
|
||||
await expectImageToolExecOk(tool, imagePath);
|
||||
|
||||
expect(fetch).toHaveBeenCalledTimes(1);
|
||||
} finally {
|
||||
await fs.rm(agentDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("sandboxes image paths like the read tool", async () => {
|
||||
const stateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-image-sandbox-"));
|
||||
const agentDir = path.join(stateDir, "agent");
|
||||
const sandboxRoot = path.join(stateDir, "sandbox");
|
||||
await fs.mkdir(agentDir, { recursive: true });
|
||||
await fs.mkdir(sandboxRoot, { recursive: true });
|
||||
await fs.writeFile(path.join(sandboxRoot, "img.png"), "fake", "utf8");
|
||||
const sandbox = { root: sandboxRoot, bridge: createHostSandboxFsBridge(sandboxRoot) };
|
||||
await withTempSandboxState(async ({ agentDir, sandboxRoot }) => {
|
||||
await fs.writeFile(path.join(sandboxRoot, "img.png"), "fake", "utf8");
|
||||
const sandbox = { root: sandboxRoot, bridge: createHostSandboxFsBridge(sandboxRoot) };
|
||||
|
||||
vi.stubEnv("OPENAI_API_KEY", "openai-test");
|
||||
const cfg: OpenClawConfig = {
|
||||
agents: { defaults: { model: { primary: "minimax/MiniMax-M2.5" } } },
|
||||
};
|
||||
const tool = requireImageTool(createImageTool({ config: cfg, agentDir, sandbox }));
|
||||
vi.stubEnv("OPENAI_API_KEY", "openai-test");
|
||||
const cfg: OpenClawConfig = {
|
||||
agents: { defaults: { model: { primary: "minimax/MiniMax-M2.5" } } },
|
||||
};
|
||||
const tool = createRequiredImageTool({ config: cfg, agentDir, sandbox });
|
||||
|
||||
await expect(tool.execute("t1", { image: "https://example.com/a.png" })).rejects.toThrow(
|
||||
/Sandboxed image tool does not allow remote URLs/i,
|
||||
);
|
||||
await expect(tool.execute("t1", { image: "https://example.com/a.png" })).rejects.toThrow(
|
||||
/Sandboxed image tool does not allow remote URLs/i,
|
||||
);
|
||||
|
||||
await expect(tool.execute("t2", { image: "../escape.png" })).rejects.toThrow(
|
||||
/escapes sandbox root/i,
|
||||
);
|
||||
await expect(tool.execute("t2", { image: "../escape.png" })).rejects.toThrow(
|
||||
/escapes sandbox root/i,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("applies tools.fs.workspaceOnly to image paths in sandbox mode", async () => {
|
||||
const stateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-image-sandbox-"));
|
||||
const agentDir = path.join(stateDir, "agent");
|
||||
const sandboxRoot = path.join(stateDir, "sandbox");
|
||||
await fs.mkdir(agentDir, { recursive: true });
|
||||
await fs.mkdir(sandboxRoot, { recursive: true });
|
||||
await fs.writeFile(path.join(agentDir, "secret.png"), Buffer.from(ONE_PIXEL_PNG_B64, "base64"));
|
||||
await withTempSandboxState(async ({ agentDir, sandboxRoot }) => {
|
||||
await fs.writeFile(
|
||||
path.join(agentDir, "secret.png"),
|
||||
Buffer.from(ONE_PIXEL_PNG_B64, "base64"),
|
||||
);
|
||||
const sandbox = createUnsafeMountedSandbox({ sandboxRoot, agentRoot: agentDir });
|
||||
const fetch = stubMinimaxOkFetch();
|
||||
const cfg: OpenClawConfig = {
|
||||
...createMinimaxImageConfig(),
|
||||
tools: { fs: { workspaceOnly: true } },
|
||||
};
|
||||
|
||||
const sandbox = createUnsafeMountedSandbox({ sandboxRoot, agentRoot: agentDir });
|
||||
const fetch = stubMinimaxOkFetch();
|
||||
const cfg: OpenClawConfig = {
|
||||
...createMinimaxImageConfig(),
|
||||
tools: { fs: { workspaceOnly: true } },
|
||||
};
|
||||
|
||||
try {
|
||||
const tools = createOpenClawCodingTools({
|
||||
config: cfg,
|
||||
agentDir,
|
||||
@@ -580,46 +599,40 @@ describe("image tool implicit imageModel config", () => {
|
||||
}),
|
||||
).rejects.toThrow(/Path escapes sandbox root/i);
|
||||
expect(fetch).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
await fs.rm(stateDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("rewrites inbound absolute paths into sandbox media/inbound", async () => {
|
||||
const stateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-image-sandbox-"));
|
||||
const agentDir = path.join(stateDir, "agent");
|
||||
const sandboxRoot = path.join(stateDir, "sandbox");
|
||||
await fs.mkdir(agentDir, { recursive: true });
|
||||
await fs.mkdir(path.join(sandboxRoot, "media", "inbound"), {
|
||||
recursive: true,
|
||||
});
|
||||
const pngB64 =
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/woAAn8B9FD5fHAAAAAASUVORK5CYII=";
|
||||
await fs.writeFile(
|
||||
path.join(sandboxRoot, "media", "inbound", "photo.png"),
|
||||
Buffer.from(pngB64, "base64"),
|
||||
);
|
||||
await withTempSandboxState(async ({ agentDir, sandboxRoot }) => {
|
||||
await fs.mkdir(path.join(sandboxRoot, "media", "inbound"), {
|
||||
recursive: true,
|
||||
});
|
||||
await fs.writeFile(
|
||||
path.join(sandboxRoot, "media", "inbound", "photo.png"),
|
||||
Buffer.from(ONE_PIXEL_PNG_B64, "base64"),
|
||||
);
|
||||
|
||||
const fetch = stubMinimaxOkFetch();
|
||||
const fetch = stubMinimaxOkFetch();
|
||||
|
||||
const cfg: OpenClawConfig = {
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "minimax/MiniMax-M2.5" },
|
||||
imageModel: { primary: "minimax/MiniMax-VL-01" },
|
||||
const cfg: OpenClawConfig = {
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "minimax/MiniMax-M2.5" },
|
||||
imageModel: { primary: "minimax/MiniMax-VL-01" },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const sandbox = { root: sandboxRoot, bridge: createHostSandboxFsBridge(sandboxRoot) };
|
||||
const tool = requireImageTool(createImageTool({ config: cfg, agentDir, sandbox }));
|
||||
};
|
||||
const sandbox = { root: sandboxRoot, bridge: createHostSandboxFsBridge(sandboxRoot) };
|
||||
const tool = createRequiredImageTool({ config: cfg, agentDir, sandbox });
|
||||
|
||||
const res = await tool.execute("t1", {
|
||||
prompt: "Describe the image.",
|
||||
image: "@/Users/steipete/.openclaw/media/inbound/photo.png",
|
||||
const res = await tool.execute("t1", {
|
||||
prompt: "Describe the image.",
|
||||
image: "@/Users/steipete/.openclaw/media/inbound/photo.png",
|
||||
});
|
||||
|
||||
expect(fetch).toHaveBeenCalledTimes(1);
|
||||
expect((res.details as { rewrittenFrom?: string }).rewrittenFrom).toContain("photo.png");
|
||||
});
|
||||
|
||||
expect(fetch).toHaveBeenCalledTimes(1);
|
||||
expect((res.details as { rewrittenFrom?: string }).rewrittenFrom).toContain("photo.png");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -658,24 +671,14 @@ describe("image tool MiniMax VLM routing", () => {
|
||||
});
|
||||
|
||||
async function createMinimaxVlmFixture(baseResp: { status_code: number; status_msg: string }) {
|
||||
const fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: "OK",
|
||||
headers: new Headers(),
|
||||
json: async () => ({
|
||||
content: baseResp.status_code === 0 ? "ok" : "",
|
||||
base_resp: baseResp,
|
||||
}),
|
||||
});
|
||||
global.fetch = withFetchPreconnect(fetch);
|
||||
const fetch = stubMinimaxFetch(baseResp, baseResp.status_code === 0 ? "ok" : "");
|
||||
|
||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-minimax-vlm-"));
|
||||
vi.stubEnv("MINIMAX_API_KEY", "minimax-test");
|
||||
const cfg: OpenClawConfig = {
|
||||
agents: { defaults: { model: { primary: "minimax/MiniMax-M2.5" } } },
|
||||
};
|
||||
const tool = requireImageTool(createImageTool({ config: cfg, agentDir }));
|
||||
const tool = createRequiredImageTool({ config: cfg, agentDir });
|
||||
return { fetch, tool };
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ async function withTempAgentDir<T>(run: (agentDir: string) => Promise<T>): Promi
|
||||
|
||||
const ANTHROPIC_PDF_MODEL = "anthropic/claude-opus-4-6";
|
||||
const OPENAI_PDF_MODEL = "openai/gpt-5-mini";
|
||||
const TEST_PDF_INPUT = { base64: "dGVzdA==", filename: "doc.pdf" } as const;
|
||||
const FAKE_PDF_MEDIA = {
|
||||
kind: "document",
|
||||
buffer: Buffer.from("%PDF-1.4 fake"),
|
||||
@@ -38,6 +39,64 @@ const FAKE_PDF_MEDIA = {
|
||||
fileName: "doc.pdf",
|
||||
} as const;
|
||||
|
||||
function requirePdfTool(tool: ReturnType<typeof createPdfTool>) {
|
||||
expect(tool).not.toBeNull();
|
||||
if (!tool) {
|
||||
throw new Error("expected pdf tool");
|
||||
}
|
||||
return tool;
|
||||
}
|
||||
|
||||
type PdfToolInstance = ReturnType<typeof requirePdfTool>;
|
||||
|
||||
async function withAnthropicPdfTool(
|
||||
run: (tool: PdfToolInstance, agentDir: string) => Promise<void>,
|
||||
) {
|
||||
await withTempAgentDir(async (agentDir) => {
|
||||
vi.stubEnv("ANTHROPIC_API_KEY", "anthropic-test");
|
||||
const cfg = withDefaultModel(ANTHROPIC_PDF_MODEL);
|
||||
const tool = requirePdfTool(createPdfTool({ config: cfg, agentDir }));
|
||||
await run(tool, agentDir);
|
||||
});
|
||||
}
|
||||
|
||||
function makeAnthropicAnalyzeParams(
|
||||
overrides: Partial<{
|
||||
apiKey: string;
|
||||
modelId: string;
|
||||
prompt: string;
|
||||
pdfs: Array<{ base64: string; filename: string }>;
|
||||
maxTokens: number;
|
||||
baseUrl: string;
|
||||
}> = {},
|
||||
) {
|
||||
return {
|
||||
apiKey: "test-key",
|
||||
modelId: "claude-opus-4-6",
|
||||
prompt: "test",
|
||||
pdfs: [TEST_PDF_INPUT],
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function makeGeminiAnalyzeParams(
|
||||
overrides: Partial<{
|
||||
apiKey: string;
|
||||
modelId: string;
|
||||
prompt: string;
|
||||
pdfs: Array<{ base64: string; filename: string }>;
|
||||
baseUrl: string;
|
||||
}> = {},
|
||||
) {
|
||||
return {
|
||||
apiKey: "test-key",
|
||||
modelId: "gemini-2.5-pro",
|
||||
prompt: "test",
|
||||
pdfs: [TEST_PDF_INPUT],
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function resetAuthEnv() {
|
||||
vi.stubEnv("OPENAI_API_KEY", "");
|
||||
vi.stubEnv("ANTHROPIC_API_KEY", "");
|
||||
@@ -291,35 +350,23 @@ describe("createPdfTool", () => {
|
||||
});
|
||||
|
||||
it("creates tool when auth is available", async () => {
|
||||
await withTempAgentDir(async (agentDir) => {
|
||||
vi.stubEnv("ANTHROPIC_API_KEY", "anthropic-test");
|
||||
const cfg = withDefaultModel(ANTHROPIC_PDF_MODEL);
|
||||
const tool = createPdfTool({ config: cfg, agentDir });
|
||||
expect(tool).not.toBeNull();
|
||||
expect(tool?.name).toBe("pdf");
|
||||
expect(tool?.label).toBe("PDF");
|
||||
expect(tool?.description).toContain("PDF documents");
|
||||
await withAnthropicPdfTool(async (tool) => {
|
||||
expect(tool.name).toBe("pdf");
|
||||
expect(tool.label).toBe("PDF");
|
||||
expect(tool.description).toContain("PDF documents");
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects when no pdf input provided", async () => {
|
||||
await withTempAgentDir(async (agentDir) => {
|
||||
vi.stubEnv("ANTHROPIC_API_KEY", "anthropic-test");
|
||||
const cfg = withDefaultModel(ANTHROPIC_PDF_MODEL);
|
||||
const tool = createPdfTool({ config: cfg, agentDir });
|
||||
expect(tool).not.toBeNull();
|
||||
await expect(tool!.execute("t1", { prompt: "test" })).rejects.toThrow("pdf required");
|
||||
await withAnthropicPdfTool(async (tool) => {
|
||||
await expect(tool.execute("t1", { prompt: "test" })).rejects.toThrow("pdf required");
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects too many PDFs", async () => {
|
||||
await withTempAgentDir(async (agentDir) => {
|
||||
vi.stubEnv("ANTHROPIC_API_KEY", "anthropic-test");
|
||||
const cfg = withDefaultModel(ANTHROPIC_PDF_MODEL);
|
||||
const tool = createPdfTool({ config: cfg, agentDir });
|
||||
expect(tool).not.toBeNull();
|
||||
await withAnthropicPdfTool(async (tool) => {
|
||||
const manyPdfs = Array.from({ length: 15 }, (_, i) => `/tmp/doc${i}.pdf`);
|
||||
const result = await tool!.execute("t1", { prompt: "test", pdfs: manyPdfs });
|
||||
const result = await tool.execute("t1", { prompt: "test", pdfs: manyPdfs });
|
||||
expect(result).toMatchObject({
|
||||
details: { error: "too_many_pdfs" },
|
||||
});
|
||||
@@ -333,18 +380,19 @@ describe("createPdfTool", () => {
|
||||
const outsideDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-pdf-out-"));
|
||||
try {
|
||||
const cfg = withDefaultModel(ANTHROPIC_PDF_MODEL);
|
||||
const tool = createPdfTool({
|
||||
config: cfg,
|
||||
agentDir,
|
||||
workspaceDir,
|
||||
fsPolicy: { workspaceOnly: true },
|
||||
});
|
||||
expect(tool).not.toBeNull();
|
||||
const tool = requirePdfTool(
|
||||
createPdfTool({
|
||||
config: cfg,
|
||||
agentDir,
|
||||
workspaceDir,
|
||||
fsPolicy: { workspaceOnly: true },
|
||||
}),
|
||||
);
|
||||
|
||||
const outsidePdf = path.join(outsideDir, "secret.pdf");
|
||||
await fs.writeFile(outsidePdf, "%PDF-1.4 fake");
|
||||
|
||||
await expect(tool!.execute("t1", { prompt: "test", pdf: outsidePdf })).rejects.toThrow(
|
||||
await expect(tool.execute("t1", { prompt: "test", pdf: outsidePdf })).rejects.toThrow(
|
||||
/not under an allowed directory/i,
|
||||
);
|
||||
} finally {
|
||||
@@ -355,12 +403,8 @@ describe("createPdfTool", () => {
|
||||
});
|
||||
|
||||
it("rejects unsupported scheme references", async () => {
|
||||
await withTempAgentDir(async (agentDir) => {
|
||||
vi.stubEnv("ANTHROPIC_API_KEY", "anthropic-test");
|
||||
const cfg = withDefaultModel(ANTHROPIC_PDF_MODEL);
|
||||
const tool = createPdfTool({ config: cfg, agentDir });
|
||||
expect(tool).not.toBeNull();
|
||||
const result = await tool!.execute("t1", {
|
||||
await withAnthropicPdfTool(async (tool) => {
|
||||
const result = await tool.execute("t1", {
|
||||
prompt: "test",
|
||||
pdf: "ftp://example.com/doc.pdf",
|
||||
});
|
||||
@@ -374,11 +418,10 @@ describe("createPdfTool", () => {
|
||||
await withTempAgentDir(async (agentDir) => {
|
||||
const { loadSpy } = await stubPdfToolInfra(agentDir, { modelFound: false });
|
||||
const cfg = withPdfModel(ANTHROPIC_PDF_MODEL);
|
||||
const tool = createPdfTool({ config: cfg, agentDir });
|
||||
expect(tool).not.toBeNull();
|
||||
const tool = requirePdfTool(createPdfTool({ config: cfg, agentDir }));
|
||||
|
||||
await expect(
|
||||
tool!.execute("t1", {
|
||||
tool.execute("t1", {
|
||||
prompt: "test",
|
||||
pdf: "/tmp/nonexistent.pdf",
|
||||
pdfs: ["/tmp/nonexistent.pdf"],
|
||||
@@ -400,10 +443,9 @@ describe("createPdfTool", () => {
|
||||
const extractSpy = vi.spyOn(extractModule, "extractPdfContent");
|
||||
|
||||
const cfg = withPdfModel(ANTHROPIC_PDF_MODEL);
|
||||
const tool = createPdfTool({ config: cfg, agentDir });
|
||||
expect(tool).not.toBeNull();
|
||||
const tool = requirePdfTool(createPdfTool({ config: cfg, agentDir }));
|
||||
|
||||
const result = await tool!.execute("t1", {
|
||||
const result = await tool.execute("t1", {
|
||||
prompt: "summarize",
|
||||
pdf: "/tmp/doc.pdf",
|
||||
});
|
||||
@@ -420,11 +462,10 @@ describe("createPdfTool", () => {
|
||||
await withTempAgentDir(async (agentDir) => {
|
||||
await stubPdfToolInfra(agentDir, { provider: "anthropic", input: ["text", "document"] });
|
||||
const cfg = withPdfModel(ANTHROPIC_PDF_MODEL);
|
||||
const tool = createPdfTool({ config: cfg, agentDir });
|
||||
expect(tool).not.toBeNull();
|
||||
const tool = requirePdfTool(createPdfTool({ config: cfg, agentDir }));
|
||||
|
||||
await expect(
|
||||
tool!.execute("t1", {
|
||||
tool.execute("t1", {
|
||||
prompt: "summarize",
|
||||
pdf: "/tmp/doc.pdf",
|
||||
pages: "1-2",
|
||||
@@ -452,10 +493,9 @@ describe("createPdfTool", () => {
|
||||
|
||||
const cfg = withPdfModel(OPENAI_PDF_MODEL);
|
||||
|
||||
const tool = createPdfTool({ config: cfg, agentDir });
|
||||
expect(tool).not.toBeNull();
|
||||
const tool = requirePdfTool(createPdfTool({ config: cfg, agentDir }));
|
||||
|
||||
const result = await tool!.execute("t1", {
|
||||
const result = await tool.execute("t1", {
|
||||
prompt: "summarize",
|
||||
pdf: "/tmp/doc.pdf",
|
||||
});
|
||||
@@ -469,12 +509,8 @@ describe("createPdfTool", () => {
|
||||
});
|
||||
|
||||
it("tool parameters have correct schema shape", async () => {
|
||||
await withTempAgentDir(async (agentDir) => {
|
||||
vi.stubEnv("ANTHROPIC_API_KEY", "anthropic-test");
|
||||
const cfg = withDefaultModel(ANTHROPIC_PDF_MODEL);
|
||||
const tool = createPdfTool({ config: cfg, agentDir });
|
||||
expect(tool).not.toBeNull();
|
||||
const schema = tool!.parameters;
|
||||
await withAnthropicPdfTool(async (tool) => {
|
||||
const schema = tool.parameters;
|
||||
expect(schema.type).toBe("object");
|
||||
expect(schema.properties).toBeDefined();
|
||||
const props = schema.properties as Record<string, { type?: string }>;
|
||||
@@ -514,11 +550,11 @@ describe("native PDF provider API calls", () => {
|
||||
});
|
||||
|
||||
const result = await anthropicAnalyzePdf({
|
||||
apiKey: "test-key",
|
||||
modelId: "claude-opus-4-6",
|
||||
prompt: "Summarize this document",
|
||||
pdfs: [{ base64: "dGVzdA==", filename: "doc.pdf" }],
|
||||
maxTokens: 4096,
|
||||
...makeAnthropicAnalyzeParams({
|
||||
modelId: "claude-opus-4-6",
|
||||
prompt: "Summarize this document",
|
||||
maxTokens: 4096,
|
||||
}),
|
||||
});
|
||||
|
||||
expect(result).toBe("Analysis of PDF");
|
||||
@@ -542,14 +578,9 @@ describe("native PDF provider API calls", () => {
|
||||
text: async () => "invalid request",
|
||||
});
|
||||
|
||||
await expect(
|
||||
anthropicAnalyzePdf({
|
||||
apiKey: "test-key",
|
||||
modelId: "claude-opus-4-6",
|
||||
prompt: "test",
|
||||
pdfs: [{ base64: "dGVzdA==", filename: "doc.pdf" }],
|
||||
}),
|
||||
).rejects.toThrow("Anthropic PDF request failed");
|
||||
await expect(anthropicAnalyzePdf(makeAnthropicAnalyzeParams())).rejects.toThrow(
|
||||
"Anthropic PDF request failed",
|
||||
);
|
||||
});
|
||||
|
||||
it("anthropicAnalyzePdf throws when response has no text", async () => {
|
||||
@@ -561,14 +592,9 @@ describe("native PDF provider API calls", () => {
|
||||
}),
|
||||
});
|
||||
|
||||
await expect(
|
||||
anthropicAnalyzePdf({
|
||||
apiKey: "test-key",
|
||||
modelId: "claude-opus-4-6",
|
||||
prompt: "test",
|
||||
pdfs: [{ base64: "dGVzdA==", filename: "doc.pdf" }],
|
||||
}),
|
||||
).rejects.toThrow("Anthropic PDF returned no text");
|
||||
await expect(anthropicAnalyzePdf(makeAnthropicAnalyzeParams())).rejects.toThrow(
|
||||
"Anthropic PDF returned no text",
|
||||
);
|
||||
});
|
||||
|
||||
it("geminiAnalyzePdf sends correct request shape", async () => {
|
||||
@@ -585,10 +611,10 @@ describe("native PDF provider API calls", () => {
|
||||
});
|
||||
|
||||
const result = await geminiAnalyzePdf({
|
||||
apiKey: "test-key",
|
||||
modelId: "gemini-2.5-pro",
|
||||
prompt: "Summarize this",
|
||||
pdfs: [{ base64: "dGVzdA==", filename: "doc.pdf" }],
|
||||
...makeGeminiAnalyzeParams({
|
||||
modelId: "gemini-2.5-pro",
|
||||
prompt: "Summarize this",
|
||||
}),
|
||||
});
|
||||
|
||||
expect(result).toBe("Gemini PDF analysis");
|
||||
@@ -611,14 +637,9 @@ describe("native PDF provider API calls", () => {
|
||||
text: async () => "server error",
|
||||
});
|
||||
|
||||
await expect(
|
||||
geminiAnalyzePdf({
|
||||
apiKey: "test-key",
|
||||
modelId: "gemini-2.5-pro",
|
||||
prompt: "test",
|
||||
pdfs: [{ base64: "dGVzdA==", filename: "doc.pdf" }],
|
||||
}),
|
||||
).rejects.toThrow("Gemini PDF request failed");
|
||||
await expect(geminiAnalyzePdf(makeGeminiAnalyzeParams())).rejects.toThrow(
|
||||
"Gemini PDF request failed",
|
||||
);
|
||||
});
|
||||
|
||||
it("geminiAnalyzePdf throws when no candidates returned", async () => {
|
||||
@@ -628,14 +649,9 @@ describe("native PDF provider API calls", () => {
|
||||
json: async () => ({ candidates: [] }),
|
||||
});
|
||||
|
||||
await expect(
|
||||
geminiAnalyzePdf({
|
||||
apiKey: "test-key",
|
||||
modelId: "gemini-2.5-pro",
|
||||
prompt: "test",
|
||||
pdfs: [{ base64: "dGVzdA==", filename: "doc.pdf" }],
|
||||
}),
|
||||
).rejects.toThrow("Gemini PDF returned no candidates");
|
||||
await expect(geminiAnalyzePdf(makeGeminiAnalyzeParams())).rejects.toThrow(
|
||||
"Gemini PDF returned no candidates",
|
||||
);
|
||||
});
|
||||
|
||||
it("anthropicAnalyzePdf supports multiple PDFs", async () => {
|
||||
@@ -648,13 +664,14 @@ describe("native PDF provider API calls", () => {
|
||||
});
|
||||
|
||||
await anthropicAnalyzePdf({
|
||||
apiKey: "test-key",
|
||||
modelId: "claude-opus-4-6",
|
||||
prompt: "Compare these documents",
|
||||
pdfs: [
|
||||
{ base64: "cGRmMQ==", filename: "doc1.pdf" },
|
||||
{ base64: "cGRmMg==", filename: "doc2.pdf" },
|
||||
],
|
||||
...makeAnthropicAnalyzeParams({
|
||||
modelId: "claude-opus-4-6",
|
||||
prompt: "Compare these documents",
|
||||
pdfs: [
|
||||
{ base64: "cGRmMQ==", filename: "doc1.pdf" },
|
||||
{ base64: "cGRmMg==", filename: "doc2.pdf" },
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
const body = JSON.parse(fetchMock.mock.calls[0][1].body);
|
||||
@@ -675,11 +692,7 @@ describe("native PDF provider API calls", () => {
|
||||
});
|
||||
|
||||
await anthropicAnalyzePdf({
|
||||
apiKey: "test-key",
|
||||
modelId: "claude-opus-4-6",
|
||||
prompt: "test",
|
||||
pdfs: [{ base64: "dGVzdA==", filename: "doc.pdf" }],
|
||||
baseUrl: "https://custom.example.com",
|
||||
...makeAnthropicAnalyzeParams({ baseUrl: "https://custom.example.com" }),
|
||||
});
|
||||
|
||||
expect(fetchMock.mock.calls[0][0]).toContain("https://custom.example.com/v1/messages");
|
||||
@@ -687,26 +700,16 @@ describe("native PDF provider API calls", () => {
|
||||
|
||||
it("anthropicAnalyzePdf requires apiKey", async () => {
|
||||
const { anthropicAnalyzePdf } = await import("./pdf-native-providers.js");
|
||||
await expect(
|
||||
anthropicAnalyzePdf({
|
||||
apiKey: "",
|
||||
modelId: "claude-opus-4-6",
|
||||
prompt: "test",
|
||||
pdfs: [{ base64: "dGVzdA==", filename: "doc.pdf" }],
|
||||
}),
|
||||
).rejects.toThrow("apiKey required");
|
||||
await expect(anthropicAnalyzePdf(makeAnthropicAnalyzeParams({ apiKey: "" }))).rejects.toThrow(
|
||||
"apiKey required",
|
||||
);
|
||||
});
|
||||
|
||||
it("geminiAnalyzePdf requires apiKey", async () => {
|
||||
const { geminiAnalyzePdf } = await import("./pdf-native-providers.js");
|
||||
await expect(
|
||||
geminiAnalyzePdf({
|
||||
apiKey: "",
|
||||
modelId: "gemini-2.5-pro",
|
||||
prompt: "test",
|
||||
pdfs: [{ base64: "dGVzdA==", filename: "doc.pdf" }],
|
||||
}),
|
||||
).rejects.toThrow("apiKey required");
|
||||
await expect(geminiAnalyzePdf(makeGeminiAnalyzeParams({ apiKey: "" }))).rejects.toThrow(
|
||||
"apiKey required",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user