fix(organize,identity,sites,pi): W10-pre backend fixes from v0.2 visual QA
- slot: new manual_verify event (attention -> verifying, with reason); a
successful organize of a download whose slot is in attention goes back
through verifying and lands by verify_passed on the actually imported
assets; the download's organize_blocked decision is resolved with
resolution {key: manual_organize | organize_retry, organizationRecordId} (U1)
- acquisition: plan purpose is upgrade only when the slot already has a
current asset; a movie slot gap is a fill (U7)
- identity: entities created from TMDB details get metadata fields projected
in the same transaction; TMDB aliases deduped by normalized text, site-title
aliases skipped when the text already exists (U9)
- pi: auth-mode test of a custom Base URL no longer marks the upstream ok, and
says to use the minimal completion to probe the endpoint (U2)
- sites: PUT /sites/{id}/credentials replaces same-kind secrets in place and
names new secrets with site id + random suffix (secret.name is unique; was a
500 on update); M-Team user data uses config/definition apiBaseUrl instead
of deriving api.<domain> from the site URL (Invalid URL on IP/local bases)
- copy: setup error says 初始化口令; backup job points at umt backup
- tests: schema expected tables add site_user_data; migration round-trip
timeout 60s; fake secrets honor label as name; vertical/resolve/pi/slot/
site-center tests for the above
- docs: 09-visual-qa W10-pre status table (U3 root cause: outbox events are
never mirrored to SSE)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
b7a7fd3958
commit
2b8fe667a5
20 files changed
+298
-42
No files matched your search
@@ -459,4 +459,46 @@ describe.skipIf(!hasPg)("acquisition-organize 纵切(真实 PG + mock qB + 临
|
||||
const ms = await h.api("GET", `/inventory/media-servers?mediaEntityId=${movieId}`);
|
||||
expect(ms.body.data).toEqual({ mediaEntityId: movieId, instances: [] });
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
@@ -967,7 +967,8 @@ async function reserve(
|
||||
savePath: p.savePath,
|
||||
...(p.evaluationExtra ?? {}),
|
||||
});
|
||||
const purpose = p.mode === "upgrade" ? ("upgrade" as const) : ("fill" as const);
|
||||
// 升级 = 替换已有资产;电影 Slot 缺口(无资产)的 mode 虽记 upgrade,用途仍是补缺
|
||||
const purpose = p.mode === "upgrade" && p.slot.current_asset_id ? ("upgrade" as const) : ("fill" as const);
|
||||
if (c.draft) {
|
||||
// 草案:同一 Slot+发布复用未提交的草案;Slot 状态与预算不动,expected_revision 记下当前 Slot 版本 +1
|
||||
const key = `draft:${p.slot.id}:${p.rel.id}`;
|
||||
|
||||
@@ -168,7 +168,7 @@ export async function applySlotEvent(
|
||||
? completion?.complete
|
||||
? "verified"
|
||||
: "verified_partial"
|
||||
: (extra.reason ?? event.type),
|
||||
: (extra.reason ?? (event.type === "manual_verify" ? event.reason : event.type)),
|
||||
...(ended ? { next_evaluate_at: d.ports.now() } : {}),
|
||||
})
|
||||
.where("id", "=", slotId)
|
||||
|
||||
@@ -1509,6 +1509,12 @@ export async function finalizeRecord(d: AoDeps, recordId: string): Promise<Execu
|
||||
.executeTakeFirst();
|
||||
if (slot && okAny) {
|
||||
const old = slot.current_asset_id;
|
||||
// 需处理的 Slot(如自动整理被拦)经人工整理入库:先回到核验,再由 verify_passed 按实际入库资产定终态
|
||||
if (slot.status === "attention")
|
||||
await applySlotEvent(d, trx, slot.id, {
|
||||
type: "manual_verify",
|
||||
reason: rec.trigger === "auto" ? "organize_retry" : "manual_organize",
|
||||
});
|
||||
// 完成判定与订阅完成(含历史)统一由 applySlotEvent(verify_passed) 按总集数账本重算,这里不再本地判定
|
||||
await applySlotEvent(
|
||||
d,
|
||||
@@ -1540,6 +1546,8 @@ export async function finalizeRecord(d: AoDeps, recordId: string): Promise<Execu
|
||||
}
|
||||
}
|
||||
}
|
||||
// 本下载任务先前被拦的整理事项:已有资产入库即视为处理完毕
|
||||
if (okAny) await closeOrganizeDecisions(d, trx, dt.id, rec);
|
||||
}
|
||||
// 手动整理指定的 Slot(文件管理整理向导):核验通过后以新资产满足该 Slot(获取中的 Slot 不动,由其计划推进)
|
||||
if (!dt && frozen.slotId && okAny) {
|
||||
@@ -1590,6 +1598,36 @@ export async function finalizeRecord(d: AoDeps, recordId: string): Promise<Execu
|
||||
return { recordId: result.recordId, status: result.status, succeeded: result.succeeded, failed: result.failed };
|
||||
}
|
||||
|
||||
/** 关闭下载任务的"整理冲突 / 做种义务阻止移动"事项(resolution.key=manual_organize,记整理记录 id) */
|
||||
async function closeOrganizeDecisions(d: AoDeps, trx: Exec, downloadTaskId: string, rec: OrganizationRecordRow) {
|
||||
const by = rec.actor_id ? `${rec.actor_type}:${rec.actor_id}` : rec.actor_type;
|
||||
const rows = await trx
|
||||
.updateTable("decision")
|
||||
.set({
|
||||
status: "resolved",
|
||||
resolved_by: by,
|
||||
resolved_at: d.ports.now(),
|
||||
resolution: jsonb({
|
||||
key: rec.trigger === "auto" ? "organize_retry" : "manual_organize",
|
||||
optionKey: null,
|
||||
by,
|
||||
note: "整理已成功入库",
|
||||
organizationRecordId: rec.id,
|
||||
}),
|
||||
})
|
||||
.where("dedupe_key", "=", `organize_blocked:${downloadTaskId}`)
|
||||
.where("status", "in", ["open", "investigating", "awaiting_user"])
|
||||
.returning("id")
|
||||
.execute();
|
||||
for (const r of rows)
|
||||
await d.ctx.outbox.emit(
|
||||
trx,
|
||||
"decision.updated",
|
||||
{ decisionId: r.id, status: "resolved" },
|
||||
{ idempotencyKey: `decision.updated:${r.id}:resolved` },
|
||||
);
|
||||
}
|
||||
|
||||
/** 按季号与集号区间找或建集单元(临时 provisional,挂在季单元下) */
|
||||
async function ensureEpisodeUnits(
|
||||
exec: Exec,
|
||||
|
||||
Reference in new issue
Block a user