Files
openclaw/extensions/alibaba/video-generation-provider.test.ts
Peter Steinberger 7975432eb5 fix(onboard): enable Runway and Alibaba video provider authentication (#126722)
* fix(onboard): enable Runway and Alibaba video provider authentication

* perf(ui): load image lightbox only when opening images

* perf(ui): defer onboarding memory import outside setup
2026-08-20 09:30:48 -07:00

430 lines
14 KiB
TypeScript

import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import {
clearRuntimeAuthProfileStoreSnapshots,
saveAuthProfileStore,
} from "openclaw/plugin-sdk/agent-runtime";
import {
capturePluginRegistration,
createRuntimeEnv,
resolveProviderPluginChoice,
} from "openclaw/plugin-sdk/plugin-test-runtime";
import {
getProviderHttpMocks,
installProviderHttpMockCleanup,
requireFirstPostJsonRecordRequest as requireFirstPostJsonRequest,
} from "openclaw/plugin-sdk/provider-http-test-mocks";
import {
expectDashscopeVideoTaskPoll,
expectExplicitVideoGenerationCapabilities,
expectSuccessfulDashscopeVideoResult,
mockSuccessfulDashscopeVideoTask,
} from "openclaw/plugin-sdk/provider-test-contracts";
// Alibaba tests cover video generation provider plugin behavior.
import { createRequireRecord } from "openclaw/plugin-sdk/test-fixtures";
import {
DASHSCOPE_WAN_VIDEO_MODELS,
DEFAULT_DASHSCOPE_WAN_VIDEO_MODEL,
} from "openclaw/plugin-sdk/video-generation";
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
const {
resolveApiKeyForProviderMock,
postJsonRequestMock,
fetchWithTimeoutMock,
fetchWithTimeoutGuardedMock,
resolveProviderHttpRequestConfigMock,
sanitizeConfiguredModelProviderRequestMock,
} = getProviderHttpMocks();
let alibabaVideoGenerationProvider: typeof import("./video-generation-provider.js").alibabaVideoGenerationProvider;
beforeAll(async () => {
({ alibabaVideoGenerationProvider } = await import("./video-generation-provider.js"));
});
installProviderHttpMockCleanup();
afterEach(() => {
clearRuntimeAuthProfileStoreSnapshots();
vi.unstubAllEnvs();
});
function clearAlibabaAuthEnvironment(): void {
for (const name of ["MODELSTUDIO_API_KEY", "DASHSCOPE_API_KEY", "QWEN_API_KEY"]) {
vi.stubEnv(name, "");
}
}
const requireRecord = createRequireRecord("record", "expected-label-record");
describe("alibaba video generation provider", () => {
it("registers media-only API-key onboarding alongside video generation", async () => {
const { default: plugin } = await import("./index.js");
const captured = capturePluginRegistration(plugin);
expect(captured.videoGenerationProviders.map((provider) => provider.id)).toEqual(["alibaba"]);
expect(captured.modelCatalogProviders).toEqual([]);
expect(captured.providers).toHaveLength(1);
expect(captured.providers[0]).toMatchObject({
id: "alibaba",
docsPath: "/providers/alibaba",
envVars: ["MODELSTUDIO_API_KEY", "DASHSCOPE_API_KEY", "QWEN_API_KEY"],
});
const choice = resolveProviderPluginChoice({
providers: captured.providers,
choice: "alibaba-model-studio-api-key",
});
expect(choice?.method.id).toBe("api-key");
expect(choice?.method.starterModel).toBeUndefined();
expect(choice?.wizard?.onboardingScopes).toEqual(["image-generation"]);
if (!choice?.method.validateNonInteractive) {
throw new Error("expected Alibaba non-interactive API-key validation");
}
const resolveApiKey = vi.fn(async () => ({ key: "alibaba-test-key", source: "flag" as const }));
expect(
await choice.method.validateNonInteractive({
authChoice: "alibaba-model-studio-api-key",
config: {},
baseConfig: {},
opts: { alibabaModelStudioApiKey: "alibaba-test-key" },
runtime: createRuntimeEnv(),
resolveApiKey,
}),
).toBe(true);
expect(resolveApiKey).toHaveBeenCalledWith({
provider: "alibaba",
flagValue: "alibaba-test-key",
flagName: "--alibaba-model-studio-api-key",
envVar: "MODELSTUDIO_API_KEY",
});
});
it("declares explicit mode capabilities", () => {
expectExplicitVideoGenerationCapabilities(alibabaVideoGenerationProvider);
expect(alibabaVideoGenerationProvider).toMatchObject({
id: "alibaba",
label: "Alibaba Model Studio",
defaultModel: DEFAULT_DASHSCOPE_WAN_VIDEO_MODEL,
models: [...DASHSCOPE_WAN_VIDEO_MODELS],
});
});
it.each(["sk-ws-alibaba-standard-key", "sk-alibaba-legacy-standard-key"])(
"advertises Wan video generation with config-only Standard API key %s",
(apiKey) => {
clearAlibabaAuthEnvironment();
expect(
alibabaVideoGenerationProvider.isConfigured?.({
cfg: {
models: {
providers: {
alibaba: {
apiKey,
baseUrl: "https://dashscope-intl.aliyuncs.com",
models: [],
},
},
},
},
}),
).toBe(true);
},
);
it("does not use Qwen Coding Plan credentials for Alibaba video discovery", () => {
clearAlibabaAuthEnvironment();
expect(
alibabaVideoGenerationProvider.isConfigured?.({
cfg: {
models: {
providers: {
qwen: {
apiKey: "qwen-coding-plan-key",
baseUrl: "https://coding-intl.dashscope.aliyuncs.com/v1",
models: [],
},
},
},
},
}),
).toBe(false);
});
it.each(["", "oauth:alibaba", "custom-local", "secretref-managed"])(
"does not advertise a non-secret Alibaba credential marker %j",
(apiKey) => {
clearAlibabaAuthEnvironment();
expect(
alibabaVideoGenerationProvider.isConfigured?.({
cfg: {
models: {
providers: {
alibaba: {
apiKey,
baseUrl: "https://dashscope-intl.aliyuncs.com",
models: [],
},
},
},
},
}),
).toBe(false);
},
);
it("tracks whether an allowed Alibaba API-key SecretRef resolves", () => {
clearAlibabaAuthEnvironment();
vi.stubEnv("ALIBABA_QA_CONFIG_KEY", "resolved-alibaba-config-key");
const cfg = {
models: {
providers: {
alibaba: {
apiKey: {
source: "env" as const,
provider: "alibaba-test-env",
id: "ALIBABA_QA_CONFIG_KEY",
},
baseUrl: "https://dashscope-intl.aliyuncs.com",
models: [],
},
},
},
secrets: {
defaults: { env: "alibaba-test-env" },
providers: {
"alibaba-test-env": {
source: "env" as const,
allowlist: ["ALIBABA_QA_CONFIG_KEY"],
},
},
},
};
expect(alibabaVideoGenerationProvider.isConfigured?.({ cfg })).toBe(true);
vi.stubEnv("ALIBABA_QA_CONFIG_KEY", "");
expect(alibabaVideoGenerationProvider.isConfigured?.({ cfg })).toBe(false);
});
it("preserves Alibaba environment API-key discovery", () => {
clearAlibabaAuthEnvironment();
vi.stubEnv("MODELSTUDIO_API_KEY", "alibaba-environment-key");
expect(alibabaVideoGenerationProvider.isConfigured?.({ cfg: {} })).toBe(true);
});
it("does not advertise an inherited Qwen Coding Plan API key", () => {
clearAlibabaAuthEnvironment();
vi.stubEnv("QWEN_API_KEY", "sk-sp-qwen-coding-plan-key");
expect(alibabaVideoGenerationProvider.isConfigured?.({ cfg: {} })).toBe(false);
});
it("keeps explicit Standard config above an inherited Coding Plan environment key", () => {
clearAlibabaAuthEnvironment();
vi.stubEnv("QWEN_API_KEY", "sk-sp-qwen-coding-plan-key");
expect(
alibabaVideoGenerationProvider.isConfigured?.({
cfg: {
models: {
providers: {
alibaba: {
auth: "api-key",
apiKey: "sk-ws-alibaba-standard-key",
baseUrl: "https://dashscope-intl.aliyuncs.com",
models: [],
},
},
},
},
}),
).toBe(true);
});
it.each([
["sk-ws-alibaba-profile", "sk-sp-qwen-environment", true],
["sk-sp-alibaba-profile", "sk-ws-qwen-environment", false],
])("preserves actual profile precedence for %s", async (profileKey, envKey, expected) => {
clearAlibabaAuthEnvironment();
vi.stubEnv("QWEN_API_KEY", envKey);
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-alibaba-wan-auth-"));
try {
saveAuthProfileStore(
{
version: 1,
profiles: {
"alibaba:standard": {
type: "api_key",
provider: "alibaba",
key: profileKey,
},
},
},
agentDir,
{ filterExternalAuthProfiles: false, syncExternalCli: false },
);
expect(alibabaVideoGenerationProvider.isConfigured?.({ cfg: {}, agentDir })).toBe(expected);
} finally {
clearRuntimeAuthProfileStoreSnapshots();
await fs.rm(agentDir, { force: true, recursive: true });
}
});
it("rejects a resolved Coding Plan API key before submitting a Wan request", async () => {
resolveApiKeyForProviderMock.mockResolvedValueOnce({ apiKey: "sk-sp-qwen-coding-plan-key" });
await expect(
alibabaVideoGenerationProvider.generateVideo({
provider: "alibaba",
model: "wan2.6-t2v",
prompt: "animate this shot",
cfg: {},
}),
).rejects.toThrow(/Standard DashScope endpoint.*same-region Standard API key/i);
expect(postJsonRequestMock).not.toHaveBeenCalled();
});
it("submits async Wan generation, polls task status, and downloads the resulting video", async () => {
mockSuccessfulDashscopeVideoTask({ postJsonRequestMock, fetchWithTimeoutMock });
const provider = alibabaVideoGenerationProvider;
const result = await provider.generateVideo({
provider: "alibaba",
model: "wan2.6-r2v-flash",
prompt: "animate this shot",
cfg: {},
inputImages: [{ url: "https://example.com/ref.png" }],
durationSeconds: 6,
audio: true,
watermark: false,
});
expect(postJsonRequestMock).toHaveBeenCalledOnce();
const request = requireFirstPostJsonRequest(postJsonRequestMock, "DashScope request");
expect(request.url).toBe(
"https://dashscope-intl.aliyuncs.com/api/v1/services/aigc/video-generation/video-synthesis",
);
const body = requireRecord(request.body, "DashScope request body");
expect(body.model).toBe("wan2.6-r2v-flash");
const input = requireRecord(body.input, "DashScope request input");
expect(input.prompt).toBe("animate this shot");
expect(input.reference_urls).toEqual(["https://example.com/ref.png"]);
const parameters = requireRecord(body.parameters, "DashScope request parameters");
expect(parameters.duration).toBe(6);
expect(parameters.audio).toBe(true);
expect(parameters.watermark).toBe(false);
expectDashscopeVideoTaskPoll(fetchWithTimeoutMock);
expectSuccessfulDashscopeVideoResult(result);
});
it("applies configured request policy to DashScope video requests", async () => {
const requestPolicy = {
allowPrivateNetwork: true,
headers: { "X-DashScope-Route": "alibaba-policy" },
};
const dispatcherPolicy = { mode: "env-proxy" as const };
resolveProviderHttpRequestConfigMock.mockImplementationOnce((params) => {
const headers = new Headers(params.defaultHeaders);
for (const [key, value] of Object.entries(params.request?.headers ?? {})) {
headers.set(key, value);
}
return {
baseUrl: params.baseUrl ?? params.defaultBaseUrl,
allowPrivateNetwork: params.request?.allowPrivateNetwork === true,
headers,
dispatcherPolicy,
};
});
mockSuccessfulDashscopeVideoTask({ postJsonRequestMock, fetchWithTimeoutMock });
const provider = alibabaVideoGenerationProvider;
await provider.generateVideo({
provider: "alibaba",
model: "wan2.6-t2v",
prompt: "animate this shot",
cfg: {
models: {
providers: {
alibaba: {
baseUrl: "https://dashscope-intl.aliyuncs.com",
models: [],
request: requestPolicy,
},
},
},
},
});
expect(sanitizeConfiguredModelProviderRequestMock).toHaveBeenCalledWith(requestPolicy);
expect(resolveProviderHttpRequestConfigMock).toHaveBeenCalledWith(
expect.objectContaining({
provider: "alibaba",
capability: "video",
transport: "http",
request: requestPolicy,
}),
);
const request = requireFirstPostJsonRequest(
postJsonRequestMock,
"DashScope request with request policy",
);
expect(request.allowPrivateNetwork).toBe(true);
expect(request.dispatcherPolicy).toBe(dispatcherPolicy);
expect(request.headers).toBeInstanceOf(Headers);
expect((request.headers as Headers).get("x-dashscope-route")).toBe("alibaba-policy");
expect(fetchWithTimeoutGuardedMock).toHaveBeenNthCalledWith(
1,
"https://dashscope-intl.aliyuncs.com/api/v1/tasks/task-1",
expect.objectContaining({
method: "GET",
headers: expect.any(Headers),
}),
expect.any(Number),
fetch,
{
ssrfPolicy: { allowPrivateNetwork: true },
dispatcherPolicy,
},
);
expect(fetchWithTimeoutGuardedMock).toHaveBeenNthCalledWith(
2,
"https://example.com/out.mp4",
{ method: "GET" },
expect.any(Number),
fetch,
{
ssrfPolicy: { allowPrivateNetwork: true },
dispatcherPolicy,
},
);
});
it("fails fast when reference inputs are local buffers instead of remote URLs", async () => {
const provider = alibabaVideoGenerationProvider;
await expect(
provider.generateVideo({
provider: "alibaba",
model: "wan2.6-i2v",
prompt: "animate this local frame",
cfg: {},
inputImages: [{ buffer: Buffer.from("png-bytes"), mimeType: "image/png" }],
}),
).rejects.toThrow(
"Alibaba Wan video generation currently requires remote http(s) URLs for reference images/videos.",
);
expect(postJsonRequestMock).not.toHaveBeenCalled();
});
});