285 lines
8.4 KiB
TypeScript
285 lines
8.4 KiB
TypeScript
// SimpleSyrup - workflow-focused ComfyUI extensions for image generation
|
|
// Copyright (C) 2026 Artificial Sweetener and contributors
|
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
import { describe, expect, it, vi } from "vitest";
|
|
|
|
import {
|
|
clearQuantCache,
|
|
deleteExternalLLMApiKey,
|
|
getExternalLLMSettings,
|
|
getMaskBatchPreview,
|
|
getQuantCacheStatus,
|
|
getSettings,
|
|
enforceQuantCacheLimit,
|
|
parseExternalLLMSettings,
|
|
parseMaskBatchPreview,
|
|
parseQuantCacheStatus,
|
|
parseSettings,
|
|
refreshExternalLLMModels,
|
|
saveExternalLLMApiKey,
|
|
saveExternalLLMSettings,
|
|
saveSettings
|
|
} from "../src/api";
|
|
import type { FetchLike } from "../src/api";
|
|
import { createJsonResponse } from "./testUtils";
|
|
|
|
describe("settings API", () => {
|
|
it("loads SimpleSyrup settings from the backend route", async () => {
|
|
const fetchImpl = vi.fn<FetchLike>().mockResolvedValue(
|
|
createJsonResponse({
|
|
show_downloadable_models: false,
|
|
quant_cache_limit_gib: 20
|
|
})
|
|
);
|
|
|
|
await expect(getSettings(fetchImpl)).resolves.toEqual({
|
|
show_downloadable_models: false,
|
|
quant_cache_limit_gib: 20
|
|
});
|
|
expect(fetchImpl).toHaveBeenCalledWith("/simple-syrup/settings");
|
|
});
|
|
|
|
it("saves SimpleSyrup settings to the backend route", async () => {
|
|
const fetchImpl = vi.fn<FetchLike>().mockResolvedValue(
|
|
createJsonResponse({
|
|
show_downloadable_models: true,
|
|
quant_cache_limit_gib: 30
|
|
})
|
|
);
|
|
|
|
await expect(
|
|
saveSettings(
|
|
{ show_downloadable_models: true, quant_cache_limit_gib: 30 },
|
|
fetchImpl
|
|
)
|
|
).resolves.toEqual({
|
|
show_downloadable_models: true,
|
|
quant_cache_limit_gib: 30
|
|
});
|
|
expect(fetchImpl).toHaveBeenCalledWith(
|
|
"/simple-syrup/settings",
|
|
expect.objectContaining({
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
show_downloadable_models: true,
|
|
quant_cache_limit_gib: 30
|
|
})
|
|
})
|
|
);
|
|
});
|
|
|
|
it("surfaces backend load errors when available", async () => {
|
|
const fetchImpl = vi
|
|
.fn<FetchLike>()
|
|
.mockResolvedValue(createJsonResponse({ error: "nope" }, { status: 500 }));
|
|
|
|
await expect(getSettings(fetchImpl)).rejects.toThrow("nope");
|
|
});
|
|
|
|
it("surfaces backend save errors when available", async () => {
|
|
const fetchImpl = vi
|
|
.fn<FetchLike>()
|
|
.mockResolvedValue(createJsonResponse({ error: "nope" }, { status: 400 }));
|
|
|
|
await expect(
|
|
saveSettings(
|
|
{ show_downloadable_models: false, quant_cache_limit_gib: 20 },
|
|
fetchImpl
|
|
)
|
|
).rejects.toThrow("nope");
|
|
});
|
|
|
|
it("rejects invalid response payloads conservatively", () => {
|
|
expect(() => parseSettings({ show_downloadable_models: "false" })).toThrow(
|
|
"SimpleSyrup settings payload is invalid"
|
|
);
|
|
});
|
|
|
|
it("loads and clears the global quant cache through its dedicated route", async () => {
|
|
const status = {
|
|
path: "models/SyrupQuants",
|
|
usage_bytes: 1024,
|
|
limit_bytes: 20 * 1024 ** 3,
|
|
artifact_count: 1,
|
|
active_artifact_count: 0
|
|
};
|
|
const fetchImpl = vi
|
|
.fn<FetchLike>()
|
|
.mockImplementation(() => Promise.resolve(createJsonResponse(status)));
|
|
|
|
await expect(getQuantCacheStatus(fetchImpl)).resolves.toEqual(status);
|
|
await expect(enforceQuantCacheLimit(fetchImpl)).resolves.toEqual(status);
|
|
await expect(clearQuantCache(fetchImpl)).resolves.toEqual(status);
|
|
expect(fetchImpl).toHaveBeenNthCalledWith(1, "/simple-syrup/quant-cache");
|
|
expect(fetchImpl).toHaveBeenNthCalledWith(2, "/simple-syrup/quant-cache", {
|
|
method: "POST"
|
|
});
|
|
expect(fetchImpl).toHaveBeenNthCalledWith(3, "/simple-syrup/quant-cache", {
|
|
method: "DELETE"
|
|
});
|
|
});
|
|
|
|
it("rejects malformed quant cache status payloads", () => {
|
|
expect(() =>
|
|
parseQuantCacheStatus({ path: "models/SyrupQuants" })
|
|
).toThrow("quant cache status is invalid");
|
|
});
|
|
});
|
|
|
|
describe("mask batch preview API", () => {
|
|
const preview = {
|
|
images: [
|
|
{
|
|
filename: "ComfyUI_temp_mask.png",
|
|
subfolder: "",
|
|
type: "temp" as const
|
|
}
|
|
],
|
|
animated: [false]
|
|
};
|
|
|
|
it("requests previews for the ordered files and selected channel", async () => {
|
|
const fetchImpl = vi
|
|
.fn<FetchLike>()
|
|
.mockResolvedValue(createJsonResponse(preview));
|
|
|
|
await expect(
|
|
getMaskBatchPreview(["right.png", "left.png"], "blue", fetchImpl)
|
|
).resolves.toEqual(preview);
|
|
expect(fetchImpl).toHaveBeenCalledWith(
|
|
"/simple-syrup/mask-batch/preview",
|
|
expect.objectContaining({
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
files: ["right.png", "left.png"],
|
|
channel: "blue"
|
|
})
|
|
})
|
|
);
|
|
});
|
|
|
|
it("surfaces preview validation errors", async () => {
|
|
const fetchImpl = vi.fn<FetchLike>().mockResolvedValue(
|
|
createJsonResponse(
|
|
{ error: "mask dimensions do not match" },
|
|
{ status: 400 }
|
|
)
|
|
);
|
|
|
|
await expect(
|
|
getMaskBatchPreview(["right.png", "left.png"], "alpha", fetchImpl)
|
|
).rejects.toThrow("mask dimensions do not match");
|
|
});
|
|
|
|
it("rejects malformed native preview responses", () => {
|
|
expect(() =>
|
|
parseMaskBatchPreview({
|
|
images: [{ filename: "mask.png", subfolder: "", type: "other" }],
|
|
animated: [false]
|
|
})
|
|
).toThrow("mask batch preview payload is invalid");
|
|
});
|
|
});
|
|
|
|
describe("external LLM settings API", () => {
|
|
const payload = {
|
|
base_url: "https://provider.example/v1",
|
|
cached_models: ["model-a"],
|
|
default_model: "model-a",
|
|
has_api_key: true
|
|
};
|
|
|
|
it("loads external LLM settings from the backend route", async () => {
|
|
const fetchImpl = vi.fn<FetchLike>().mockResolvedValue(createJsonResponse(payload));
|
|
|
|
await expect(getExternalLLMSettings(fetchImpl)).resolves.toEqual(payload);
|
|
expect(fetchImpl).toHaveBeenCalledWith(
|
|
"/simple-syrup/external-llm/settings"
|
|
);
|
|
});
|
|
|
|
it("saves external LLM endpoint settings", async () => {
|
|
const fetchImpl = vi.fn<FetchLike>().mockResolvedValue(createJsonResponse(payload));
|
|
|
|
await expect(
|
|
saveExternalLLMSettings(
|
|
{
|
|
base_url: "https://provider.example/v1",
|
|
default_model: "model-a"
|
|
},
|
|
fetchImpl
|
|
)
|
|
).resolves.toEqual(payload);
|
|
expect(fetchImpl).toHaveBeenCalledWith(
|
|
"/simple-syrup/external-llm/settings",
|
|
expect.objectContaining({ method: "POST" })
|
|
);
|
|
});
|
|
|
|
it("saves external LLM API keys without expecting the secret back", async () => {
|
|
const fetchImpl = vi.fn<FetchLike>().mockResolvedValue(createJsonResponse(payload));
|
|
|
|
await expect(
|
|
saveExternalLLMApiKey({ api_key: "secret" }, fetchImpl)
|
|
).resolves.toEqual(payload);
|
|
expect(fetchImpl).toHaveBeenCalledWith(
|
|
"/simple-syrup/external-llm/api-key",
|
|
expect.objectContaining({
|
|
method: "POST",
|
|
body: JSON.stringify({ api_key: "secret" })
|
|
})
|
|
);
|
|
});
|
|
|
|
it("surfaces backend API key save errors", async () => {
|
|
const fetchImpl = vi.fn<FetchLike>().mockResolvedValue(
|
|
createJsonResponse(
|
|
{ error: "Configure an external LLM endpoint first." },
|
|
{ status: 400 }
|
|
)
|
|
);
|
|
|
|
await expect(
|
|
saveExternalLLMApiKey({ api_key: "secret" }, fetchImpl)
|
|
).rejects.toThrow("Configure an external LLM endpoint first.");
|
|
});
|
|
|
|
it("deletes external LLM API keys", async () => {
|
|
const fetchImpl = vi.fn<FetchLike>().mockResolvedValue(
|
|
createJsonResponse({ ...payload, has_api_key: false })
|
|
);
|
|
|
|
await expect(deleteExternalLLMApiKey(fetchImpl)).resolves.toMatchObject({
|
|
has_api_key: false
|
|
});
|
|
expect(fetchImpl).toHaveBeenCalledWith(
|
|
"/simple-syrup/external-llm/api-key",
|
|
{ method: "DELETE" }
|
|
);
|
|
});
|
|
|
|
it("refreshes external LLM models", async () => {
|
|
const fetchImpl = vi.fn<FetchLike>().mockResolvedValue(createJsonResponse(payload));
|
|
|
|
await expect(refreshExternalLLMModels(fetchImpl)).resolves.toEqual(payload);
|
|
expect(fetchImpl).toHaveBeenCalledWith(
|
|
"/simple-syrup/external-llm/models/refresh",
|
|
{ method: "POST" }
|
|
);
|
|
});
|
|
|
|
it("rejects malformed external LLM settings payloads", () => {
|
|
expect(() =>
|
|
parseExternalLLMSettings({
|
|
base_url: "https://provider.example/v1",
|
|
cached_models: [1],
|
|
default_model: "model-a",
|
|
has_api_key: true
|
|
})
|
|
).toThrow("External LLM settings payload is invalid");
|
|
});
|
|
});
|