import { createMigrator, type DB, migrateToLatest, sql } from "@umt/db"; import { Kysely, PostgresDialect } from "kysely"; import pg from "pg"; import { afterAll, describe, expect, it } from "vitest"; import { createSecretStore } from "../../src/modules/auth-settings/provided.ts"; import { MASTER_KEY, PG_URL, pgAvailable, withSchemaLock } from "../auth-settings/harness.ts"; // 0027:已保存的旧 settings.pi(v0.1 单上游)一次性转成一条上游并设为默认;密钥所有权移交给上游。 const hasPg = await pgAvailable(); describe.skipIf(!hasPg)("0027 迁移旧 settings.pi → 上游", () => { const schema = `w7a_mig_${process.pid.toString(36)}${Math.random().toString(36).slice(2, 6)}`; const admin = async (q: string) => { const c = new pg.Client({ connectionString: PG_URL }); await c.connect(); await c.query(q).finally(() => c.end()); }; let db: Kysely; afterAll(async () => { await db?.destroy(); await withSchemaLock(() => admin(`drop schema if exists ${schema} cascade`)); }); it("anthropic + baseUrl + apiKeySecretRef → 默认上游(api_key, ok);重复迁移不再生成", async () => { await admin(`create schema ${schema}`); db = new Kysely({ dialect: new PostgresDialect({ pool: new pg.Pool({ connectionString: PG_URL, max: 2, options: `-c search_path=${schema},public` }), }), }); await withSchemaLock(async () => { const r = await createMigrator(db, { migrationTableSchema: schema }).migrateTo("0026_organization_file_actions"); if (r.error) throw r.error; }); const secrets = createSecretStore({ db, masterKey: MASTER_KEY }); const ref = await secrets.create({ purpose: "llm_api_key", plaintext: "sk-ant-legacy-key" }); const legacy = { provider: "anthropic", baseUrl: "https://proxy.local/v1", model: "claude-sonnet-4-5", thinkingLevel: "high", contextBudget: 100_000, compaction: { enabled: true, thresholdRatio: 0.8 }, subagentsEnabled: false, apiKeySecretRef: ref, }; await sql`insert into settings (key, value) values ('pi', ${JSON.stringify(legacy)}::jsonb)`.execute(db); await withSchemaLock(() => migrateToLatest(db, { migrationTableSchema: schema })); const rows = await db.selectFrom("pi_upstream").selectAll().execute(); expect(rows).toHaveLength(1); expect(rows[0]).toMatchObject({ provider: "anthropic", auth_kind: "api_key", secret_ref: ref, base_url: "https://proxy.local/v1", default_model: "claude-sonnet-4-5", thinking_level: "high", status: "ok", enabled: true, }); const s = await sql<{ value: Record }>`select value from settings where key = 'pi'`.execute(db); expect(s.rows[0]!.value).toMatchObject({ defaultUpstreamId: rows[0]!.id, apiKeySecretRef: null, contextBudget: 100_000, }); expect(await secrets.resolve(ref)).toBe("sk-ant-legacy-key"); // 回滚再迁移:密钥交还旧字段,再次迁移得到一条新上游(不会重复) await withSchemaLock(async () => { const r = await createMigrator(db, { migrationTableSchema: schema }).migrateTo("0026_organization_file_actions"); if (r.error) throw r.error; }); const back = await sql<{ value: Record }>`select value from settings where key = 'pi'`.execute(db); expect(back.rows[0]!.value.apiKeySecretRef).toBe(ref); expect(back.rows[0]!.value.defaultUpstreamId).toBeUndefined(); await withSchemaLock(() => migrateToLatest(db, { migrationTableSchema: schema })); expect(await db.selectFrom("pi_upstream").select("id").execute()).toHaveLength(1); }, 120_000); });