fix: resume transient original-job collection and guard probe input

This commit is contained in:
Wenxin Duan
2026-09-15 01:29:13 +08:00
parent cc80265bb2
commit 2ad089d68a
7 changed files with 133 additions and 16 deletions
@@ -17,6 +17,10 @@ describe("image video remote boundary", () => {
for (const url of ["http://cdn.example/v", "https://user:pass@cdn.example/v", "https://127.0.0.1/v", "https://[::ffff:127.0.0.1]/v"]) await expect(harness.validateImageVideoUrl(url)).rejects.toBeDefined();
await expect(harness.validateImageVideoUrl("https://cdn.example/v", () => Promise.resolve([{ address: "192.168.1.2", family: 4 }]))).rejects.toBeDefined();
});
it("classifies a temporary DNS lookup failure as retryable without allowing private results", async () => {
await expect(harness.validateImageVideoUrl("https://cdn.example/video", () => Promise.reject(new Error("EAI_AGAIN")))).rejects.toMatchObject({ code: "I2V_TRANSFER_TRANSIENT" });
await expect(harness.validateImageVideoUrl("https://cdn.example/video", () => Promise.resolve([{ address: "127.0.0.1", family: 4 }]))).rejects.toMatchObject({ code: "BASE_URL_NOT_ALLOWED" });
});
it("extracts only the documented response envelope and rejects malformed or error responses", () => {
expect(harness).toHaveProperty("imageVideoResponse");
expect(harness.imageVideoResponse({ content: [{ type: "text", text: '{"job_id":"j1"}' }] })).toEqual({ job_id: "j1" });
@@ -0,0 +1,14 @@
import { spawnSync } from "node:child_process";
import { resolve } from "node:path";
import { expect, it } from "vitest";
it("rejects a TTY before reading credentials and directs the operator to a controlled pipe", () => {
const script = resolve(import.meta.dirname, "../../../scripts/probe-image-video-mcp.mjs");
// Instrument only the process boundary; do not provide a token or allow a network call.
const source = `Object.defineProperty(process.stdin,'isTTY',{value:true}); const fakeRead=()=>{throw new Error('UNSAFE_STDIN_READ');}; process.stdin.on=fakeRead; await import(${JSON.stringify(script)});`;
const result = spawnSync(process.execPath, ["--input-type=module", "-e", source], { encoding: "utf8", timeout: 5000 });
expect(result.status).toBe(1);
expect(result.stderr).toContain("controlled pipe");
expect(result.stderr).not.toContain("UNSAFE_STDIN_READ");
expect(result.stdout).not.toContain("hidden stdin");
});
@@ -30,4 +30,12 @@ describe("bounded pinned HTTPS media transfer", () => {
response.status = 200; response.type = "text/html"; await expect(imageVideoTransfer("https://cdn.example/video")).rejects.toThrow();
response.type = "video/mp4"; await expect(imageVideoTransfer("https://cdn.example/video", { maxBytes: 2 })).rejects.toThrow();
});
it("marks temporary HTTP download failures retryable but redirect and content failures permanent", async () => {
response.status = 503; response.type = "video/mp4";
await expect(imageVideoTransfer("https://cdn.example/video")).rejects.toMatchObject({ code: "I2V_TRANSFER_TRANSIENT" });
response.status = 302;
await expect(imageVideoTransfer("https://cdn.example/video")).rejects.toMatchObject({ code: "I2V_TRANSFER_REJECTED" });
response.status = 200; response.type = "text/html";
await expect(imageVideoTransfer("https://cdn.example/video")).rejects.toMatchObject({ code: "I2V_TRANSFER_REJECTED" });
});
});
+39 -1
View File
@@ -3,7 +3,7 @@ import { tmpdir } from "node:os";
import { join } from "node:path";
import { Readable } from "node:stream";
import sharp from "sharp";
import { describe, expect, it } from "vitest";
import { describe, expect, it, vi } from "vitest";
import * as harness from "../src/index.js";
import type { ImageVideoRemote, ImageVideoRecord } from "../src/image-video/contracts.js";
@@ -111,6 +111,44 @@ describe("durable image video tasks", () => {
await new harness.ImageVideoService(f.options).submit("ses_owner", task.id);
expect(f.calls).toEqual(["status:accepted_before_crash"]);
});
it("retries a transient download after restart using the original job's current artifact, at the polling cadence", async () => {
const f = await fixture(); const task = await f.service.create("ses_owner", f.draft);
await f.service.upload("ses_owner", task.id); await f.service.refresh(task.id); await f.service.submit("ses_owner", task.id);
const clock = vi.spyOn(Date, "now").mockReturnValue(2_000_000_000_000);
const downloaded: string[] = []; let queries = 0;
f.remote.status = r => { f.calls.push(`status:${r.jobId ?? "missing"}`); queries++; return Promise.resolve({ state: "succeeded", artifactUrl: `https://public.example/${String(queries)}.mp4` }); };
const collect = (url: string) => {
downloaded.push(url);
if (downloaded.length === 1) return Promise.reject(new harness.HarnessError("I2V_TRANSFER_TRANSIENT", "Temporary download timeout"));
return Promise.resolve({ videoSeconds: 10, audioSeconds: 10, width: 720, height: 1280, fps: 24, decoded: true as const, sha256: "a".repeat(64), visualReview: "pending" as const, audioReview: "pending" as const });
};
try {
const initial = new harness.ImageVideoService({ ...f.options, collect });
expect(await initial.refresh(task.id)).toMatchObject({ state: "validating", jobId: "remote_job_1" });
const restored = new harness.ImageVideoService({ ...f.options, collect });
await restored.refresh(task.id); expect(downloaded).toHaveLength(1);
clock.mockReturnValue(2_000_000_029_999); await restored.refresh(task.id); expect(downloaded).toHaveLength(1);
clock.mockReturnValue(2_000_000_030_000);
expect(await restored.refresh(task.id)).toMatchObject({ state: "ready", jobId: "remote_job_1" });
await restored.submit("ses_owner", task.id);
expect(downloaded).toEqual(["https://public.example/1.mp4", "https://public.example/2.mp4"]);
expect(f.calls.filter(call => call.startsWith("status:"))).toEqual(["status:remote_job_1", "status:remote_job_1"]);
expect(f.calls.filter(call => call === "submit")).toHaveLength(1);
} finally { clock.mockRestore(); }
});
it("keeps deterministic invalid media terminal across restart with no false ready or second generation", async () => {
const f = await fixture(); const task = await f.service.create("ses_owner", f.draft);
await f.service.upload("ses_owner", task.id); await f.service.refresh(task.id); await f.service.submit("ses_owner", task.id);
f.remote.status = () => Promise.resolve({ state: "succeeded", artifactUrl: "https://public.example/invalid.mp4" });
let collections = 0;
const collect = () => { collections++; return Promise.reject(new harness.HarnessError("I2V_INVALID_MP4", "Invalid MP4")); };
expect((await new harness.ImageVideoService({ ...f.options, collect }).refresh(task.id)).state).toBe("failed");
const restored = new harness.ImageVideoService({ ...f.options, collect });
expect(await restored.refresh(task.id)).toMatchObject({ state: "failed", jobId: "remote_job_1" });
await restored.submit("ses_owner", task.id);
expect(collections).toBe(1); expect(f.calls.filter(call => call === "submit")).toHaveLength(1);
await expect(restored.media(task.id, "video")).rejects.toMatchObject({ code: "I2V_MEDIA_NOT_FOUND" });
});
it.each(["task", "hash", "stale", "future", "malformed", "empty_job", "version", "extra_field", "conflicting_job"] as const)("quarantines a %s receipt without polling it or submitting again", async (kind) => {
const f = await fixture(); const task = await f.service.create("ses_owner", f.draft);
await f.service.upload("ses_owner", task.id); await f.service.refresh(task.id);