feat(sources,organize): site agent capabilities and organize.minSizeBytes setting (W10-pre2)
- site.list / site.health_check / site.refresh_login / site.user_data capabilities over the W9-A site center services; outputs whitelisted (no secret refs, cookies, passwords, login names); execute kinds need automation grant. IDs use snake_case (CapabilityId regex); registry count 17 -> 21. - settings.organize.minSizeBytes (default 50 MiB); file-manager identify / organize and planImport take request > setting > port default. - tests and runbook notes (sites, files). Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
c08d8ce62a
commit
81bf010e3e
18 files changed
+563
-15
No files matched your search
@@ -144,7 +144,12 @@ export async function createFmHarness(
|
||||
const out: Record<string, unknown> = {};
|
||||
for (const k of Object.keys(DEFAULT_SETTINGS) as SettingsKey[]) {
|
||||
const v = stored.get(k);
|
||||
out[k] = GlobalSettings.shape[k].parse({ ...DEFAULT_SETTINGS[k], ...((v as object) ?? {}) });
|
||||
// 整理最小体积缺省跟随端口注入的 minVideoBytes(测试用小文件)
|
||||
const base =
|
||||
k === "organize"
|
||||
? { ...DEFAULT_SETTINGS.organize, minSizeBytes: overrides.ao?.options?.minVideoBytes ?? 10 }
|
||||
: DEFAULT_SETTINGS[k];
|
||||
out[k] = GlobalSettings.shape[k].parse({ ...base, ...((v as object) ?? {}) });
|
||||
}
|
||||
return out as GlobalSettings;
|
||||
},
|
||||
|
||||
@@ -53,7 +53,7 @@ describe.skipIf(skip)("files 识别 / 整理 / 探测 / 刮削", () => {
|
||||
const r = await h.api(
|
||||
"POST",
|
||||
"/files/identify",
|
||||
{ items: [{ root: h.dlRoot, path: "Some.Show.S01.1080p.WEB-DL" }] },
|
||||
{ items: [{ root: h.dlRoot, path: "Some.Show.S01.1080p.WEB-DL" }], minSizeBytes: 0 },
|
||||
READ_KEY,
|
||||
);
|
||||
expect(r.status).toBe(200);
|
||||
@@ -72,6 +72,7 @@ describe.skipIf(skip)("files 识别 / 整理 / 探测 / 刮削", () => {
|
||||
it("identify:远端候选(catalog.lookup)", async () => {
|
||||
const r = await h.api("POST", "/files/identify", {
|
||||
items: [{ root: h.dlRoot, path: "Remote.Film.2019.1080p.mkv" }],
|
||||
minSizeBytes: 0,
|
||||
});
|
||||
expect(r.status).toBe(200);
|
||||
const c = r.body.data.items[0].candidates;
|
||||
@@ -82,7 +83,12 @@ describe.skipIf(skip)("files 识别 / 整理 / 探测 / 刮削", () => {
|
||||
await mkdir(join(h.dl, "many"), { recursive: true });
|
||||
const names = Array.from({ length: 23 }, (_, i) => `Distinct Title ${String.fromCharCode(65 + i)}.2019.1080p.mkv`);
|
||||
for (const n of names) await writeFile(join(h.dl, "many", n), "x");
|
||||
const r = await h.api("POST", "/files/identify", { items: [{ root: h.dlRoot, path: "many" }] }, READ_KEY);
|
||||
const r = await h.api(
|
||||
"POST",
|
||||
"/files/identify",
|
||||
{ items: [{ root: h.dlRoot, path: "many" }], minSizeBytes: 0 },
|
||||
READ_KEY,
|
||||
);
|
||||
expect(r.status).toBe(200);
|
||||
expect(r.body.data.items).toHaveLength(23);
|
||||
const capped = r.body.data.items.filter((i: { warnings: string[] }) => i.warnings.some((w) => w.includes("上限")));
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { basename, join } from "node:path";
|
||||
import { DEFAULT_SETTINGS, GlobalSettings } from "@umt/contracts";
|
||||
import { sql } from "@umt/db";
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import { createFmHarness, EXEC_KEY, type FmHarness, key, pgAvailable, READ_KEY, seedEntity } from "./harness.ts";
|
||||
|
||||
// W10-pre2:settings.organize.minSizeBytes(缺省 50 MiB)是识别与整理(planImport)的默认最小体积;请求 / 向导给的值优先。
|
||||
|
||||
const skip = !(await pgAvailable());
|
||||
|
||||
describe("settings.organize.minSizeBytes 契约", () => {
|
||||
it("缺省 50 MiB;非负整数;旧的 organize 设置补缺省后仍合法", () => {
|
||||
expect(DEFAULT_SETTINGS.organize.minSizeBytes).toBe(50 * 1024 * 1024);
|
||||
expect(GlobalSettings.shape.organize.safeParse({ ...DEFAULT_SETTINGS.organize, minSizeBytes: -1 }).success).toBe(
|
||||
false,
|
||||
);
|
||||
expect(
|
||||
GlobalSettings.shape.organize.parse({ ...DEFAULT_SETTINGS.organize, defaultMode: "move" }).minSizeBytes,
|
||||
).toBe(50 * 1024 * 1024);
|
||||
});
|
||||
});
|
||||
|
||||
describe.skipIf(skip)("最小体积读设置(W10-pre2)", () => {
|
||||
let h: FmHarness;
|
||||
let film: string;
|
||||
const setMin = (n: number) =>
|
||||
sql`insert into settings (key, value) values ('organize', ${JSON.stringify({ minSizeBytes: n })}::jsonb)
|
||||
on conflict (key) do update set value = excluded.value`.execute(h.db);
|
||||
|
||||
beforeAll(async () => {
|
||||
h = await createFmHarness("minsize");
|
||||
film = await seedEntity(h.db, { kind: "movie", title: "Size Film", year: 2022 });
|
||||
await writeFile(join(h.dl, "Size.Film.2022.2160p.BluRay.x265-GRP.mkv"), "x".repeat(64));
|
||||
await writeFile(join(h.dl, "Size.Film.2022.1080p.small.mkv"), "0123456789ab");
|
||||
});
|
||||
afterAll(async () => {
|
||||
await h?.close();
|
||||
});
|
||||
|
||||
const items = () => [
|
||||
{ root: h.dlRoot, path: "Size.Film.2022.2160p.BluRay.x265-GRP.mkv", mediaEntityId: film },
|
||||
{ root: h.dlRoot, path: "Size.Film.2022.1080p.small.mkv", mediaEntityId: film },
|
||||
];
|
||||
const organize = (body: Record<string, unknown>) =>
|
||||
h.api("POST", "/files/organize", { idempotencyKey: key(), action: "copy", dryRun: true, ...body }, EXEC_KEY);
|
||||
|
||||
it("整理:不给 minSizeBytes 时用设置值;请求给的值优先", async () => {
|
||||
await setMin(32);
|
||||
const r = await organize({ items: items() });
|
||||
expect(r.status).toBe(200);
|
||||
const g = r.body.data.groups[0];
|
||||
expect(g.minSizeBytes).toBe(32);
|
||||
expect(g.files.map((f: { source: string }) => basename(f.source))).toEqual([
|
||||
"Size.Film.2022.2160p.BluRay.x265-GRP.mkv",
|
||||
]);
|
||||
expect(g.excluded).toContainEqual(expect.objectContaining({ size: 12, reason: "below_min_size" }));
|
||||
|
||||
const over = await organize({ items: items(), minSizeBytes: 0 });
|
||||
expect(over.body.data.groups[0].excluded).toEqual([]);
|
||||
expect(over.body.data.groups[0].files).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("识别:不给 minSizeBytes 时按设置跳过小文件;请求 0 不过滤", async () => {
|
||||
await setMin(32);
|
||||
const body = { items: items().map(({ root, path }) => ({ root, path })) };
|
||||
const r = await h.api("POST", "/files/identify", body, READ_KEY);
|
||||
expect(r.status).toBe(200);
|
||||
expect(r.body.data.excludedBySize).toBe(1);
|
||||
const r0 = await h.api("POST", "/files/identify", { ...body, minSizeBytes: 0 }, READ_KEY);
|
||||
expect(r0.body.data.excludedBySize).toBe(0);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user