feat: publish Agent Studio integrations and setup guide
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
import { afterEach, expect, it, vi } from "vitest";
|
||||
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
||||
import { ImageVideoMcpRemote, imageVideoResponse } from "../src/image-video/mcp.js";
|
||||
import type { ImageVideoRecord } from "../src/image-video/contracts.js";
|
||||
import { mkdtemp, mkdir, readdir, readFile, stat } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
vi.mock("node:dns/promises", () => ({ lookup: () => Promise.resolve([{ address: "8.8.8.8", family: 4 }]) }));
|
||||
afterEach(() => vi.restoreAllMocks());
|
||||
|
||||
it("accepts the live reused-image receipt only when hash, type and size match the frozen reference", async () => {
|
||||
vi.spyOn(Client.prototype, "connect").mockResolvedValue();
|
||||
vi.spyOn(Client.prototype, "listTools").mockResolvedValue({ tools: ["swads_whoami", "creative_upload_from_url", "creative_import_status", "video_h3_submit", "video_job_status"].map(name => ({ name, inputSchema: { type: "object", properties: Object.fromEntries(["tenant_id", "platform", "platforms", "media_kind", "review_status", "asset_role", "ratio", "prompt", "file_name", "content_type", "size_bytes", "content_sha256"].map(key => [key, {}])) } })) });
|
||||
const file = { file_id: "file_1", filename: "i2v_test.jpeg", stored_path: "https://cdn.example/image.jpeg", object_key: "image.jpeg", asset_url: "https://cdn.example/image.jpeg", mime_type: "image/jpeg", size_bytes: 9, attachment_kind: "image", creative_factory_asset_id: "existing_asset", creative_factory_request_id: "request_1", asset_metadata: { content_sha256: "a".repeat(64) } };
|
||||
let reusedFile: unknown = file;
|
||||
vi.spyOn(Client.prototype, "callTool").mockImplementation(request => Promise.resolve({ content: [], structuredContent: request.name === "swads_whoami" ? { access: { current_account: { tenant_id: "tenant", platform: "tiktok" } } } : { upload_id: "du-reuse-1", object_key: "image.jpeg", upload_url: "", public_url: "https://cdn.example/image.jpeg", method: "PUT", headers: {}, expires_in_seconds: 0, reused: true, reused_file: reusedFile } }));
|
||||
const remote = new ImageVideoMcpRemote({ token: "test" });
|
||||
const record = { id: "i2v_test", identity: { tenantId: "tenant", platform: "tiktok" }, draft: { prompt: "waves" }, referenceType: "image/jpeg", referenceHash: "a".repeat(64) } as ImageVideoRecord;
|
||||
await expect(remote.initialize(record, Buffer.from("reference"))).resolves.toEqual({ reused: true, assetId: "existing_asset" });
|
||||
for (const changed of [{ asset_metadata: { content_sha256: "b".repeat(64) } }, { mime_type: "image/png" }, { size_bytes: 10 }, { creative_factory_asset_id: "" }]) {
|
||||
reusedFile = { ...file, ...changed };
|
||||
await expect(remote.initialize(record, Buffer.from("reference"))).rejects.toBeDefined();
|
||||
}
|
||||
});
|
||||
|
||||
it("collects the registered job's storage_url MP4 and ignores JSON artifacts, rejecting foreign or conflicting receipts", async () => {
|
||||
vi.spyOn(Client.prototype, "connect").mockResolvedValue();
|
||||
vi.spyOn(Client.prototype, "listTools").mockResolvedValue({ tools: ["swads_whoami", "creative_upload_from_url", "creative_import_status", "video_h3_submit", "video_job_status"].map(name => ({ name, inputSchema: { type: "object", properties: { job_id: {} } } })) });
|
||||
const video = { artifact_id: "artifact_video", job_id: "job_1", org_id: "org", asset_id: null, version_id: null, artifact_kind: "video", content_type: "video/mp4", storage_url: "https://cdn.example/video.mp4", object_key: "video.mp4", json_payload: null, metadata: {}, created_at: "2026-09-15T00:00:00Z" };
|
||||
let result: Record<string, unknown> = { job_id: "job_1", status: "succeeded", artifacts: [video, { ...video, artifact_id: "artifact_json", artifact_kind: "metadata", content_type: "application/json", storage_url: null, object_key: null, json_payload: { provider: "h3" } }] };
|
||||
vi.spyOn(Client.prototype, "callTool").mockImplementation(request => Promise.resolve({ content: [], structuredContent: request.name === "swads_whoami" ? { access: { current_account: { tenant_id: "tenant", platform: "tiktok" } } } : result }));
|
||||
const remote = new ImageVideoMcpRemote({ token: "test" });
|
||||
const record = { jobId: "job_1" } as ImageVideoRecord;
|
||||
await expect(remote.status(record)).resolves.toEqual({ state: "succeeded", artifactUrl: "https://cdn.example/video.mp4" });
|
||||
result = { job_id: "job_1", status: "succeeded", artifacts: [{ ...video, job_id: "job_other" }] };
|
||||
await expect(remote.status(record)).rejects.toBeDefined();
|
||||
result = { job_id: "job_1", status: "succeeded", artifacts: [{ ...video, url: "https://cdn.example/other.mp4" }] };
|
||||
await expect(remote.status(record)).rejects.toBeDefined();
|
||||
});
|
||||
|
||||
it("submits within the gateway key budget and deduplicates the same frozen request without merging different requests", async () => {
|
||||
vi.spyOn(Client.prototype, "connect").mockResolvedValue();
|
||||
vi.spyOn(Client.prototype, "listTools").mockResolvedValue({ tools: ["swads_whoami", "creative_upload_from_url", "creative_import_status", "video_h3_submit", "video_job_status"].map(name => ({ name, inputSchema: { type: "object", properties: Object.fromEntries(["prompt", "title", "reference_asset_ids", "aspect_ratio", "duration_seconds", "resolution", "h3_mode", "idempotency_key", "skill_id"].map(key => [key, {}])) } })) });
|
||||
const jobs = new Map<string, string>();
|
||||
vi.spyOn(Client.prototype, "callTool").mockImplementation(request => {
|
||||
if (request.name === "swads_whoami") return Promise.resolve({ content: [], structuredContent: { access: { current_account: { tenant_id: "tenant", platform: "tiktok" } } } });
|
||||
const key = String(request.arguments?.idempotency_key);
|
||||
if (key.length > 87) return Promise.resolve({ content: [], structuredContent: { error_code: "invalid_arguments", detail: "idempotency_key must be at most 87 characters" } });
|
||||
if (!jobs.has(key)) jobs.set(key, `job_${jobs.size + 1}`);
|
||||
return Promise.resolve({ content: [], structuredContent: { job_id: jobs.get(key) } });
|
||||
});
|
||||
const remote = new ImageVideoMcpRemote({ token: "test" });
|
||||
const record = { id: "i2v_11111111-1111-1111-1111-111111111111", requestHash: "a".repeat(64), draft: { prompt: "waves", title: "test", durationSeconds: 10 }, assetId: "asset_1" } as ImageVideoRecord;
|
||||
await expect(remote.submit(record)).resolves.toEqual({ jobId: "job_1" });
|
||||
await expect(remote.submit(record)).resolves.toEqual({ jobId: "job_1" });
|
||||
await expect(remote.submit({ ...record, requestHash: "b".repeat(64) })).resolves.toEqual({ jobId: "job_2" });
|
||||
await expect(remote.submit({ ...record, id: "i2v_22222222-2222-2222-2222-222222222222" })).resolves.toEqual({ jobId: "job_3" });
|
||||
});
|
||||
|
||||
it("uploads with the frozen prompt and ratio required by the live begin/finish contract", async () => {
|
||||
vi.spyOn(Client.prototype, "connect").mockResolvedValue();
|
||||
const properties = Object.fromEntries(["tenant_id", "platform", "platforms", "media_kind", "review_status", "asset_role", "ratio", "prompt", "file_name", "content_type", "size_bytes", "content_sha256", "upload_id", "object_key"].map(key => [key, {}]));
|
||||
vi.spyOn(Client.prototype, "listTools").mockResolvedValue({ tools: ["swads_whoami", "creative_upload_from_url", "creative_import_status", "video_h3_submit", "video_job_status"].map(name => ({ name, inputSchema: { type: "object", properties } })) });
|
||||
vi.spyOn(Client.prototype, "callTool").mockImplementation(request => {
|
||||
const args = request.arguments;
|
||||
const result = request.name === "swads_whoami" ? { access: { current_account: { tenant_id: "tenant", platform: "tiktok" } } }
|
||||
: args?.ratio !== "9:16" || args.prompt !== "Subtle waves and breeze" ? { error_code: "invalid_arguments", detail: "push begin needs: ratio, prompt" }
|
||||
: args.file_name !== "i2v_test.jpeg" || args.content_type !== "image/jpeg" || args.size_bytes !== 9 ? { error_code: "invalid_arguments", detail: "push finish needs: file_name, content_type, size_bytes" }
|
||||
: args.upload_id ? { status: "finalizing" } : { upload_id: "upload_1", object_key: "image.jpeg", upload_url: "https://public.example/put?signature=private" };
|
||||
return Promise.resolve({ content: [{ type: "text", text: JSON.stringify(result) }] });
|
||||
});
|
||||
const remote = new ImageVideoMcpRemote({ token: "test" });
|
||||
const record = { id: "i2v_test", identity: { tenantId: "tenant", platform: "tiktok" }, draft: { prompt: "Subtle waves and breeze" }, referenceType: "image/jpeg", referenceHash: "a".repeat(64) } as ImageVideoRecord;
|
||||
const upload = await remote.initialize(record, Buffer.from("reference"));
|
||||
if ("reused" in upload) throw new Error("Expected a new upload ticket");
|
||||
expect(upload).toEqual({ uploadId: "upload_1", objectKey: "image.jpeg", uploadUrl: "https://public.example/put?signature=private" });
|
||||
await expect(remote.finalize({ ...record, upload }, Buffer.from("reference"))).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("recognizes error_code failures even when MCP isError is false without exposing remote details", () => {
|
||||
expect(() => imageVideoResponse({ isError: false, content: [{ type: "text", text: JSON.stringify({ error_code: "invalid_arguments", detail: "token=private https://private.example/?signature=secret" }) }] })).toThrow(expect.objectContaining({ code: "I2V_REMOTE_REJECTED", message: "SW Ads rejected the request (invalid_arguments)" }));
|
||||
});
|
||||
|
||||
it("retains an unparseable submission response privately before rejecting it, without a second submit", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "i2v-receipt-"));
|
||||
const id = "i2v_11111111-1111-1111-1111-111111111111";
|
||||
await mkdir(join(root, id), { mode: 0o700 });
|
||||
vi.spyOn(Client.prototype, "connect").mockResolvedValue();
|
||||
vi.spyOn(Client.prototype, "listTools").mockResolvedValue({ tools: ["swads_whoami", "creative_upload_from_url", "creative_import_status", "video_h3_submit", "video_job_status"].map(name => ({ name, inputSchema: { type: "object", properties: Object.fromEntries(["prompt", "title", "reference_asset_ids", "aspect_ratio", "duration_seconds", "resolution", "h3_mode", "idempotency_key", "skill_id"].map(key => [key, {}])) } })) });
|
||||
let submissions = 0;
|
||||
const raw = { content: [{ type: "text" as const, text: '{"unexpected_job":{"id":"recoverable_job"},"private_url":"https://public.example/?signature=secret"}' }] };
|
||||
vi.spyOn(Client.prototype, "callTool").mockImplementation(request => {
|
||||
if (request.name === "swads_whoami") return Promise.resolve({ structuredContent: { access: { current_account: { tenant_id: "tenant", platform: "tiktok" } } }, content: [] });
|
||||
submissions++; return Promise.resolve(raw);
|
||||
});
|
||||
const remote = new ImageVideoMcpRemote({ token: "private-token", receiptRoot: root });
|
||||
const record = { id, revision: 4, requestHash: "a".repeat(64), draft: { prompt: "waves", title: "test", durationSeconds: 10 }, assetId: "asset_1" } as ImageVideoRecord;
|
||||
await expect(remote.submit(record)).rejects.toBeDefined();
|
||||
const files = await readdir(join(root, id));
|
||||
expect(files).toHaveLength(1);
|
||||
const file = join(root, id, files[0]!);
|
||||
expect(JSON.parse(await readFile(file, "utf8"))).toMatchObject({ id, revision: 4, requestHash: "a".repeat(64), operation: "submit", response: raw });
|
||||
expect((await stat(file)).mode & 0o777).toBe(0o600);
|
||||
expect(await readFile(file, "utf8")).not.toContain("private-token");
|
||||
expect(submissions).toBe(1);
|
||||
});
|
||||
Reference in New Issue
Block a user