mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-13 12:36:04 +00:00
fix: preserve guarded video operation cleanup
This commit is contained in:
@@ -11,6 +11,7 @@ const {
|
||||
fetchWithTimeoutMock,
|
||||
fetchWithTimeoutGuardedMock,
|
||||
pollProviderOperationJsonMock,
|
||||
assertOkOrThrowHttpErrorMock,
|
||||
resolveProviderHttpRequestConfigMock,
|
||||
sanitizeConfiguredModelProviderRequestMock,
|
||||
} = getProviderHttpMocks();
|
||||
@@ -303,6 +304,60 @@ describe("openai video generation provider", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("releases guarded local video download requests when HTTP errors throw", async () => {
|
||||
const release = vi.fn(async () => {});
|
||||
assertOkOrThrowHttpErrorMock
|
||||
.mockImplementationOnce(async () => {})
|
||||
.mockImplementationOnce(async () => {})
|
||||
.mockImplementationOnce(async (_response, label) => {
|
||||
throw new Error(label);
|
||||
});
|
||||
postJsonRequestMock.mockResolvedValue({
|
||||
response: {
|
||||
json: async () => ({
|
||||
id: "vid_local",
|
||||
model: "sora-2",
|
||||
status: "queued",
|
||||
}),
|
||||
},
|
||||
release: vi.fn(async () => {}),
|
||||
});
|
||||
fetchWithTimeoutMock.mockResolvedValueOnce({
|
||||
json: async () => ({
|
||||
id: "vid_local",
|
||||
model: "sora-2",
|
||||
status: "completed",
|
||||
}),
|
||||
});
|
||||
fetchWithTimeoutGuardedMock.mockResolvedValueOnce({
|
||||
response: new Response("busy", { status: 503, statusText: "Service Unavailable" }),
|
||||
finalUrl: "http://127.0.0.1:44080/v1/videos/vid_local/content?variant=video",
|
||||
release,
|
||||
});
|
||||
|
||||
const provider = buildOpenAIVideoGenerationProvider();
|
||||
await expect(
|
||||
provider.generateVideo({
|
||||
provider: "openai",
|
||||
model: "sora-2",
|
||||
prompt: "Render via local relay",
|
||||
cfg: {
|
||||
models: {
|
||||
providers: {
|
||||
openai: {
|
||||
baseUrl: "http://127.0.0.1:44080/v1",
|
||||
request: { allowPrivateNetwork: true },
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
).rejects.toThrow("OpenAI video download failed");
|
||||
|
||||
expect(release).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("uses multipart input_reference for video-to-video uploads", async () => {
|
||||
fetchWithTimeoutMock
|
||||
.mockResolvedValueOnce({
|
||||
|
||||
@@ -198,8 +198,13 @@ async function fetchOpenAIVideoDownload(
|
||||
auditContext: "openai-video-download",
|
||||
},
|
||||
);
|
||||
await assertOkOrThrowHttpError(result.response, "OpenAI video download failed");
|
||||
return result;
|
||||
try {
|
||||
await assertOkOrThrowHttpError(result.response, "OpenAI video download failed");
|
||||
return result;
|
||||
} catch (error) {
|
||||
await result.release();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadOpenAIVideo(
|
||||
|
||||
@@ -188,6 +188,48 @@ describe("provider operation deadlines", () => {
|
||||
expect(release).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("retries guarded transient provider status failures while polling", async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(1_000);
|
||||
const firstRelease = vi.fn(async () => {});
|
||||
const secondRelease = vi.fn(async () => {});
|
||||
fetchWithSsrFGuardMock
|
||||
.mockResolvedValueOnce({
|
||||
response: new Response("busy", { status: 503, statusText: "Service Unavailable" }),
|
||||
finalUrl: "https://api.example.com/v1/videos/task-1",
|
||||
release: firstRelease,
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
response: new Response(JSON.stringify({ status: "completed" })),
|
||||
finalUrl: "https://api.example.com/v1/videos/task-1",
|
||||
release: secondRelease,
|
||||
});
|
||||
|
||||
const result = pollProviderOperationJson<{ status?: string }>({
|
||||
url: "https://api.example.com/v1/videos/task-1",
|
||||
headers: new Headers({ authorization: "Bearer test" }),
|
||||
deadline: createProviderOperationDeadline({
|
||||
label: "video generation task task-1",
|
||||
timeoutMs: 10_000,
|
||||
}),
|
||||
defaultTimeoutMs: 5_000,
|
||||
fetchFn: fetch,
|
||||
maxAttempts: 3,
|
||||
pollIntervalMs: 1_000,
|
||||
requestFailedMessage: "status failed",
|
||||
timeoutMessage: "task timed out",
|
||||
allowPrivateNetwork: true,
|
||||
isComplete: (payload) => payload.status === "completed",
|
||||
});
|
||||
|
||||
await vi.advanceTimersByTimeAsync(250);
|
||||
|
||||
await expect(result).resolves.toEqual({ status: "completed" });
|
||||
expect(fetchWithSsrFGuardMock).toHaveBeenCalledTimes(2);
|
||||
expect(firstRelease).toHaveBeenCalledTimes(1);
|
||||
expect(secondRelease).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("throws provider failure messages while polling status JSON", async () => {
|
||||
const fetchFn = vi
|
||||
.fn<typeof fetch>()
|
||||
|
||||
@@ -174,15 +174,16 @@ export async function pollProviderOperationJson<TPayload>(
|
||||
const guardedOptions = resolveGuardedRequestOptions(params);
|
||||
const payload = guardedOptions
|
||||
? await (async () => {
|
||||
const result = await fetchWithTimeoutGuarded(
|
||||
params.url,
|
||||
const result = await fetchGuardedProviderOperationResponse({
|
||||
stage: "poll",
|
||||
url: params.url,
|
||||
init,
|
||||
timeoutMs(),
|
||||
params.fetchFn,
|
||||
timeoutMs,
|
||||
fetchFn: params.fetchFn,
|
||||
requestFailedMessage: params.requestFailedMessage,
|
||||
guardedOptions,
|
||||
);
|
||||
});
|
||||
try {
|
||||
await assertOkOrThrowHttpError(result.response, params.requestFailedMessage);
|
||||
return (await readProviderJsonObjectResponse(
|
||||
result.response,
|
||||
params.requestFailedMessage,
|
||||
@@ -473,6 +474,42 @@ function resolveGuardedRequestOptions(
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchGuardedProviderOperationResponse(params: {
|
||||
stage: ProviderOperationRetryStage;
|
||||
url: string;
|
||||
init: RequestInit;
|
||||
timeoutMs?: ProviderOperationTimeoutMs;
|
||||
fetchFn: typeof fetch;
|
||||
provider?: string;
|
||||
requestFailedMessage?: string;
|
||||
retry?: TransientProviderRetryConfig;
|
||||
guardedOptions: GuardedProviderRequestOptions;
|
||||
}): Promise<GuardedFetchResult> {
|
||||
return await executeProviderOperationWithRetry({
|
||||
provider: params.provider ?? "provider-http",
|
||||
stage: params.stage,
|
||||
retry: params.retry,
|
||||
operation: async () => {
|
||||
const result = await fetchWithTimeoutGuarded(
|
||||
params.url,
|
||||
params.init,
|
||||
resolveProviderOperationRequestTimeoutMs(params.timeoutMs),
|
||||
params.fetchFn,
|
||||
params.guardedOptions,
|
||||
);
|
||||
try {
|
||||
if (params.requestFailedMessage) {
|
||||
await assertOkOrThrowHttpError(result.response, params.requestFailedMessage);
|
||||
}
|
||||
return result;
|
||||
} catch (error) {
|
||||
await result.release();
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
type GuardedPostRequestRetryOptions = {
|
||||
/**
|
||||
* POST requests default to no retry because many provider endpoints create
|
||||
|
||||
Reference in New Issue
Block a user