import { describe, expect, it } from "vitest"; import { applyObservation, DownloadTransitionError, detectNoProgress, isDownloadComplete, mapQbState, type ProgressSample, } from "../src/index.ts"; const ctx = { pausedByThreshold: false }; describe("qB 状态映射", () => { it.each([ ["downloading", 0.5, "downloading"], ["stalledDL", 0.5, "downloading"], ["metaDL", 0, "downloading"], ["forcedDL", 0.1, "downloading"], ["queuedDL", 0, "queued"], ["checkingResumeData", 0.4, "queued"], ["uploading", 1, "completed"], ["stalledUP", 1, "completed"], ["pausedUP", 1, "completed"], ["stoppedUP", 1, "completed"], ["queuedUP", 1, "completed"], ["checkingUP", 1, "downloading"], ["uploading", 0.9, "downloading"], ["error", 1, "error"], ["missingFiles", 1, "missing_files"], ["pausedDL", 0.3, "paused_by_user"], ["stoppedDL", 0.3, "paused_by_user"], ] as const)("%s @%s → %s", (state, progress, want) => { expect(mapQbState({ state, progress }, ctx).status).toBe(want); }); it("阈值暂停与用户暂停由业务账本区分", () => { expect(mapQbState({ state: "pausedDL", progress: 0.3 }, { pausedByThreshold: true }).status).toBe( "paused_by_threshold", ); }); it("任务消失 → removed;未知状态沿用上次并标记不确定", () => { expect(mapQbState(null, ctx).status).toBe("removed"); expect(mapQbState({ state: "weird", progress: 0 }, { ...ctx, previous: "downloading" })).toEqual({ status: "downloading", uncertain: true, reason: "unknown_state:weird", }); expect(mapQbState({ state: "weird", progress: 0 }, ctx).status).toBe("queued"); }); }); describe("完成判定", () => { it("error/missingFiles/校验中不算完成", () => { expect(isDownloadComplete({ state: "uploading", progress: 1 })).toBe(true); expect(isDownloadComplete({ state: "error", progress: 1 })).toBe(false); expect(isDownloadComplete({ state: "missingFiles", progress: 1 })).toBe(false); expect(isDownloadComplete({ state: "checkingUP", progress: 1 })).toBe(false); expect(isDownloadComplete({ state: "pausedUP", progress: 1, amountLeft: 10 })).toBe(false); expect(isDownloadComplete({ state: "downloading", progress: 0.99 })).toBe(false); expect(isDownloadComplete(null)).toBe(false); }); }); describe("状态机应用", () => { it("正常迁移与异常标记", () => { expect(applyObservation("queued", { state: "downloading", progress: 0.1 }, ctx)).toEqual({ status: "downloading", changed: true, }); expect(applyObservation("downloading", { state: "downloading", progress: 0.2 }, ctx).changed).toBe(false); expect(applyObservation("completed", { state: "downloading", progress: 0.9 }, ctx).anomaly).toBe( "completed_regressed", ); expect(applyObservation("downloading", { state: "??", progress: 0.9 }, ctx)).toEqual({ status: "downloading", changed: false, anomaly: "unknown_state", }); }); it("removed 是终态", () => { expect(() => applyObservation("removed", { state: "downloading", progress: 0 }, ctx)).toThrow( DownloadTransitionError, ); expect(applyObservation("removed", null, ctx).status).toBe("removed"); }); }); describe("无进展判定", () => { const min = 60_000; const s = (t: number, bytes: number, status: ProgressSample["status"] = "downloading"): ProgressSample => ({ at: t * min, downloadedBytes: bytes, status, }); it("持续下载中无增长 → 只记录证据", () => { const r = detectNoProgress([s(0, 100), s(30, 100), s(61, 100)], { thresholdMs: 60 * min }); expect(r).toMatchObject({ stalled: true, action: "record_evidence", stalledMs: 61 * min, lastProgressAt: 0, lastBytes: 100, }); }); it("有增长则清零;暂停/排队时间不计入", () => { expect(detectNoProgress([s(0, 100), s(50, 200), s(100, 200)], { thresholdMs: 60 * min }).stalled).toBe(false); const paused = detectNoProgress( [s(0, 100), s(10, 100, "paused_by_threshold"), s(200, 100, "paused_by_threshold"), s(210, 100)], { thresholdMs: 60 * min }, ); expect(paused.stalled).toBe(false); expect(paused.excludedMs).toBe(210 * min); const ending = detectNoProgress([s(0, 1), s(100, 1), s(200, 1, "paused_by_user")], { thresholdMs: 60 * min }); expect(ending.stalled).toBe(false); }); it("样本不足", () => { expect(detectNoProgress([], { thresholdMs: 1 })).toMatchObject({ stalled: false, sampleCount: 0, action: "none" }); expect(detectNoProgress([s(0, 1)], { thresholdMs: 0 }).stalled).toBe(false); }); });