feat(identity): open identity_unresolved decisions for resources with no candidates
no_match resources (and lookup failures after auto-review gives up) now
open a decision in the inbox with options to set a TMDB ID, pick a local
work, or dismiss; ambiguous decisions get the same manual options.
Resolve accepts identity {tmdb | mediaEntityId} and assigns with a manual
lock. The inbox option dialog collects the TMDB type/ID or searches local
works; the identity pool links each pending item to its inbox filter.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
a0b0a64246
commit
fb7727cf66
14 files changed
+280
-37
No files matched your search
@@ -132,7 +132,13 @@ describe.skipIf(!up)("resolveObservation / 身份与元数据", () => {
|
||||
expect(pool.next_review_at!.getTime()).toBeGreaterThan(Date.now());
|
||||
const d = await h.db.selectFrom("decision").selectAll().where("id", "=", res.decisionId!).executeTakeFirstOrThrow();
|
||||
expect(d).toMatchObject({ kind: "identity_ambiguous", status: "open", subject_id: r.id });
|
||||
expect((d.options as { key: string }[]).map((o) => o.key)).toEqual(["c0", "c1", "dismiss"]);
|
||||
expect((d.options as { key: string }[]).map((o) => o.key)).toEqual([
|
||||
"c0",
|
||||
"c1",
|
||||
"manual_tmdb",
|
||||
"local_entity",
|
||||
"dismiss",
|
||||
]);
|
||||
expect((await h.outbox("decision.created")).some((e) => e.payload.decisionId === d.id)).toBe(true);
|
||||
const rel = await h.db.selectFrom("source_release").selectAll().where("id", "=", r.id).executeTakeFirstOrThrow();
|
||||
expect(rel.resolution_status).toBe("ambiguous");
|
||||
@@ -203,17 +209,60 @@ describe.skipIf(!up)("resolveObservation / 身份与元数据", () => {
|
||||
expect(after.media_entity_id).toBe(rel.media_entity_id);
|
||||
});
|
||||
|
||||
it("no_match 只进池不开待处理;TMDB 失败记为 lookup_failed 而不是无结果", async () => {
|
||||
it("no_match 进池并开未识别事项;手动指定 TMDB id / 选本地作品即人工指定加锁;TMDB 失败记为 lookup_failed", async () => {
|
||||
const r1 = await release("Zzqxv.Nonexistent.2019.1080p.WEB-DL-NOPE");
|
||||
const a = await resolveRelease(h.rt, { sourceReleaseId: r1.id });
|
||||
expect(a.outcome).toBe("no_match");
|
||||
expect(a.decisionId).toBeNull();
|
||||
expect(a.decisionId).not.toBeNull();
|
||||
const rel = await h.db
|
||||
.selectFrom("source_release")
|
||||
.select(["resolution_status"])
|
||||
.where("id", "=", r1.id)
|
||||
.executeTakeFirstOrThrow();
|
||||
expect(rel.resolution_status).toBe("unidentifiable");
|
||||
const d = await h.db.selectFrom("decision").selectAll().where("id", "=", a.decisionId!).executeTakeFirstOrThrow();
|
||||
expect(d).toMatchObject({ kind: "identity_unresolved", status: "open", subject_id: r1.id });
|
||||
expect((d.options as { key: string }[]).map((o) => o.key)).toEqual(["manual_tmdb", "local_entity", "dismiss"]);
|
||||
const pool1 = await h.db
|
||||
.selectFrom("identity_candidate")
|
||||
.select(["decision_id"])
|
||||
.where("source_release_id", "=", r1.id)
|
||||
.executeTakeFirstOrThrow();
|
||||
expect(pool1.decision_id).toBe(d.id);
|
||||
// 缺少输入 → 400;给 TMDB id → 人工指定加锁,事项关闭
|
||||
expect((await json(await h.req(`/decisions/${d.id}/resolve`, { body: { optionKey: "manual_tmdb" } }))).status).toBe(
|
||||
400,
|
||||
);
|
||||
const fx = TMDB_FIXTURES.find((f) => f.type === "movie")!;
|
||||
const ok = await json(
|
||||
await h.req(`/decisions/${d.id}/resolve`, {
|
||||
body: { optionKey: "manual_tmdb", identity: { tmdb: { type: "movie", id: String(fx.id) } } },
|
||||
}),
|
||||
);
|
||||
expect(ok.status).toBe(200);
|
||||
expect(ok.body.data.status).toBe("resolved");
|
||||
const rel1 = await h.db.selectFrom("source_release").selectAll().where("id", "=", r1.id).executeTakeFirstOrThrow();
|
||||
expect(rel1).toMatchObject({ resolution_status: "resolved", resolution_locked: true });
|
||||
const ident = await h.db
|
||||
.selectFrom("external_identity")
|
||||
.selectAll()
|
||||
.where("media_entity_id", "=", rel1.media_entity_id!)
|
||||
.where("provider", "=", "tmdb")
|
||||
.executeTakeFirstOrThrow();
|
||||
expect(ident.external_id).toBe(String(fx.id));
|
||||
|
||||
// 选本地作品
|
||||
const r3 = await release("Xyzzy.Plugh.2018.1080p.WEB-DL-NOPE");
|
||||
const c = await resolveRelease(h.rt, { sourceReleaseId: r3.id });
|
||||
expect(c.outcome).toBe("no_match");
|
||||
const local = await json(
|
||||
await h.req(`/decisions/${c.decisionId}/resolve`, {
|
||||
body: { optionKey: "local_entity", identity: { mediaEntityId: oppId } },
|
||||
}),
|
||||
);
|
||||
expect(local.status).toBe(200);
|
||||
const rel3 = await h.db.selectFrom("source_release").selectAll().where("id", "=", r3.id).executeTakeFirstOrThrow();
|
||||
expect(rel3).toMatchObject({ media_entity_id: oppId, resolution_locked: true });
|
||||
|
||||
tmdb.fault(/\/search\//, { status: 500, times: 20 });
|
||||
const r2 = await release("Qwerty.Unknown.2020.1080p.WEB-DL-NOPE");
|
||||
|
||||
Reference in new issue
Block a user