mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 17:34:35 +00:00
refactor: dedupe gateway config and infra flows
This commit is contained in:
@@ -64,6 +64,26 @@ async function ensureMinimaxApiKey(params: {
|
||||
});
|
||||
}
|
||||
|
||||
async function ensureMinimaxApiKeyWithEnvRefPrompter(params: {
|
||||
config?: Parameters<typeof ensureApiKeyFromEnvOrPrompt>[0]["config"];
|
||||
note: WizardPrompter["note"];
|
||||
select: WizardPrompter["select"];
|
||||
setCredential: Parameters<typeof ensureApiKeyFromEnvOrPrompt>[0]["setCredential"];
|
||||
text: WizardPrompter["text"];
|
||||
}) {
|
||||
return await ensureApiKeyFromEnvOrPrompt({
|
||||
config: params.config ?? {},
|
||||
provider: "minimax",
|
||||
envLabel: "MINIMAX_API_KEY",
|
||||
promptMessage: "Enter key",
|
||||
normalize: (value) => value.trim(),
|
||||
validate: () => undefined,
|
||||
prompter: createPrompter({ select: params.select, text: params.text, note: params.note }),
|
||||
secretInputMode: "ref",
|
||||
setCredential: params.setCredential,
|
||||
});
|
||||
}
|
||||
|
||||
async function runEnsureMinimaxApiKeyFlow(params: { confirmResult: boolean; textResult: string }) {
|
||||
process.env.MINIMAX_API_KEY = "env-key";
|
||||
delete process.env.MINIMAX_OAUTH_TOKEN;
|
||||
@@ -229,7 +249,7 @@ describe("ensureApiKeyFromEnvOrPrompt", () => {
|
||||
const note = vi.fn(async () => undefined);
|
||||
const setCredential = vi.fn(async () => undefined);
|
||||
|
||||
const result = await ensureApiKeyFromEnvOrPrompt({
|
||||
const result = await ensureMinimaxApiKeyWithEnvRefPrompter({
|
||||
config: {
|
||||
secrets: {
|
||||
providers: {
|
||||
@@ -241,13 +261,9 @@ describe("ensureApiKeyFromEnvOrPrompt", () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
provider: "minimax",
|
||||
envLabel: "MINIMAX_API_KEY",
|
||||
promptMessage: "Enter key",
|
||||
normalize: (value) => value.trim(),
|
||||
validate: () => undefined,
|
||||
prompter: createPrompter({ select, text, note }),
|
||||
secretInputMode: "ref",
|
||||
select,
|
||||
text,
|
||||
note,
|
||||
setCredential,
|
||||
});
|
||||
|
||||
@@ -271,15 +287,11 @@ describe("ensureApiKeyFromEnvOrPrompt", () => {
|
||||
const note = vi.fn(async () => undefined);
|
||||
const setCredential = vi.fn(async () => undefined);
|
||||
|
||||
const result = await ensureApiKeyFromEnvOrPrompt({
|
||||
const result = await ensureMinimaxApiKeyWithEnvRefPrompter({
|
||||
config: {},
|
||||
provider: "minimax",
|
||||
envLabel: "MINIMAX_API_KEY",
|
||||
promptMessage: "Enter key",
|
||||
normalize: (value) => value.trim(),
|
||||
validate: () => undefined,
|
||||
prompter: createPrompter({ select, text, note }),
|
||||
secretInputMode: "ref",
|
||||
select,
|
||||
text,
|
||||
note,
|
||||
setCredential,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { shouldMoveSingleAccountChannelKey } from "../channels/plugins/setup-helpers.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import {
|
||||
formatSlackStreamingBooleanMigrationMessage,
|
||||
formatSlackStreamModeMigrationMessage,
|
||||
resolveDiscordPreviewStreamMode,
|
||||
resolveSlackNativeStreaming,
|
||||
resolveSlackStreamingMode,
|
||||
@@ -175,13 +177,11 @@ export function normalizeCompatibilityConfigValues(cfg: OpenClawConfig): {
|
||||
const { streamMode: _ignored, ...rest } = updated;
|
||||
updated = rest;
|
||||
changed = true;
|
||||
changes.push(
|
||||
`Moved ${params.pathPrefix}.streamMode → ${params.pathPrefix}.streaming (${resolvedStreaming}).`,
|
||||
);
|
||||
changes.push(formatSlackStreamModeMigrationMessage(params.pathPrefix, resolvedStreaming));
|
||||
}
|
||||
if (typeof legacyStreaming === "boolean") {
|
||||
changes.push(
|
||||
`Moved ${params.pathPrefix}.streaming (boolean) → ${params.pathPrefix}.nativeStreaming (${resolvedNativeStreaming}).`,
|
||||
formatSlackStreamingBooleanMigrationMessage(params.pathPrefix, resolvedNativeStreaming),
|
||||
);
|
||||
} else if (typeof legacyStreaming === "string" && legacyStreaming !== resolvedStreaming) {
|
||||
changes.push(
|
||||
|
||||
@@ -20,6 +20,12 @@ async function makeTempRoot() {
|
||||
return root;
|
||||
}
|
||||
|
||||
async function makeRootWithEmptyCfg() {
|
||||
const root = await makeTempRoot();
|
||||
const cfg: OpenClawConfig = {};
|
||||
return { root, cfg };
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
resetAutoMigrateLegacyStateForTest();
|
||||
resetAutoMigrateLegacyStateDirForTest();
|
||||
@@ -129,6 +135,26 @@ function expectTargetAlreadyExistsWarning(result: StateDirMigrationResult, targe
|
||||
]);
|
||||
}
|
||||
|
||||
function expectUnmigratedWithoutWarnings(result: StateDirMigrationResult) {
|
||||
expect(result.migrated).toBe(false);
|
||||
expect(result.warnings).toEqual([]);
|
||||
}
|
||||
|
||||
function writeLegacyAgentFiles(root: string, files: Record<string, string>) {
|
||||
const legacyAgentDir = path.join(root, "agent");
|
||||
fs.mkdirSync(legacyAgentDir, { recursive: true });
|
||||
for (const [fileName, content] of Object.entries(files)) {
|
||||
fs.writeFileSync(path.join(legacyAgentDir, fileName), content, "utf-8");
|
||||
}
|
||||
return legacyAgentDir;
|
||||
}
|
||||
|
||||
function ensureCredentialsDir(root: string) {
|
||||
const oauthDir = path.join(root, "credentials");
|
||||
fs.mkdirSync(oauthDir, { recursive: true });
|
||||
return oauthDir;
|
||||
}
|
||||
|
||||
describe("doctor legacy state migrations", () => {
|
||||
it("migrates legacy sessions into agents/<id>/sessions", async () => {
|
||||
const root = await makeTempRoot();
|
||||
@@ -177,23 +203,17 @@ describe("doctor legacy state migrations", () => {
|
||||
});
|
||||
|
||||
it("migrates legacy agent dir with conflict fallback", async () => {
|
||||
const root = await makeTempRoot();
|
||||
const cfg: OpenClawConfig = {};
|
||||
|
||||
const legacyAgentDir = path.join(root, "agent");
|
||||
fs.mkdirSync(legacyAgentDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(legacyAgentDir, "foo.txt"), "legacy", "utf-8");
|
||||
fs.writeFileSync(path.join(legacyAgentDir, "baz.txt"), "legacy2", "utf-8");
|
||||
const { root, cfg } = await makeRootWithEmptyCfg();
|
||||
writeLegacyAgentFiles(root, {
|
||||
"foo.txt": "legacy",
|
||||
"baz.txt": "legacy2",
|
||||
});
|
||||
|
||||
const targetAgentDir = path.join(root, "agents", "main", "agent");
|
||||
fs.mkdirSync(targetAgentDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(targetAgentDir, "foo.txt"), "new", "utf-8");
|
||||
|
||||
const detected = await detectLegacyStateMigrations({
|
||||
cfg,
|
||||
env: { OPENCLAW_STATE_DIR: root } as NodeJS.ProcessEnv,
|
||||
});
|
||||
await runLegacyStateMigrations({ detected, now: () => 123 });
|
||||
await detectAndRunMigrations({ root, cfg, now: () => 123 });
|
||||
|
||||
expect(fs.readFileSync(path.join(targetAgentDir, "baz.txt"), "utf-8")).toBe("legacy2");
|
||||
const backupDir = path.join(root, "agents", "main", "agent.legacy-123");
|
||||
@@ -201,12 +221,8 @@ describe("doctor legacy state migrations", () => {
|
||||
});
|
||||
|
||||
it("auto-migrates legacy agent dir on startup", async () => {
|
||||
const root = await makeTempRoot();
|
||||
const cfg: OpenClawConfig = {};
|
||||
|
||||
const legacyAgentDir = path.join(root, "agent");
|
||||
fs.mkdirSync(legacyAgentDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(legacyAgentDir, "auth.json"), "{}", "utf-8");
|
||||
const { root, cfg } = await makeRootWithEmptyCfg();
|
||||
writeLegacyAgentFiles(root, { "auth.json": "{}" });
|
||||
|
||||
const { result, log } = await runAutoMigrateLegacyStateWithLog({ root, cfg });
|
||||
|
||||
@@ -217,8 +233,7 @@ describe("doctor legacy state migrations", () => {
|
||||
});
|
||||
|
||||
it("auto-migrates legacy sessions on startup", async () => {
|
||||
const root = await makeTempRoot();
|
||||
const cfg: OpenClawConfig = {};
|
||||
const { root, cfg } = await makeRootWithEmptyCfg();
|
||||
const legacySessionsDir = writeLegacySessionsFixture({
|
||||
root,
|
||||
sessions: {
|
||||
@@ -245,20 +260,13 @@ describe("doctor legacy state migrations", () => {
|
||||
});
|
||||
|
||||
it("migrates legacy WhatsApp auth files without touching oauth.json", async () => {
|
||||
const root = await makeTempRoot();
|
||||
const cfg: OpenClawConfig = {};
|
||||
|
||||
const oauthDir = path.join(root, "credentials");
|
||||
fs.mkdirSync(oauthDir, { recursive: true });
|
||||
const { root, cfg } = await makeRootWithEmptyCfg();
|
||||
const oauthDir = ensureCredentialsDir(root);
|
||||
fs.writeFileSync(path.join(oauthDir, "oauth.json"), "{}", "utf-8");
|
||||
fs.writeFileSync(path.join(oauthDir, "creds.json"), "{}", "utf-8");
|
||||
fs.writeFileSync(path.join(oauthDir, "session-abc.json"), "{}", "utf-8");
|
||||
|
||||
const detected = await detectLegacyStateMigrations({
|
||||
cfg,
|
||||
env: { OPENCLAW_STATE_DIR: root } as NodeJS.ProcessEnv,
|
||||
});
|
||||
await runLegacyStateMigrations({ detected, now: () => 123 });
|
||||
await detectAndRunMigrations({ root, cfg, now: () => 123 });
|
||||
|
||||
const target = path.join(oauthDir, "whatsapp", "default");
|
||||
expect(fs.existsSync(path.join(target, "creds.json"))).toBe(true);
|
||||
@@ -268,11 +276,8 @@ describe("doctor legacy state migrations", () => {
|
||||
});
|
||||
|
||||
it("migrates legacy Telegram pairing allowFrom store to account-scoped default file", async () => {
|
||||
const root = await makeTempRoot();
|
||||
const cfg: OpenClawConfig = {};
|
||||
|
||||
const oauthDir = path.join(root, "credentials");
|
||||
fs.mkdirSync(oauthDir, { recursive: true });
|
||||
const { root, cfg } = await makeRootWithEmptyCfg();
|
||||
const oauthDir = ensureCredentialsDir(root);
|
||||
fs.writeFileSync(
|
||||
path.join(oauthDir, "telegram-allowFrom.json"),
|
||||
JSON.stringify(
|
||||
@@ -359,8 +364,7 @@ describe("doctor legacy state migrations", () => {
|
||||
});
|
||||
|
||||
it("canonicalizes legacy main keys inside the target sessions store", async () => {
|
||||
const root = await makeTempRoot();
|
||||
const cfg: OpenClawConfig = {};
|
||||
const { root, cfg } = await makeRootWithEmptyCfg();
|
||||
const targetDir = path.join(root, "agents", "main", "sessions");
|
||||
writeJson5(path.join(targetDir, "sessions.json"), {
|
||||
main: { sessionId: "legacy", updatedAt: 10 },
|
||||
@@ -415,8 +419,7 @@ describe("doctor legacy state migrations", () => {
|
||||
});
|
||||
|
||||
it("auto-migrates when only target sessions contain legacy keys", async () => {
|
||||
const root = await makeTempRoot();
|
||||
const cfg: OpenClawConfig = {};
|
||||
const { root, cfg } = await makeRootWithEmptyCfg();
|
||||
const targetDir = path.join(root, "agents", "main", "sessions");
|
||||
writeJson5(path.join(targetDir, "sessions.json"), {
|
||||
main: { sessionId: "legacy", updatedAt: 10 },
|
||||
@@ -469,9 +472,7 @@ describe("doctor legacy state migrations", () => {
|
||||
fs.symlinkSync(path.join(targetDir, "agent"), path.join(legacyDir, "agent"), DIR_LINK_TYPE);
|
||||
|
||||
const result = await runStateDirMigration(root);
|
||||
|
||||
expect(result.migrated).toBe(false);
|
||||
expect(result.warnings).toEqual([]);
|
||||
expectUnmigratedWithoutWarnings(result);
|
||||
});
|
||||
|
||||
it("warns when legacy state dir is empty and target already exists", async () => {
|
||||
@@ -504,9 +505,7 @@ describe("doctor legacy state migrations", () => {
|
||||
);
|
||||
|
||||
const result = await runStateDirMigration(root);
|
||||
|
||||
expect(result.migrated).toBe(false);
|
||||
expect(result.warnings).toEqual([]);
|
||||
expectUnmigratedWithoutWarnings(result);
|
||||
});
|
||||
|
||||
it("warns when legacy state dir symlink points outside the target tree", async () => {
|
||||
|
||||
@@ -43,6 +43,18 @@ function createRuntime(): RuntimeEnv {
|
||||
};
|
||||
}
|
||||
|
||||
async function runCodexOAuth(params: { isRemote: boolean }) {
|
||||
const { prompter, spin } = createPrompter();
|
||||
const runtime = createRuntime();
|
||||
const result = await loginOpenAICodexOAuth({
|
||||
prompter,
|
||||
runtime,
|
||||
isRemote: params.isRemote,
|
||||
openUrl: async () => {},
|
||||
});
|
||||
return { result, prompter, spin, runtime };
|
||||
}
|
||||
|
||||
describe("loginOpenAICodexOAuth", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
@@ -64,14 +76,7 @@ describe("loginOpenAICodexOAuth", () => {
|
||||
});
|
||||
mocks.loginOpenAICodex.mockResolvedValue(creds);
|
||||
|
||||
const { prompter, spin } = createPrompter();
|
||||
const runtime = createRuntime();
|
||||
const result = await loginOpenAICodexOAuth({
|
||||
prompter,
|
||||
runtime,
|
||||
isRemote: false,
|
||||
openUrl: async () => {},
|
||||
});
|
||||
const { result, spin, runtime } = await runCodexOAuth({ isRemote: false });
|
||||
|
||||
expect(result).toEqual(creds);
|
||||
expect(mocks.loginOpenAICodex).toHaveBeenCalledOnce();
|
||||
@@ -124,14 +129,7 @@ describe("loginOpenAICodexOAuth", () => {
|
||||
});
|
||||
mocks.loginOpenAICodex.mockResolvedValue(creds);
|
||||
|
||||
const { prompter } = createPrompter();
|
||||
const runtime = createRuntime();
|
||||
const result = await loginOpenAICodexOAuth({
|
||||
prompter,
|
||||
runtime,
|
||||
isRemote: false,
|
||||
openUrl: async () => {},
|
||||
});
|
||||
const { result, prompter, runtime } = await runCodexOAuth({ isRemote: false });
|
||||
|
||||
expect(result).toEqual(creds);
|
||||
expect(mocks.loginOpenAICodex).toHaveBeenCalledOnce();
|
||||
|
||||
Reference in New Issue
Block a user