Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

252 lines
8.7 KiB
TypeScript

/**
* Behavior tests for the Groq plugin: init key requirement, model-handler
* registration, and retry classification, against a stubbed runtime and mocked
* globals (no live Groq API).
*/
import { type IAgentRuntime, ModelType } from "@elizaos/core";
import { afterEach, describe, expect, it, vi } from "vitest";
import groqPlugin, { classifyRetryError } from "../index";
function runtime(settings: Record<string, string | undefined> = {}): IAgentRuntime {
return {
character: { system: "system prompt" },
emitEvent: vi.fn(),
getService: vi.fn(),
getSetting: (key: string) => settings[key],
} as unknown as IAgentRuntime;
}
describe("@elizaos/plugin-groq behavior", () => {
afterEach(() => {
vi.unstubAllGlobals();
});
it("requires GROQ_API_KEY during node init", async () => {
await expect(groqPlugin.init?.({}, runtime())).rejects.toThrow("GROQ_API_KEY is required");
await expect(groqPlugin.init?.({}, runtime({ GROQ_API_KEY: " \t\n " }))).rejects.toThrow(
"GROQ_API_KEY is required"
);
await expect(
groqPlugin.init?.({}, runtime({ GROQ_API_KEY: "gsk-test" }))
).resolves.toBeUndefined();
});
it("classifies retryable rate-limit and transient errors separately from fatal errors", () => {
expect(classifyRetryError(new Error("Rate limit exceeded, try again in 2.5s"))).toBe(
"rate-limit"
);
expect(classifyRetryError(new Error("fetch failed: ECONNRESET"))).toBe("transient");
expect(classifyRetryError(new Error("invalid API key"))).toBe("fatal");
expect(classifyRetryError("plain string")).toBe("fatal");
});
it("sends transcription requests with auth, model, and audio form data", async () => {
const fetchMock = vi.fn(
async () =>
new Response(JSON.stringify({ text: "transcribed text" }), {
status: 200,
headers: { "Content-Type": "application/json" },
})
);
vi.stubGlobal("fetch", fetchMock);
await expect(
groqPlugin.models?.[ModelType.TRANSCRIPTION]?.(
runtime({ GROQ_API_KEY: "gsk-test", GROQ_BASE_URL: "https://groq.test/v1" }),
{ audioData: new Uint8Array([1, 2, 3]) }
)
).resolves.toBe("transcribed text");
const [, init] = fetchMock.mock.calls[0];
expect(fetchMock.mock.calls[0][0]).toBe("https://groq.test/v1/audio/transcriptions");
expect(init).toMatchObject({
method: "POST",
headers: { Authorization: "Bearer gsk-test" },
});
expect(init.body).toBeInstanceOf(FormData);
expect((init.body as FormData).get("model")).toBe("whisper-large-v3-turbo");
expect((init.body as FormData).get("file")).toBeInstanceOf(File);
});
it("lets the transcription model be overridden via GROQ_TRANSCRIPTION_MODEL", async () => {
const fetchMock = vi.fn(
async () =>
new Response(JSON.stringify({ text: "ok" }), {
status: 200,
headers: { "Content-Type": "application/json" },
})
);
vi.stubGlobal("fetch", fetchMock);
await groqPlugin.models?.[ModelType.TRANSCRIPTION]?.(
runtime({ GROQ_API_KEY: "gsk-test", GROQ_TRANSCRIPTION_MODEL: "whisper-large-v3" }),
{ audioData: new Uint8Array([1, 2, 3]) }
);
const [, init] = fetchMock.mock.calls[0];
expect((init.body as FormData).get("model")).toBe("whisper-large-v3");
});
it("falls back to the generic TRANSCRIPTION_MODEL setting when no Groq-specific one is set", async () => {
const fetchMock = vi.fn(
async () =>
new Response(JSON.stringify({ text: "ok" }), {
status: 200,
headers: { "Content-Type": "application/json" },
})
);
vi.stubGlobal("fetch", fetchMock);
await groqPlugin.models?.[ModelType.TRANSCRIPTION]?.(
runtime({ GROQ_API_KEY: "gsk-test", TRANSCRIPTION_MODEL: "distil-whisper-large-v3-en" }),
{ audioData: new Uint8Array([1, 2, 3]) }
);
const [, init] = fetchMock.mock.calls[0];
expect((init.body as FormData).get("model")).toBe("distil-whisper-large-v3-en");
});
it("rejects empty or malformed transcription input before calling the API", async () => {
const fetchMock = vi.fn();
vi.stubGlobal("fetch", fetchMock);
await expect(
groqPlugin.models?.[ModelType.TRANSCRIPTION]?.(runtime({ GROQ_API_KEY: "gsk-test" }), {})
).rejects.toThrow("Groq TRANSCRIPTION requires non-empty audio data.");
await expect(
groqPlugin.models?.[ModelType.TRANSCRIPTION]?.(runtime({ GROQ_API_KEY: "gsk-test" }), "")
).rejects.toThrow("Groq TRANSCRIPTION requires non-empty audio data.");
expect(fetchMock).not.toHaveBeenCalled();
});
it("throws transcription response details on non-ok responses", async () => {
vi.stubGlobal(
"fetch",
vi.fn(async () => new Response("bad audio", { status: 400, statusText: "Bad Request" }))
);
await expect(
groqPlugin.models?.[ModelType.TRANSCRIPTION]?.(
runtime({ GROQ_API_KEY: "gsk-test" }),
Buffer.from("audio")
)
).rejects.toThrow("Transcription failed: 400 bad audio");
});
it("sends text-to-speech requests with runtime and payload overrides", async () => {
const bytes = new Uint8Array([10, 20, 30]).buffer;
const fetchMock = vi.fn(async () => new Response(bytes, { status: 200 }));
vi.stubGlobal("fetch", fetchMock);
await expect(
groqPlugin.models?.[ModelType.TEXT_TO_SPEECH]?.(
runtime({
GROQ_API_KEY: "gsk-test",
GROQ_BASE_URL: "https://groq.test/v1",
GROQ_TTS_MODEL: "runtime-model",
GROQ_TTS_VOICE: "runtime-voice",
GROQ_TTS_RESPONSE_FORMAT: "mp3",
}),
{
text: "hello",
model: "payload-model",
voice: "payload-voice",
response_format: "wav",
}
)
).resolves.toEqual(new Uint8Array([10, 20, 30]));
expect(fetchMock).toHaveBeenCalledWith("https://groq.test/v1/audio/speech", {
method: "POST",
headers: {
Authorization: "Bearer gsk-test",
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "payload-model",
voice: "payload-voice",
input: "hello",
response_format: "wav",
}),
});
});
it("rejects malformed text-to-speech input before calling the API", async () => {
const fetchMock = vi.fn();
vi.stubGlobal("fetch", fetchMock);
for (const params of [null, undefined, {}, { text: "" }, { text: " \n\t " }]) {
await expect(
groqPlugin.models?.[ModelType.TEXT_TO_SPEECH]?.(
runtime({ GROQ_API_KEY: "gsk-test" }),
params
)
).rejects.toThrow("Groq TEXT_TO_SPEECH requires non-empty text.");
}
expect(fetchMock).not.toHaveBeenCalled();
});
it("rejects invalid GROQ_BASE_URL values before calling the API", async () => {
const fetchMock = vi.fn();
vi.stubGlobal("fetch", fetchMock);
for (const baseURL of ["not a url", "ftp://groq.test/v1", "javascript:alert(1)"]) {
await expect(
groqPlugin.models?.[ModelType.TEXT_TO_SPEECH]?.(
runtime({ GROQ_API_KEY: "gsk-test", GROQ_BASE_URL: baseURL }),
{ text: "hello" }
)
).rejects.toThrow("GROQ_BASE_URL must be a valid http(s) URL");
}
expect(fetchMock).not.toHaveBeenCalled();
});
it("trims trailing slash from GROQ_BASE_URL when building audio endpoints", async () => {
const fetchMock = vi.fn(async () => new Response(new Uint8Array([1]).buffer, { status: 200 }));
vi.stubGlobal("fetch", fetchMock);
await groqPlugin.models?.[ModelType.TEXT_TO_SPEECH]?.(
runtime({ GROQ_API_KEY: "gsk-test", GROQ_BASE_URL: "https://groq.test/v1/" }),
{ text: "hello" }
);
expect(fetchMock.mock.calls[0]?.[0]).toBe("https://groq.test/v1/audio/speech");
});
it("throws text-to-speech response details on non-ok responses", async () => {
vi.stubGlobal(
"fetch",
vi.fn(async () => new Response("voice unavailable", { status: 422 }))
);
await expect(
groqPlugin.models?.[ModelType.TEXT_TO_SPEECH]?.(runtime({ GROQ_API_KEY: "gsk-test" }), {
text: "hello",
})
).rejects.toThrow("TTS failed: 422 voice unavailable");
});
it("keeps text-to-speech defaults aligned with plugin metadata", async () => {
const bytes = new Uint8Array([10, 20, 30]).buffer;
const fetchMock = vi.fn(async () => new Response(bytes, { status: 200 }));
vi.stubGlobal("fetch", fetchMock);
await expect(
groqPlugin.models?.[ModelType.TEXT_TO_SPEECH]?.(runtime({ GROQ_API_KEY: "gsk-test" }), {
text: "hello",
})
).resolves.toEqual(new Uint8Array([10, 20, 30]));
const [, init] = fetchMock.mock.calls[0];
expect(JSON.parse(init.body as string)).toMatchObject({
model: "canopylabs/orpheus-v1-english",
voice: "troy",
input: "hello",
response_format: "wav",
});
});
});