refactor(core): migrate to Bun and converge acquisition policies and Agent boundaries

This commit is contained in:
root committed 2026-10-13 14:37:03 +08:00
1 parent 3f1c6d02f6
commit 27c0f27434
268 files changed
+7468 -7852

No files matched your search

@@ -99,8 +99,8 @@ describe.skipIf(!up)("元数据语言回刷", () => {
.values({ key: "metadata", value: JSON.stringify({ tmdbBaseUrl: `${tmdb.url}/3` }) } as never)
.onConflict((oc) => oc.column("key").doUpdateSet({ value: JSON.stringify({ tmdbBaseUrl: `${tmdb.url}/3` }) }))
.execute();
const prev = Deno.env.get("TMDB_API_KEY");
Deno.env.set("TMDB_API_KEY", "test-tmdb-key");
const prev = process.env.TMDB_API_KEY;
process.env.TMDB_API_KEY = "test-tmdb-key";
try {
const rt = new SourcesRuntime(h.ctx, {});
const t0 = performance.now();
@@ -112,8 +112,8 @@ describe.skipIf(!up)("元数据语言回刷", () => {
const second = await backfillLanguages(rt, { minIntervalMs: 100 });
expect(second).toMatchObject({ refreshed: 2, requests: 0 });
} finally {
if (prev === undefined) Deno.env.delete("TMDB_API_KEY");
else Deno.env.set("TMDB_API_KEY", prev);
if (prev === undefined) delete process.env.TMDB_API_KEY;
else process.env.TMDB_API_KEY = prev;
}
});
@@ -118,7 +118,7 @@ describe.skipIf(!up)("CF-2 Cloudflare 级联", () => {
}
cf.passed++;
return globalThis.fetch(input, init);
}) as typeof fetch;
}) as (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
const site = () => h.db.selectFrom("site").selectAll().where("id", "=", siteId).executeTakeFirstOrThrow();
const account = () =>
@@ -285,7 +285,7 @@ describe.skipIf(!up)("DH-1 内容单元数据治理", () => {
const u = new URL(PG_URL);
u.searchParams.set("options", `-c search_path=${schema},public`);
const run = (...args: string[]) =>
execFileSync(Deno.execPath(), ["run", "-A", SCRIPT, "--merge-duplicates", ...args], {
execFileSync(process.execPath, [SCRIPT, "--merge-duplicates", ...args], {
cwd: ROOT,
env: { ...process.env, DATABASE_URL: u.toString(), NO_COLOR: "1" },
encoding: "utf8",
@@ -1,4 +1,5 @@
import { DEFAULT_SETTINGS } from "@umt/contracts";
import { type HttpServer, serve } from "@umt/runtime";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import {
directHttpClient,
@@ -27,66 +28,58 @@ const PROXY_KEYS = [
];
describe("出口选择(直连 / 代理)", () => {
let proxy: Deno.HttpServer<Deno.NetAddr>;
let target: Deno.HttpServer<Deno.NetAddr>;
let proxy: HttpServer;
let target: HttpServer;
let proxyUrl = "";
let targetUrl = "";
const proxied: string[] = [];
const saved = new Map<string, string | undefined>();
beforeAll(() => {
proxy = Deno.serve({ hostname: "127.0.0.1", port: 0, onListen() {} }, (req) => {
proxy = serve({ hostname: "127.0.0.1", port: 0, onListen() {} }, (req) => {
proxied.push(req.url);
return new Response("via-proxy");
});
target = Deno.serve({ hostname: "127.0.0.1", port: 0, onListen() {} }, () => new Response("direct"));
target = serve({ hostname: "127.0.0.1", port: 0, onListen() {} }, () => new Response("direct"));
proxyUrl = `http://127.0.0.1:${proxy.addr.port}`;
targetUrl = `http://127.0.0.1:${target.addr.port}/x`;
for (const k of PROXY_KEYS) {
saved.set(k, Deno.env.get(k));
Deno.env.delete(k);
saved.set(k, process.env[k]);
delete process.env[k];
}
// 模拟容器环境:HTTP_PROXY 指向"中继",没有 NO_PROXY
Deno.env.set("HTTP_PROXY", proxyUrl);
process.env.HTTP_PROXY = proxyUrl;
});
afterAll(async () => {
for (const [k, v] of saved) {
if (v === undefined) Deno.env.delete(k);
else Deno.env.set(k, v);
if (v === undefined) delete process.env[k];
else process.env[k] = v;
}
await proxy.shutdown();
await target.shutdown();
});
const body = async (f: typeof fetch) => (await f(targetUrl)).text();
it("Deno 2.9 语义:createHttpClient({}) 仍会读 HTTP_PROXY(所以直连要显式做)", async () => {
const implicit = Deno.createHttpClient({});
try {
expect(await body(fetchVia(implicit))).toBe("via-proxy");
} finally {
implicit.close();
}
});
const body = async (f: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>) =>
(await f(targetUrl)).text();
it("directHttpClient:环境变量里有代理也直连;创建后环境变量原样还原", async () => {
proxied.length = 0;
expect(await body(fetchVia(directHttpClient()))).toBe("direct");
expect(proxied).toEqual([]);
expect(Deno.env.get("HTTP_PROXY")).toBe(proxyUrl);
expect(process.env.HTTP_PROXY).toBe(proxyUrl);
});
it("proxyHttpClient:只走给定代理(NO_PROXY 命中也不绕过)", async () => {
Deno.env.set("NO_PROXY", "127.0.0.1");
process.env.NO_PROXY = "127.0.0.1";
try {
proxied.length = 0;
const client = proxyHttpClient(proxyUrl);
expect(await body(fetchVia(client))).toBe("via-proxy");
expect(proxied).toHaveLength(1);
expect(Deno.env.get("NO_PROXY")).toBe("127.0.0.1");
expect(process.env.NO_PROXY).toBe("127.0.0.1");
} finally {
Deno.env.delete("NO_PROXY");
delete process.env.NO_PROXY;
}
});
@@ -64,7 +64,7 @@ const mockFetch = (async (input: string | URL | Request) => {
if (url.href === EVIL)
return new Response("<html><title>x</title><body>x</body></html>", { headers: { "content-type": "text/html" } });
return new Response("not found", { status: 404 });
}) as typeof fetch;
}) as (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
const resolveHost = async () => ["93.184.216.34"];
/** fake 模型:照网页给值,简介是改写过的(不是原文),海报给一个网页里没有的地址(应被丢弃) */
@@ -360,7 +360,7 @@ describe.skipIf(!pg)("META-1 元数据补全", () => {
return new Response("User-agent: *\nDisallow: /series/\n", { headers: { "content-type": "text/plain" } });
if (u.hostname === "robots-b.example.com") return new Response("err", { status: 503 });
return new Response("nf", { status: 404 });
}) as typeof fetch;
}) as (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
const o = { fetch: f, resolveHost };
expect(await robotsAllowedFor(h.ctx, "https://robots-a.example.com/series/1", o)).toBe(false);
expect(await robotsAllowedFor(h.ctx, "https://robots-a.example.com/detail?id=1", o)).toBe(true);
@@ -42,7 +42,7 @@ describe.skipIf(!up)("站点失败留样", () => {
new Response(page, {
status: 200,
headers: { "content-type": "text/html; charset=utf-8", server: "nginx" },
})) as typeof fetch;
})) as (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
const site = () => h.db.selectFrom("site").selectAll().where("id", "=", siteId).executeTakeFirstOrThrow();
const rows = () =>
h.db.selectFrom("site_failure_sample").selectAll().where("site_id", "=", siteId).orderBy("created_at").execute();
@@ -18,7 +18,7 @@ describe.skipIf(!up)("identity-cleanup 孤立单元判定", () => {
let mappedNoLayout: string;
const run = (...args: string[]) =>
execFileSync(Deno.execPath(), ["run", "-A", SCRIPT, ...args], {
execFileSync(process.execPath, [SCRIPT, ...args], {
cwd: ROOT,
env: { ...process.env, DATABASE_URL: url, NO_COLOR: "1" },
encoding: "utf8",
@@ -123,7 +123,9 @@ export function trimTmdb(body: unknown): unknown {
}
/** 离线回放:按键取录制的响应;没录到的请求直接报错(说明召回逻辑变了,需要重新录制) */
export function fixtureFetch(fixtures: Record<string, unknown>): typeof fetch {
export function fixtureFetch(
fixtures: Record<string, unknown>,
): (input: RequestInfo | URL, init?: RequestInit) => Promise<Response> {
return (async (input: string | URL | Request) => {
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
const key = fixtureKey(url);
@@ -134,10 +136,13 @@ export function fixtureFetch(fixtures: Record<string, unknown>): typeof fetch {
status,
headers: { "content-type": "application/json" },
});
}) as typeof fetch;
}) as (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
}
export function tmdbFor(fetchImpl: typeof fetch, baseUrl = "http://tmdb.fixture/3"): TmdbClient {
export function tmdbFor(
fetchImpl: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>,
baseUrl = "http://tmdb.fixture/3",
): TmdbClient {
return new TmdbClient({ apiKey: "x", baseUrl, fetch: fetchImpl, retries: 0 });
}
@@ -2,7 +2,7 @@
* 重新录制身份匹配差分(ID-2):我们侧的 TMDB 请求走本地缓存代理(scratchpad tmdb_proxy.py,上行计数封顶),
* 录下的响应裁剪后写入 tmdb-fixtures.json,逐条结果写入 ours.json 并打印汇总。
*
* TMDB_PROXY=http://127.0.0.1:18731 deno run -A apps/server/test/sources-identity/identity-differential.run.ts
* TMDB_PROXY=http://127.0.0.1:18731 bun apps/server/test/sources-identity/identity-differential.run.ts
*/
import { writeFileSync } from "node:fs";
import { join } from "node:path";
@@ -16,9 +16,9 @@ import {
trimTmdb,
} from "./identity-differential.lib.ts";
const proxy = Deno.env.get("TMDB_PROXY") ?? "http://127.0.0.1:18731";
const proxy = process.env.TMDB_PROXY ?? "http://127.0.0.1:18731";
const fixtures: Record<string, unknown> = {};
const recording: typeof fetch = async (input, init) => {
const recording: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response> = async (input, init) => {
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
const res = await fetch(url, init);
const body = await res.clone().json();
@@ -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");
});
});
@@ -15,8 +15,13 @@ const TiB = 1024 ** 4;
const SITE_IDS = ["site.list", "site.health_check", "site.refresh_login", "site.user_data"] as const;
describe("站点能力规格", () => {
it("进入能力目录(41 个已有能力 + 整理元数据补全 + 起草巡检 + 集数映射 3 个 + 指定整理作品);类别 / 风险 / 重放按约定", () => {
expect(CAPABILITY_IDS).toHaveLength(47);
it("进入能力目录(41 个已有能力 + 整理元数据补全 + 起草巡检 + 集数映射 3 个 + 指定整理作品 + 资源属性补全);类别 / 风险 / 重放按约定", () => {
expect(CAPABILITY_IDS).toHaveLength(48);
expect(capabilitySpec("release.apply_facts")).toMatchObject({
kind: "execute",
riskLevel: "medium",
replay: "safe",
});
expect(capabilitySpec("episode_mapping.inspect")).toMatchObject({ kind: "read", replay: "safe" });
expect(capabilitySpec("episode_mapping.simulate")).toMatchObject({ kind: "preview", replay: "safe" });
expect(capabilitySpec("episode_mapping.apply_rule")).toMatchObject({
@@ -380,7 +380,7 @@ describe.skipIf(!up)("站点中心(馒头 API 基址)", () => {
return new Response(JSON.stringify({ code: "0", data: { id: 7, username: "mt", memberCount: {} } }), {
headers: { "content-type": "application/json" },
});
}) as typeof globalThis.fetch;
}) as (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
const h = await createHarness("sitecenter_mtbase", { fetch });
try {
const site = await json(
@@ -18,7 +18,10 @@ describe.skipIf(!up)("快照截止时间", () => {
let h: Harness;
let siteId: string;
// 永不返回,也不理会 signal
const hang = (() => new Promise<Response>(() => {})) as typeof fetch;
const hang = (() => new Promise<Response>(() => {})) as (
input: RequestInfo | URL,
init?: RequestInit,
) => Promise<Response>;
beforeAll(async () => {
h = await createHarness("snapdeadline", { fetch: hang });
@@ -70,7 +70,7 @@ describe.skipIf(!up)("TMDB 多语言保留规则", () => {
const actor = { type: "user" as const, id: "tester" };
const run = (...args: string[]) =>
execFileSync(Deno.execPath(), ["run", "-A", SCRIPT, ...args], {
execFileSync(process.execPath, [SCRIPT, ...args], {
cwd: ROOT,
env: { ...process.env, DATABASE_URL: url, NO_COLOR: "1" },
encoding: "utf8",