test: server 测试共用建库与假引擎,sse-pg 归入串行组

- 新增 apps/server/test/support/{pg,engine}.ts:测试库地址、可用性探测、schema 咨询锁、
  建 schema + 加锁迁移 + 删除、可选去重的假引擎;6 份 harness 改用它(acquisition-organize 与
  file-manager 原先迁移不加锁,并行下有冲突隐患,一并修正)
- sse-pg 起子进程,并行下子进程启动超时,归入 serial
- 修正 b61335e 挪动 / 改名测试后遗留的 import 排序与格式(当时 lint 检查经管道被吞掉退出码)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-10-02 17:40:43 +08:00
1 parent 7cc3742d31
commit c9e8120fc6
25 files changed
+254 -328

No files matched your search

@@ -1,10 +1,10 @@
import { sql } from "@umt/db";
import { createMockQb, createTempTree, type MockQb, makeTorrent, type TempTree } from "@umt/testkit";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { createSchedulesModule } from "../../src/modules/schedules/index.ts";
import { evaluateAcquisition } from "../../src/modules/acquisition-organize/services/acquisition.ts";
import { submitDownload, syncDownloads } from "../../src/modules/acquisition-organize/services/downloads.ts";
import { executeRecord, planImport } from "../../src/modules/acquisition-organize/services/organize.ts";
import { createSchedulesModule } from "../../src/modules/schedules/index.ts";
import {
createHarness,
fakeDisk,
@@ -3,10 +3,10 @@ import type { TmdbClient } from "@umt/adapters";
import { DEFAULT_CLASSIFICATION_YAML } from "@umt/domain";
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import type { AuthEnv } from "../../src/http/auth.ts";
import { classifyEntity } from "../../src/modules/acquisition-organize/services/classification.ts";
import { createSettingsReader } from "../../src/modules/auth-settings/provided.ts";
import { registerSettingsRoutes } from "../../src/modules/auth-settings/settings-routes.ts";
import { SourcesRuntime } from "../../src/modules/sources-identity/runtime.ts";
import { classifyEntity } from "../../src/modules/acquisition-organize/services/classification.ts";
import { createHarness, type Harness, pgAvailable, seedEntity } from "./harness.ts";
const hasPg = await pgAvailable();
@@ -3,7 +3,11 @@ import { createTempTree, type TempTree } from "@umt/testkit";
import { sql } from "kysely";
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import { enqueueSweep, enqueueTracked } from "../../src/modules/acquisition-organize/services/common.ts";
import { type QueueScrapeInput, queueScrape, runScrapeRecord } from "../../src/modules/acquisition-organize/services/scrape.ts";
import {
type QueueScrapeInput,
queueScrape,
runScrapeRecord,
} from "../../src/modules/acquisition-organize/services/scrape.ts";
import { createHarness, type Harness, pgAvailable, seedBase, seedEntity } from "./harness.ts";
const hasPg = await pgAvailable();
@@ -3,25 +3,28 @@ import { join } from "node:path";
import type { DiskUsage } from "@umt/adapters";
import { createQbClient } from "@umt/adapters";
import { DEFAULT_SETTINGS, GlobalSettings, type SettingsKey } from "@umt/contracts";
import { type DB, migrateToLatest } from "@umt/db";
import type { TaskEngine } from "@umt/tasks";
import { Kysely, PostgresDialect, sql } from "kysely";
import pg from "pg";
import type { DB } from "@umt/db";
import { type Kysely, sql } from "kysely";
import { ConfigSchema } from "../../src/config.ts";
import { createOutboxPort } from "../../src/context.ts";
import { createApp } from "../../src/http/app.ts";
import { hashApiKey } from "../../src/http/auth.ts";
import { EventBus } from "../../src/http/sse.ts";
import { createLogger } from "../../src/logger.ts";
import { type AoOverrides, createAcquisitionOrganizeModule } from "../../src/modules/acquisition-organize/index.ts";
import type { AoDeps } from "../../src/modules/acquisition-organize/services/common.ts";
import { createCapabilityRegistry } from "../../src/modules/agent/capability-registry.ts";
import { createSourcesIdentityModule } from "../../src/modules/sources-identity/index.ts";
import type { ServerContext } from "../../src/modules/types.ts";
import { type AoOverrides, createAcquisitionOrganizeModule } from "../../src/modules/acquisition-organize/index.ts";
import type { AoDeps } from "../../src/modules/acquisition-organize/services/common.ts";
import { fakeEngine } from "../support/engine.ts";
import { createTestSchema, PG_URL as DB_URL, uniqueSchema } from "../support/pg.ts";
// 真实 PG(独立 schema)+ mock qB + 临时目录 的测试装配;引擎用记录入队的假实现,测试直接驱动服务函数。
export const DB_URL = process.env.DATABASE_URL ?? "postgres://umt:umt@localhost:55433/umt";
export type { Enqueued } from "../support/engine.ts";
export { fakeEngine } from "../support/engine.ts";
export { pgAvailable } from "../support/pg.ts";
export { DB_URL };
export const DEV_KEY = "dev-admin-ao-test-key";
/** 只读整理记录 / 可发起整理 的 API Key */
export const READ_KEY = "ao-organize-read-key";
@@ -31,52 +34,6 @@ const KEYS = [
{ key: EXEC_KEY, scopes: ["organize.execute"] },
];
export async function pgAvailable() {
if (process.env.UMT_SKIP_DB_TESTS === "1") return false;
const c = new pg.Client({ connectionString: DB_URL, connectionTimeoutMillis: 2000 });
try {
await c.connect();
await c.end();
return true;
} catch {
return false;
}
}
export interface Enqueued {
task: string;
input: unknown;
key: string;
}
/** 记录入队、不执行的引擎(处理器由测试直接调服务函数) */
export function fakeEngine(): TaskEngine & { enqueued: Enqueued[] } {
const enqueued: Enqueued[] = [];
let n = 0;
return {
enqueued,
async enqueue(task, input, opts) {
enqueued.push({ task, input, key: opts.idempotencyKey });
n++;
return { runId: `run-${n}`, deduplicated: false };
},
async signal() {
return { delivered: 0 };
},
async cancel() {
return false;
},
async get() {
return null;
},
async list() {
return [];
},
async startWorker() {},
async stop() {},
};
}
/** 可调的磁盘用量:used = base + 目录下文件总大小(清理删文件后自然下降) */
export function fakeDisk(total: number) {
const state = { total, base: 0, watch: [] as string[], fail: false };
@@ -136,17 +93,8 @@ export async function createHarness(
/** withSources:同时装配 sources-identity 模块(待处理中心路由等) */
opts: { withSources?: boolean } = {},
): Promise<Harness> {
const schema = `ao_test_${name}_${process.pid.toString(36)}${Math.random().toString(36).slice(2, 7)}`;
const admin = new pg.Client({ connectionString: DB_URL });
await admin.connect();
await admin.query(`create schema ${schema}`);
await admin.end();
const db = new Kysely<DB>({
dialect: new PostgresDialect({
pool: new pg.Pool({ connectionString: DB_URL, max: 6, options: `-c search_path=${schema},public` }),
}),
});
await migrateToLatest(db, { migrationTableSchema: schema });
const ts = await createTestSchema(uniqueSchema(`ao_test_${name}`));
const { db } = ts;
const logger = createLogger({ level: "silent" });
const engine = fakeEngine();
const config = ConfigSchema.parse({
@@ -254,11 +202,7 @@ export async function createHarness(
return r.rows;
},
async close() {
await db.destroy();
const c = new pg.Client({ connectionString: DB_URL });
await c.connect();
await c.query(`drop schema if exists ${schema} cascade`);
await c.end();
await ts.drop();
},
};
}
@@ -4,7 +4,11 @@ import { readdir, readFile, rm, utimes, writeFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import { createTempTree, type TempTree } from "@umt/testkit";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { executeRecord, type FrozenPlan, planImport } from "../../src/modules/acquisition-organize/services/organize.ts";
import {
executeRecord,
type FrozenPlan,
planImport,
} from "../../src/modules/acquisition-organize/services/organize.ts";
import { createHarness, type Harness, pgAvailable, seedBase, seedEntity } from "./harness.ts";
// W4-E4 执行侧:replace_with_verify 接线(写临时→校验→原子替换,旧文件进回收目录)、写前清 partial、
@@ -14,7 +14,11 @@ import {
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { evaluateAcquisition } from "../../src/modules/acquisition-organize/services/acquisition.ts";
import { submitDownload, syncDownloads } from "../../src/modules/acquisition-organize/services/downloads.ts";
import { executeRecord, type FrozenPlan, planImport } from "../../src/modules/acquisition-organize/services/organize.ts";
import {
executeRecord,
type FrozenPlan,
planImport,
} from "../../src/modules/acquisition-organize/services/organize.ts";
import {
createHarness,
EXEC_KEY,
@@ -4,9 +4,9 @@ import type { TmdbClient } from "@umt/adapters";
import { DEFAULT_SETTINGS, type OrganizeSourceConfig } from "@umt/contracts";
import { createMockQb, createTempTree, type MockQb, type TempTree } from "@umt/testkit";
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import { SourcesRuntime } from "../../src/modules/sources-identity/runtime.ts";
import { syncDownloads } from "../../src/modules/acquisition-organize/services/downloads.ts";
import { executeRecord, planImport } from "../../src/modules/acquisition-organize/services/organize.ts";
import { SourcesRuntime } from "../../src/modules/sources-identity/runtime.ts";
import { createHarness, type Harness, pgAvailable, seedBase, seedEntity } from "./harness.ts";
const hasPg = await pgAvailable();
@@ -3,7 +3,12 @@ import { join, relative } from "node:path";
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";
import { entityScrapeLocations, runScrapeRecord, SCRAPE_TASK, scrapeEntity } from "../../src/modules/acquisition-organize/services/scrape.ts";
import {
entityScrapeLocations,
runScrapeRecord,
SCRAPE_TASK,
scrapeEntity,
} from "../../src/modules/acquisition-organize/services/scrape.ts";
import { createHarness, type Harness, pgAvailable, seedBase, seedEntity } from "./harness.ts";
// 刮削(SCR-1 / SCR-2):入库成功后自动对作品目录排 scrapeMedia → 写 NFO 与图片(临时库根);只补缺失 / 全部覆盖;
@@ -4,7 +4,11 @@ import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { evaluateAcquisition } from "../../src/modules/acquisition-organize/services/acquisition.ts";
import { submitDownload, syncDownloads } from "../../src/modules/acquisition-organize/services/downloads.ts";
import { executeRecord, planImport } from "../../src/modules/acquisition-organize/services/organize.ts";
import { applyEpisodeTotal, subscriptionProgress, sweepDueSlots } from "../../src/modules/acquisition-organize/services/subscriptions.ts";
import {
applyEpisodeTotal,
subscriptionProgress,
sweepDueSlots,
} from "../../src/modules/acquisition-organize/services/subscriptions.ts";
import { createHarness, fakeDisk, type Harness, pgAvailable, seedBase, seedEntity, seedRelease } from "./harness.ts";
// W5-C 订阅增强集成:Slot×单元多在途 + 忙时延后重评 + 移除释放;季包按缺集选文件(mock qB files/filePrio);
@@ -2,10 +2,14 @@ import { existsSync, readdirSync, statSync } from "node:fs";
import { join } from "node:path";
import { createMockQb, createTempTree, type MockQb, type TempTree } from "@umt/testkit";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { addAliasIfAbsent } from "../../src/modules/sources-identity/service/entities.ts";
import { syncDownloads } from "../../src/modules/acquisition-organize/services/downloads.ts";
import { executeRecord, planImport } from "../../src/modules/acquisition-organize/services/organize.ts";
import { organizeDecisionHook, pickMainVideo, takeoverParsed } from "../../src/modules/acquisition-organize/services/takeover.ts";
import {
organizeDecisionHook,
pickMainVideo,
takeoverParsed,
} from "../../src/modules/acquisition-organize/services/takeover.ts";
import { addAliasIfAbsent } from "../../src/modules/sources-identity/service/entities.ts";
import { createHarness, type Harness, pgAvailable, seedBase, seedEntity } from "./harness.ts";
// QB-1:接管任务(用户手动加进 qB、贴管理标签,没有站点资源)完成后按 种子名 + 主视频文件名 识别 → 直接整理;
@@ -5,7 +5,11 @@ import { sql } from "@umt/db";
import { createMockQb, createTempTree, type MockQb, makeTorrent, type TempTree } from "@umt/testkit";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { evaluateAcquisition } from "../../src/modules/acquisition-organize/services/acquisition.ts";
import { cleanupDownloadCache, submitDownload, syncDownloads } from "../../src/modules/acquisition-organize/services/downloads.ts";
import {
cleanupDownloadCache,
submitDownload,
syncDownloads,
} from "../../src/modules/acquisition-organize/services/downloads.ts";
import { executeRecord, planImport } from "../../src/modules/acquisition-organize/services/organize.ts";
import { reconcileLibrary } from "../../src/modules/acquisition-organize/services/reconcile.ts";
import {