cdcb032e45
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 3s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 27s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 28s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 2m28s
CI/CD Pipeline / CI Gate (pull_request) Successful in 3s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m39s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m12s
AI Code Review / AI Code Review (pull_request) Failing after 6m15s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 0s
CI/CD Pipeline / Check push changed paths (push) Successful in 1s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 21s
CI/CD Pipeline / Build Staging API Image (push) Successful in 21s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 22s
CI/CD Pipeline / Integration Tests (push) Successful in 1m44s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m30s
CI/CD Pipeline / Validate - Style (push) Successful in 2m20s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 3m19s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m14s
CI/CD Pipeline / Unit Tests (push) Successful in 8m4s
CI/CD Pipeline / Validate - Security (push) Successful in 16m53s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Failing after 255h51m59s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 255h52m3s
CI/CD Pipeline / Build Production Web Image (push) Failing after 255h52m3s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 256h8m32s
CI/CD Pipeline / Deploy Production (push) Failing after 255h51m59s
CI/CD Pipeline / Frontend Lint (push) Failing after 256h8m58s
CI/CD Pipeline / Build Production API Image (push) Failing after 255h52m3s
CI/CD Pipeline / Staging API Integration Tests (push) Failing after 256h6m56s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 256h6m56s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 256h27m48s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 256h29m25s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 256h29m30s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 256h29m31s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 256h29m25s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 256h29m34s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 256h29m35s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 256h29m36s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 256h29m35s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 256h29m37s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 256h31m30s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 256h31m30s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 256h31m30s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 256h31m33s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 256h31m34s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 256h31m34s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 256h31m35s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 256h8m58s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 256h31m35s
CI/CD Pipeline / PR Build API Image (push) Failing after 256h8m58s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 256h31m37s
CI/CD Pipeline / CI Gate (push) Failing after 256h26m38s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 256h43m7s
CI/CD Pipeline / PR Build Web Image (push) Failing after 256h43m32s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 256h43m36s
CI/CD Pipeline / ACR Image Cleanup (push) Failing after 256h41m30s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 256h43m6s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 257h4m4s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 257h4m8s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 257h4m10s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 257h6m5s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 257h6m8s
131 lines
5.4 KiB
TypeScript
131 lines
5.4 KiB
TypeScript
/**
|
||
* 上传去重/幂等工具单测(Issue #1714)
|
||
*/
|
||
import { describe, it, expect, vi } from "vitest"
|
||
import {
|
||
computeFileHash,
|
||
findDuplicateInQueue,
|
||
HASH_FULL_READ_LIMIT,
|
||
HASH_SAMPLE_CHUNK,
|
||
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 大文件抽样(>64MB)", () => {
|
||
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)
|
||
})
|
||
|
||
it("≤64MB 走全量读取(slice 一次覆盖整个文件)", async () => {
|
||
const f = new File([new Uint8Array(1024).fill(9)], "full.mp4", { type: "video/mp4" })
|
||
Object.defineProperty(f, "size", { value: HASH_FULL_READ_LIMIT, configurable: true })
|
||
const sliceSpy = vi.spyOn(f, "slice")
|
||
await computeFileHash(f)
|
||
// 全量路径:唯一一次 slice 为 (0, size)
|
||
expect(sliceSpy).toHaveBeenCalledTimes(1)
|
||
expect(sliceSpy).toHaveBeenCalledWith(0, HASH_FULL_READ_LIMIT)
|
||
sliceSpy.mockRestore()
|
||
})
|
||
|
||
it(">64MB 只读取头尾各 16MB 抽样,绝不整文件读入内存", async () => {
|
||
const f = new File([new Uint8Array(1024).fill(9)], "big.mp4", { type: "video/mp4" })
|
||
Object.defineProperty(f, "size", { value: HASH_FULL_READ_LIMIT + 1, configurable: true })
|
||
const sliceSpy = vi.spyOn(f, "slice")
|
||
await computeFileHash(f)
|
||
// 抽样路径:两次 slice —— 头部 (0, 16MB) 与尾部 (size-16MB, size)
|
||
expect(sliceSpy).toHaveBeenCalledTimes(2)
|
||
expect(sliceSpy).toHaveBeenNthCalledWith(1, 0, HASH_SAMPLE_CHUNK)
|
||
expect(sliceSpy).toHaveBeenNthCalledWith(
|
||
2,
|
||
HASH_FULL_READ_LIMIT + 1 - HASH_SAMPLE_CHUNK,
|
||
HASH_FULL_READ_LIMIT + 1,
|
||
)
|
||
sliceSpy.mockRestore()
|
||
})
|
||
})
|