mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 09:30:43 +00:00
test: trim doctor and auth choice hotspots
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import type { OAuthCredentials } from "@mariozechner/pi-ai";
|
||||
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import { resolveAgentDir } from "../agents/agent-scope.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { resolveAgentModelPrimaryValue } from "../config/model-input.js";
|
||||
@@ -9,11 +10,9 @@ import type { ProviderAuthMethod, ProviderAuthResult, ProviderPlugin } from "../
|
||||
import type { WizardPrompter } from "../wizard/prompts.js";
|
||||
import { applyAuthChoice } from "./auth-choice.apply.js";
|
||||
import {
|
||||
authProfilePathForAgent,
|
||||
createAuthTestLifecycle,
|
||||
createExitThrowingRuntime,
|
||||
createWizardPrompter,
|
||||
readAuthProfilesForAgent,
|
||||
requireOpenClawAgentDir,
|
||||
setupAuthTestEnv,
|
||||
} from "./test-wizard-helpers.js";
|
||||
@@ -83,12 +82,49 @@ type StoredAuthProfile = {
|
||||
keyRef?: { source: string; provider: string; id: string };
|
||||
access?: string;
|
||||
refresh?: string;
|
||||
expires?: number;
|
||||
provider?: string;
|
||||
type?: string;
|
||||
email?: string;
|
||||
metadata?: Record<string, string>;
|
||||
};
|
||||
|
||||
const testAuthProfileStores = vi.hoisted(
|
||||
() => new Map<string, { profiles: Record<string, StoredAuthProfile> }>(),
|
||||
);
|
||||
|
||||
// These tests verify profile payloads, not file locking; keep auth stores in memory.
|
||||
function resolveTestAuthStoreKey(agentDir?: string): string {
|
||||
return agentDir?.trim() || process.env.OPENCLAW_AGENT_DIR || "__main__";
|
||||
}
|
||||
|
||||
function readTestAuthProfileStore(agentDir?: string): {
|
||||
profiles: Record<string, StoredAuthProfile>;
|
||||
} {
|
||||
return testAuthProfileStores.get(resolveTestAuthStoreKey(agentDir)) ?? { profiles: {} };
|
||||
}
|
||||
|
||||
function seedTestAuthProfile(params: {
|
||||
profileId: string;
|
||||
credential: StoredAuthProfile;
|
||||
agentDir?: string;
|
||||
}): void {
|
||||
const key = resolveTestAuthStoreKey(params.agentDir);
|
||||
const store = testAuthProfileStores.get(key) ?? { profiles: {} };
|
||||
store.profiles[params.profileId] = params.credential;
|
||||
testAuthProfileStores.set(key, store);
|
||||
}
|
||||
|
||||
vi.mock("../agents/auth-profiles.js", () => ({
|
||||
upsertAuthProfile: (params: {
|
||||
profileId: string;
|
||||
credential: StoredAuthProfile;
|
||||
agentDir?: string;
|
||||
}) => {
|
||||
seedTestAuthProfile(params);
|
||||
},
|
||||
}));
|
||||
|
||||
function normalizeText(value: unknown): string {
|
||||
return typeof value === "string" ? value.trim() : "";
|
||||
}
|
||||
@@ -723,14 +759,18 @@ describe("applyAuthChoice", () => {
|
||||
"SSH_TTY",
|
||||
"CHUTES_CLIENT_ID",
|
||||
]);
|
||||
let activeStateDir: string | null = null;
|
||||
let authTestRoot: string | null = null;
|
||||
let authStateCounter = 0;
|
||||
async function setupTempState() {
|
||||
if (activeStateDir) {
|
||||
await fs.rm(activeStateDir, { recursive: true, force: true });
|
||||
if (!authTestRoot) {
|
||||
throw new Error("auth test root not initialized");
|
||||
}
|
||||
const env = await setupAuthTestEnv("openclaw-auth-");
|
||||
activeStateDir = env.stateDir;
|
||||
lifecycle.setStateDir(env.stateDir);
|
||||
testAuthProfileStores.clear();
|
||||
const stateDir = path.join(authTestRoot, `state-${++authStateCounter}`);
|
||||
const agentDir = path.join(stateDir, "agent");
|
||||
process.env.OPENCLAW_STATE_DIR = stateDir;
|
||||
process.env.OPENCLAW_AGENT_DIR = agentDir;
|
||||
process.env.PI_CODING_AGENT_DIR = agentDir;
|
||||
}
|
||||
function createPrompter(overrides: Partial<WizardPrompter>): WizardPrompter {
|
||||
return createWizardPrompter(overrides, { defaultSelect: "" });
|
||||
@@ -759,9 +799,10 @@ describe("applyAuthChoice", () => {
|
||||
};
|
||||
}
|
||||
async function readAuthProfiles() {
|
||||
return await readAuthProfilesForAgent<{
|
||||
profiles?: Record<string, StoredAuthProfile>;
|
||||
}>(requireOpenClawAgentDir());
|
||||
return readTestAuthProfileStore(requireOpenClawAgentDir());
|
||||
}
|
||||
async function readAuthProfilesForAgentDir(agentDir: string) {
|
||||
return readTestAuthProfileStore(agentDir);
|
||||
}
|
||||
async function readAuthProfile(profileId: string) {
|
||||
return (await readAuthProfiles()).profiles?.[profileId];
|
||||
@@ -770,10 +811,17 @@ describe("applyAuthChoice", () => {
|
||||
let defaultProviderPlugins: ProviderPlugin[] = [];
|
||||
|
||||
beforeAll(async () => {
|
||||
authTestRoot = (await setupAuthTestEnv("openclaw-auth-")).stateDir;
|
||||
defaultProviderPlugins = await createDefaultProviderPlugins();
|
||||
resolvePluginProviders.mockReturnValue(defaultProviderPlugins);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
if (authTestRoot) {
|
||||
await fs.rm(authTestRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
vi.unstubAllGlobals();
|
||||
resolvePluginProviders.mockReset();
|
||||
@@ -783,8 +831,8 @@ describe("applyAuthChoice", () => {
|
||||
detectZaiEndpoint.mockResolvedValue(null);
|
||||
loginOpenAICodexOAuth.mockReset();
|
||||
loginOpenAICodexOAuth.mockResolvedValue(null);
|
||||
testAuthProfileStores.clear();
|
||||
await lifecycle.cleanup();
|
||||
activeStateDir = null;
|
||||
});
|
||||
|
||||
it("applies Anthropic setup-token auth when the provider exposes the setup flow", async () => {
|
||||
@@ -1464,18 +1512,14 @@ describe("applyAuthChoice", () => {
|
||||
});
|
||||
const profileStore =
|
||||
scenario.agentId && scenario.agentId !== "default"
|
||||
? await readAuthProfilesForAgent<{ profiles?: Record<string, StoredAuthProfile> }>(
|
||||
resolveAgentDir(result.config, scenario.agentId),
|
||||
)
|
||||
? await readAuthProfilesForAgentDir(resolveAgentDir(result.config, scenario.agentId))
|
||||
: await readAuthProfiles();
|
||||
expect(profileStore.profiles?.[scenario.profileId]?.key).toBe(scenario.token);
|
||||
}
|
||||
if (scenario.extraProfileId) {
|
||||
const profileStore =
|
||||
scenario.agentId && scenario.agentId !== "default"
|
||||
? await readAuthProfilesForAgent<{ profiles?: Record<string, StoredAuthProfile> }>(
|
||||
resolveAgentDir(result.config, scenario.agentId),
|
||||
)
|
||||
? await readAuthProfilesForAgentDir(resolveAgentDir(result.config, scenario.agentId))
|
||||
: await readAuthProfiles();
|
||||
expect(profileStore.profiles?.[scenario.extraProfileId]?.key).toBe(scenario.token);
|
||||
}
|
||||
@@ -1580,27 +1624,17 @@ describe("applyAuthChoice", () => {
|
||||
await setupTempState();
|
||||
process.env.LITELLM_API_KEY = "sk-litellm-test"; // pragma: allowlist secret
|
||||
|
||||
const authProfilePath = authProfilePathForAgent(requireOpenClawAgentDir());
|
||||
await fs.writeFile(
|
||||
authProfilePath,
|
||||
JSON.stringify(
|
||||
{
|
||||
version: 1,
|
||||
profiles: {
|
||||
"litellm:legacy": {
|
||||
type: "oauth",
|
||||
provider: "litellm",
|
||||
access: "access-token",
|
||||
refresh: "refresh-token",
|
||||
expires: Date.now() + 60_000,
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf8",
|
||||
);
|
||||
seedTestAuthProfile({
|
||||
profileId: "litellm:legacy",
|
||||
credential: {
|
||||
type: "oauth",
|
||||
provider: "litellm",
|
||||
access: "access-token",
|
||||
refresh: "refresh-token",
|
||||
expires: Date.now() + 60_000,
|
||||
},
|
||||
agentDir: requireOpenClawAgentDir(),
|
||||
});
|
||||
|
||||
const text = vi.fn();
|
||||
const confirm = vi.fn(async () => true);
|
||||
|
||||
@@ -1145,29 +1145,13 @@ describe("doctor config flow", () => {
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("drops unknown keys on repair", async () => {
|
||||
it("repairs generic legacy config surfaces in one pass", async () => {
|
||||
const result = await runDoctorConfigWithInput({
|
||||
repair: true,
|
||||
config: {
|
||||
bridge: { bind: "auto" },
|
||||
gateway: { auth: { mode: "token", token: "ok", extra: true } },
|
||||
agents: { list: [{ id: "pi" }] },
|
||||
},
|
||||
run: loadAndMaybeMigrateDoctorConfig,
|
||||
});
|
||||
|
||||
const cfg = result.cfg as Record<string, unknown>;
|
||||
expect(cfg.bridge).toBeUndefined();
|
||||
expect((cfg.gateway as Record<string, unknown>)?.auth).toEqual({
|
||||
mode: "token",
|
||||
token: "ok",
|
||||
});
|
||||
});
|
||||
|
||||
it("migrates legacy browser extension profiles to existing-session on repair", async () => {
|
||||
const result = await runDoctorConfigWithInput({
|
||||
repair: true,
|
||||
config: {
|
||||
browser: {
|
||||
relayBindHost: "0.0.0.0",
|
||||
profiles: {
|
||||
@@ -1177,21 +1161,6 @@ describe("doctor config flow", () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
run: loadAndMaybeMigrateDoctorConfig,
|
||||
});
|
||||
|
||||
const browser = (result.cfg as { browser?: Record<string, unknown> }).browser ?? {};
|
||||
expect(browser.relayBindHost).toBeUndefined();
|
||||
expect(
|
||||
((browser.profiles as Record<string, { driver?: string }>)?.chromeLive ?? {}).driver,
|
||||
).toBe("existing-session");
|
||||
});
|
||||
|
||||
it("repairs restrictive plugins.allow when browser is referenced via tools.alsoAllow", async () => {
|
||||
const result = await runDoctorConfigWithInput({
|
||||
repair: true,
|
||||
config: {
|
||||
tools: {
|
||||
alsoAllow: ["browser"],
|
||||
},
|
||||
@@ -1202,6 +1171,17 @@ describe("doctor config flow", () => {
|
||||
run: loadAndMaybeMigrateDoctorConfig,
|
||||
});
|
||||
|
||||
const cfg = result.cfg as Record<string, unknown>;
|
||||
expect(cfg.bridge).toBeUndefined();
|
||||
expect((cfg.gateway as Record<string, unknown>)?.auth).toEqual({
|
||||
mode: "token",
|
||||
token: "ok",
|
||||
});
|
||||
const browser = (result.cfg as { browser?: Record<string, unknown> }).browser ?? {};
|
||||
expect(browser.relayBindHost).toBeUndefined();
|
||||
expect(
|
||||
((browser.profiles as Record<string, { driver?: string }>)?.chromeLive ?? {}).driver,
|
||||
).toBe("existing-session");
|
||||
expect(result.cfg.plugins?.allow).toEqual(["telegram", "browser"]);
|
||||
expect(result.cfg.plugins?.entries?.browser?.enabled).toBe(true);
|
||||
});
|
||||
@@ -1735,7 +1715,7 @@ describe("doctor config flow", () => {
|
||||
expect(cfg.channels.discord.accounts.default.allowFrom).toEqual(["123"]);
|
||||
});
|
||||
|
||||
it('adds allowFrom ["*"] when dmPolicy="open" and allowFrom is missing on repair', async () => {
|
||||
it('repairs open dmPolicy allowFrom variants with ["*"] in one pass', async () => {
|
||||
const result = await runDoctorConfigWithInput({
|
||||
repair: true,
|
||||
config: {
|
||||
@@ -1745,16 +1725,40 @@ describe("doctor config flow", () => {
|
||||
dmPolicy: "open",
|
||||
groupPolicy: "open",
|
||||
},
|
||||
googlechat: {
|
||||
accounts: {
|
||||
work: {
|
||||
dm: {
|
||||
policy: "open",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
run: loadAndMaybeMigrateDoctorConfig,
|
||||
});
|
||||
|
||||
const cfg = result.cfg as unknown as {
|
||||
channels: { discord: { allowFrom: string[]; dmPolicy: string } };
|
||||
channels: {
|
||||
discord: { allowFrom: string[]; dmPolicy: string };
|
||||
googlechat: {
|
||||
accounts: {
|
||||
work: {
|
||||
dm: {
|
||||
policy: string;
|
||||
allowFrom: string[];
|
||||
};
|
||||
allowFrom?: string[];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
expect(cfg.channels.discord.allowFrom).toEqual(["*"]);
|
||||
expect(cfg.channels.discord.dmPolicy).toBe("open");
|
||||
expect(cfg.channels.googlechat.accounts.work.dm.allowFrom).toEqual(["*"]);
|
||||
expect(cfg.channels.googlechat.accounts.work.allowFrom).toBeUndefined();
|
||||
});
|
||||
|
||||
it('repairs dmPolicy="allowlist" by restoring allowFrom from pairing store on repair', async () => {
|
||||
@@ -1847,13 +1851,37 @@ describe("doctor config flow", () => {
|
||||
expect(toolsBySender["*"]).toEqual({ deny: ["exec"] });
|
||||
});
|
||||
|
||||
it("migrates top-level heartbeat into agents.defaults.heartbeat on repair", async () => {
|
||||
it("repairs legacy root runtime config surfaces in one pass", async () => {
|
||||
const result = await runDoctorConfigWithInput({
|
||||
repair: true,
|
||||
config: {
|
||||
heartbeat: {
|
||||
model: "anthropic/claude-3-5-haiku-20241022",
|
||||
every: "30m",
|
||||
showOk: true,
|
||||
showAlerts: false,
|
||||
},
|
||||
gateway: {
|
||||
bind: "0.0.0.0",
|
||||
},
|
||||
session: {
|
||||
threadBindings: {
|
||||
ttlHours: 24,
|
||||
},
|
||||
},
|
||||
channels: {
|
||||
discord: {
|
||||
threadBindings: {
|
||||
ttlHours: 12,
|
||||
},
|
||||
accounts: {
|
||||
alpha: {
|
||||
threadBindings: {
|
||||
ttlHours: 6,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
run: loadAndMaybeMigrateDoctorConfig,
|
||||
@@ -1861,6 +1889,15 @@ describe("doctor config flow", () => {
|
||||
|
||||
const cfg = result.cfg as {
|
||||
heartbeat?: unknown;
|
||||
gateway?: {
|
||||
bind?: string;
|
||||
};
|
||||
session?: {
|
||||
threadBindings?: {
|
||||
idleHours?: number;
|
||||
ttlHours?: number;
|
||||
};
|
||||
};
|
||||
agents?: {
|
||||
defaults?: {
|
||||
heartbeat?: {
|
||||
@@ -1869,12 +1906,53 @@ describe("doctor config flow", () => {
|
||||
};
|
||||
};
|
||||
};
|
||||
channels?: {
|
||||
defaults?: {
|
||||
heartbeat?: {
|
||||
showOk?: boolean;
|
||||
showAlerts?: boolean;
|
||||
useIndicator?: boolean;
|
||||
};
|
||||
};
|
||||
discord?: {
|
||||
threadBindings?: {
|
||||
idleHours?: number;
|
||||
ttlHours?: number;
|
||||
};
|
||||
accounts?: Record<
|
||||
string,
|
||||
{
|
||||
threadBindings?: {
|
||||
idleHours?: number;
|
||||
ttlHours?: number;
|
||||
};
|
||||
}
|
||||
>;
|
||||
};
|
||||
};
|
||||
};
|
||||
expect(cfg.heartbeat).toBeUndefined();
|
||||
expect(cfg.agents?.defaults?.heartbeat).toMatchObject({
|
||||
model: "anthropic/claude-3-5-haiku-20241022",
|
||||
every: "30m",
|
||||
});
|
||||
expect(cfg.gateway?.bind).toBe("lan");
|
||||
expect(cfg.session?.threadBindings).toMatchObject({
|
||||
idleHours: 24,
|
||||
});
|
||||
expect(cfg.channels?.discord?.threadBindings).toMatchObject({
|
||||
idleHours: 12,
|
||||
});
|
||||
expect(cfg.channels?.discord?.accounts?.alpha?.threadBindings).toMatchObject({
|
||||
idleHours: 6,
|
||||
});
|
||||
expect(cfg.session?.threadBindings?.ttlHours).toBeUndefined();
|
||||
expect(cfg.channels?.discord?.threadBindings?.ttlHours).toBeUndefined();
|
||||
expect(cfg.channels?.discord?.accounts?.alpha?.threadBindings?.ttlHours).toBeUndefined();
|
||||
expect(cfg.channels?.defaults?.heartbeat).toMatchObject({
|
||||
showOk: true,
|
||||
showAlerts: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("warns clearly about legacy config surfaces and points to doctor --fix", async () => {
|
||||
@@ -1985,161 +2063,6 @@ describe("doctor config flow", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("repairs legacy gateway.bind host aliases on repair", async () => {
|
||||
const result = await runDoctorConfigWithInput({
|
||||
repair: true,
|
||||
config: {
|
||||
gateway: {
|
||||
bind: "0.0.0.0",
|
||||
},
|
||||
},
|
||||
run: loadAndMaybeMigrateDoctorConfig,
|
||||
});
|
||||
|
||||
const cfg = result.cfg as {
|
||||
gateway?: {
|
||||
bind?: string;
|
||||
};
|
||||
};
|
||||
expect(cfg.gateway?.bind).toBe("lan");
|
||||
});
|
||||
|
||||
it("repairs legacy thread binding ttlHours config on repair", async () => {
|
||||
const result = await runDoctorConfigWithInput({
|
||||
repair: true,
|
||||
config: {
|
||||
session: {
|
||||
threadBindings: {
|
||||
ttlHours: 24,
|
||||
},
|
||||
},
|
||||
channels: {
|
||||
discord: {
|
||||
threadBindings: {
|
||||
ttlHours: 12,
|
||||
},
|
||||
accounts: {
|
||||
alpha: {
|
||||
threadBindings: {
|
||||
ttlHours: 6,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
run: loadAndMaybeMigrateDoctorConfig,
|
||||
});
|
||||
|
||||
const cfg = result.cfg as {
|
||||
session?: {
|
||||
threadBindings?: {
|
||||
idleHours?: number;
|
||||
ttlHours?: number;
|
||||
};
|
||||
};
|
||||
channels?: {
|
||||
discord?: {
|
||||
threadBindings?: {
|
||||
idleHours?: number;
|
||||
ttlHours?: number;
|
||||
};
|
||||
accounts?: Record<
|
||||
string,
|
||||
{
|
||||
threadBindings?: {
|
||||
idleHours?: number;
|
||||
ttlHours?: number;
|
||||
};
|
||||
}
|
||||
>;
|
||||
};
|
||||
};
|
||||
};
|
||||
expect(cfg.session?.threadBindings).toMatchObject({
|
||||
idleHours: 24,
|
||||
});
|
||||
expect(cfg.channels?.discord?.threadBindings).toMatchObject({
|
||||
idleHours: 12,
|
||||
});
|
||||
expect(cfg.channels?.discord?.accounts?.alpha?.threadBindings).toMatchObject({
|
||||
idleHours: 6,
|
||||
});
|
||||
expect(cfg.session?.threadBindings?.ttlHours).toBeUndefined();
|
||||
expect(cfg.channels?.discord?.threadBindings?.ttlHours).toBeUndefined();
|
||||
expect(cfg.channels?.discord?.accounts?.alpha?.threadBindings?.ttlHours).toBeUndefined();
|
||||
});
|
||||
|
||||
it("migrates top-level heartbeat visibility into channels.defaults.heartbeat on repair", async () => {
|
||||
const result = await runDoctorConfigWithInput({
|
||||
repair: true,
|
||||
config: {
|
||||
heartbeat: {
|
||||
showOk: true,
|
||||
showAlerts: false,
|
||||
},
|
||||
},
|
||||
run: loadAndMaybeMigrateDoctorConfig,
|
||||
});
|
||||
|
||||
const cfg = result.cfg as {
|
||||
heartbeat?: unknown;
|
||||
channels?: {
|
||||
defaults?: {
|
||||
heartbeat?: {
|
||||
showOk?: boolean;
|
||||
showAlerts?: boolean;
|
||||
useIndicator?: boolean;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
expect(cfg.heartbeat).toBeUndefined();
|
||||
expect(cfg.channels?.defaults?.heartbeat).toMatchObject({
|
||||
showOk: true,
|
||||
showAlerts: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("repairs googlechat account dm.policy open by setting dm.allowFrom on repair", async () => {
|
||||
const result = await runDoctorConfigWithInput({
|
||||
repair: true,
|
||||
config: {
|
||||
channels: {
|
||||
googlechat: {
|
||||
accounts: {
|
||||
work: {
|
||||
dm: {
|
||||
policy: "open",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
run: loadAndMaybeMigrateDoctorConfig,
|
||||
});
|
||||
|
||||
const cfg = result.cfg as unknown as {
|
||||
channels: {
|
||||
googlechat: {
|
||||
accounts: {
|
||||
work: {
|
||||
dm: {
|
||||
policy: string;
|
||||
allowFrom: string[];
|
||||
};
|
||||
allowFrom?: string[];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
expect(cfg.channels.googlechat.accounts.work.dm.allowFrom).toEqual(["*"]);
|
||||
expect(cfg.channels.googlechat.accounts.work.allowFrom).toBeUndefined();
|
||||
});
|
||||
|
||||
it("recovers from stale googlechat top-level allowFrom by repairing dm.allowFrom", async () => {
|
||||
const result = await runDoctorConfigWithInput({
|
||||
repair: true,
|
||||
|
||||
@@ -81,12 +81,12 @@ export function maybeRepairLegacyToolsBySenderKeys(cfg: OpenClawConfig): {
|
||||
config: OpenClawConfig;
|
||||
changes: string[];
|
||||
} {
|
||||
const next = structuredClone(cfg);
|
||||
const hits = scanLegacyToolsBySenderKeys(next);
|
||||
const hits = scanLegacyToolsBySenderKeys(cfg);
|
||||
if (hits.length === 0) {
|
||||
return { config: cfg, changes: [] };
|
||||
}
|
||||
|
||||
const next = structuredClone(cfg);
|
||||
const summary = new Map<string, { migrated: number; dropped: number; examples: string[] }>();
|
||||
let changed = false;
|
||||
|
||||
|
||||
@@ -50,7 +50,14 @@ export function scanStalePluginConfig(
|
||||
return [];
|
||||
}
|
||||
|
||||
const { knownIds } = collectPluginRegistryState(cfg, env);
|
||||
return scanStalePluginConfigWithState(plugins, collectPluginRegistryState(cfg, env));
|
||||
}
|
||||
|
||||
function scanStalePluginConfigWithState(
|
||||
plugins: Record<string, unknown>,
|
||||
registryState: StalePluginRegistryState,
|
||||
): StalePluginConfigHit[] {
|
||||
const { knownIds } = registryState;
|
||||
const hits: StalePluginConfigHit[] = [];
|
||||
|
||||
const allow = Array.isArray(plugins.allow) ? plugins.allow : [];
|
||||
@@ -117,11 +124,17 @@ export function maybeRepairStalePluginConfig(
|
||||
config: OpenClawConfig;
|
||||
changes: string[];
|
||||
} {
|
||||
if (isStalePluginAutoRepairBlocked(cfg, env)) {
|
||||
const plugins = asObjectRecord(cfg.plugins);
|
||||
if (!plugins) {
|
||||
return { config: cfg, changes: [] };
|
||||
}
|
||||
|
||||
const hits = scanStalePluginConfig(cfg, env);
|
||||
const registryState = collectPluginRegistryState(cfg, env);
|
||||
if (registryState.hasDiscoveryErrors) {
|
||||
return { config: cfg, changes: [] };
|
||||
}
|
||||
|
||||
const hits = scanStalePluginConfigWithState(plugins, registryState);
|
||||
if (hits.length === 0) {
|
||||
return { config: cfg, changes: [] };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user