refactor(core): migrate to Bun and converge acquisition policies and Agent boundaries
This commit is contained in:
1 parent
3f1c6d02f6
commit
27c0f27434
268 files changed
+7468
-7852
No files matched your search
@@ -0,0 +1,438 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import {
|
||||
CAPABILITY_CATALOG,
|
||||
type DecisionStatus,
|
||||
type KnownCapabilityId,
|
||||
ReleaseApplyFactsInput,
|
||||
type ReleaseStaticFacts,
|
||||
} from "@umt/contracts";
|
||||
import { evaluateCandidate, parseRelease, type SlotSpec } from "@umt/domain";
|
||||
import { toParsedColumns } from "@umt/mapping";
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
import { createCapabilityRegistry } from "../../src/modules/agent/capability-registry.ts";
|
||||
import { autoInvestigate, checkRecovery, recoveryActions } from "../../src/modules/agent/investigate.ts";
|
||||
import { registerSourcesCapabilities } from "../../src/modules/sources-identity/capabilities.ts";
|
||||
import { parsedOf } from "../../src/modules/sources-identity/service/release-facts.ts";
|
||||
import { applyReleaseFacts } from "../../src/modules/sources-identity/service/release-facts-admin.ts";
|
||||
import { createHarness, type Harness, pgAvailable, seedSite } from "./harness.ts";
|
||||
|
||||
// 真实 PG、假引擎、不调用模型或站点:事实补全必须受事项和授权约束,补完仍按原规则判断。
|
||||
const pg = await pgAvailable();
|
||||
const EVIDENCE = "资源详情 MediaInfo 的视频轨道标明宽 1920、高 1080,编码 AVC。";
|
||||
|
||||
describe.skipIf(!pg)("资源事实补全与恢复", () => {
|
||||
let h: Harness;
|
||||
let siteId: string;
|
||||
let entityId: string;
|
||||
let anotherEntityId: string;
|
||||
let n = 0;
|
||||
|
||||
beforeAll(async () => {
|
||||
h = await createHarness("release_facts_recovery");
|
||||
siteId = (await seedSite(h, { baseUrl: "https://facts.example.test/" })).site.id;
|
||||
const entities = await h.db
|
||||
.insertInto("media_entity")
|
||||
.values([
|
||||
{ kind: "movie", title: "Silent Meridian", year: 2025, status: "active" },
|
||||
{ kind: "movie", title: "Another Film", year: 2024, status: "active" },
|
||||
])
|
||||
.returning("id")
|
||||
.execute();
|
||||
entityId = entities[0]!.id;
|
||||
anotherEntityId = entities[1]!.id;
|
||||
for (const [key, value] of Object.entries({
|
||||
automation: { enabled: true },
|
||||
agent: { autoInvestigate: true, autoInvestigateMode: "recover", investigateMaxAttempts: 3 },
|
||||
})) {
|
||||
await h.db
|
||||
.insertInto("settings")
|
||||
.values({ key, value: JSON.stringify(value) })
|
||||
.onConflict((oc) => oc.column("key").doUpdateSet({ value: JSON.stringify(value) }))
|
||||
.execute();
|
||||
}
|
||||
const registry = createCapabilityRegistry({ logger: h.ctx.logger, db: h.db, settings: h.ctx.settings });
|
||||
registerSourcesCapabilities(registry, () => h.rt);
|
||||
h.ctx.capabilities = registry;
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await h.db.deleteFrom("automation_grant").execute();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await h?.close();
|
||||
});
|
||||
|
||||
async function seed(opts: { title?: string; missing?: string[]; status?: DecisionStatus } = {}) {
|
||||
const title = opts.title ?? "Silent Meridian 2025";
|
||||
const columns = toParsedColumns(parseRelease(title));
|
||||
const release = await h.db
|
||||
.insertInto("source_release")
|
||||
.values({
|
||||
site_id: siteId,
|
||||
site_release_id: String(++n),
|
||||
title,
|
||||
media_entity_id: entityId,
|
||||
resolution_status: "resolved",
|
||||
parsed: JSON.stringify(columns.parsed),
|
||||
parser_version: columns.parserVersion,
|
||||
raw: JSON.stringify({ _site: { marker: "snapshot" }, _detail: { imdbId: "tt1234567" } }),
|
||||
})
|
||||
.returningAll()
|
||||
.executeTakeFirstOrThrow();
|
||||
const decision = await h.db
|
||||
.insertInto("decision")
|
||||
.values({
|
||||
kind: "other",
|
||||
title: "资源属性缺失",
|
||||
status: opts.status ?? "open",
|
||||
subject_type: "source_release",
|
||||
subject_id: release.id,
|
||||
dedupe_key: `release_facts:${release.id}`,
|
||||
evidence: JSON.stringify({
|
||||
sourceReleaseId: release.id,
|
||||
mediaEntityId: entityId,
|
||||
missing: opts.missing ?? ["resolution"],
|
||||
}),
|
||||
})
|
||||
.returningAll()
|
||||
.executeTakeFirstOrThrow();
|
||||
return { releaseId: release.id, decisionId: decision.id };
|
||||
}
|
||||
|
||||
function input(target: Awaited<ReturnType<typeof seed>>, facts: ReleaseStaticFacts = { resolution: "1080p" }) {
|
||||
return ReleaseApplyFactsInput.parse({
|
||||
decisionId: target.decisionId,
|
||||
sourceReleaseId: target.releaseId,
|
||||
facts,
|
||||
evidence: EVIDENCE,
|
||||
evidenceUrl: "https://facts.example.test/details.php?id=1",
|
||||
});
|
||||
}
|
||||
|
||||
const releaseOf = (id: string) =>
|
||||
h.db.selectFrom("source_release").selectAll().where("id", "=", id).executeTakeFirstOrThrow();
|
||||
const decisionOf = (id: string) =>
|
||||
h.db.selectFrom("decision").selectAll().where("id", "=", id).executeTakeFirstOrThrow();
|
||||
const eventsOf = async (id: string) =>
|
||||
(await h.outbox("release.resolved")).filter((event) => event.payload.sourceReleaseId === id);
|
||||
const actionOf = (target: Awaited<ReturnType<typeof seed>>) => [
|
||||
{
|
||||
capabilityId: "release.apply_facts",
|
||||
input: input(target),
|
||||
status: "succeeded",
|
||||
},
|
||||
];
|
||||
|
||||
async function grant(allowedEntityId = entityId, action = "release.apply_facts") {
|
||||
return h.db
|
||||
.insertInto("automation_grant")
|
||||
.values({
|
||||
action,
|
||||
scope: JSON.stringify({ mediaEntityIds: [allowedEntityId] }),
|
||||
granted_by: "admin:test",
|
||||
reason: "只补全资源事实",
|
||||
})
|
||||
.returning("id")
|
||||
.executeTakeFirstOrThrow();
|
||||
}
|
||||
|
||||
function invoke(target: Awaited<ReturnType<typeof seed>>, runId: string) {
|
||||
return h.ctx.capabilities.invoke("release.apply_facts", input(target), {
|
||||
actorId: "system:auto-recover",
|
||||
taskId: randomUUID(),
|
||||
scopeIds: [],
|
||||
signal: new AbortController().signal,
|
||||
source: "agent",
|
||||
agentRunId: runId,
|
||||
invocationId: randomUUID(),
|
||||
});
|
||||
}
|
||||
|
||||
it("事项与资源必须一致,拒绝后没有写入或重评事件", async () => {
|
||||
const first = await seed();
|
||||
const second = await seed();
|
||||
const before = await releaseOf(second.releaseId);
|
||||
await expect(applyReleaseFacts(h.ctx, { ...input(first), sourceReleaseId: second.releaseId })).rejects.toThrow(
|
||||
/事项.*资源.*不一致/,
|
||||
);
|
||||
expect((await releaseOf(second.releaseId)).raw).toEqual(before.raw);
|
||||
expect((await decisionOf(first.decisionId)).status).toBe("open");
|
||||
expect(await eventsOf(second.releaseId)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("只填事项里仍缺失的字段,已知字段及未请求字段均不能覆盖", async () => {
|
||||
const target = await seed({ title: "Silent Meridian 2025 2160p", missing: ["resolution", "codec"] });
|
||||
const before = await releaseOf(target.releaseId);
|
||||
await expect(applyReleaseFacts(h.ctx, input(target, { resolution: "1080p", videoCodec: "H.264" }))).rejects.toThrow(
|
||||
/只能补全/,
|
||||
);
|
||||
await expect(applyReleaseFacts(h.ctx, input(target, { source: "WEB-DL" }))).rejects.toThrow(/只能补全/);
|
||||
expect((await releaseOf(target.releaseId)).raw).toEqual(before.raw);
|
||||
expect(parsedOf(await releaseOf(target.releaseId))).toMatchObject({ resolution: "2160p" });
|
||||
expect(await eventsOf(target.releaseId)).toHaveLength(0);
|
||||
expect(await applyReleaseFacts(h.ctx, input(target, { videoCodec: "H.264" }))).toEqual({
|
||||
resolved: true,
|
||||
remaining: [],
|
||||
});
|
||||
expect(parsedOf(await releaseOf(target.releaseId))).toMatchObject({ resolution: "2160p", videoCodec: "H.264" });
|
||||
});
|
||||
|
||||
it.each(["dismissed", "expired"] as const)("%s 的事项不能再写事实或重新打开", async (status) => {
|
||||
const target = await seed({ status });
|
||||
const before = await releaseOf(target.releaseId);
|
||||
await expect(applyReleaseFacts(h.ctx, input(target))).rejects.toThrow(/已关闭/);
|
||||
expect((await releaseOf(target.releaseId)).raw).toEqual(before.raw);
|
||||
expect((await decisionOf(target.decisionId)).status).toBe(status);
|
||||
expect(await eventsOf(target.releaseId)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("事实与出处持久化,解析版本过期仍保留事实和规范季集,后来的明确解析优先", async () => {
|
||||
const target = await seed({ title: "Silent Meridian S01E25", missing: ["resolution", "codec", "source", "group"] });
|
||||
await h.db
|
||||
.updateTable("source_release")
|
||||
.set({
|
||||
episode_resolution: JSON.stringify({
|
||||
method: "absolute_cross_season",
|
||||
canonical: { season: 2, episodes: { start: 13, end: 13 } },
|
||||
}),
|
||||
})
|
||||
.where("id", "=", target.releaseId)
|
||||
.execute();
|
||||
const facts = { resolution: "1080p", videoCodec: "H.264", source: "BluRay", releaseGroup: "EvidenceTeam" } as const;
|
||||
expect(await applyReleaseFacts(h.ctx, input(target, facts))).toEqual({ resolved: true, remaining: [] });
|
||||
const stored = await releaseOf(target.releaseId);
|
||||
expect(stored.raw).toMatchObject({
|
||||
_site: { marker: "snapshot" },
|
||||
_detail: { imdbId: "tt1234567" },
|
||||
_facts: {
|
||||
values: facts,
|
||||
evidence: { resolution: { text: EVIDENCE, url: "https://facts.example.test/details.php?id=1" } },
|
||||
},
|
||||
});
|
||||
expect(parsedOf(stored)).toMatchObject({
|
||||
resolution: "1080p",
|
||||
videoCodec: "H.264",
|
||||
source: "BluRay",
|
||||
isEncode: true,
|
||||
releaseGroup: { name: "EvidenceTeam" },
|
||||
});
|
||||
await h.db.updateTable("source_release").set({ parser_version: "0" }).where("id", "=", target.releaseId).execute();
|
||||
expect(parsedOf(await releaseOf(target.releaseId))).toMatchObject({
|
||||
resolution: "1080p",
|
||||
videoCodec: "H.264",
|
||||
source: "BluRay",
|
||||
releaseGroup: { name: "EvidenceTeam" },
|
||||
season: 2,
|
||||
episodes: { start: 13, end: 13 },
|
||||
seasonInferred: false,
|
||||
});
|
||||
const reparsed = toParsedColumns(parseRelease("Silent Meridian S02E13 2160p WEB-DL H.265"));
|
||||
await h.db
|
||||
.updateTable("source_release")
|
||||
.set({ parsed: JSON.stringify(reparsed.parsed), parser_version: reparsed.parserVersion })
|
||||
.where("id", "=", target.releaseId)
|
||||
.execute();
|
||||
expect(parsedOf(await releaseOf(target.releaseId))).toMatchObject({
|
||||
resolution: "2160p",
|
||||
videoCodec: "H.265",
|
||||
source: "WEB-DL",
|
||||
isEncode: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("原生站点字段刷新后空补丁可以关闭事项,并触发原资源重评", async () => {
|
||||
const target = await seed({ missing: ["size", "seeders"] });
|
||||
await h.db
|
||||
.updateTable("source_release")
|
||||
.set({ size_bytes: 400_000_000, seeders: 0 })
|
||||
.where("id", "=", target.releaseId)
|
||||
.execute();
|
||||
expect(await applyReleaseFacts(h.ctx, input(target, {}))).toEqual({ resolved: true, remaining: [] });
|
||||
expect((await decisionOf(target.decisionId)).status).toBe("resolved");
|
||||
expect(await eventsOf(target.releaseId)).toEqual([
|
||||
expect.objectContaining({
|
||||
payload: expect.objectContaining({
|
||||
sourceReleaseId: target.releaseId,
|
||||
mediaEntityId: entityId,
|
||||
contentUnitIds: [],
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
expect((await checkRecovery(h.ctx, actionOf(target))).state).toBe("recovered");
|
||||
});
|
||||
|
||||
it("部分补全保持待处理,补齐后恢复;重复调用不会重复发重评事件", async () => {
|
||||
const target = await seed({ missing: ["resolution", "codec"] });
|
||||
expect(await applyReleaseFacts(h.ctx, input(target))).toEqual({ resolved: false, remaining: ["codec"] });
|
||||
expect((await decisionOf(target.decisionId)).status).toBe("open");
|
||||
expect((await checkRecovery(h.ctx, actionOf(target))).state).toBe("pending");
|
||||
const finalInput = input(target, { videoCodec: "H.264" });
|
||||
expect(await applyReleaseFacts(h.ctx, finalInput)).toEqual({ resolved: true, remaining: [] });
|
||||
expect((await checkRecovery(h.ctx, actionOf(target))).state).toBe("recovered");
|
||||
const events = await eventsOf(target.releaseId);
|
||||
expect(events).toHaveLength(2);
|
||||
expect(await applyReleaseFacts(h.ctx, finalInput)).toEqual({ resolved: true, remaining: [] });
|
||||
expect(await eventsOf(target.releaseId)).toEqual(events);
|
||||
});
|
||||
|
||||
it("部分完成后的同值补丁可以重试,保留事实和原出处;不同值仍拒绝", async () => {
|
||||
const target = await seed({ missing: ["resolution", "size"] });
|
||||
const result = { resolved: false, remaining: ["size"] };
|
||||
expect(await applyReleaseFacts(h.ctx, input(target))).toEqual(result);
|
||||
const stored = await releaseOf(target.releaseId);
|
||||
const events = await eventsOf(target.releaseId);
|
||||
expect(events).toHaveLength(1);
|
||||
expect(stored.raw).toMatchObject({
|
||||
_facts: {
|
||||
values: { resolution: "1080p" },
|
||||
evidence: { resolution: { text: EVIDENCE, url: "https://facts.example.test/details.php?id=1" } },
|
||||
},
|
||||
});
|
||||
|
||||
const retry = {
|
||||
...input(target),
|
||||
evidence: "重试提供了另一段说明,不应覆盖第一次补全的出处。",
|
||||
evidenceUrl: "https://facts.example.test/retry",
|
||||
};
|
||||
expect(await applyReleaseFacts(h.ctx, retry)).toEqual(result);
|
||||
expect((await releaseOf(target.releaseId)).raw).toEqual(stored.raw);
|
||||
expect(await eventsOf(target.releaseId)).toEqual(events);
|
||||
expect((await decisionOf(target.decisionId)).status).toBe("open");
|
||||
expect((await checkRecovery(h.ctx, actionOf(target))).state).toBe("pending");
|
||||
|
||||
await expect(applyReleaseFacts(h.ctx, input(target, { resolution: "2160p" }))).rejects.toThrow(/只能补全/);
|
||||
expect((await releaseOf(target.releaseId)).raw).toEqual(stored.raw);
|
||||
expect(parsedOf(await releaseOf(target.releaseId)).resolution).toBe("1080p");
|
||||
expect(await eventsOf(target.releaseId)).toEqual(events);
|
||||
expect((await decisionOf(target.decisionId)).status).toBe("open");
|
||||
});
|
||||
|
||||
it("先补分辨率、后由原生详情补容量:空补丁完成时另发重评事件", async () => {
|
||||
const target = await seed({ missing: ["resolution", "size"] });
|
||||
expect(await applyReleaseFacts(h.ctx, input(target))).toEqual({ resolved: false, remaining: ["size"] });
|
||||
const partialEvents = await eventsOf(target.releaseId);
|
||||
expect(partialEvents).toHaveLength(1);
|
||||
expect((await decisionOf(target.decisionId)).status).toBe("open");
|
||||
|
||||
await h.db
|
||||
.updateTable("source_release")
|
||||
.set({ size_bytes: 400_000_000 })
|
||||
.where("id", "=", target.releaseId)
|
||||
.execute();
|
||||
expect(await applyReleaseFacts(h.ctx, input(target, {}))).toEqual({ resolved: true, remaining: [] });
|
||||
const completeEvents = await eventsOf(target.releaseId);
|
||||
expect(completeEvents).toHaveLength(2);
|
||||
expect(completeEvents.map((event) => event.idempotency_key)).not.toContain(null);
|
||||
expect(new Set(completeEvents.map((event) => event.idempotency_key)).size).toBe(2);
|
||||
expect((await decisionOf(target.decisionId)).status).toBe("resolved");
|
||||
expect(parsedOf(await releaseOf(target.releaseId))).toMatchObject({ resolution: "1080p", sizeBytes: 400_000_000 });
|
||||
expect((await checkRecovery(h.ctx, actionOf(target))).state).toBe("recovered");
|
||||
expect(await applyReleaseFacts(h.ctx, input(target, {}))).toEqual({ resolved: true, remaining: [] });
|
||||
expect(await eventsOf(target.releaseId)).toEqual(completeEvents);
|
||||
});
|
||||
|
||||
it("补出的事实超出原收录范围时仍拒绝,事实能力不接受策略字段", async () => {
|
||||
const target = await seed();
|
||||
const spec: SlotSpec = { name: "1080p", hard: { resolutions: ["1080p"] }, unknownPolicy: "hold" };
|
||||
const original = structuredClone(spec);
|
||||
expect(evaluateCandidate(spec, parsedOf(await releaseOf(target.releaseId))).decision).toBe("unknown_field");
|
||||
await applyReleaseFacts(h.ctx, input(target, { resolution: "2160p" }));
|
||||
expect(evaluateCandidate(spec, parsedOf(await releaseOf(target.releaseId)))).toMatchObject({
|
||||
decision: "reject",
|
||||
violations: [{ field: "resolution", code: "not_allowed" }],
|
||||
});
|
||||
expect(spec).toEqual(original);
|
||||
expect(
|
||||
ReleaseApplyFactsInput.safeParse({ ...input(target), facts: { resolution: "2160p", priority: 100 } }).success,
|
||||
).toBe(false);
|
||||
expect(ReleaseApplyFactsInput.safeParse({ ...input(target), resolutions: ["2160p"] }).success).toBe(false);
|
||||
});
|
||||
|
||||
it("无事实授权时只调查,身份授权和其他作品的事实授权不能代替", async () => {
|
||||
const target = await seed();
|
||||
const investigation = await autoInvestigate(h.ctx, target.decisionId);
|
||||
expect(investigation).toMatchObject({ created: true, mode: "investigate" });
|
||||
await expect(invoke(target, investigation.runId!)).rejects.toThrow(/授权/);
|
||||
await grant(entityId, "policy.identity");
|
||||
await expect(invoke(target, investigation.runId!)).rejects.toThrow(/授权/);
|
||||
await grant(anotherEntityId);
|
||||
await expect(invoke(target, investigation.runId!)).rejects.toThrow(/授权|作品/);
|
||||
expect(parsedOf(await releaseOf(target.releaseId)).resolution).toBeUndefined();
|
||||
expect(await eventsOf(target.releaseId)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("资源事实会话只开放读取,有授权时只增加事实补全能力", async () => {
|
||||
const readTarget = await seed();
|
||||
const read = await autoInvestigate(h.ctx, readTarget.decisionId);
|
||||
expect(read).toMatchObject({ created: true, mode: "investigate" });
|
||||
const readRun = await h.db
|
||||
.selectFrom("agent_run")
|
||||
.selectAll()
|
||||
.where("id", "=", read.runId!)
|
||||
.executeTakeFirstOrThrow();
|
||||
const readCapabilities = (readRun.scope as { capabilities: string[] }).capabilities;
|
||||
expect(readCapabilities).toEqual(
|
||||
expect.arrayContaining(["release.inspect", "decision.list", "web.search", "web.fetch"]),
|
||||
);
|
||||
expect(readCapabilities.filter((id) => CAPABILITY_CATALOG[id as KnownCapabilityId]?.kind !== "read")).toEqual([]);
|
||||
expect(readCapabilities).not.toContain("release.apply_facts");
|
||||
|
||||
await grant();
|
||||
const recoverTarget = await seed();
|
||||
const recover = await autoInvestigate(h.ctx, recoverTarget.decisionId);
|
||||
expect(recover).toMatchObject({ created: true, mode: "recover" });
|
||||
const recoverRun = await h.db
|
||||
.selectFrom("agent_run")
|
||||
.selectAll()
|
||||
.where("id", "=", recover.runId!)
|
||||
.executeTakeFirstOrThrow();
|
||||
const recoverCapabilities = (recoverRun.scope as { capabilities: string[] }).capabilities;
|
||||
expect(recoverCapabilities.filter((id) => id !== "release.apply_facts").sort()).toEqual(
|
||||
[...readCapabilities].sort(),
|
||||
);
|
||||
expect(recoverCapabilities.filter((id) => CAPABILITY_CATALOG[id as KnownCapabilityId]?.kind !== "read")).toEqual([
|
||||
"release.apply_facts",
|
||||
]);
|
||||
for (const id of [
|
||||
"identity.resolve",
|
||||
"task.retry",
|
||||
"download.recheck",
|
||||
"episode_mapping.apply_rule",
|
||||
"policy.apply",
|
||||
"download.submit",
|
||||
]) {
|
||||
expect(readCapabilities).not.toContain(id);
|
||||
expect(recoverCapabilities).not.toContain(id);
|
||||
}
|
||||
});
|
||||
|
||||
it("有持续授权时仅写本次调查资源,恢复动作进入账本并按持久事实核实", async () => {
|
||||
await grant();
|
||||
const target = await seed();
|
||||
const other = await seed();
|
||||
const investigation = await autoInvestigate(h.ctx, target.decisionId);
|
||||
expect(investigation).toMatchObject({ created: true, mode: "recover" });
|
||||
await expect(invoke(other, investigation.runId!)).rejects.toThrow(/当前调查事项/);
|
||||
expect(await invoke(target, investigation.runId!)).toEqual({ resolved: true, remaining: [] });
|
||||
const acts = await recoveryActions(h.ctx, investigation.runId!);
|
||||
expect(acts.actions.filter((action) => action.status === "succeeded")).toEqual([
|
||||
expect.objectContaining({
|
||||
capabilityId: "release.apply_facts",
|
||||
input: expect.objectContaining({ decisionId: target.decisionId, sourceReleaseId: target.releaseId }),
|
||||
}),
|
||||
]);
|
||||
expect((await checkRecovery(h.ctx, acts.actions)).state).toBe("recovered");
|
||||
expect(parsedOf(await releaseOf(target.releaseId)).resolution).toBe("1080p");
|
||||
expect(parsedOf(await releaseOf(other.releaseId)).resolution).toBeUndefined();
|
||||
expect((await decisionOf(other.decisionId)).status).toBe("open");
|
||||
expect(await eventsOf(target.releaseId)).toHaveLength(1);
|
||||
expect(await eventsOf(other.releaseId)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("事项状态单独为 resolved 不足以证明缺失事实已经恢复", async () => {
|
||||
const target = await seed({ status: "resolved" });
|
||||
expect(parsedOf(await releaseOf(target.releaseId)).resolution).toBeUndefined();
|
||||
expect((await checkRecovery(h.ctx, actionOf(target))).state).toBe("pending");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user