- 新增 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]>
129 lines
4.4 KiB
TypeScript
129 lines
4.4 KiB
TypeScript
import type { DB } from "@umt/db";
|
|
import { migrateEngineSchema } from "@umt/tasks";
|
|
import type { Kysely } from "kysely";
|
|
import { vi } from "vitest";
|
|
import { ConfigSchema } from "../../src/config.ts";
|
|
import { createOutboxPort } from "../../src/context.ts";
|
|
import { createApp } from "../../src/http/app.ts";
|
|
import { EventBus } from "../../src/http/sse.ts";
|
|
import { createCapabilityRegistry } from "../../src/modules/agent/capability-registry.ts";
|
|
import { createAuthSettingsModule } from "../../src/modules/auth-settings/index.ts";
|
|
import { createSecretStore, createSettingsReader } from "../../src/modules/auth-settings/provided.ts";
|
|
import type { ServerContext, ServerModule } from "../../src/modules/types.ts";
|
|
import { DEV_KEY, memoryLogger } from "../helpers.ts";
|
|
|
|
// 真实 PG 的 W2-A 测试夹具:独立业务 schema + 独立引擎 schema;任务引擎用假对象(只测 cancel 透传)。
|
|
|
|
import { createTestSchema, PG_URL, uniqueSchema, withSchemaLock } from "../support/pg.ts";
|
|
|
|
export { PG_URL, pgAvailable, withSchemaLock } from "../support/pg.ts";
|
|
export const MASTER_KEY = "test-master-key-0123456789";
|
|
|
|
export async function createHarness(opts: { engine?: boolean; extraModules?: ServerModule[] } = {}) {
|
|
const ts = await createTestSchema(uniqueSchema("w2a_test"), { max: 5 });
|
|
const { schema } = ts;
|
|
const db: Kysely<DB> = ts.db;
|
|
const engineSchema = `${schema}_ow`;
|
|
if (opts.engine) await withSchemaLock(() => migrateEngineSchema(PG_URL, engineSchema));
|
|
const { logger, lines } = memoryLogger();
|
|
const bus = new EventBus(100);
|
|
const config = ConfigSchema.parse({
|
|
DATABASE_URL: PG_URL,
|
|
SECRET_MASTER_KEY: MASTER_KEY,
|
|
NODE_ENV: "development",
|
|
DEV_ADMIN_KEY: DEV_KEY,
|
|
});
|
|
const cancel = vi.fn(async (_runId: string, _reason?: string) => true);
|
|
const ctx = {
|
|
role: "all",
|
|
db,
|
|
config,
|
|
logger,
|
|
events: bus,
|
|
engine: { cancel } as unknown as ServerContext["engine"],
|
|
outbox: createOutboxPort(),
|
|
secrets: createSecretStore({ db, masterKey: MASTER_KEY }),
|
|
settings: createSettingsReader({ db, logger, events: bus }),
|
|
capabilities: createCapabilityRegistry({ logger }),
|
|
} as ServerContext;
|
|
const mod = createAuthSettingsModule({ setupTokenOnStart: false, engineSchema });
|
|
const app = createApp({
|
|
config,
|
|
logger,
|
|
bus,
|
|
db,
|
|
role: "all",
|
|
version: "0.0.0-test",
|
|
modules: [mod, ...(opts.extraModules ?? [])],
|
|
ctx,
|
|
});
|
|
return {
|
|
db,
|
|
app,
|
|
ctx,
|
|
bus,
|
|
lines,
|
|
cancel,
|
|
schema,
|
|
engineSchema,
|
|
async close() {
|
|
await ts.drop(engineSchema);
|
|
},
|
|
};
|
|
}
|
|
export type Harness = Awaited<ReturnType<typeof createHarness>>;
|
|
|
|
/** 极简 cookie jar:记住 Set-Cookie,请求时带上;写方法自动带 CSRF 头 */
|
|
export class Browser {
|
|
cookies = new Map<string, string>();
|
|
constructor(
|
|
private readonly app: Harness["app"],
|
|
readonly ip = "203.0.113.10",
|
|
) {}
|
|
|
|
absorb(res: Response) {
|
|
for (const sc of res.headers.getSetCookie()) {
|
|
const [pair] = sc.split(";");
|
|
const i = pair!.indexOf("=");
|
|
const k = pair!.slice(0, i).trim();
|
|
const v = pair!.slice(i + 1).trim();
|
|
if (!v || /max-age=0/i.test(sc)) this.cookies.delete(k);
|
|
else this.cookies.set(k, decodeURIComponent(v));
|
|
}
|
|
}
|
|
|
|
async req(
|
|
method: string,
|
|
path: string,
|
|
body?: unknown,
|
|
opts: { csrf?: boolean; headers?: Record<string, string> } = {},
|
|
) {
|
|
const headers: Record<string, string> = { "x-real-ip": this.ip, ...opts.headers };
|
|
if (this.cookies.size)
|
|
headers.cookie = [...this.cookies].map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join("; ");
|
|
if (body !== undefined) headers["content-type"] = "application/json";
|
|
const csrf = this.cookies.get("umt_csrf");
|
|
if (method !== "GET" && opts.csrf !== false && csrf) headers["x-csrf-token"] = csrf;
|
|
const res = await this.app.request(`/api/v1${path}`, {
|
|
method,
|
|
headers,
|
|
...(body !== undefined ? { body: JSON.stringify(body) } : {}),
|
|
});
|
|
this.absorb(res);
|
|
return res;
|
|
}
|
|
}
|
|
|
|
/** Bearer 调用 */
|
|
export function bearer(app: Harness["app"], key: string) {
|
|
return (method: string, path: string, body?: unknown) =>
|
|
app.request(`/api/v1${path}`, {
|
|
method,
|
|
headers: {
|
|
authorization: `Bearer ${key}`,
|
|
...(body !== undefined ? { "content-type": "application/json" } : {}),
|
|
},
|
|
...(body !== undefined ? { body: JSON.stringify(body) } : {}),
|
|
});
|
|
}
|