Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ node_modules
dist
*.log
.DS_Store
tests/e2e-output
packages/sdk/tests/e2e-output
.env
3 changes: 3 additions & 0 deletions packages/sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"dev:example": "vite dev --port 3000",
"test": "vitest unit",
"test:e2e": "vitest e2e",
"test:e2e:realtime": "vitest --config vitest.config.e2e-realtime.ts",
"typecheck": "tsc --noEmit",
"format": "biome format --write",
"format:check": "biome check",
Expand All @@ -44,9 +45,11 @@
"@biomejs/biome": "2.3.8",
"@types/bun": "^1.3.3",
"@types/node": "^22.15.17",
"@vitest/browser": "~3.2.0",
"bumpp": "^10.1.0",
"msw": "^2.11.3",
"pkg-pr-new": "^0.0.56",
"playwright": "^1.58.2",
"tsdown": "^0.14.1",
"typescript": "^5.8.3",
"vite": "^7.1.2",
Expand Down
4 changes: 2 additions & 2 deletions packages/sdk/src/queue/polling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,11 @@ export async function pollUntilComplete({

if (status.status === "completed") {
const data = await getContent();
return { status: "completed", data };
return { status: "completed", job_id: status.job_id, data };
}

if (status.status === "failed") {
return { status: "failed", error: "Job failed" };
return { status: "failed", job_id: status.job_id, error: "Job failed" };
}

// Still pending or processing, wait and poll again
Expand Down
4 changes: 3 additions & 1 deletion packages/sdk/src/queue/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ export type JobStatusResponse = {
/**
* Result from submitAndPoll - discriminated union for success/failure.
*/
export type QueueJobResult = { status: "completed"; data: Blob } | { status: "failed"; error: string };
export type QueueJobResult =
| { status: "completed"; job_id: string; data: Blob }
| { status: "failed"; job_id: string; error: string };

/**
* Queue-specific inputs extending ProcessInputs.
Expand Down
Binary file removed packages/sdk/tests/e2e-output/lucy-dev-i2v.mp4
Binary file not shown.
Binary file removed packages/sdk/tests/e2e-output/lucy-pro-i2i.png
Binary file not shown.
Binary file removed packages/sdk/tests/e2e-output/lucy-pro-i2v.mp4
Binary file not shown.
Binary file removed packages/sdk/tests/e2e-output/lucy-pro-t2i.png
Binary file not shown.
Binary file removed packages/sdk/tests/e2e-output/lucy-pro-t2v.mp4
Binary file not shown.
Binary file removed packages/sdk/tests/e2e-output/lucy-pro-v2v.mp4
Binary file not shown.
65 changes: 65 additions & 0 deletions packages/sdk/tests/e2e-realtime.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
declare const __DECART_API_KEY__: string;

import { createDecartClient, type DecartSDKError, models, type RealTimeModels } from "@decartai/sdk";
import { beforeAll, describe, expect, it } from "vitest";

function createSyntheticStream(fps: number, width: number, height: number): MediaStream {
const canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
return canvas.captureStream(fps);
}

const REALTIME_MODELS: RealTimeModels[] = ["mirage", "mirage_v2", "lucy_v2v_720p_rt", "lucy_2_rt"];

describe.concurrent("Realtime E2E Tests", { timeout: 30_000, retry: 2 }, () => {
let client: ReturnType<typeof createDecartClient>;

beforeAll(() => {
// Injected at build time via vitest.config.e2e-realtime.ts `define`
const apiKey = __DECART_API_KEY__;
if (!apiKey) {
throw new Error(
"DECART_API_KEY environment variable not set. Run with: DECART_API_KEY=your_key pnpm test:e2e:realtime",
);
}
client = createDecartClient({ apiKey });
});

for (const modelName of REALTIME_MODELS) {
it(modelName, async () => {
const model = models.realtime(modelName);
const stream = createSyntheticStream(model.fps, model.width, model.height);

let remoteStreamReceived = false;

const realtimeClient = await client.realtime.connect(stream, {
model,
onRemoteStream: () => {
remoteStreamReceived = true;
},
initialState: {
prompt: { text: "Anime style", enhance: false },
},
});

const errors: DecartSDKError[] = [];
realtimeClient.on("error", (err) => errors.push(err));

try {
expect(["connected", "generating"]).toContain(realtimeClient.getConnectionState());
expect(realtimeClient.sessionId).toBeTruthy();
expect(remoteStreamReceived).toBe(true);

await realtimeClient.setPrompt("Cyberpunk city");

expect(errors).toEqual([]);
} finally {
realtimeClient.disconnect();
for (const track of stream.getTracks()) track.stop();
}

expect(realtimeClient.getConnectionState()).toBe("disconnected");
});
}
});
93 changes: 60 additions & 33 deletions packages/sdk/tests/e2e.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { createDecartClient, models } from "@decartai/sdk";
import { createDecartClient, models, type QueueJobResult } from "@decartai/sdk";
import { beforeAll, describe, expect, it } from "vitest";

const __filename = fileURLToPath(import.meta.url);
Expand All @@ -11,7 +11,7 @@ const OUTPUT_DIR = join(__dirname, "e2e-output");
const VIDEO_FIXTURE = join(__dirname, "fixtures", "video.mp4");
const IMAGE_FIXTURE = join(__dirname, "fixtures", "image.png");

describe("E2E Tests", { timeout: 120_000 }, () => {
describe.concurrent("E2E Tests", { timeout: 120_000, retry: 2 }, () => {
let client: ReturnType<typeof createDecartClient>;
let videoBlob: Blob;
let imageBlob: Blob;
Expand Down Expand Up @@ -45,6 +45,24 @@ describe("E2E Tests", { timeout: 120_000 }, () => {
return outputPath;
}

async function expectResult(result: Blob | QueueJobResult, modelName: string, ext: string): Promise<void> {
let blob: Blob;
if (result instanceof Blob) {
blob = result;
} else if (result.status === "failed") {
throw new Error(`${modelName} job failed. job_id: ${result.job_id}`);
} else {
blob = result.data;
}

expect(blob).toBeInstanceOf(Blob);
if (blob.size === 0) {
throw new Error(`${modelName} returned empty blob`);
}
const path = await saveOutput(blob, modelName, ext);
console.log(`Saved to: ${path}`);
}

describe("Process API - Image Models", () => {
it("lucy-pro-t2i: text-to-image", async () => {
const result = await client.process({
Expand All @@ -54,9 +72,7 @@ describe("E2E Tests", { timeout: 120_000 }, () => {
orientation: "landscape",
});

expect(result).toBeInstanceOf(Blob);
const path = await saveOutput(result, "lucy-pro-t2i", ".png");
console.log(`Saved to: ${path}`);
await expectResult(result, "lucy-pro-t2i", ".png");
});

it("lucy-pro-i2i: image-to-image", async () => {
Expand All @@ -68,9 +84,7 @@ describe("E2E Tests", { timeout: 120_000 }, () => {
enhance_prompt: false,
});

expect(result).toBeInstanceOf(Blob);
const path = await saveOutput(result, "lucy-pro-i2i", ".png");
console.log(`Saved to: ${path}`);
await expectResult(result, "lucy-pro-i2i", ".png");
});
});

Expand All @@ -84,11 +98,7 @@ describe("E2E Tests", { timeout: 120_000 }, () => {
orientation: "landscape",
});

expect(result.status).toBe("completed");
if (result.status === "completed") {
const path = await saveOutput(result.data, "lucy-pro-t2v", ".mp4");
console.log(`Saved to: ${path}`);
}
await expectResult(result, "lucy-pro-t2v", ".mp4");
});

it("lucy-dev-i2v: image-to-video (dev)", async () => {
Expand All @@ -100,11 +110,7 @@ describe("E2E Tests", { timeout: 120_000 }, () => {
resolution: "720p",
});

expect(result.status).toBe("completed");
if (result.status === "completed") {
const path = await saveOutput(result.data, "lucy-dev-i2v", ".mp4");
console.log(`Saved to: ${path}`);
}
await expectResult(result, "lucy-dev-i2v", ".mp4");
});

it("lucy-pro-i2v: image-to-video (pro)", async () => {
Expand All @@ -116,11 +122,7 @@ describe("E2E Tests", { timeout: 120_000 }, () => {
resolution: "720p",
});

expect(result.status).toBe("completed");
if (result.status === "completed") {
const path = await saveOutput(result.data, "lucy-pro-i2v", ".mp4");
console.log(`Saved to: ${path}`);
}
await expectResult(result, "lucy-pro-i2v", ".mp4");
});

it("lucy-pro-v2v: video-to-video", async () => {
Expand All @@ -132,11 +134,40 @@ describe("E2E Tests", { timeout: 120_000 }, () => {
enhance_prompt: true,
});

expect(result.status).toBe("completed");
if (result.status === "completed") {
const path = await saveOutput(result.data, "lucy-pro-v2v", ".mp4");
console.log(`Saved to: ${path}`);
}
await expectResult(result, "lucy-pro-v2v", ".mp4");
});

it("lucy-fast-v2v: video-to-video (fast)", async () => {
const result = await client.queue.submitAndPoll({
model: models.video("lucy-fast-v2v"),
prompt: "Watercolor painting style",
data: videoBlob,
seed: 888,
});

await expectResult(result, "lucy-fast-v2v", ".mp4");
});

it("lucy-restyle-v2v: video restyling (prompt)", async () => {
const result = await client.queue.submitAndPoll({
model: models.video("lucy-restyle-v2v"),
prompt: "Cyberpunk neon city style",
data: videoBlob,
seed: 777,
});

await expectResult(result, "lucy-restyle-v2v-prompt", ".mp4");
});

it("lucy-restyle-v2v: video restyling (reference_image)", async () => {
const result = await client.queue.submitAndPoll({
model: models.video("lucy-restyle-v2v"),
reference_image: imageBlob,
data: videoBlob,
seed: 777,
});

await expectResult(result, "lucy-restyle-v2v-reference_image", ".mp4");
});

it.skip("lucy-pro-flf2v: first-last-frame-to-video", async () => {
Expand All @@ -149,11 +180,7 @@ describe("E2E Tests", { timeout: 120_000 }, () => {
resolution: "720p",
});

expect(result.status).toBe("completed");
if (result.status === "completed") {
const path = await saveOutput(result.data, "lucy-pro-flf2v", ".mp4");
console.log(`Saved to: ${path}`);
}
await expectResult(result, "lucy-pro-flf2v", ".mp4");
});
});
});
16 changes: 16 additions & 0 deletions packages/sdk/vitest.config.e2e-realtime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { defineConfig } from "vitest/config";

export default defineConfig({
define: {
__DECART_API_KEY__: JSON.stringify(process.env.DECART_API_KEY),
},
test: {
include: ["tests/e2e-realtime.test.ts"],
browser: {
enabled: true,
provider: "playwright",
headless: true,
instances: [{ browser: "chromium" }],
},
},
});
6 changes: 5 additions & 1 deletion packages/sdk/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import { defineConfig } from "vitest/config";

export default defineConfig({});
export default defineConfig({
test: {
exclude: ["tests/e2e-realtime.test.ts"],
},
});
Loading
Loading