feat(#1714): 前端 prepare 短路(后端 skip_transfer 命中时跳过 OSS 直传) (#1729)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 3s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
CI/CD Pipeline / Check push changed paths (push) Successful in 9s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 26s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 30s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 28s
CI/CD Pipeline / Build Staging API Image (push) Successful in 41s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 39s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 38s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 4s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 1m54s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m16s
CI/CD Pipeline / Integration Tests (push) Successful in 2m28s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m16s
CI/CD Pipeline / Validate - Style (push) Successful in 2m51s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m10s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m31s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m32s
CI/CD Pipeline / Validate - Security (push) Successful in 5m18s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m41s
AI Code Review / AI Code Review (pull_request) Successful in 6m20s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m32s
CI/CD Pipeline / Unit Tests (push) Successful in 8m24s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Failing after 243h31m40s
CI/CD Pipeline / CI Gate (push) Failing after 243h31m45s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 243h31m45s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 243h37m0s
CI/CD Pipeline / Build Production Web Image (push) Failing after 243h31m45s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 243h38m36s
CI/CD Pipeline / Build Production API Image (push) Failing after 243h31m45s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 243h38m46s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 243h38m47s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 243h38m49s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 243h38m48s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 243h38m51s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 243h38m52s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 243h38m55s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 243h38m57s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 243h39m39s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 243h39m40s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 243h40m7s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 243h40m9s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 243h40m8s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 243h40m9s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 243h40m11s
CI/CD Pipeline / Frontend Lint (push) Failing after 243h40m11s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 243h40m12s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 243h40m13s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 243h40m13s
CI/CD Pipeline / PR Build API Image (push) Failing after 243h40m14s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 243h40m15s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 243h40m17s
CI/CD Pipeline / Deploy Production (push) Failing after 244h6m15s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 244h13m21s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 244h13m25s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 244h13m28s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 244h14m14s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 244h14m43s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 244h14m45s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 244h14m47s
CI/CD Pipeline / PR Build Web Image (push) Failing after 244h14m49s

Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
This commit was merged in pull request #1729.
This commit is contained in:
2026-09-06 11:43:59 +08:00
committed by auto-approve-bot
parent 06b0bacce1
commit ff60fdf956
5 changed files with 170 additions and 0 deletions
+106
View File
@@ -259,6 +259,112 @@ describe("assets API", () => {
})
})
describe("uploadAssetDirect skip_transfer 短路", () => {
it("prepare 返回 skip_transfer=true → 直接返回 duplicated,不调 transfer/complete", async () => {
mockPost.mockImplementation((url: string) => {
if (url === "/upload/direct/prepare") {
return Promise.resolve({
data: {
upload_url: "https://oss/x",
method: "POST",
storage_key: "uploads/skip/y.mp4",
expires_at: "2099",
fields: {},
max_size_bytes: 1e9,
asset_id: "existing-asset",
skip_transfer: true,
duplicated: true,
},
})
}
if (url === "/upload/direct/complete") {
throw new Error("complete 不应被调用")
}
throw new Error("unexpected url " + url)
})
const putSpy = vi.spyOn(globalThis, "XMLHttpRequest")
const file = new File(["x"], "x.mp4", { type: "video/mp4" })
const result = await uploadAssetDirect({ file, library_id: "lib-1" })
expect(result.duplicated).toBe(true)
expect(result.asset_id).toBe("existing-asset")
// complete 未被调用(mockPost 只记录 prepare,complete 若调用会抛 "不应被调用")
const completeCalls = mockPost.mock.calls.filter(
([u]: [string]) => u === "/upload/direct/complete",
)
expect(completeCalls).toHaveLength(0)
putSpy.mockRestore()
})
it("prepare 返回 skip_transfer=false → 走老流程(complete 被调用)", async () => {
mockPost.mockImplementation((url: string) => {
if (url === "/upload/direct/prepare") {
return Promise.resolve({
data: {
upload_url: "https://oss/x",
method: "POST",
storage_key: "uploads/normal/y.mp4",
expires_at: "2099",
fields: {},
max_size_bytes: 1e9,
asset_id: "new-asset",
},
})
}
if (url === "/upload/direct/complete") {
return Promise.resolve({
data: {
storage_key: "uploads/normal/y.mp4",
ingest_job_id: "job-1",
url: "https://oss/y.mp4",
duplicated: false,
asset_id: "new-asset",
},
})
}
throw new Error("unexpected url " + url)
})
// mock XMLHttpRequest:send 之后下一 tick 触发 onload 让 transfer 立即成功
const origOpen = XMLHttpRequest.prototype.open
const origSend = XMLHttpRequest.prototype.send
const origSetReadyState = Object.getOwnPropertyDescriptor(
XMLHttpRequest.prototype,
"readyState",
) as PropertyDescriptor | undefined
const origStatus = Object.getOwnPropertyDescriptor(XMLHttpRequest.prototype, "status")
Object.defineProperty(XMLHttpRequest.prototype, "readyState", {
configurable: true,
writable: true,
value: 4,
})
Object.defineProperty(XMLHttpRequest.prototype, "status", {
configurable: true,
writable: true,
value: 200,
})
XMLHttpRequest.prototype.open = vi.fn() as unknown as typeof origOpen
XMLHttpRequest.prototype.send = vi.fn(function (this: XMLHttpRequest) {
// 下一 tick 触发 onload(模拟 XHR 异步完成)
setTimeout(() => this.onload?.(new ProgressEvent("load")), 0)
}) as unknown as typeof origSend
const file = new File(["x"], "x.mp4", { type: "video/mp4" })
const result = await uploadAssetDirect({ file, library_id: "lib-1" })
expect(result.duplicated).toBeFalsy()
expect(result.asset_id).toBe("new-asset")
const completeCalls = mockPost.mock.calls.filter(
([u]: [string]) => u === "/upload/direct/complete",
)
expect(completeCalls).toHaveLength(1)
XMLHttpRequest.prototype.open = origOpen
XMLHttpRequest.prototype.send = origSend
if (origSetReadyState) {
Object.defineProperty(XMLHttpRequest.prototype, "readyState", origSetReadyState)
}
if (origStatus) {
Object.defineProperty(XMLHttpRequest.prototype, "status", origStatus)
}
})
})
describe("getIngestJob", () => {
it("should resolve successfully", async () => {
await expect(getIngestJob("test-jobId")).resolves.not.toThrow()