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
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com> Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
102 lines
4.1 KiB
TypeScript
102 lines
4.1 KiB
TypeScript
/**
|
||
* 上传去重/幂等工具单测(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)
|
||
})
|
||
})
|