refactor(core): extract shared dedup helpers

This commit is contained in:
Peter Steinberger
2026-03-07 10:40:49 +00:00
parent 14c61bb33f
commit 3c71e2bd48
114 changed files with 3400 additions and 2040 deletions

View File

@@ -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"]);

View File

@@ -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[] {

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

View File

@@ -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,
};
}