mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 08:47:40 +00:00
test(image-tool): share temp agent dirs and table-drive validation cases
This commit is contained in:
@@ -18,6 +18,15 @@ async function writeAuthProfiles(agentDir: string, profiles: unknown) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function withTempAgentDir<T>(run: (agentDir: string) => Promise<T>): Promise<T> {
|
||||||
|
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-image-"));
|
||||||
|
try {
|
||||||
|
return await run(agentDir);
|
||||||
|
} finally {
|
||||||
|
await fs.rm(agentDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const ONE_PIXEL_PNG_B64 =
|
const ONE_PIXEL_PNG_B64 =
|
||||||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/woAAn8B9FD5fHAAAAAASUVORK5CYII=";
|
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/woAAn8B9FD5fHAAAAAASUVORK5CYII=";
|
||||||
const ONE_PIXEL_GIF_B64 = "R0lGODlhAQABAIABAP///wAAACwAAAAAAQABAAACAkQBADs=";
|
const ONE_PIXEL_GIF_B64 = "R0lGODlhAQABAIABAP///wAAACwAAAAAAQABAAACAkQBADs=";
|
||||||
@@ -141,84 +150,89 @@ describe("image tool implicit imageModel config", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("stays disabled without auth when no pairing is possible", async () => {
|
it("stays disabled without auth when no pairing is possible", async () => {
|
||||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-image-"));
|
await withTempAgentDir(async (agentDir) => {
|
||||||
const cfg: OpenClawConfig = {
|
const cfg: OpenClawConfig = {
|
||||||
agents: { defaults: { model: { primary: "openai/gpt-5.2" } } },
|
agents: { defaults: { model: { primary: "openai/gpt-5.2" } } },
|
||||||
};
|
};
|
||||||
expect(resolveImageModelConfigForTool({ cfg, agentDir })).toBeNull();
|
expect(resolveImageModelConfigForTool({ cfg, agentDir })).toBeNull();
|
||||||
expect(createImageTool({ config: cfg, agentDir })).toBeNull();
|
expect(createImageTool({ config: cfg, agentDir })).toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("pairs minimax primary with MiniMax-VL-01 (and fallbacks) when auth exists", async () => {
|
it("pairs minimax primary with MiniMax-VL-01 (and fallbacks) when auth exists", async () => {
|
||||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-image-"));
|
await withTempAgentDir(async (agentDir) => {
|
||||||
vi.stubEnv("MINIMAX_API_KEY", "minimax-test");
|
vi.stubEnv("MINIMAX_API_KEY", "minimax-test");
|
||||||
vi.stubEnv("OPENAI_API_KEY", "openai-test");
|
vi.stubEnv("OPENAI_API_KEY", "openai-test");
|
||||||
vi.stubEnv("ANTHROPIC_API_KEY", "anthropic-test");
|
vi.stubEnv("ANTHROPIC_API_KEY", "anthropic-test");
|
||||||
const cfg: OpenClawConfig = {
|
const cfg: OpenClawConfig = {
|
||||||
agents: { defaults: { model: { primary: "minimax/MiniMax-M2.1" } } },
|
agents: { defaults: { model: { primary: "minimax/MiniMax-M2.1" } } },
|
||||||
};
|
};
|
||||||
expect(resolveImageModelConfigForTool({ cfg, agentDir })).toEqual({
|
expect(resolveImageModelConfigForTool({ cfg, agentDir })).toEqual({
|
||||||
primary: "minimax/MiniMax-VL-01",
|
primary: "minimax/MiniMax-VL-01",
|
||||||
fallbacks: ["openai/gpt-5-mini", "anthropic/claude-opus-4-5"],
|
fallbacks: ["openai/gpt-5-mini", "anthropic/claude-opus-4-5"],
|
||||||
|
});
|
||||||
|
expect(createImageTool({ config: cfg, agentDir })).not.toBeNull();
|
||||||
});
|
});
|
||||||
expect(createImageTool({ config: cfg, agentDir })).not.toBeNull();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("pairs zai primary with glm-4.6v (and fallbacks) when auth exists", async () => {
|
it("pairs zai primary with glm-4.6v (and fallbacks) when auth exists", async () => {
|
||||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-image-"));
|
await withTempAgentDir(async (agentDir) => {
|
||||||
vi.stubEnv("ZAI_API_KEY", "zai-test");
|
vi.stubEnv("ZAI_API_KEY", "zai-test");
|
||||||
vi.stubEnv("OPENAI_API_KEY", "openai-test");
|
vi.stubEnv("OPENAI_API_KEY", "openai-test");
|
||||||
vi.stubEnv("ANTHROPIC_API_KEY", "anthropic-test");
|
vi.stubEnv("ANTHROPIC_API_KEY", "anthropic-test");
|
||||||
const cfg: OpenClawConfig = {
|
const cfg: OpenClawConfig = {
|
||||||
agents: { defaults: { model: { primary: "zai/glm-4.7" } } },
|
agents: { defaults: { model: { primary: "zai/glm-4.7" } } },
|
||||||
};
|
};
|
||||||
expect(resolveImageModelConfigForTool({ cfg, agentDir })).toEqual({
|
expect(resolveImageModelConfigForTool({ cfg, agentDir })).toEqual({
|
||||||
primary: "zai/glm-4.6v",
|
primary: "zai/glm-4.6v",
|
||||||
fallbacks: ["openai/gpt-5-mini", "anthropic/claude-opus-4-5"],
|
fallbacks: ["openai/gpt-5-mini", "anthropic/claude-opus-4-5"],
|
||||||
|
});
|
||||||
|
expect(createImageTool({ config: cfg, agentDir })).not.toBeNull();
|
||||||
});
|
});
|
||||||
expect(createImageTool({ config: cfg, agentDir })).not.toBeNull();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("pairs a custom provider when it declares an image-capable model", async () => {
|
it("pairs a custom provider when it declares an image-capable model", async () => {
|
||||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-image-"));
|
await withTempAgentDir(async (agentDir) => {
|
||||||
await writeAuthProfiles(agentDir, {
|
await writeAuthProfiles(agentDir, {
|
||||||
version: 1,
|
version: 1,
|
||||||
profiles: {
|
profiles: {
|
||||||
"acme:default": { type: "api_key", provider: "acme", key: "sk-test" },
|
"acme:default": { type: "api_key", provider: "acme", key: "sk-test" },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const cfg: OpenClawConfig = {
|
const cfg: OpenClawConfig = {
|
||||||
agents: { defaults: { model: { primary: "acme/text-1" } } },
|
agents: { defaults: { model: { primary: "acme/text-1" } } },
|
||||||
models: {
|
models: {
|
||||||
providers: {
|
providers: {
|
||||||
acme: {
|
acme: {
|
||||||
baseUrl: "https://example.com",
|
baseUrl: "https://example.com",
|
||||||
models: [
|
models: [
|
||||||
makeModelDefinition("text-1", ["text"]),
|
makeModelDefinition("text-1", ["text"]),
|
||||||
makeModelDefinition("vision-1", ["text", "image"]),
|
makeModelDefinition("vision-1", ["text", "image"]),
|
||||||
],
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
};
|
||||||
};
|
expect(resolveImageModelConfigForTool({ cfg, agentDir })).toEqual({
|
||||||
expect(resolveImageModelConfigForTool({ cfg, agentDir })).toEqual({
|
primary: "acme/vision-1",
|
||||||
primary: "acme/vision-1",
|
});
|
||||||
|
expect(createImageTool({ config: cfg, agentDir })).not.toBeNull();
|
||||||
});
|
});
|
||||||
expect(createImageTool({ config: cfg, agentDir })).not.toBeNull();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("prefers explicit agents.defaults.imageModel", async () => {
|
it("prefers explicit agents.defaults.imageModel", async () => {
|
||||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-image-"));
|
await withTempAgentDir(async (agentDir) => {
|
||||||
const cfg: OpenClawConfig = {
|
const cfg: OpenClawConfig = {
|
||||||
agents: {
|
agents: {
|
||||||
defaults: {
|
defaults: {
|
||||||
model: { primary: "minimax/MiniMax-M2.1" },
|
model: { primary: "minimax/MiniMax-M2.1" },
|
||||||
imageModel: { primary: "openai/gpt-5-mini" },
|
imageModel: { primary: "openai/gpt-5-mini" },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
};
|
||||||
};
|
expect(resolveImageModelConfigForTool({ cfg, agentDir })).toEqual({
|
||||||
expect(resolveImageModelConfigForTool({ cfg, agentDir })).toEqual({
|
primary: "openai/gpt-5-mini",
|
||||||
primary: "openai/gpt-5-mini",
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -227,30 +241,33 @@ describe("image tool implicit imageModel config", () => {
|
|||||||
// because images are auto-injected into prompts. The tool description is
|
// because images are auto-injected into prompts. The tool description is
|
||||||
// adjusted via modelHasVision to discourage redundant usage.
|
// adjusted via modelHasVision to discourage redundant usage.
|
||||||
vi.stubEnv("OPENAI_API_KEY", "test-key");
|
vi.stubEnv("OPENAI_API_KEY", "test-key");
|
||||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-image-"));
|
await withTempAgentDir(async (agentDir) => {
|
||||||
const cfg: OpenClawConfig = {
|
const cfg: OpenClawConfig = {
|
||||||
agents: {
|
agents: {
|
||||||
defaults: {
|
defaults: {
|
||||||
model: { primary: "acme/vision-1" },
|
model: { primary: "acme/vision-1" },
|
||||||
imageModel: { primary: "openai/gpt-5-mini" },
|
imageModel: { primary: "openai/gpt-5-mini" },
|
||||||
},
|
|
||||||
},
|
|
||||||
models: {
|
|
||||||
providers: {
|
|
||||||
acme: {
|
|
||||||
baseUrl: "https://example.com",
|
|
||||||
models: [makeModelDefinition("vision-1", ["text", "image"])],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
models: {
|
||||||
};
|
providers: {
|
||||||
// Tool should still be available for explicit image analysis requests
|
acme: {
|
||||||
expect(resolveImageModelConfigForTool({ cfg, agentDir })).toEqual({
|
baseUrl: "https://example.com",
|
||||||
primary: "openai/gpt-5-mini",
|
models: [makeModelDefinition("vision-1", ["text", "image"])],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// Tool should still be available for explicit image analysis requests
|
||||||
|
expect(resolveImageModelConfigForTool({ cfg, agentDir })).toEqual({
|
||||||
|
primary: "openai/gpt-5-mini",
|
||||||
|
});
|
||||||
|
const tool = createImageTool({ config: cfg, agentDir, modelHasVision: true });
|
||||||
|
expect(tool).not.toBeNull();
|
||||||
|
expect(tool?.description).toContain(
|
||||||
|
"Only use this tool when images were NOT already provided",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
const tool = createImageTool({ config: cfg, agentDir, modelHasVision: true });
|
|
||||||
expect(tool).not.toBeNull();
|
|
||||||
expect(tool?.description).toContain("Only use this tool when images were NOT already provided");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("exposes an Anthropic-safe image schema without union keywords", async () => {
|
it("exposes an Anthropic-safe image schema without union keywords", async () => {
|
||||||
@@ -598,41 +615,50 @@ describe("image tool response validation", () => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
it("caps image-tool max tokens by model capability", () => {
|
it.each([
|
||||||
expect(__testing.resolveImageToolMaxTokens(4000)).toBe(4000);
|
{
|
||||||
|
name: "caps image-tool max tokens by model capability",
|
||||||
|
maxOutputTokens: 4000,
|
||||||
|
expected: 4000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "keeps requested image-tool max tokens when model capability is higher",
|
||||||
|
maxOutputTokens: 8192,
|
||||||
|
expected: 4096,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "falls back to requested image-tool max tokens when model capability is missing",
|
||||||
|
maxOutputTokens: undefined,
|
||||||
|
expected: 4096,
|
||||||
|
},
|
||||||
|
])("$name", ({ maxOutputTokens, expected }) => {
|
||||||
|
expect(__testing.resolveImageToolMaxTokens(maxOutputTokens)).toBe(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps requested image-tool max tokens when model capability is higher", () => {
|
it.each([
|
||||||
expect(__testing.resolveImageToolMaxTokens(8192)).toBe(4096);
|
{
|
||||||
});
|
name: "rejects image-model responses with no final text",
|
||||||
|
message: createAssistantMessage({
|
||||||
it("falls back to requested image-tool max tokens when model capability is missing", () => {
|
content: [{ type: "thinking", thinking: "hmm" }],
|
||||||
expect(__testing.resolveImageToolMaxTokens(undefined)).toBe(4096);
|
}) as never,
|
||||||
});
|
expectedError: /returned no text/i,
|
||||||
|
},
|
||||||
it("rejects image-model responses with no final text", () => {
|
{
|
||||||
|
name: "surfaces provider errors from image-model responses",
|
||||||
|
message: createAssistantMessage({
|
||||||
|
stopReason: "error",
|
||||||
|
errorMessage: "boom",
|
||||||
|
}) as never,
|
||||||
|
expectedError: /boom/i,
|
||||||
|
},
|
||||||
|
])("$name", ({ message, expectedError }) => {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
__testing.coerceImageAssistantText({
|
__testing.coerceImageAssistantText({
|
||||||
provider: "openai",
|
provider: "openai",
|
||||||
model: "gpt-5-mini",
|
model: "gpt-5-mini",
|
||||||
message: createAssistantMessage({
|
message,
|
||||||
content: [{ type: "thinking", thinking: "hmm" }],
|
|
||||||
}) as never,
|
|
||||||
}),
|
}),
|
||||||
).toThrow(/returned no text/i);
|
).toThrow(expectedError);
|
||||||
});
|
|
||||||
|
|
||||||
it("surfaces provider errors from image-model responses", () => {
|
|
||||||
expect(() =>
|
|
||||||
__testing.coerceImageAssistantText({
|
|
||||||
provider: "openai",
|
|
||||||
model: "gpt-5-mini",
|
|
||||||
message: createAssistantMessage({
|
|
||||||
stopReason: "error",
|
|
||||||
errorMessage: "boom",
|
|
||||||
}) as never,
|
|
||||||
}),
|
|
||||||
).toThrow(/boom/i);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns trimmed text from image-model responses", () => {
|
it("returns trimmed text from image-model responses", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user