Files
rootandClaude Opus 5.5 fd3ffe5a8d feat(server): pi module with upstream CRUD, server-side OAuth and token refresh
- /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]>
2026-09-28 02:23:22 +08:00

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);
});