mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 08:57:40 +00:00
refactor(core): extract shared dedup helpers
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { normalizeAccountId } from "../../routing/session-key.js";
|
||||
import { createAccountListHelpers } from "./account-helpers.js";
|
||||
|
||||
const { listConfiguredAccountIds, listAccountIds, resolveDefaultAccountId } =
|
||||
@@ -52,6 +53,22 @@ describe("createAccountListHelpers", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("with normalizeAccountId option", () => {
|
||||
const normalized = createAccountListHelpers("testchannel", { normalizeAccountId });
|
||||
|
||||
it("normalizes and deduplicates configured account ids", () => {
|
||||
expect(
|
||||
normalized.listConfiguredAccountIds(
|
||||
cfg({
|
||||
"Router D": {},
|
||||
"router-d": {},
|
||||
"Personal A": {},
|
||||
}),
|
||||
),
|
||||
).toEqual(["router-d", "personal-a"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("listAccountIds", () => {
|
||||
it('returns ["default"] for empty config', () => {
|
||||
expect(listAccountIds({} as OpenClawConfig)).toEqual(["default"]);
|
||||
|
||||
@@ -5,7 +5,10 @@ import {
|
||||
normalizeOptionalAccountId,
|
||||
} from "../../routing/session-key.js";
|
||||
|
||||
export function createAccountListHelpers(channelKey: string) {
|
||||
export function createAccountListHelpers(
|
||||
channelKey: string,
|
||||
options?: { normalizeAccountId?: (id: string) => string },
|
||||
) {
|
||||
function resolveConfiguredDefaultAccountId(cfg: OpenClawConfig): string | undefined {
|
||||
const channel = cfg.channels?.[channelKey] as Record<string, unknown> | undefined;
|
||||
const preferred = normalizeOptionalAccountId(
|
||||
@@ -27,7 +30,12 @@ export function createAccountListHelpers(channelKey: string) {
|
||||
if (!accounts || typeof accounts !== "object") {
|
||||
return [];
|
||||
}
|
||||
return Object.keys(accounts as Record<string, unknown>).filter(Boolean);
|
||||
const ids = Object.keys(accounts as Record<string, unknown>).filter(Boolean);
|
||||
const normalizeConfiguredAccountId = options?.normalizeAccountId;
|
||||
if (!normalizeConfiguredAccountId) {
|
||||
return ids;
|
||||
}
|
||||
return [...new Set(ids.map((id) => normalizeConfiguredAccountId(id)).filter(Boolean))];
|
||||
}
|
||||
|
||||
function listAccountIds(cfg: OpenClawConfig): string[] {
|
||||
|
||||
110
src/channels/plugins/config-helpers.test.ts
Normal file
110
src/channels/plugins/config-helpers.test.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { clearAccountEntryFields } from "./config-helpers.js";
|
||||
|
||||
describe("clearAccountEntryFields", () => {
|
||||
it("clears configured values and removes empty account entries", () => {
|
||||
const result = clearAccountEntryFields({
|
||||
accounts: {
|
||||
default: {
|
||||
botToken: "abc123",
|
||||
},
|
||||
},
|
||||
accountId: "default",
|
||||
fields: ["botToken"],
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
nextAccounts: undefined,
|
||||
changed: true,
|
||||
cleared: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("treats empty string values as not configured by default", () => {
|
||||
const result = clearAccountEntryFields({
|
||||
accounts: {
|
||||
default: {
|
||||
botToken: " ",
|
||||
},
|
||||
},
|
||||
accountId: "default",
|
||||
fields: ["botToken"],
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
nextAccounts: undefined,
|
||||
changed: true,
|
||||
cleared: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("can mark cleared when fields are present even if values are empty", () => {
|
||||
const result = clearAccountEntryFields({
|
||||
accounts: {
|
||||
default: {
|
||||
tokenFile: "",
|
||||
},
|
||||
},
|
||||
accountId: "default",
|
||||
fields: ["tokenFile"],
|
||||
markClearedOnFieldPresence: true,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
nextAccounts: undefined,
|
||||
changed: true,
|
||||
cleared: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps other account fields intact", () => {
|
||||
const result = clearAccountEntryFields({
|
||||
accounts: {
|
||||
default: {
|
||||
botToken: "abc123",
|
||||
name: "Primary",
|
||||
},
|
||||
backup: {
|
||||
botToken: "keep",
|
||||
},
|
||||
},
|
||||
accountId: "default",
|
||||
fields: ["botToken"],
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
nextAccounts: {
|
||||
default: {
|
||||
name: "Primary",
|
||||
},
|
||||
backup: {
|
||||
botToken: "keep",
|
||||
},
|
||||
},
|
||||
changed: true,
|
||||
cleared: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("returns unchanged when account entry is missing", () => {
|
||||
const result = clearAccountEntryFields({
|
||||
accounts: {
|
||||
default: {
|
||||
botToken: "abc123",
|
||||
},
|
||||
},
|
||||
accountId: "other",
|
||||
fields: ["botToken"],
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
nextAccounts: {
|
||||
default: {
|
||||
botToken: "abc123",
|
||||
},
|
||||
},
|
||||
changed: false,
|
||||
cleared: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -6,6 +6,13 @@ type ChannelSection = {
|
||||
enabled?: boolean;
|
||||
};
|
||||
|
||||
function isConfiguredSecretValue(value: unknown): boolean {
|
||||
if (typeof value === "string") {
|
||||
return value.trim().length > 0;
|
||||
}
|
||||
return Boolean(value);
|
||||
}
|
||||
|
||||
export function setAccountEnabledInConfigSection(params: {
|
||||
cfg: OpenClawConfig;
|
||||
sectionKey: string;
|
||||
@@ -111,3 +118,58 @@ export function deleteAccountFromConfigSection(params: {
|
||||
}
|
||||
return nextCfg;
|
||||
}
|
||||
|
||||
export function clearAccountEntryFields<TAccountEntry extends object>(params: {
|
||||
accounts?: Record<string, TAccountEntry>;
|
||||
accountId: string;
|
||||
fields: string[];
|
||||
isValueSet?: (value: unknown) => boolean;
|
||||
markClearedOnFieldPresence?: boolean;
|
||||
}): {
|
||||
nextAccounts?: Record<string, TAccountEntry>;
|
||||
changed: boolean;
|
||||
cleared: boolean;
|
||||
} {
|
||||
const accountKey = params.accountId || DEFAULT_ACCOUNT_ID;
|
||||
const baseAccounts =
|
||||
params.accounts && typeof params.accounts === "object" ? { ...params.accounts } : undefined;
|
||||
if (!baseAccounts || !(accountKey in baseAccounts)) {
|
||||
return { nextAccounts: baseAccounts, changed: false, cleared: false };
|
||||
}
|
||||
|
||||
const entry = baseAccounts[accountKey];
|
||||
if (!entry || typeof entry !== "object") {
|
||||
return { nextAccounts: baseAccounts, changed: false, cleared: false };
|
||||
}
|
||||
|
||||
const nextEntry = { ...(entry as Record<string, unknown>) };
|
||||
const hasAnyField = params.fields.some((field) => field in nextEntry);
|
||||
if (!hasAnyField) {
|
||||
return { nextAccounts: baseAccounts, changed: false, cleared: false };
|
||||
}
|
||||
|
||||
const isValueSet = params.isValueSet ?? isConfiguredSecretValue;
|
||||
let cleared = Boolean(params.markClearedOnFieldPresence);
|
||||
for (const field of params.fields) {
|
||||
if (!(field in nextEntry)) {
|
||||
continue;
|
||||
}
|
||||
if (isValueSet(nextEntry[field])) {
|
||||
cleared = true;
|
||||
}
|
||||
delete nextEntry[field];
|
||||
}
|
||||
|
||||
if (Object.keys(nextEntry).length === 0) {
|
||||
delete baseAccounts[accountKey];
|
||||
} else {
|
||||
baseAccounts[accountKey] = nextEntry as TAccountEntry;
|
||||
}
|
||||
|
||||
const nextAccounts = Object.keys(baseAccounts).length > 0 ? baseAccounts : undefined;
|
||||
return {
|
||||
nextAccounts,
|
||||
changed: true,
|
||||
cleared,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user