426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
510 lines
18 KiB
TypeScript
510 lines
18 KiB
TypeScript
/**
|
|
* WS3 sd-cpp probe + backend availability tests.
|
|
*
|
|
* Covers two related surfaces:
|
|
*
|
|
* 1. `scripts/probe-sd-cpp.mjs` — first-run probe used by the
|
|
* Settings flow and CI bundle-prep. Forks the script under both
|
|
* "binary missing" (SD_CPP_BIN points at a path that doesn't
|
|
* exist) and "binary available" (SD_CPP_BIN points at a tiny shell
|
|
* stub that prints a fake version line) regimes; asserts the JSON
|
|
* shape the runtime depends on.
|
|
*
|
|
* 2. `services/imagegen/sd-cpp.ts` — the `loadSdCppImageGenBackend`
|
|
* load path. Confirms that when the binary is missing it raises a
|
|
* structured `ImageGenBackendUnavailableError` with
|
|
* reason="binary_missing" / "subprocess_failed", and that the
|
|
* selector caller can detect the failure without an exception
|
|
* bleeding through.
|
|
*
|
|
* Why both layers in one file: the probe and the runtime share the same
|
|
* binary-resolution rules (env var → PATH); a regression in one almost
|
|
* always tracks a regression in the other.
|
|
*/
|
|
|
|
import { spawnSync } from "node:child_process";
|
|
import { chmodSync, mkdtempSync, writeFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { describe, expect, it } from "vitest";
|
|
import {
|
|
ImageGenBackendUnavailableError,
|
|
isImageGenUnavailable,
|
|
} from "../src/services/imagegen/errors";
|
|
import {
|
|
buildArgs,
|
|
loadSdCppImageGenBackend,
|
|
} from "../src/services/imagegen/sd-cpp";
|
|
|
|
const PROBE_SCRIPT = fileURLToPath(
|
|
new URL("../scripts/probe-sd-cpp.mjs", import.meta.url),
|
|
);
|
|
|
|
interface ProbeResult {
|
|
available: boolean;
|
|
binary: string;
|
|
version?: string;
|
|
supportedModels?: string[];
|
|
accelerators?: string[];
|
|
evidence?: string[];
|
|
requiredAccelerator?: string;
|
|
reason?: string;
|
|
hint?: string;
|
|
}
|
|
|
|
function runProbe(env: Record<string, string | undefined>): ProbeResult {
|
|
const nodeBin = process.env.NODE_BIN ?? process.env.NODE ?? "node";
|
|
const mergedEnv = { ...process.env, ...env };
|
|
const result =
|
|
typeof Bun !== "undefined"
|
|
? Bun.spawnSync({
|
|
cmd: [nodeBin, PROBE_SCRIPT, "--json"],
|
|
env: mergedEnv as Record<string, string>,
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
})
|
|
: spawnSync(nodeBin, [PROBE_SCRIPT, "--json"], {
|
|
env: mergedEnv,
|
|
encoding: "utf8",
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
});
|
|
const status = "exitCode" in result ? result.exitCode : result.status;
|
|
const stdout =
|
|
typeof result.stdout === "string"
|
|
? result.stdout
|
|
: new TextDecoder().decode(result.stdout ?? new Uint8Array());
|
|
const stderr =
|
|
typeof result.stderr === "string"
|
|
? result.stderr
|
|
: new TextDecoder().decode(result.stderr ?? new Uint8Array());
|
|
if (status !== 0) {
|
|
throw new Error(
|
|
`probe-sd-cpp exited with ${status}: ${stderr}`,
|
|
);
|
|
}
|
|
const trimmed = stdout.trim();
|
|
const firstLine = trimmed.split("\n").find((line) => line.trim().length > 0);
|
|
if (!firstLine) {
|
|
throw new Error(
|
|
`probe-sd-cpp produced no output (status=${status}, stderr=${JSON.stringify(stderr)}, stdout=${JSON.stringify(stdout)})`,
|
|
);
|
|
}
|
|
return JSON.parse(firstLine) as ProbeResult;
|
|
}
|
|
|
|
// Tests that write a fake POSIX shell-script `sd` binary and have the probe
|
|
// SPAWN it are inherently POSIX-only: Windows does not honor a `#!/usr/bin/env
|
|
// bash` shebang and `spawn` cannot exec an extensionless script (→ ENOENT), so
|
|
// the probe reports `binary_missing` instead of the scripted result. The probe
|
|
// LOGIC is covered on POSIX CI; the missing/bogus-binary path (no fake spawned)
|
|
// still runs on every platform below. On Windows the real path uses a packaged
|
|
// `sd.exe`, which these unit fakes cannot stand in for.
|
|
const itPosix = it.skipIf(process.platform === "win32");
|
|
|
|
describe("WS3 sd-cpp probe — first-run script", () => {
|
|
it("reports unavailable when SD_CPP_BIN points at a missing path", () => {
|
|
const probe = runProbe({
|
|
SD_CPP_BIN: "/definitely/does/not/exist/sd-fake-bin",
|
|
});
|
|
expect(probe.available).toBe(false);
|
|
expect(probe.binary).toBe("/definitely/does/not/exist/sd-fake-bin");
|
|
expect(probe.reason).toBe("binary_missing");
|
|
expect(typeof probe.hint).toBe("string");
|
|
expect(probe.hint).toMatch(/SD_CPP_BIN|stable-diffusion\.cpp/i);
|
|
expect(probe.version).toBeUndefined();
|
|
expect(probe.supportedModels).toBeUndefined();
|
|
});
|
|
|
|
itPosix("reports CPU-only when the binary returns version but no CUDA proof", () => {
|
|
const dir = mkdtempSync(join(tmpdir(), "sd-cpp-probe-"));
|
|
const fakeBin = join(dir, "fake-sd");
|
|
writeFileSync(
|
|
fakeBin,
|
|
"#!/usr/bin/env bash\nif [ \"$1\" = \"--version\" ]; then echo 'stable-diffusion.cpp test-build-001'; exit 0; fi\nexit 2\n",
|
|
);
|
|
chmodSync(fakeBin, 0o755);
|
|
const probe = runProbe({ SD_CPP_BIN: fakeBin });
|
|
expect(probe.available).toBe(true);
|
|
expect(probe.binary).toBe(fakeBin);
|
|
expect(probe.version).toBe("stable-diffusion.cpp test-build-001");
|
|
expect(Array.isArray(probe.supportedModels)).toBe(true);
|
|
expect(probe.supportedModels).toContain("imagegen-sd-1_5-q5_0");
|
|
expect(probe.supportedModels).toContain(
|
|
"imagegen-z-image-turbo-q4_k_m",
|
|
);
|
|
expect(Array.isArray(probe.accelerators)).toBe(true);
|
|
expect(probe.accelerators).not.toContain("cuda");
|
|
expect(probe.accelerators).toContain("cpu");
|
|
});
|
|
|
|
itPosix("does not treat generic --rng cuda help text as CUDA build proof", () => {
|
|
const dir = mkdtempSync(join(tmpdir(), "sd-cpp-probe-"));
|
|
const fakeBin = join(dir, "fake-sd-metal");
|
|
writeFileSync(
|
|
fakeBin,
|
|
[
|
|
"#!/usr/bin/env bash",
|
|
"if [ \"$1\" = \"--version\" ]; then echo 'stable-diffusion.cpp test-build-metal'; exit 0; fi",
|
|
"if [ \"$1\" = \"--help\" ]; then echo '--rng cuda, default, consistent with webui GPU RNG'; echo 'backend: Metal'; exit 0; fi",
|
|
"exit 2",
|
|
"",
|
|
].join("\n"),
|
|
);
|
|
chmodSync(fakeBin, 0o755);
|
|
const probe = runProbe({ SD_CPP_BIN: fakeBin });
|
|
expect(probe.available).toBe(true);
|
|
expect(probe.accelerators).toContain("metal");
|
|
expect(probe.accelerators).not.toContain("cuda");
|
|
});
|
|
|
|
itPosix("reports CUDA when help/version proves a CUDA-capable binary", () => {
|
|
const dir = mkdtempSync(join(tmpdir(), "sd-cpp-probe-"));
|
|
const fakeBin = join(dir, "fake-sd-cuda");
|
|
writeFileSync(
|
|
fakeBin,
|
|
[
|
|
"#!/usr/bin/env bash",
|
|
"if [ \"$1\" = \"--version\" ]; then echo 'stable-diffusion.cpp test-build-002 SD_CUDA=ON'; exit 0; fi",
|
|
"if [ \"$1\" = \"--help\" ]; then echo 'build: CUDA cuBLAS'; exit 0; fi",
|
|
"exit 2",
|
|
"",
|
|
].join("\n"),
|
|
);
|
|
chmodSync(fakeBin, 0o755);
|
|
const probe = runProbe({ SD_CPP_BIN: fakeBin });
|
|
expect(probe.available).toBe(true);
|
|
expect(probe.accelerators).toContain("cuda");
|
|
expect(probe.evidence).toContain("help_or_version");
|
|
});
|
|
|
|
itPosix("fails closed when ELIZA_IMAGEGEN_ACCELERATOR is not supported", () => {
|
|
const dir = mkdtempSync(join(tmpdir(), "sd-cpp-probe-"));
|
|
const fakeBin = join(dir, "fake-sd");
|
|
writeFileSync(
|
|
fakeBin,
|
|
"#!/usr/bin/env bash\nif [ \"$1\" = \"--version\" ]; then echo 'stable-diffusion.cpp test-build-001'; exit 0; fi\nif [ \"$1\" = \"--help\" ]; then echo 'stable-diffusion.cpp help'; exit 0; fi\nexit 2\n",
|
|
);
|
|
chmodSync(fakeBin, 0o755);
|
|
const probe = runProbe({
|
|
SD_CPP_BIN: fakeBin,
|
|
ELIZA_IMAGEGEN_ACCELERATOR: "vulkan",
|
|
});
|
|
expect(probe.available).toBe(false);
|
|
expect(probe.binary).toBe(fakeBin);
|
|
expect(probe.requiredAccelerator).toBe("vulkan");
|
|
expect(probe.reason).toBe("vulkan_missing");
|
|
expect(probe.accelerators).toContain("cpu");
|
|
expect(probe.accelerators).not.toContain("vulkan");
|
|
expect(probe.hint).toMatch(/vulkan support/i);
|
|
});
|
|
|
|
itPosix("passes when ELIZA_IMAGEGEN_ACCELERATOR is supported", () => {
|
|
const dir = mkdtempSync(join(tmpdir(), "sd-cpp-probe-"));
|
|
const fakeBin = join(dir, "fake-sd-vulkan");
|
|
writeFileSync(
|
|
join(dir, "sd-cpp.manifest.json"),
|
|
JSON.stringify({ accelerators: ["vulkan"] }),
|
|
);
|
|
writeFileSync(
|
|
fakeBin,
|
|
[
|
|
"#!/usr/bin/env bash",
|
|
"if [ \"$1\" = \"--version\" ]; then echo 'stable-diffusion.cpp Vulkan'; exit 0; fi",
|
|
"if [ \"$1\" = \"--help\" ]; then echo 'backend: Vulkan'; exit 0; fi",
|
|
"exit 2",
|
|
"",
|
|
].join("\n"),
|
|
);
|
|
chmodSync(fakeBin, 0o755);
|
|
const probe = runProbe({
|
|
SD_CPP_BIN: fakeBin,
|
|
ELIZA_IMAGEGEN_ACCELERATOR: "vulkan",
|
|
});
|
|
expect(probe.available).toBe(true);
|
|
expect(probe.requiredAccelerator).toBe("vulkan");
|
|
expect(probe.accelerators).toContain("vulkan");
|
|
expect(probe.reason).toBeUndefined();
|
|
});
|
|
|
|
itPosix("reports binary_version_mismatch when the binary exits non-zero on --version", () => {
|
|
const dir = mkdtempSync(join(tmpdir(), "sd-cpp-probe-"));
|
|
const fakeBin = join(dir, "fake-sd-broken");
|
|
writeFileSync(fakeBin, "#!/usr/bin/env bash\nexit 7\n");
|
|
chmodSync(fakeBin, 0o755);
|
|
const probe = runProbe({ SD_CPP_BIN: fakeBin });
|
|
expect(probe.available).toBe(false);
|
|
expect(probe.binary).toBe(fakeBin);
|
|
expect(probe.reason).toBe("binary_version_mismatch");
|
|
});
|
|
});
|
|
|
|
describe("WS3 sd-cpp backend — binary missing yields structured error", () => {
|
|
it("loadSdCppImageGenBackend with a bogus binary path throws ImageGenBackendUnavailableError", async () => {
|
|
try {
|
|
await loadSdCppImageGenBackend({
|
|
modelKey: "imagegen-sd-1_5-q5_0",
|
|
loadArgs: {
|
|
modelPath: "/tmp/this-model-does-not-exist.gguf",
|
|
},
|
|
binaryPath: "/definitely/does/not/exist/sd-fake-bin",
|
|
});
|
|
expect.fail("loadSdCppImageGenBackend should have thrown");
|
|
} catch (err) {
|
|
if (!(err instanceof ImageGenBackendUnavailableError)) throw err;
|
|
expect(isImageGenUnavailable(err)).toBe(true);
|
|
expect(err.backendId).toBe("sd-cpp");
|
|
// ENOENT from spawn() gets wrapped into binary_missing.
|
|
expect(err.reason).toBe("binary_missing");
|
|
}
|
|
});
|
|
|
|
it("error message references SD_CPP_BIN so first-run can surface a fix", async () => {
|
|
try {
|
|
await loadSdCppImageGenBackend({
|
|
modelKey: "imagegen-sd-1_5-q5_0",
|
|
loadArgs: {
|
|
modelPath: "/tmp/this-model-does-not-exist.gguf",
|
|
},
|
|
binaryPath: "/definitely/does/not/exist/sd-fake-bin",
|
|
});
|
|
expect.fail("loadSdCppImageGenBackend should have thrown");
|
|
} catch (err) {
|
|
if (!(err instanceof ImageGenBackendUnavailableError)) throw err;
|
|
expect(err.message).toMatch(/SD_CPP_BIN/);
|
|
}
|
|
});
|
|
|
|
itPosix("rejects CUDA load when the sd-cpp binary is CPU-only", async () => {
|
|
const dir = mkdtempSync(join(tmpdir(), "sd-cpp-probe-"));
|
|
const fakeBin = join(dir, "fake-sd-cpu");
|
|
writeFileSync(
|
|
fakeBin,
|
|
"#!/usr/bin/env bash\nif [ \"$1\" = \"--version\" ]; then echo 'stable-diffusion.cpp cpu-only'; exit 0; fi\nif [ \"$1\" = \"--help\" ]; then echo 'stable-diffusion.cpp help'; exit 0; fi\nexit 2\n",
|
|
);
|
|
chmodSync(fakeBin, 0o755);
|
|
try {
|
|
await loadSdCppImageGenBackend({
|
|
modelKey: "imagegen-sd-1_5-q5_0",
|
|
loadArgs: {
|
|
modelPath: "/tmp/this-model-does-not-exist.gguf",
|
|
accelerator: "cuda",
|
|
},
|
|
binaryPath: fakeBin,
|
|
});
|
|
expect.fail("loadSdCppImageGenBackend should have thrown");
|
|
} catch (err) {
|
|
if (!(err instanceof ImageGenBackendUnavailableError)) throw err;
|
|
expect(err.backendId).toBe("sd-cpp");
|
|
expect(err.reason).toBe("cuda_binary_missing");
|
|
expect(err.message).toMatch(/CUDA support/);
|
|
}
|
|
});
|
|
|
|
itPosix("rejects Vulkan load when the sd-cpp binary is CPU-only", async () => {
|
|
const dir = mkdtempSync(join(tmpdir(), "sd-cpp-probe-"));
|
|
const fakeBin = join(dir, "fake-sd-cpu");
|
|
writeFileSync(
|
|
fakeBin,
|
|
"#!/usr/bin/env bash\nif [ \"$1\" = \"--version\" ]; then echo 'stable-diffusion.cpp cpu-only'; exit 0; fi\nif [ \"$1\" = \"--help\" ]; then echo 'stable-diffusion.cpp help'; exit 0; fi\nexit 2\n",
|
|
);
|
|
chmodSync(fakeBin, 0o755);
|
|
try {
|
|
await loadSdCppImageGenBackend({
|
|
modelKey: "imagegen-sd-1_5-q5_0",
|
|
loadArgs: {
|
|
modelPath: "/tmp/this-model-does-not-exist.gguf",
|
|
accelerator: "vulkan",
|
|
},
|
|
binaryPath: fakeBin,
|
|
});
|
|
expect.fail("loadSdCppImageGenBackend should have thrown");
|
|
} catch (err) {
|
|
if (!(err instanceof ImageGenBackendUnavailableError)) throw err;
|
|
expect(err.backendId).toBe("sd-cpp");
|
|
expect(err.reason).toBe("vulkan_binary_missing");
|
|
expect(err.message).toMatch(/VULKAN support/);
|
|
}
|
|
});
|
|
|
|
itPosix("rejects Metal load when the sd-cpp binary is CPU-only", async () => {
|
|
const dir = mkdtempSync(join(tmpdir(), "sd-cpp-probe-"));
|
|
const fakeBin = join(dir, "fake-sd-cpu");
|
|
writeFileSync(
|
|
fakeBin,
|
|
"#!/usr/bin/env bash\nif [ \"$1\" = \"--version\" ]; then echo 'stable-diffusion.cpp cpu-only'; exit 0; fi\nif [ \"$1\" = \"--help\" ]; then echo 'stable-diffusion.cpp help'; exit 0; fi\nexit 2\n",
|
|
);
|
|
chmodSync(fakeBin, 0o755);
|
|
try {
|
|
await loadSdCppImageGenBackend({
|
|
modelKey: "imagegen-sd-1_5-q5_0",
|
|
loadArgs: {
|
|
modelPath: "/tmp/this-model-does-not-exist.gguf",
|
|
accelerator: "metal",
|
|
},
|
|
binaryPath: fakeBin,
|
|
});
|
|
expect.fail("loadSdCppImageGenBackend should have thrown");
|
|
} catch (err) {
|
|
if (!(err instanceof ImageGenBackendUnavailableError)) throw err;
|
|
expect(err.backendId).toBe("sd-cpp");
|
|
expect(err.reason).toBe("metal_binary_missing");
|
|
expect(err.message).toMatch(/METAL support/);
|
|
}
|
|
});
|
|
|
|
itPosix("accepts CUDA load when sidecar manifest proves CUDA support", async () => {
|
|
const dir = mkdtempSync(join(tmpdir(), "sd-cpp-probe-"));
|
|
const fakeBin = join(dir, "fake-sd-cuda");
|
|
const fakeModel = join(dir, "model.gguf");
|
|
writeFileSync(fakeModel, "fake model");
|
|
writeFileSync(
|
|
join(dir, "sd-cpp.manifest.json"),
|
|
JSON.stringify({ accelerators: ["cuda"] }),
|
|
);
|
|
writeFileSync(
|
|
fakeBin,
|
|
[
|
|
"#!/usr/bin/env bash",
|
|
"if [ \"$1\" = \"--version\" ]; then echo 'stable-diffusion.cpp SD_CUDA=ON'; exit 0; fi",
|
|
"if [ \"$1\" = \"--help\" ]; then echo 'backend: CUDA cuBLAS'; exit 0; fi",
|
|
"exit 2",
|
|
"",
|
|
].join("\n"),
|
|
);
|
|
chmodSync(fakeBin, 0o755);
|
|
const backend = await loadSdCppImageGenBackend({
|
|
modelKey: "imagegen-sd-1_5-q5_0",
|
|
loadArgs: { modelPath: fakeModel, accelerator: "cuda" },
|
|
binaryPath: fakeBin,
|
|
});
|
|
expect(backend.id).toBe("sd-cpp");
|
|
await backend.dispose();
|
|
});
|
|
|
|
itPosix("accepts Vulkan load when sidecar manifest proves Vulkan support", async () => {
|
|
const dir = mkdtempSync(join(tmpdir(), "sd-cpp-probe-"));
|
|
const fakeBin = join(dir, "fake-sd-vulkan");
|
|
const fakeModel = join(dir, "model.gguf");
|
|
writeFileSync(fakeModel, "fake model");
|
|
writeFileSync(
|
|
join(dir, "sd-cpp.manifest.json"),
|
|
JSON.stringify({ accelerators: ["vulkan"] }),
|
|
);
|
|
writeFileSync(
|
|
fakeBin,
|
|
[
|
|
"#!/usr/bin/env bash",
|
|
"if [ \"$1\" = \"--version\" ]; then echo 'stable-diffusion.cpp Vulkan'; exit 0; fi",
|
|
"if [ \"$1\" = \"--help\" ]; then echo 'backend: Vulkan'; exit 0; fi",
|
|
"exit 2",
|
|
"",
|
|
].join("\n"),
|
|
);
|
|
chmodSync(fakeBin, 0o755);
|
|
const backend = await loadSdCppImageGenBackend({
|
|
modelKey: "imagegen-sd-1_5-q5_0",
|
|
loadArgs: { modelPath: fakeModel, accelerator: "vulkan" },
|
|
binaryPath: fakeBin,
|
|
});
|
|
expect(backend.id).toBe("sd-cpp");
|
|
await backend.dispose();
|
|
});
|
|
|
|
it("backend honors a stub binary + fakeImageBytes to bypass the spawn", async () => {
|
|
// Same shape sd-cpp.ts uses internally for the test seam: when
|
|
// fakeImageBytes is provided, the load path skips --version and
|
|
// generate writes the bytes directly. This is what
|
|
// imagegen-handler.test.ts exercises in its end-to-end stub.
|
|
const fakePng = new Uint8Array([
|
|
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
|
|
0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52,
|
|
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01,
|
|
0x08, 0x06, 0x00, 0x00, 0x00, 0x1f, 0x15, 0xc4, 0x89,
|
|
0x00, 0x00, 0x00, 0x0d, 0x49, 0x44, 0x41, 0x54,
|
|
0x78, 0x9c, 0x62, 0x00, 0x01, 0x00, 0x00, 0x05,
|
|
0x00, 0x01, 0x0d, 0x0a, 0x2d, 0xb4,
|
|
0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44,
|
|
0xae, 0x42, 0x60, 0x82,
|
|
]);
|
|
const backend = await loadSdCppImageGenBackend({
|
|
modelKey: "imagegen-sd-1_5-q5_0",
|
|
loadArgs: { modelPath: "/tmp/not-read.gguf" },
|
|
fakeImageBytes: fakePng,
|
|
});
|
|
expect(backend.id).toBe("sd-cpp");
|
|
expect(backend.supports({ prompt: "x", width: 512, height: 512 })).toBe(
|
|
true,
|
|
);
|
|
const result = await backend.generate({
|
|
prompt: "a smiling cat",
|
|
width: 512,
|
|
height: 512,
|
|
steps: 4,
|
|
seed: 7,
|
|
});
|
|
expect(result.mime).toBe("image/png");
|
|
expect(result.seed).toBe(7);
|
|
expect(result.image[0]).toBe(0x89);
|
|
expect(result.image[1]).toBe(0x50);
|
|
await backend.dispose();
|
|
});
|
|
});
|
|
|
|
describe("WS3 sd-cpp backend — CLI argument contract", () => {
|
|
const baseArgs = {
|
|
modelPath: "/models/imagegen/sd-1.5-Q5_0.gguf",
|
|
prompt: "a small workspace",
|
|
width: 64,
|
|
height: 64,
|
|
steps: 1,
|
|
guidanceScale: 7.5,
|
|
seed: 42,
|
|
output: "/tmp/out.png",
|
|
};
|
|
|
|
it("uses --model for monolithic SD-family checkpoints", () => {
|
|
const args = buildArgs(baseArgs);
|
|
expect(args.slice(0, 2)).toEqual([
|
|
"--model",
|
|
"/models/imagegen/sd-1.5-Q5_0.gguf",
|
|
]);
|
|
expect(args).not.toContain("--diffusion-model");
|
|
});
|
|
|
|
it("uses --diffusion-model with split Z-Image companion assets", () => {
|
|
const args = buildArgs({
|
|
...baseArgs,
|
|
modelPath: "/models/imagegen/z-image-turbo-Q4_K_M.gguf",
|
|
splitDiffusionModel: true,
|
|
vae: "/models/imagegen/vae/ae.safetensors",
|
|
llm: "/models/imagegen/text-encoders/split-llm-text-encoder-Q4_K_M.gguf",
|
|
});
|
|
expect(args.slice(0, 2)).toEqual([
|
|
"--diffusion-model",
|
|
"/models/imagegen/z-image-turbo-Q4_K_M.gguf",
|
|
]);
|
|
expect(args).toContain("--vae");
|
|
expect(args).toContain("/models/imagegen/vae/ae.safetensors");
|
|
expect(args).toContain("--llm");
|
|
expect(args).toContain(
|
|
"/models/imagegen/text-encoders/split-llm-text-encoder-Q4_K_M.gguf",
|
|
);
|
|
expect(args).not.toContain("--model");
|
|
});
|
|
|
|
it("uses current upstream backend flags for CPU and Vulkan", () => {
|
|
expect(buildArgs({ ...baseArgs, accelerator: "cpu" })).toEqual(
|
|
expect.arrayContaining([
|
|
"--backend",
|
|
"cpu",
|
|
"--params-backend",
|
|
"cpu",
|
|
]),
|
|
);
|
|
expect(buildArgs({ ...baseArgs, accelerator: "vulkan" })).toEqual(
|
|
expect.arrayContaining(["--backend", "vulkan0"]),
|
|
);
|
|
});
|
|
});
|