mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-15 17:18:38 +00:00
* feat(artifacts): download run files as zip * fix(artifacts): address archive review feedback * fix(artifacts): gate unavailable archive downloads * fix(artifacts): verify archive availability * fix(artifacts): harden archive consistency * fix(artifacts): reject archive path aliases
111 lines
3.3 KiB
TypeScript
111 lines
3.3 KiB
TypeScript
import { afterEach, describe, expect, it, rs } from "@rstest/core";
|
|
|
|
import {
|
|
downloadArtifactArchive,
|
|
getArtifactArchiveManifest,
|
|
updateArtifactContent,
|
|
} from "@/core/artifacts/api";
|
|
|
|
afterEach(() => {
|
|
rs.restoreAllMocks();
|
|
});
|
|
|
|
describe("updateArtifactContent", () => {
|
|
it("sends the draft and expected revision to the opened artifact URL", async () => {
|
|
const fetchMock = rs.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
new Response(
|
|
JSON.stringify({
|
|
path: "/mnt/user-data/outputs/report.md",
|
|
sha256: "b".repeat(64),
|
|
size: 7,
|
|
}),
|
|
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
),
|
|
);
|
|
|
|
await updateArtifactContent({
|
|
threadId: "thread-1",
|
|
filepath: "/mnt/user-data/outputs/report.md",
|
|
content: "updated",
|
|
expectedSha256: "a".repeat(64),
|
|
});
|
|
|
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
const [url, init] = fetchMock.mock.calls[0]!;
|
|
expect(typeof url).toBe("string");
|
|
expect(url as string).toContain(
|
|
"/api/threads/thread-1/artifacts/mnt/user-data/outputs/report.md",
|
|
);
|
|
expect(init?.method).toBe("PUT");
|
|
expect(typeof init?.body).toBe("string");
|
|
expect(JSON.parse(init?.body as string)).toEqual({
|
|
content: "updated",
|
|
expected_sha256: "a".repeat(64),
|
|
});
|
|
});
|
|
|
|
it("preserves the response status for conflict handling", async () => {
|
|
rs.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
new Response(JSON.stringify({ detail: "Artifact changed" }), {
|
|
status: 412,
|
|
headers: { "Content-Type": "application/json" },
|
|
}),
|
|
);
|
|
|
|
await expect(
|
|
updateArtifactContent({
|
|
threadId: "thread-1",
|
|
filepath: "/mnt/user-data/outputs/report.md",
|
|
content: "updated",
|
|
expectedSha256: "a".repeat(64),
|
|
}),
|
|
).rejects.toMatchObject({ status: 412 });
|
|
});
|
|
});
|
|
|
|
describe("downloadArtifactArchive", () => {
|
|
it("posts to the encoded run endpoint and preserves the server filename", async () => {
|
|
const blob = new Blob(["zip"]);
|
|
const fetchMock = rs.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
new Response(blob, {
|
|
headers: {
|
|
"Content-Disposition": 'attachment; filename="artifacts-run-1.zip"',
|
|
},
|
|
}),
|
|
);
|
|
|
|
const result = await downloadArtifactArchive({
|
|
threadId: "thread #1",
|
|
runId: "run/1",
|
|
});
|
|
|
|
expect(fetchMock).toHaveBeenCalledWith(
|
|
"/api/threads/thread%20%231/runs/run%2F1/artifacts/archive",
|
|
expect.objectContaining({ method: "POST", credentials: "include" }),
|
|
);
|
|
expect(result.filename).toBe("artifacts-run-1.zip");
|
|
expect(await result.blob.text()).toBe("zip");
|
|
});
|
|
});
|
|
|
|
describe("getArtifactArchiveManifest", () => {
|
|
it("reads the verified delivery count from the encoded run endpoint", async () => {
|
|
const fetchMock = rs.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
new Response(JSON.stringify({ file_count: 3 }), {
|
|
headers: { "Content-Type": "application/json" },
|
|
}),
|
|
);
|
|
|
|
const result = await getArtifactArchiveManifest({
|
|
threadId: "thread #1",
|
|
runId: "run/1",
|
|
});
|
|
|
|
expect(fetchMock).toHaveBeenCalledWith(
|
|
"/api/threads/thread%20%231/runs/run%2F1/artifacts/archive",
|
|
expect.objectContaining({ credentials: "include" }),
|
|
);
|
|
expect(result).toEqual({ fileCount: 3 });
|
|
});
|
|
});
|