fix(qqbot): type active config provider

This commit is contained in:
statxc
2026-04-29 14:14:24 +02:00
committed by Peter Steinberger
parent d69b663021
commit 278ffbdb53
4 changed files with 54 additions and 60 deletions

View File

@@ -93,9 +93,7 @@ describe("codex plugin", () => {
registerMediaUnderstandingProvider: vi.fn(),
registerProvider,
on: vi.fn(),
}) as ReturnType<typeof createTestPluginApi> & {
onConversationBindingResolved?: ReturnType<typeof vi.fn>;
};
});
delete (api as { onConversationBindingResolved?: unknown }).onConversationBindingResolved;
plugin.register(api);

View File

@@ -1,71 +1,64 @@
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
import { describe, expect, it, vi } from "vitest";
import {
createActiveCfgProvider,
resolveActiveCfg,
type GatewayCfg,
type GatewayCfgFetcher,
} from "./active-cfg.js";
import { createActiveCfgProvider, resolveActiveCfg, type GatewayCfgLoader } from "./active-cfg.js";
const getRuntimeConfigMock = vi.hoisted(() => vi.fn<() => GatewayCfg | undefined>());
const getRuntimeConfigMock = vi.hoisted(() => vi.fn<() => OpenClawConfig>());
vi.mock("openclaw/plugin-sdk/runtime-config-snapshot", () => ({
getRuntimeConfig: getRuntimeConfigMock,
}));
function asCfg(shape: { bindings: Array<{ id: string }> }): OpenClawConfig {
return shape as unknown as OpenClawConfig;
}
describe("resolveActiveCfg", () => {
it("returns the freshly fetched value when present", () => {
const fresh = { bindings: [{ id: "fresh" }] };
const fallback = { bindings: [{ id: "stale" }] };
const fetch: GatewayCfgFetcher = () => fresh;
it("returns the freshly loaded value when the loader succeeds", () => {
const fresh = asCfg({ bindings: [{ id: "fresh" }] });
const fallback = asCfg({ bindings: [{ id: "stale" }] });
const loader: GatewayCfgLoader = () => fresh;
expect(resolveActiveCfg(fetch, fallback)).toBe(fresh);
expect(resolveActiveCfg(loader, fallback)).toBe(fresh);
});
it("falls back when the fetcher returns undefined", () => {
const fallback = { bindings: [{ id: "stale" }] };
const fetch: GatewayCfgFetcher = () => undefined;
expect(resolveActiveCfg(fetch, fallback)).toBe(fallback);
});
it("falls back when the fetcher throws", () => {
const fallback = { bindings: [{ id: "stale" }] };
const fetch: GatewayCfgFetcher = () => {
it("falls back when the loader throws", () => {
const fallback = asCfg({ bindings: [{ id: "stale" }] });
const loader: GatewayCfgLoader = () => {
throw new Error("snapshot not initialised");
};
expect(resolveActiveCfg(fetch, fallback)).toBe(fallback);
expect(resolveActiveCfg(loader, fallback)).toBe(fallback);
});
});
describe("createActiveCfgProvider", () => {
it("invokes the injected fetcher on every getActiveCfg call", () => {
const fallback = { bindings: [] };
const first = { bindings: [{ id: "first" }] };
const second = { bindings: [{ id: "second" }] };
const fetch = vi
.fn<() => GatewayCfg | undefined>()
it("invokes the injected loader on every getActiveCfg call", () => {
const fallback = asCfg({ bindings: [] });
const first = asCfg({ bindings: [{ id: "first" }] });
const second = asCfg({ bindings: [{ id: "second" }] });
const load = vi
.fn<() => OpenClawConfig>()
.mockReturnValueOnce(first)
.mockReturnValueOnce(second);
const provider = createActiveCfgProvider({ fallback, fetch });
const provider = createActiveCfgProvider({ fallback, load });
expect(provider.getActiveCfg()).toBe(first);
expect(provider.getActiveCfg()).toBe(second);
expect(fetch).toHaveBeenCalledTimes(2);
expect(load).toHaveBeenCalledTimes(2);
});
it("never caches a previously fetched value", () => {
const fallback = { bindings: [] };
const calls: GatewayCfg[] = [
{ bindings: [{ id: "a" }] },
{ bindings: [{ id: "b" }] },
{ bindings: [{ id: "c" }] },
it("never caches a previously loaded value", () => {
const fallback = asCfg({ bindings: [] });
const calls: OpenClawConfig[] = [
asCfg({ bindings: [{ id: "a" }] }),
asCfg({ bindings: [{ id: "b" }] }),
asCfg({ bindings: [{ id: "c" }] }),
];
let index = 0;
const provider = createActiveCfgProvider({
fallback,
fetch: () => calls[index++],
load: () => calls[index++],
});
expect(provider.getActiveCfg()).toBe(calls[0]);
@@ -73,19 +66,19 @@ describe("createActiveCfgProvider", () => {
expect(provider.getActiveCfg()).toBe(calls[2]);
});
it("delegates to getRuntimeConfig when no fetcher is provided", () => {
const live = { bindings: [{ id: "live" }] };
it("delegates to getRuntimeConfig when no loader is provided", () => {
const live = asCfg({ bindings: [{ id: "live" }] });
getRuntimeConfigMock.mockReset();
getRuntimeConfigMock.mockReturnValue(live);
const provider = createActiveCfgProvider({ fallback: { bindings: [] } });
const provider = createActiveCfgProvider({ fallback: asCfg({ bindings: [] }) });
expect(provider.getActiveCfg()).toBe(live);
expect(getRuntimeConfigMock).toHaveBeenCalledTimes(1);
});
it("falls back to the supplied snapshot when the SDK getter throws", () => {
const fallback = { bindings: [{ id: "snapshot" }] };
const fallback = asCfg({ bindings: [{ id: "snapshot" }] });
getRuntimeConfigMock.mockReset();
getRuntimeConfigMock.mockImplementation(() => {
throw new Error("not ready");

View File

@@ -5,46 +5,48 @@
* peer/account binding edits made via the CLI take effect without
* restarting the gateway. The provider hides the per-event lookup
* behind a typed seam and falls back to the startup snapshot when the
* runtime registry is not yet (or no longer) populated.
* runtime registry getter throws (e.g. snapshot not yet initialised).
*
* Issue #69546.
*/
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
import { getRuntimeConfig } from "openclaw/plugin-sdk/runtime-config-snapshot";
export type GatewayCfg = unknown;
export type GatewayCfg = OpenClawConfig;
export type GatewayCfgFetcher = () => GatewayCfg | undefined;
export type GatewayCfgLoader = () => OpenClawConfig;
export interface ActiveCfgProvider {
getActiveCfg(): GatewayCfg;
getActiveCfg(): OpenClawConfig;
}
export interface ActiveCfgProviderOptions {
fallback: GatewayCfg;
fetch?: GatewayCfgFetcher;
fallback: OpenClawConfig;
load?: GatewayCfgLoader;
}
export function createActiveCfgProvider(options: ActiveCfgProviderOptions): ActiveCfgProvider {
const fetch = options.fetch ?? defaultGatewayCfgFetcher;
const loader = options.load ?? defaultGatewayCfgLoader;
const fallback = options.fallback;
return {
getActiveCfg(): GatewayCfg {
return resolveActiveCfg(fetch, fallback);
getActiveCfg(): OpenClawConfig {
return resolveActiveCfg(loader, fallback);
},
};
}
export function resolveActiveCfg(fetch: GatewayCfgFetcher, fallback: GatewayCfg): GatewayCfg {
let fresh: GatewayCfg | undefined;
export function resolveActiveCfg(
loader: GatewayCfgLoader,
fallback: OpenClawConfig,
): OpenClawConfig {
try {
fresh = fetch();
return loader();
} catch {
return fallback;
}
return fresh ?? fallback;
}
function defaultGatewayCfgFetcher(): GatewayCfg | undefined {
function defaultGatewayCfgLoader(): OpenClawConfig {
return getRuntimeConfig();
}

View File

@@ -1,3 +1,4 @@
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
import type { EngineLogger } from "../types.js";
export type { EngineLogger };
@@ -210,7 +211,7 @@ interface GatewayGroupOptions {
export interface CoreGatewayContext {
account: GatewayAccount;
abortSignal: AbortSignal;
cfg: unknown;
cfg: OpenClawConfig;
onReady?: (data: unknown) => void;
/**
* Invoked when a RESUMED event is received after reconnect.