- /pi/providers, /pi/upstreams CRUD, test (auth-only by default, no cost), models - subscription login via pi models.login with a suspended interaction: browser mode waits for the pasted redirect URL, device mode polls server-side; per-provider login lock, readable callback-port-in-use errors - credential store locks the pi_upstream row (select for update) so api and worker refreshes never double-write a rotated refresh token - refreshPiTokens task: refresh before expiry; failure marks expired and opens a relogin decision, closed again after a successful login - runAgent picks upstreamId > agent_run.upstream_id > default upstream > legacy settings; credentials injected by the server, never logged Co-Authored-By: Claude Opus 5.5 <[email protected]>
85 lines
3.6 KiB
TypeScript
85 lines
3.6 KiB
TypeScript
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<DB>;
|
|
|
|
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<DB>({
|
|
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<string, unknown> }>`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<string, unknown> }>`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);
|
|
});
|