test: tolerate transient google tts and openrouter tool probes

This commit is contained in:
Peter Steinberger
2026-04-29 14:46:08 +01:00
parent 3a875e7549
commit a972c9ec45
3 changed files with 70 additions and 1 deletions

View File

@@ -277,6 +277,54 @@ describe("Google speech provider", () => {
expect(result.audioBuffer.subarray(44)).toEqual(pcm);
});
it("retries once when Gemini TTS fetch aborts", async () => {
const pcm = Buffer.from([7, 0, 8, 0]);
const abortError = new Error("This operation was aborted");
abortError.name = "AbortError";
const requestSequence = vi
.fn()
.mockRejectedValueOnce(abortError)
.mockResolvedValueOnce({
response: googleTtsResponse(pcm),
release: vi.fn(async () => {}),
});
postJsonRequestMock.mockImplementation(requestSequence);
const provider = buildGoogleSpeechProvider();
const result = await provider.synthesize({
text: "Retry aborted fetch.",
cfg: {},
providerConfig: {
apiKey: "google-test-key",
},
target: "audio-file",
timeoutMs: 5_000,
});
expect(requestSequence).toHaveBeenCalledTimes(2);
expect(result.audioBuffer.subarray(44)).toEqual(pcm);
});
it("does not retry non-transient Gemini TTS request failures", async () => {
const requestSequence = vi.fn().mockRejectedValueOnce(new Error("invalid request"));
postJsonRequestMock.mockImplementation(requestSequence);
const provider = buildGoogleSpeechProvider();
await expect(
provider.synthesize({
text: "Do not retry this.",
cfg: {},
providerConfig: {
apiKey: "google-test-key",
},
target: "audio-file",
timeoutMs: 5_000,
}),
).rejects.toThrow("invalid request");
expect(requestSequence).toHaveBeenCalledTimes(1);
});
it("falls back to GEMINI_API_KEY and configured Google API base URL", async () => {
vi.stubEnv("GEMINI_API_KEY", "env-google-key");
const requestMock = installGoogleTtsRequestMock();

View File

@@ -107,6 +107,25 @@ class GoogleTtsRetryableError extends Error {
}
}
function isGoogleTtsRetryableError(err: unknown): boolean {
if (err instanceof GoogleTtsRetryableError) {
return true;
}
if (!(err instanceof Error)) {
return false;
}
if (err.name === "AbortError") {
return true;
}
const message = err.message.toLowerCase();
return (
message.includes("aborted") ||
message.includes("timeout") ||
message.includes("fetch failed") ||
message.includes("network")
);
}
function normalizeGoogleTtsModel(model: unknown): string {
const trimmed = normalizeOptionalString(model);
if (!trimmed) {
@@ -509,7 +528,7 @@ async function synthesizeGoogleTtsPcm(params: {
return await synthesizeGoogleTtsPcmOnce(params);
} catch (err) {
lastError = err;
if (!(err instanceof GoogleTtsRetryableError) || attempt > 0) {
if (!isGoogleTtsRetryableError(err) || attempt > 0) {
throw err;
}
}

View File

@@ -685,6 +685,7 @@ function shouldSkipToolNonceProbeMissForLiveModel(modelKey?: string): boolean {
provider === "minimax" ||
provider === "opencode" ||
provider === "opencode-go" ||
provider === "openrouter" ||
provider === "xai" ||
provider === "zai"
) {
@@ -703,6 +704,7 @@ describe("shouldSkipToolNonceProbeMissForLiveModel", () => {
{ modelKey: "minimax/minimax-m1", expected: true },
{ modelKey: "opencode/big-pickle", expected: true },
{ modelKey: "opencode-go/glm-5", expected: true },
{ modelKey: "openrouter/ai21/jamba-large-1.7", expected: true },
{ modelKey: "xai/grok-4.1-fast", expected: true },
{ modelKey: "zai/glm-5.1", expected: true },
{ modelKey: "google/gemini-3-flash-preview", expected: true },