test: 测试按行为命名、server 测试统一到 test/、PG 测试分组并行
- server 的 src/modules/*/__tests__ 挪到 test/<模块>/;e2e 去掉 eN- 前缀, w5a2 / v2 / search2 / *-dh1 / *-md1 / *-md2、web 的 agui3 / search2、domain 的 id2 / id3 改为按行为命名 - vitest 分三组:unit、pg(真实 PG + 假引擎,文件并行,最多 6 个)、serial(真实引擎 / worker / e2e,串行); pg 组用例超时放宽到 60s(迁移类用例并行时排队等 schema 锁)。server 测试 615s → 约 350s - CLAUDE.md 补测试约定;待办补 harness 去重与 continuous-series 偶发超时(疑似 Slot 评估竞争) - ADR-004 补充:维持 Deno,记录实际只用到 --allow-run 白名单 Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
b343dddbf6
commit
b61335eaf6
75 files changed
+159
-115
No files matched your search
@@ -0,0 +1,240 @@
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import type { CapabilityRegistry, InvokeContext } from "@umt/agent";
|
||||
import { type DB, type Kysely, migrateToLatest, sql } from "@umt/db";
|
||||
import {
|
||||
buildFakeTorrent,
|
||||
createMockCamofox,
|
||||
type FakeTorrent,
|
||||
type MockCamofox,
|
||||
type MockCamofoxSite,
|
||||
} from "@umt/testkit";
|
||||
import { Kysely as K, PostgresDialect } from "kysely";
|
||||
import pg from "pg";
|
||||
import { withSchemaLock } from "../auth-settings/harness.ts";
|
||||
import { DEV_KEY, memoryLogger, memoryStore } from "../helpers.ts";
|
||||
import { fakeEngine, fakeSecrets, PG_URL, plainSettings } from "../sources-identity/harness.ts";
|
||||
import { createOutboxPort } from "../../src/context.ts";
|
||||
import { createApp } from "../../src/http/app.ts";
|
||||
import { EventBus } from "../../src/http/sse.ts";
|
||||
import { createAcquisitionOrganizeModule } from "../../src/modules/acquisition-organize/index.ts";
|
||||
import { createCapabilityRegistry } from "../../src/modules/agent/capability-registry.ts";
|
||||
import { createAgentModule } from "../../src/modules/agent/index.ts";
|
||||
import { createSourcesIdentityModule } from "../../src/modules/sources-identity/index.ts";
|
||||
import type { ServerContext } from "../../src/modules/types.ts";
|
||||
import { registerBrowserCapabilities } from "../../src/modules/browser/capabilities.ts";
|
||||
import { createBrowserModule } from "../../src/modules/browser/index.ts";
|
||||
import type { BrowserModuleOptions } from "../../src/modules/browser/service.ts";
|
||||
|
||||
// C1-E 浏览器模块测试装配:独立 PG schema + 真实迁移 + 交互式 mock camofox(观众风格站点)+ 真实能力注册中心。
|
||||
|
||||
export { pgAvailable } from "../sources-identity/harness.ts";
|
||||
|
||||
export const ORIGIN = "https://audiences.example";
|
||||
export const PASSWORD = "brw-s3cret-login-pw";
|
||||
export const PASSKEY = "0f0e0d0c0b0a09080706050403020100";
|
||||
export const ACCESS_KEY = "camofox-access-key-c1e";
|
||||
/** 第二位管理员(接管冲突) */
|
||||
export const OTHER_KEY = "brw-other-admin-key";
|
||||
export const READ_KEY = "brw-read-key";
|
||||
|
||||
// biome-ignore lint/suspicious/noExplicitAny: 测试断言直接读 JSON
|
||||
export type Loose = any;
|
||||
|
||||
export interface BrowserHarness {
|
||||
db: Kysely<DB>;
|
||||
ctx: ServerContext;
|
||||
mock: MockCamofox;
|
||||
torrent: FakeTorrent;
|
||||
registry: CapabilityRegistry;
|
||||
siteId: string;
|
||||
accountId: string;
|
||||
providerId: string;
|
||||
releaseId: string;
|
||||
stagingDir: string;
|
||||
secrets: ReturnType<typeof fakeSecrets>;
|
||||
req(path: string, init?: { method?: string; body?: unknown; key?: string }): Promise<{ status: number; body: Loose }>;
|
||||
agentCtx(): InvokeContext;
|
||||
grant(...actions: string[]): Promise<void>;
|
||||
close(): Promise<void>;
|
||||
}
|
||||
|
||||
export async function createBrowserHarness(
|
||||
name: string,
|
||||
opts: {
|
||||
captcha?: boolean;
|
||||
/** BR-2:mock 站点按这个 Cookie 判定登录态 */
|
||||
sessionCookie?: { name: string; valid: string[]; fresh: string };
|
||||
cookieApi?: "plugin" | "upstream" | "none";
|
||||
cookieImportKey?: string;
|
||||
/** CF-2:mock 站点的 Cloudflare 挑战与页面 HTML */
|
||||
cloudflare?: MockCamofoxSite["cloudflare"];
|
||||
html?: MockCamofoxSite["html"];
|
||||
/** 浏览器模块的额外选项(挑战轮询间隔等) */
|
||||
browserOptions?: Partial<BrowserModuleOptions>;
|
||||
} = {},
|
||||
): Promise<BrowserHarness> {
|
||||
const torrent = buildFakeTorrent({ name: "Oppenheimer.2023.2160p.UHD.BluRay.x265-GRP" });
|
||||
const mock = await createMockCamofox({
|
||||
accessKey: ACCESS_KEY,
|
||||
...(opts.cookieApi ? { cookieApi: opts.cookieApi } : {}),
|
||||
...(opts.cookieImportKey ? { cookieImportKey: opts.cookieImportKey } : {}),
|
||||
site: {
|
||||
...(opts.sessionCookie ? { sessionCookie: opts.sessionCookie } : {}),
|
||||
...(opts.cloudflare ? { cloudflare: opts.cloudflare } : {}),
|
||||
...(opts.html ? { html: opts.html } : {}),
|
||||
origin: ORIGIN,
|
||||
username: "fixture-user",
|
||||
password: PASSWORD,
|
||||
passkey: PASSKEY,
|
||||
...(opts.captcha ? { captcha: true, captchaAnswer: "4321" } : {}),
|
||||
releases: [{ id: "42", title: "Oppenheimer 2023 2160p UHD BluRay" }],
|
||||
torrent: () => torrent.bytes,
|
||||
},
|
||||
});
|
||||
const stagingDir = await mkdtemp(join(tmpdir(), "umt-browser-"));
|
||||
const schema = `c1e_${name}_${process.pid.toString(36)}${Math.random().toString(36).slice(2, 7)}`;
|
||||
const admin = new K<unknown>({
|
||||
dialect: new PostgresDialect({ pool: new pg.Pool({ connectionString: PG_URL, max: 1 }) }),
|
||||
});
|
||||
await sql.raw(`create schema ${schema}`).execute(admin);
|
||||
await import("@umt/db");
|
||||
const db = new K<DB>({
|
||||
dialect: new PostgresDialect({
|
||||
pool: new pg.Pool({ connectionString: PG_URL, max: 8, options: `-c search_path=${schema},public` }),
|
||||
}),
|
||||
}) as unknown as Kysely<DB>;
|
||||
await withSchemaLock(() => migrateToLatest(db, { migrationTableSchema: schema }));
|
||||
|
||||
const { logger } = memoryLogger();
|
||||
const secrets = fakeSecrets(() => db);
|
||||
const settings = plainSettings(() => db);
|
||||
const registry = createCapabilityRegistry({ logger, db, settings });
|
||||
const ctx = {
|
||||
role: "all",
|
||||
db,
|
||||
config: { NODE_ENV: "development", DEV_ADMIN_KEY: DEV_KEY } as never,
|
||||
logger,
|
||||
events: new EventBus(5),
|
||||
engine: fakeEngine(),
|
||||
outbox: createOutboxPort(),
|
||||
secrets,
|
||||
settings,
|
||||
capabilities: registry,
|
||||
} as ServerContext;
|
||||
const sources = createSourcesIdentityModule({ tmdb: () => null });
|
||||
const browser = createBrowserModule({
|
||||
runtime: (c) => sources.runtime(c),
|
||||
stagingDir,
|
||||
downloadPollMs: 20,
|
||||
...opts.browserOptions,
|
||||
});
|
||||
registerBrowserCapabilities(registry, () => browser.service(ctx));
|
||||
const app = createApp({
|
||||
config: { NODE_ENV: "development", DEV_ADMIN_KEY: DEV_KEY },
|
||||
logger,
|
||||
bus: new EventBus(5),
|
||||
role: "all",
|
||||
version: "0.0.0-test",
|
||||
// POST /acquisition-plans 在 agent 模块
|
||||
modules: [sources, createAcquisitionOrganizeModule(), createAgentModule(), browser],
|
||||
ctx,
|
||||
apiKeyStore: memoryStore({
|
||||
[OTHER_KEY]: { id: "other-admin", scopes: ["admin"] },
|
||||
[READ_KEY]: { id: "reader", scopes: ["read"] },
|
||||
}),
|
||||
});
|
||||
|
||||
const siteId = (
|
||||
await db
|
||||
.insertInto("site")
|
||||
.values({
|
||||
key: "audiences",
|
||||
name: "观众",
|
||||
adapter: "nexusphp",
|
||||
base_url: `${ORIGIN}/`,
|
||||
rate_limit: JSON.stringify({ minIntervalMs: 0, concurrency: 5 }),
|
||||
})
|
||||
.returning("id")
|
||||
.executeTakeFirstOrThrow()
|
||||
).id;
|
||||
const pwRef = await secrets.create({ purpose: "site_password", plaintext: PASSWORD });
|
||||
const accountId = (
|
||||
await db
|
||||
.insertInto("site_account")
|
||||
.values({ site_id: siteId, label: "main", auth_kind: "password", secret_ref: pwRef, login_name: "fixture-user" })
|
||||
.returning("id")
|
||||
.executeTakeFirstOrThrow()
|
||||
).id;
|
||||
const keyRef = await secrets.create({ purpose: "other", plaintext: ACCESS_KEY });
|
||||
const providerId = (
|
||||
await db
|
||||
.insertInto("browser_provider")
|
||||
.values({
|
||||
name: "camofox",
|
||||
kind: "camofox",
|
||||
base_url: mock.url,
|
||||
secret_ref: keyRef,
|
||||
enabled: true,
|
||||
config: JSON.stringify({ verified: { snapshot: true }, navigationTimeoutMs: 2_000, maxRetries: 1 }),
|
||||
})
|
||||
.returning("id")
|
||||
.executeTakeFirstOrThrow()
|
||||
).id;
|
||||
const releaseId = (
|
||||
await db
|
||||
.insertInto("source_release")
|
||||
.values({
|
||||
site_id: siteId,
|
||||
site_release_id: "42",
|
||||
title: "Oppenheimer 2023 2160p UHD BluRay",
|
||||
infohash_v1: torrent.infoHash,
|
||||
})
|
||||
.returning("id")
|
||||
.executeTakeFirstOrThrow()
|
||||
).id;
|
||||
|
||||
return {
|
||||
db,
|
||||
ctx,
|
||||
mock,
|
||||
torrent,
|
||||
registry,
|
||||
siteId,
|
||||
accountId,
|
||||
providerId,
|
||||
releaseId,
|
||||
stagingDir,
|
||||
secrets,
|
||||
async req(path, init = {}) {
|
||||
const res = await app.request(`/api/v1${path}`, {
|
||||
method: init.method ?? (init.body !== undefined ? "POST" : "GET"),
|
||||
headers: {
|
||||
authorization: `Bearer ${init.key ?? DEV_KEY}`,
|
||||
...(init.body !== undefined ? { "content-type": "application/json" } : {}),
|
||||
},
|
||||
...(init.body !== undefined ? { body: JSON.stringify(init.body) } : {}),
|
||||
});
|
||||
return { status: res.status, body: (await res.json()) as Loose };
|
||||
},
|
||||
agentCtx: () => ({
|
||||
actorId: "agent:c1e",
|
||||
taskId: crypto.randomUUID(),
|
||||
scopeIds: [],
|
||||
signal: new AbortController().signal,
|
||||
source: "agent",
|
||||
}),
|
||||
async grant(...actions) {
|
||||
for (const action of actions)
|
||||
await db.insertInto("automation_grant").values({ action, granted_by: "test" }).execute();
|
||||
},
|
||||
async close() {
|
||||
await db.destroy();
|
||||
await withSchemaLock(() => sql.raw(`drop schema if exists ${schema} cascade`).execute(admin));
|
||||
await admin.destroy();
|
||||
await mock.close();
|
||||
await rm(stagingDir, { recursive: true, force: true });
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user