fix(matrix): centralize initial sync limit coercion

This commit is contained in:
Peter Steinberger
2026-05-29 01:22:24 -04:00
parent b43910b590
commit 1e5ccd1ce8
9 changed files with 76 additions and 9 deletions

View File

@@ -1,6 +1,5 @@
import { resolveIntegerOption } from "openclaw/plugin-sdk/number-runtime";
export function resolveMatrixActionLimit(raw: unknown, fallback: number): number {
if (typeof raw !== "number" || !Number.isFinite(raw)) {
return fallback;
}
return Math.max(1, Math.floor(raw));
return resolveIntegerOption(raw, fallback, { min: 1 });
}

View File

@@ -85,6 +85,24 @@ describe("Matrix auth/config live surfaces", () => {
expect(resolved.encryption).toBe(false);
});
it("ignores non-finite initial sync limits", () => {
const cfg = {
channels: {
matrix: {
initialSyncLimit: Number.NaN,
accounts: {
ops: {
initialSyncLimit: Number.POSITIVE_INFINITY,
},
},
},
},
} as unknown as CoreConfig;
const resolved = resolveMatrixConfigForAccount(cfg, "ops", {} as NodeJS.ProcessEnv);
expect(resolved.initialSyncLimit).toBeUndefined();
});
it("resolves accessToken SecretRef against the provided env", () => {
const cfg = {
channels: {

View File

@@ -1,4 +1,5 @@
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import { resolveOptionalIntegerOption } from "openclaw/plugin-sdk/number-runtime";
import { requireRuntimeConfig } from "openclaw/plugin-sdk/plugin-config-runtime";
import { retryAsync } from "openclaw/plugin-sdk/retry-runtime";
import {
@@ -412,7 +413,7 @@ function readMatrixAccountConfigField(
}
function clampMatrixInitialSyncLimit(value: unknown): number | undefined {
return typeof value === "number" ? Math.max(0, Math.floor(value)) : undefined;
return resolveOptionalIntegerOption(value, { min: 0 });
}
function buildMatrixNetworkFields(params: {

View File

@@ -54,6 +54,27 @@ describe("updateMatrixAccountConfig", () => {
expect(account?.userId).toBeUndefined();
});
it("does not store non-finite initial sync limits", () => {
const cfg = {
channels: {
matrix: {
accounts: {
default: {
initialSyncLimit: 20,
},
},
},
},
} as CoreConfig;
const updated = updateMatrixAccountConfig(cfg, "default", {
initialSyncLimit: Number.NaN,
});
expect(updated.channels?.matrix?.initialSyncLimit).toBeUndefined();
expect(updated.channels?.matrix?.accounts?.default?.initialSyncLimit).toBeUndefined();
});
it("preserves SecretRef auth inputs when updating config", () => {
const updated = updateMatrixAccountConfig({} as CoreConfig, "default", {
accessToken: { source: "env", provider: "default", id: "MATRIX_ACCESS_TOKEN" },

View File

@@ -1,4 +1,5 @@
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
import { resolveOptionalIntegerOption } from "openclaw/plugin-sdk/number-runtime";
import { coerceSecretRef } from "openclaw/plugin-sdk/secret-ref-runtime";
import { normalizeSecretInputString } from "openclaw/plugin-sdk/setup";
import type { CoreConfig, MatrixConfig } from "../types.js";
@@ -188,7 +189,12 @@ export function updateMatrixAccountConfig(
if (patch.initialSyncLimit === null) {
delete nextAccount.initialSyncLimit;
} else {
nextAccount.initialSyncLimit = Math.max(0, Math.floor(patch.initialSyncLimit));
const initialSyncLimit = resolveOptionalIntegerOption(patch.initialSyncLimit, { min: 0 });
if (initialSyncLimit === undefined) {
delete nextAccount.initialSyncLimit;
} else {
nextAccount.initialSyncLimit = initialSyncLimit;
}
}
}

View File

@@ -3,6 +3,7 @@ import { CHANNEL_APPROVAL_NATIVE_RUNTIME_CONTEXT_CAPABILITY } from "openclaw/plu
import type { ChannelRuntimeSurface } from "openclaw/plugin-sdk/channel-contract";
import { waitUntilAbort } from "openclaw/plugin-sdk/channel-outbound";
import { registerChannelRuntimeContext } from "openclaw/plugin-sdk/channel-runtime-context";
import { resolveOptionalIntegerOption } from "openclaw/plugin-sdk/number-runtime";
import {
GROUP_POLICY_BLOCKED_LABEL,
resolveThreadBindingIdleTimeoutMsForChannel,
@@ -214,9 +215,7 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi
const auth = await resolveMatrixAuth({ cfg, accountId: effectiveAccountId });
const resolvedInitialSyncLimit =
typeof opts.initialSyncLimit === "number"
? Math.max(0, Math.floor(opts.initialSyncLimit))
: auth.initialSyncLimit;
resolveOptionalIntegerOption(opts.initialSyncLimit, { min: 0 }) ?? auth.initialSyncLimit;
const authWithLimit =
resolvedInitialSyncLimit === auth.initialSyncLimit
? auth

View File

@@ -4,6 +4,7 @@ export {
parseFiniteNumber,
resolveIntegerOption,
resolveNonNegativeIntegerOption,
resolveOptionalIntegerOption,
parseStrictInteger,
parseStrictFiniteNumber,
parseStrictNonNegativeInteger,

View File

@@ -6,6 +6,7 @@ import {
parseFiniteNumber,
resolveIntegerOption,
resolveNonNegativeIntegerOption,
resolveOptionalIntegerOption,
parseStrictFiniteNumber,
parseStrictInteger,
parseStrictNonNegativeInteger,
@@ -76,4 +77,12 @@ describe("number-coercion", () => {
expect(resolveIntegerOption(40, 1, { max: 10 })).toBe(10);
expect(resolveNonNegativeIntegerOption(Number.NaN, 3.9)).toBe(3);
});
test("optional integer option helper rejects non-finite values", () => {
expect(resolveOptionalIntegerOption(7.9, { min: 1, max: 10 })).toBe(7);
expect(resolveOptionalIntegerOption(Number.NaN, { min: 1 })).toBeUndefined();
expect(resolveOptionalIntegerOption(Number.POSITIVE_INFINITY, { min: 1 })).toBeUndefined();
expect(resolveOptionalIntegerOption(-4, { min: 0 })).toBe(0);
expect(resolveOptionalIntegerOption(40, { max: 10 })).toBe(10);
});
});

View File

@@ -107,6 +107,19 @@ export function resolveIntegerOption(
return range.max === undefined ? minBounded : Math.min(range.max, minBounded);
}
export function resolveOptionalIntegerOption(
value: unknown,
range: {
min?: number;
max?: number;
} = {},
): number | undefined {
if (typeof value !== "number" || !Number.isFinite(value)) {
return undefined;
}
return resolveIntegerOption(value, value, range);
}
export function resolveNonNegativeIntegerOption(value: unknown, fallback: number): number {
return resolveIntegerOption(value, fallback, { min: 0 });
}