Files
UHD_Media_Tool/apps/server/test/acquisition-organize/vertical.test.ts
T
rootandClaude Opus 5.5 c9e8120fc6 test: server 测试共用建库与假引擎,sse-pg 归入串行组
- 新增 apps/server/test/support/{pg,engine}.ts:测试库地址、可用性探测、schema 咨询锁、
  建 schema + 加锁迁移 + 删除、可选去重的假引擎;6 份 harness 改用它(acquisition-organize 与
  file-manager 原先迁移不加锁,并行下有冲突隐患,一并修正)
- sse-pg 起子进程,并行下子进程启动超时,归入 serial
- 修正 b61335e 挪动 / 改名测试后遗留的 import 排序与格式(当时 lint 检查经管道被吞掉退出码)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-02 17:40:43 +08:00

528 lines
24 KiB
TypeScript

import { existsSync, readFileSync } from "node:fs";
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { sql } from "@umt/db";
import { createMockQb, createTempTree, type MockQb, makeTorrent, type TempTree } from "@umt/testkit";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { evaluateAcquisition } from "../../src/modules/acquisition-organize/services/acquisition.ts";
import {
cleanupDownloadCache,
submitDownload,
syncDownloads,
} from "../../src/modules/acquisition-organize/services/downloads.ts";
import { executeRecord, planImport } from "../../src/modules/acquisition-organize/services/organize.ts";
import { reconcileLibrary } from "../../src/modules/acquisition-organize/services/reconcile.ts";
import {
createHarness,
fakeDisk,
type Harness,
pgAvailable,
seedBase,
seedEntity,
seedRelease,
seedSlot,
} from "./harness.ts";
// 纵切:release.resolved → 计划 → 提交 mock qB → 完成(落盘)→ Copy 入库 → 整理记录 API(含 changes 游标);
// 以及硬阈值暂停/恢复、already_exists 接管、缓存清理安全边界、资产对账。
const hasPg = await pgAvailable();
const SIZE = 300_000;
describe.skipIf(!hasPg)("acquisition-organize 纵切(真实 PG + mock qB + 临时目录)", () => {
let h: Harness;
let qb: MockQb;
let lib: TempTree;
let cache: TempTree;
const disk = fakeDisk(10_000_000);
const torrents = new Map<string, Uint8Array>();
let base: Awaited<ReturnType<typeof seedBase>>;
beforeAll(async () => {
qb = await createMockQb({ addVisibleAfterPolls: 0 });
lib = await createTempTree({}, { sentinel: true, prefix: "umt-lib-" });
cache = await createTempTree({}, { prefix: "umt-cache-" });
disk.state.watch = [cache.root];
disk.setPct(40);
h = await createHarness("vertical", {
diskUsage: disk.fn,
torrentSource: async ({ release }) => ({ kind: "file", bytes: torrents.get(release.id)! }),
});
base = await seedBase(h.db, { libraryPath: lib.root, cachePath: cache.root, qbUrl: qb.url });
await h.setSettings("retention", { seedRetentionDays: 0 });
}, 60_000);
afterAll(async () => {
await h?.close();
await qb?.close();
await lib?.cleanup();
await cache?.cleanup();
});
async function release(entityId: string, title: string, opts: { hr?: boolean } = {}) {
const t = makeTorrent({ name: `${title}.mkv`, length: SIZE, pieceSeed: Math.floor(Math.random() * 1e6) });
const id = await seedRelease(h.db, {
siteId: base.siteId,
entityId,
title,
size: SIZE,
hash: t.hash,
...(opts.hr !== undefined ? { hr: opts.hr } : {}),
});
torrents.set(id, t.bytes);
return { id, hash: t.hash, name: t.name };
}
async function runToCompleted(entityId: string, title: string, opts: { hr?: boolean } = {}) {
const rel = await release(entityId, title, opts);
const ev = await evaluateAcquisition(h.d, { sourceReleaseId: rel.id });
expect(ev.plans).toHaveLength(1);
const sub = await submitDownload(h.d, ev.plans[0]!);
expect(sub.status).toBe("submitted");
await qb.complete(rel.hash, { writeFiles: true });
const sync = await syncDownloads(h.d);
expect(sync.completed).toContain(sub.downloadTaskId);
return { rel, planId: ev.plans[0]!, dtId: sub.downloadTaskId! };
}
let movieId: string;
let slotId: string;
let firstRecord: string;
it("release → 计划(预留 Slot)→ 提交 qB(管理标签)→ 完成 → Copy 入库 → 登记资产与绑定", async () => {
movieId = await seedEntity(h.db, { kind: "movie", title: "沙丘2", year: 2024 });
slotId = await seedSlot(h.db, { entityId: movieId, hard: { resolutions: ["1080p"] } });
// 不满足硬条件的候选:写拒绝理由,不建计划
const bad = await release(movieId, "Dune.Part.Two.2024.720p.WEB-DL.x264-AAA");
const r0 = await evaluateAcquisition(h.d, { sourceReleaseId: bad.id });
expect(r0.plans).toHaveLength(0);
const rel = await release(movieId, "Dune.Part.Two.2024.1080p.BluRay.x264-GRP");
const ev = await evaluateAcquisition(h.d, { sourceReleaseId: rel.id });
expect(ev.plans).toHaveLength(1);
const slot1 = await h.db
.selectFrom("acquisition_slot")
.selectAll()
.where("id", "=", slotId)
.executeTakeFirstOrThrow();
expect(slot1.status).toBe("reserved");
expect(h.engine.enqueued.map((e) => e.task)).toContain("submitDownload");
const sub = await submitDownload(h.d, ev.plans[0]!);
expect(sub).toMatchObject({ status: "submitted", reason: "added" });
const t = qb.get(rel.hash)!;
expect(t.tags).toContain("umt");
expect(t.save_path).toBe(cache.root);
// 重放:账本已有结果,不重复提交
expect((await submitDownload(h.d, ev.plans[0]!)).status).toBe("skipped");
expect(qb.callsTo("torrents/add")).toHaveLength(1);
expect((await h.outbox("download.submitted")).length).toBe(1);
expect(
(await h.db.selectFrom("acquisition_slot").select("status").where("id", "=", slotId).executeTakeFirstOrThrow())
.status,
).toBe("downloading");
// 未完成:同步只更新进度
qb.setProgress(rel.hash, 0.5);
const s1 = await syncDownloads(h.d);
expect(s1.completed).toHaveLength(0);
await qb.complete(rel.hash, { writeFiles: true });
const s2 = await syncDownloads(h.d);
expect(s2.completed).toEqual([sub.downloadTaskId]);
const done = await h.outbox("download.status_changed");
expect(done.filter((e) => e.payload.to === "completed")).toHaveLength(1);
expect(
(await h.db.selectFrom("acquisition_slot").select("status").where("id", "=", slotId).executeTakeFirstOrThrow())
.status,
).toBe("verifying");
const plan = await planImport(h.d, { downloadTaskId: sub.downloadTaskId, trigger: "auto", actorType: "system" });
expect(plan.status).toBe("running");
firstRecord = plan.recordId!;
const ex = await executeRecord(h.d, firstRecord);
expect(ex).toMatchObject({ status: "succeeded", succeeded: 1, failed: 0 });
const target = join(lib.root, "沙丘2 (2024)", "沙丘2 (2024) - 1080p.mkv");
expect(existsSync(target)).toBe(true);
// Copy:源继续做种
expect(existsSync(join(cache.root, `${rel.name}`))).toBe(true);
const slot = await h.db
.selectFrom("acquisition_slot")
.selectAll()
.where("id", "=", slotId)
.executeTakeFirstOrThrow();
expect(slot.status).toBe("satisfied");
expect(slot.current_asset_id).not.toBeNull();
const binding = await h.db.selectFrom("path_binding").selectAll().where("media_entity_id", "=", movieId).execute();
expect(binding.map((b) => b.relative_path)).toEqual(["沙丘2 (2024)"]);
const loc = await h.db
.selectFrom("asset_location")
.selectAll()
.where("media_asset_id", "=", slot.current_asset_id!)
.executeTakeFirstOrThrow();
expect(loc.relative_path).toBe("沙丘2 (2024)/沙丘2 (2024) - 1080p.mkv");
const dt = await h.db
.selectFrom("download_task")
.selectAll()
.where("id", "=", sub.downloadTaskId!)
.executeTakeFirstOrThrow();
expect(dt.status).toBe("imported");
expect(
(
await h.db
.selectFrom("acquisition_plan")
.select("status")
.where("id", "=", ev.plans[0]!)
.executeTakeFirstOrThrow()
).status,
).toBe("completed");
// 重跑执行:幂等,不重复登记
await executeRecord(h.d, firstRecord);
expect(
await h.db.selectFrom("media_asset").select("id").where("media_entity_id", "=", movieId).execute(),
).toHaveLength(1);
}, 60_000);
it("整理记录 API:详情含文件明细;changes 游标增量(含既有记录状态更新);changes 先于 {id}", async () => {
const det = await h.api("GET", `/organization-records/${firstRecord}`);
expect(det.status).toBe(200);
expect(det.body.data).toMatchObject({
id: firstRecord,
status: "succeeded",
action: "copy",
origin: "auto",
counts: { total: 1, succeeded: 1 },
});
expect(det.body.data.files[0]).toMatchObject({
role: "main",
status: "succeeded",
target: { path: "沙丘2 (2024)/沙丘2 (2024) - 1080p.mkv", view: "library" },
});
expect(det.body.data.media).toMatchObject({ entityId: movieId, title: "沙丘2" });
const poll = async (after: string) => {
for (let i = 0; i < 50; i++) {
const r = await h.api("GET", `/organization-records/changes?after=${after}&limit=100`);
if (r.body.data.items.length > 0) return r;
await new Promise((res) => setTimeout(res, 100));
}
return h.api("GET", `/organization-records/changes?after=${after}&limit=100`);
};
const first = await poll("0");
expect(first.status).toBe(200);
expect(first.body.data.items.map((i: { id: string }) => i.id)).toContain(firstRecord);
const cursor = first.body.data.nextCursor as string;
const again = await h.api("GET", `/organization-records/changes?after=${cursor}`);
expect(again.body.data).toMatchObject({ items: [], nextCursor: cursor, hasMore: false });
// 既有记录状态更新 → 以更大的 changeSeq 再次出现
await sql`update organization_record set reason = 'touched' where id = ${firstRecord}`.execute(h.db);
const upd = await poll(cursor);
expect(upd.body.data.items.map((i: { id: string }) => i.id)).toEqual([firstRecord]);
expect(BigInt(upd.body.data.nextCursor)).toBeGreaterThan(BigInt(cursor));
const list = await h.api("GET", "/organization-records?limit=10");
expect(list.body.data.items[0].files).toBeNull();
expect((await h.api("GET", "/organization-records/changes?after=abc")).status).toBe(422);
});
it("硬阈值:达到时暂停纳管下载中任务并阻止新计划;低于恢复水位只恢复阈值暂停项", async () => {
const e = await seedEntity(h.db, { kind: "movie", title: "阈值片", year: 2024 });
await seedSlot(h.db, { entityId: e, hard: {} });
// 一个下载中的纳管任务 + 一个用户手动暂停的纳管任务(贴标签接管)
const a = makeTorrent({ name: "busy.mkv", length: SIZE, pieceSeed: 11 });
const b = makeTorrent({ name: "user-paused.mkv", length: SIZE, pieceSeed: 12 });
qb.seed({ hash: a.hash, name: "busy.mkv", tags: ["umt"], progress: 0.2, save_path: cache.root, size: SIZE });
qb.seed({
hash: b.hash,
name: "user-paused.mkv",
tags: ["umt"],
progress: 0.2,
save_path: cache.root,
size: SIZE,
state: "pausedDL",
});
const s0 = await syncDownloads(h.d);
expect(s0.adopted).toBe(2);
const rowB = await h.db
.selectFrom("download_task")
.selectAll()
.where("torrent_hash", "=", b.hash)
.executeTakeFirstOrThrow();
expect(rowB).toMatchObject({ status: "paused", pause_reason: "user", origin: "adopted" });
disk.setPct(95);
const s1 = await syncDownloads(h.d);
expect(s1.thresholdPaused).toBe(1);
expect(qb.get(a.hash)!.state).toMatch(/^paused|^stopped/i);
expect(
(await h.db.selectFrom("download_task").selectAll().where("torrent_hash", "=", a.hash).executeTakeFirstOrThrow())
.pause_reason,
).toBe("threshold");
const tripped = await h.outbox("download.threshold");
expect(tripped.at(-1)?.payload.state).toBe("tripped");
const rel = await release(e, "Threshold.Movie.2024.1080p.WEB-DL-X");
const ev = await evaluateAcquisition(h.d, { sourceReleaseId: rel.id });
expect(ev.plans).toHaveLength(0);
expect(ev.skipped[0]?.reason).toMatch(/^cache:/);
// 手动恢复阈值暂停项被拒绝(不绕过阈值)
const rowA = await h.db
.selectFrom("download_task")
.select("id")
.where("torrent_hash", "=", a.hash)
.executeTakeFirstOrThrow();
expect((await h.api("POST", `/download-tasks/${rowA.id}/resume`)).status).toBe(412);
// 下一轮观察:暂停中不计入无进展,原因保持 threshold
await syncDownloads(h.d);
expect(
(await h.db.selectFrom("download_task").selectAll().where("id", "=", rowA.id).executeTakeFirstOrThrow())
.pause_reason,
).toBe("threshold");
disk.setPct(88); // 高于恢复水位:保持暂停
expect((await syncDownloads(h.d)).thresholdResumed).toBe(0);
disk.setPct(70);
const s3 = await syncDownloads(h.d);
expect(s3.thresholdResumed).toBe(1);
expect(qb.get(a.hash)!.state).not.toMatch(/^paused|^stopped/i);
expect(qb.get(b.hash)!.state).toMatch(/^paused|^stopped/i);
expect(
(await h.db.selectFrom("download_task").selectAll().where("torrent_hash", "=", b.hash).executeTakeFirstOrThrow())
.pause_reason,
).toBe("user");
// 读不到磁盘:保守阻止
disk.state.fail = true;
const ev2 = await evaluateAcquisition(h.d, { sourceReleaseId: rel.id });
expect(ev2.plans).toHaveLength(0);
disk.state.fail = false;
disk.setPct(40);
qb.setProgress(a.hash, 0);
}, 60_000);
it("already_exists:种子原本在 qB 里 → 接管,只补管理标签,不改保存路径和其它标签", async () => {
const e = await seedEntity(h.db, { kind: "movie", title: "旧种", year: 2023 });
await seedSlot(h.db, { entityId: e });
const rel = await release(e, "Old.Seed.2023.1080p.BluRay-OLD");
qb.seed({ hash: rel.hash, name: rel.name, tags: ["keep"], save_path: "/elsewhere", progress: 0.1, size: SIZE });
const ev = await evaluateAcquisition(h.d, { sourceReleaseId: rel.id });
const sub = await submitDownload(h.d, ev.plans[0]!);
expect(sub).toMatchObject({ status: "submitted", reason: "already_exists" });
const t = qb.get(rel.hash)!;
expect(t.save_path).toBe("/elsewhere");
expect(t.tags.sort()).toEqual(["keep", "umt"]);
const row = await h.db
.selectFrom("download_task")
.selectAll()
.where("id", "=", sub.downloadTaskId!)
.executeTakeFirstOrThrow();
expect(row).toMatchObject({
origin: "adopted",
managed: true,
acquisition_plan_id: ev.plans[0],
media_entity_id: e,
});
expect(qb.callsTo("torrents/add").length).toBe(1);
}, 30_000);
it("H&R 未满足时 Move 变冲突:记录失败 + 待处理,不搬源、不静默改 Copy", async () => {
await h.setSettings("organize", { defaultMode: "move" });
const e = await seedEntity(h.db, { kind: "movie", title: "做种片", year: 2022 });
const slot = await seedSlot(h.db, { entityId: e });
const { dtId, rel } = await runToCompleted(e, "Seeding.Movie.2022.1080p.BluRay-HR", { hr: true });
const plan = await planImport(h.d, { downloadTaskId: dtId, trigger: "auto", actorType: "system" });
expect(plan.status).toBe("failed");
expect(plan.reason).toContain("hnr_unsatisfied");
const rec = await h.db
.selectFrom("organization_record")
.selectAll()
.where("id", "=", plan.recordId!)
.executeTakeFirstOrThrow();
expect(rec.action).toBe("move");
expect(existsSync(join(cache.root, rel.name))).toBe(true);
expect(existsSync(join(lib.root, "做种片 (2022)"))).toBe(false);
const dec = await h.db
.selectFrom("decision")
.selectAll()
.where("subject_id", "=", plan.recordId!)
.executeTakeFirstOrThrow();
expect(dec).toMatchObject({ kind: "approval_required", status: "open" });
expect(
(await h.db.selectFrom("acquisition_slot").select("status").where("id", "=", slot).executeTakeFirstOrThrow())
.status,
).toBe("attention");
await h.setSettings("organize", { defaultMode: "copy" });
}, 30_000);
it("缓存清理:低于阈值不动;超阈值只删已验证入库且满足保留/H&R 的任务,删数据必经安全检查", async () => {
// 已有:第一部片(已入库、hr=false、保留 0 天)→ 可清理;做种片(hr 未满足,未入库)→ 锁定
disk.setPct(50);
const r0 = await cleanupDownloadCache(h.d);
expect(r0.volumes[0]!.action).toBe("none");
const firstDt = await h.db.selectFrom("download_task").selectAll().where("status", "=", "imported").execute();
expect(firstDt.length).toBeGreaterThanOrEqual(1);
// 另造一个"已入库"但 content_path 落在媒体库内的任务:安全检查必须拒绝删数据
const trap = await h.db
.insertInto("download_task")
.values({
instance_id: base.instanceId,
torrent_hash: "f".repeat(40),
name: "trap",
source_release_id: firstDt[0]!.source_release_id,
save_path: lib.root,
content_path: join(lib.root, "沙丘2 (2024)"),
status: "imported",
managed: true,
completed_at: new Date(0),
size_bytes: 1,
})
.returning("id")
.executeTakeFirstOrThrow();
await h.db
.insertInto("organization_record")
.values({ trigger: "auto", actor_type: "system", action: "copy", status: "succeeded", download_task_id: trap.id })
.execute();
qb.seed({ hash: "f".repeat(40), name: "trap", tags: ["umt"], progress: 1, save_path: lib.root, size: 1 });
disk.setPct(95);
const r = await cleanupDownloadCache(h.d);
const v = r.volumes[0]!;
expect(v.removed).toContain(firstDt[0]!.id);
expect(v.removed).not.toContain(trap.id);
expect(v.skipped.find((s) => s.downloadTaskId === trap.id)?.reason).toMatch(/拒绝删除|媒体库/);
expect(qb.get(firstDt[0]!.torrent_hash)).toBeUndefined();
// 库内副本不受影响
expect(existsSync(join(lib.root, "沙丘2 (2024)", "沙丘2 (2024) - 1080p.mkv"))).toBe(true);
const locked = v.skipped.map((s) => s.reason);
expect(locked.some((x) => x === "hnr_unsatisfied" || x === "import_not_verified")).toBe(false); // 做种片未入库,不在候选里
const op = await h.db
.selectFrom("asset_operation")
.selectAll()
.where("operation_id", "=", `cleanup:${firstDt[0]!.id}`)
.executeTakeFirstOrThrow();
expect(op).toMatchObject({ kind: "qb_remove", status: "succeeded" });
disk.setPct(40);
}, 30_000);
it("资产对账:库根离线只标 unavailable;缺失按 suspected → confirmed 推进,不删记录不补下载", async () => {
const file = join(lib.root, "沙丘2 (2024)", "沙丘2 (2024) - 1080p.mkv");
const content = readFileSync(file);
const loc = await h.db
.selectFrom("asset_location")
.selectAll()
.where("relative_path", "=", "沙丘2 (2024)/沙丘2 (2024) - 1080p.mkv")
.executeTakeFirstOrThrow();
const { rm } = await import("node:fs/promises");
await rm(join(lib.root, ".uhd-root"));
const off = await reconcileLibrary(h.d);
expect(off.stats.rootsOffline).toHaveLength(1);
expect(
(await h.db.selectFrom("asset_location").select("status").where("id", "=", loc.id).executeTakeFirstOrThrow())
.status,
).toBe("unavailable");
await writeFile(join(lib.root, ".uhd-root"), "x");
await rm(file);
for (const want of ["suspected_missing", "suspected_missing", "confirmed_missing"]) {
await reconcileLibrary(h.d);
expect(
(await h.db.selectFrom("asset_location").select("status").where("id", "=", loc.id).executeTakeFirstOrThrow())
.status,
).toBe(want);
}
expect(
(await h.db.selectFrom("acquisition_slot").select("status").where("id", "=", slotId).executeTakeFirstOrThrow())
.status,
).toBe("attention");
expect(
await h.db.selectFrom("media_asset").select("id").where("id", "=", loc.media_asset_id).executeTakeFirst(),
).toBeDefined();
await writeFile(file, content);
await reconcileLibrary(h.d);
expect(
(await h.db.selectFrom("asset_location").select("status").where("id", "=", loc.id).executeTakeFirstOrThrow())
.status,
).toBe("available");
const runs = await h.db.selectFrom("reconcile_run").select(["kind", "status"]).execute();
expect(runs.every((r) => r.kind === "asset")).toBe(true);
}, 30_000);
it("首页、媒体库、库存叠加 API", async () => {
const dash = await h.api("GET", "/dashboard");
expect(dash.status).toBe(200);
expect(dash.body.data.recentOutput.length).toBeGreaterThan(0);
expect(dash.body.data.cache.volumes[0]).toMatchObject({ hardThresholdPercent: 90, resumePercent: 85 });
expect(dash.body.data.needsAttention.total).toBeGreaterThan(0);
const auto = dash.body.data.automation;
expect(auto.library.entities).toBeGreaterThanOrEqual(1);
expect(auto.library.movies).toBeGreaterThanOrEqual(1);
expect(auto.library.assets).toBeGreaterThanOrEqual(1);
const slotTotal = Object.values(auto.slotCounts as Record<string, number>).reduce((a, b) => a + b, 0);
expect(auto.slotSatisfiedRate).toBe(slotTotal ? auto.slotCounts.satisfied / slotTotal : null);
const libList = await h.api("GET", "/library?q=沙丘");
expect(libList.body.data.items[0]).toMatchObject({ entity: { id: movieId }, assetCount: 1 });
const det = await h.api("GET", `/library/${movieId}`);
expect(det.status).toBe(200);
expect(det.body.data.assets[0].locations[0].absolutePath).toBe(
join(lib.root, "沙丘2 (2024)", "沙丘2 (2024) - 1080p.mkv"),
);
expect(det.body.data.candidates.some((c: { decision: string }) => c.decision === "reject")).toBe(true);
expect(det.body.data.downloads[0].organizationRecordIds.length).toBeGreaterThan(0);
// QB-1:下载任务直接记作品;整理记录按作品列出
const dune = det.body.data.downloads.find((x: { downloadTask: { name: string } }) =>
x.downloadTask.name.startsWith("Dune.Part.Two"),
);
expect(dune.downloadTask.mediaEntityId).toBe(movieId);
expect(det.body.data.organizationRecords.map((r: { id: string }) => r.id)).toEqual(
expect.arrayContaining(dune.organizationRecordIds),
);
const inv = await h.api("GET", `/inventory?entityIds=${movieId}`);
expect(inv.body.data.items[0].versions[0]).toMatchObject({ resolution: "1080p", available: true });
const ms = await h.api("GET", `/inventory/media-servers?mediaEntityId=${movieId}`);
expect(ms.body.data).toEqual({ mediaEntityId: movieId, instances: [], partial: false });
});
it("自动整理被拦(attention)后手动整理成功:Slot 经 verifying 落 satisfied,整理事项自动关闭(QA U1)", async () => {
await h.setSettings("organize", { defaultMode: "move" });
const e = await seedEntity(h.db, { kind: "movie", title: "被拦片", year: 2021 });
const slot = await seedSlot(h.db, { entityId: e });
const { dtId, planId } = await runToCompleted(e, "Blocked.Movie.2021.1080p.BluRay-HR", { hr: true });
// 新作品首次下载:计划用途是补缺,不是升级(QA U7)
expect(
(await h.db.selectFrom("acquisition_plan").select("purpose").where("id", "=", planId).executeTakeFirstOrThrow())
.purpose,
).toBe("fill");
const auto = await planImport(h.d, { downloadTaskId: dtId, trigger: "auto", actorType: "system" });
expect(auto.status).toBe("failed");
const slotRow = () =>
h.db.selectFrom("acquisition_slot").selectAll().where("id", "=", slot).executeTakeFirstOrThrow();
expect((await slotRow()).status).toBe("attention");
const dec = await h.db
.selectFrom("decision")
.selectAll()
.where("dedupe_key", "=", `organize_blocked:${dtId}`)
.executeTakeFirstOrThrow();
expect(dec.status).toBe("open");
await h.setSettings("organize", { defaultMode: "copy" });
const manual = await planImport(h.d, { downloadTaskId: dtId, trigger: "manual", actorType: "user", actorId: null });
expect(manual.status).toBe("running");
const done = await executeRecord(h.d, manual.recordId!);
expect(done.status).toBe("succeeded");
const after = await slotRow();
expect(after.status).toBe("satisfied");
expect(after.current_asset_id).not.toBeNull();
const asset = await h.db
.selectFrom("media_asset")
.select("media_entity_id")
.where("id", "=", after.current_asset_id!)
.executeTakeFirstOrThrow();
expect(asset.media_entity_id).toBe(e);
const closed = await h.db.selectFrom("decision").selectAll().where("id", "=", dec.id).executeTakeFirstOrThrow();
expect(closed.status).toBe("resolved");
expect(closed.resolution).toMatchObject({ key: "manual_organize", organizationRecordId: manual.recordId });
expect(closed.resolved_by).toBe("user");
}, 30_000);
});