fix(organize): deduplicate concurrent identity recovery plans

This commit is contained in:
root committed 2026-11-05 23:56:42 +08:00
1 parent 8d8bda152b
commit 32e1d446b5
3 files changed
+64 -3

No files matched your search

@@ -1,6 +1,7 @@
import { randomUUID } from "node:crypto";
import { symlink, unlink, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { sql } from "@umt/db";
import { createTempTree, type TempTree } from "@umt/testkit";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { executeRecord, planImport } from "../../src/modules/acquisition-organize/services/organize.ts";
@@ -10,6 +11,7 @@ import {
} from "../../src/modules/acquisition-organize/services/organize-retry.ts";
import {
assignOrganizeIdentity,
OrganizeDecisionError,
organizeDecisionHook,
} from "../../src/modules/acquisition-organize/services/takeover.ts";
import { autoInvestigate, checkRecovery } from "../../src/modules/agent/investigate.ts";
@@ -123,6 +125,50 @@ describe.skipIf(!hasPg)("按路径身份补齐后恢复原整理", () => {
).plan,
).toMatchObject({ retryOfRecordId: plan.recordId });
});
it("人工与智能体并发指定同一作品,只冻结并入队一次整理计划", async () => {
const { plan, dec } = await unknown("Concurrent.Path", { action: "copy" });
const entityId = await seedEntity(h.db, { kind: "movie", title: "Concurrent Path", year: 2022 });
const before = h.engine.enqueued.length;
const [agent, human] = await Promise.all([
assignOrganizeIdentity(h.d, dec.id, { mediaEntityId: entityId }, actor, "标题一致"),
organizeDecisionHook(h.d).execute(
dec,
{ optionKey: "select_entity", identity: { mediaEntityId: entityId } },
actor,
),
]);
expect(agent.recordId).toBe(human.recordId);
expect(h.engine.enqueued.slice(before)).toHaveLength(1);
const records = await h.db
.selectFrom("organization_record")
.select("id")
.where(sql<string>`plan->>'retryOfRecordId'`, "=", plan.recordId!)
.execute();
expect(records).toHaveLength(1);
});
it("同一事项并发指定不同作品时拒绝后到的选择,不创建第二个计划", async () => {
const { plan, dec } = await unknown("Conflicting.Path", { action: "copy" });
const entities = await Promise.all(
["First Choice", "Second Choice"].map((title) => seedEntity(h.db, { kind: "movie", title, year: 2022 })),
);
const before = h.engine.enqueued.length;
const results = await Promise.allSettled(
entities.map((mediaEntityId) =>
organizeDecisionHook(h.d).execute(dec, { optionKey: "select_entity", identity: { mediaEntityId } }, actor),
),
);
expect(results.filter((result) => result.status === "fulfilled")).toHaveLength(1);
const rejected = results.find((result) => result.status === "rejected");
expect(rejected?.status === "rejected" && rejected.reason).toMatchObject({ code: "conflict" });
expect(rejected?.status === "rejected" && rejected.reason).toBeInstanceOf(OrganizeDecisionError);
expect(h.engine.enqueued.slice(before)).toHaveLength(1);
const records = await h.db
.selectFrom("organization_record")
.select("id")
.where(sql<string>`plan->>'retryOfRecordId'`, "=", plan.recordId!)
.execute();
expect(records).toHaveLength(1);
});
it("身份已找到但重新规划失败,保持原事项待处理", async () => {
const { dec } = await unknown("Blocked.Path", { targetLibraryRootId: randomUUID() });
const entityId = await seedEntity(h.db, { kind: "movie", title: "Blocked Path", year: 2022 });