Files
xiaoxia-saas/apps/web/src/test/api/uploadDedup.test.ts
T
xiaoxia 6521be5426
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 3s
CI/CD Pipeline / Check push changed paths (push) Successful in 6s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 31s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 32s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 20s
CI/CD Pipeline / Build Staging API Image (push) Successful in 45s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 44s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 42s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m51s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 1m58s
CI/CD Pipeline / Integration Tests (push) Successful in 2m13s
CI/CD Pipeline / Validate - Style (push) Successful in 2m45s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m4s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m17s
AI Code Review / AI Code Review (pull_request) Failing after 4m7s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m35s
CI/CD Pipeline / Validate - Security (push) Successful in 5m22s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m20s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m5s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 5m5s
CI/CD Pipeline / Unit Tests (push) Successful in 8m14s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Failing after 12m54s
CI/CD Pipeline / Canary Release to Production (push) Failing after 262h35m59s
CI/CD Pipeline / Build Production Web Image (push) Failing after 262h36m3s
CI/CD Pipeline / Deploy Production (push) Failing after 262h35m59s
CI/CD Pipeline / Build Production API Image (push) Failing after 262h36m3s
CI/CD Pipeline / CI Gate (push) Failing after 262h36m3s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 262h41m3s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 262h42m18s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 262h42m21s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 262h42m24s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 262h42m34s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 262h42m35s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 262h42m35s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 262h42m35s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 262h42m36s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 262h42m47s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 262h42m49s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 262h42m49s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 262h42m51s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 262h44m11s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 262h44m10s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 262h44m11s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 262h44m12s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 262h44m13s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 262h44m14s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 262h44m16s
CI/CD Pipeline / Frontend Lint (push) Failing after 262h44m17s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 262h44m22s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 262h44m23s
CI/CD Pipeline / PR Build API Image (push) Failing after 262h44m23s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 262h44m24s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 262h44m24s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 263h10m36s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 263h16m56s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 263h17m23s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 263h17m24s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 263h18m46s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 263h18m49s
CI/CD Pipeline / PR Build Web Image (push) Failing after 263h18m57s
fix(#1714): 素材上传去重防重 + complete幂等传递 + 轮询收敛(前端P0) (#1717)
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-05 16:40:05 +08:00

102 lines
4.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 上传去重/幂等工具单测(Issue #1714)
*/
import { describe, it, expect } from "vitest"
import {
computeFileHash,
findDuplicateInQueue,
makeClientUploadId,
makeFileFingerprint,
} from "@/api/assets/uploadDedup"
const makeFile = (name: string, size = 100, lastModified = 1_700_000_000_000) =>
new File([new Uint8Array(size)], name, { type: "video/mp4", lastModified })
describe("makeFileFingerprint", () => {
it("同一文件(name+size+lastModified 相同)指纹一致", () => {
const a = makeFile("a.mp4", 1000, 12345)
const b = makeFile("a.mp4", 1000, 12345)
expect(makeFileFingerprint(a)).toBe(makeFileFingerprint(b))
})
it("文件名/大小/修改时间任一不同指纹即不同", () => {
const base = makeFile("a.mp4", 1000, 100)
expect(makeFileFingerprint(base)).not.toBe(makeFileFingerprint(makeFile("b.mp4", 1000, 100)))
expect(makeFileFingerprint(base)).not.toBe(makeFileFingerprint(makeFile("a.mp4", 1001, 100)))
expect(makeFileFingerprint(base)).not.toBe(makeFileFingerprint(makeFile("a.mp4", 1000, 101)))
})
})
describe("findDuplicateInQueue", () => {
const queue = [
{ fileKey: "k1", status: "preparing" },
{ fileKey: "k2", status: "uploading" },
{ fileKey: "k3", status: "ingesting" },
{ fileKey: "k4", status: "done" },
{ fileKey: "k5", status: "error" },
]
it("在途状态(preparing/uploading/ingesting/done)命中重复", () => {
expect(findDuplicateInQueue(queue, "k1")?.status).toBe("preparing")
expect(findDuplicateInQueue(queue, "k2")?.status).toBe("uploading")
expect(findDuplicateInQueue(queue, "k3")?.status).toBe("ingesting")
expect(findDuplicateInQueue(queue, "k4")?.status).toBe("done")
})
it("未命中返回 null", () => {
expect(findDuplicateInQueue(queue, "missing")).toBeNull()
})
it("排除 error 状态后,失败项不算重复(允许重新激活)", () => {
expect(findDuplicateInQueue(queue, "k5", ["error"])).toBeNull()
})
it("同时排除 done 后,已完成项也不算重复", () => {
expect(findDuplicateInQueue(queue, "k4", ["error", "done"])).toBeNull()
// 但在途的仍然命中
expect(findDuplicateInQueue(queue, "k1", ["error", "done"])).not.toBeNull()
})
})
describe("makeClientUploadId", () => {
it("生成带前缀且互不相同的幂等 token", () => {
const ids = new Set(Array.from({ length: 20 }, () => makeClientUploadId()))
expect(ids.size).toBe(20)
for (const id of ids) expect(id.startsWith("up_")).toBe(true)
})
})
describe("computeFileHash", () => {
it("相同内容 hash 一致、不同内容 hash 不同", async () => {
const f1 = makeFile("a.mp4", 4096)
const f2 = makeFile("b.mp4", 4096)
// 两个文件都是 0 填充,内容相同 → hash 一致
expect(await computeFileHash(f1)).toBe(await computeFileHash(f2))
const f3 = new File([new Uint8Array(4096).fill(7)], "c.mp4", { type: "video/mp4" })
expect(await computeFileHash(f1)).not.toBe(await computeFileHash(f3))
})
it("返回 64 位十六进制(SHA-256,与后端 file_hash 长度一致)", async () => {
const hash = await computeFileHash(makeFile("a.mp4", 1024))
expect(hash).toMatch(/^[0-9a-f]{64}$/)
})
})
describe("computeFileHash 大文件抽样(>256MB)", () => {
it("抽样路径正常返回 64 位 hex,且大小不同则 hash 不同", async () => {
// mock 一个「声称」300MB 的 File:slice 返回小 buffer 即可,不真分配 300MB
const makeBig = (declaredSize: number, head: number) => {
const f = new File([new Uint8Array([head, 2, 3])], "big.mov", { type: "video/quicktime" })
Object.defineProperty(f, "size", { value: declaredSize, configurable: true })
// slice 仍按真实内容返回小片段(头尾片段内容由底层小 buffer 决定)
return f
}
const h1 = await computeFileHash(makeBig(300 * 1024 * 1024, 1))
const h2 = await computeFileHash(makeBig(301 * 1024 * 1024, 1))
expect(h1).toMatch(/^[0-9a-f]{64}$/)
// 声明大小不同 → 写入的 64 位 size 字段不同 → hash 必须不同(锁定 setBigUint64 路径)
expect(h1).not.toBe(h2)
})
})