From 2e40ca2c1533308325889f2deba64ed2ef82e127 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 23 Apr 2026 04:42:54 +0100 Subject: [PATCH] build: enable additional oxlint rules --- .oxlintrc.json | 18 +++- extensions/feishu/src/bot.ts | 1 - .../mattermost/src/mattermost/client.ts | 2 +- .../nostr/src/nostr-bus.integration.test.ts | 2 +- extensions/qqbot/src/engine/api/retry.ts | 4 +- extensions/synology-chat/src/client.ts | 2 +- extensions/tlon/src/monitor/index.ts | 2 +- extensions/tlon/src/urbit/sse-client.ts | 2 +- src/agents/subagent-announce-queue.ts | 2 +- ...sion-conversation.bundled-fallback.test.ts | 10 +- src/docs/install-cloud-secrets.test.ts | 6 +- src/gateway/openai-http.test.ts | 92 +++++++++---------- src/logging/diagnostic-support-export.test.ts | 2 +- src/logging/diagnostic-support-redaction.ts | 2 +- test/scripts/oxlint-config.test.ts | 27 ++++++ .../config/form-utils.node.test.ts | 2 +- 16 files changed, 103 insertions(+), 73 deletions(-) diff --git a/.oxlintrc.json b/.oxlintrc.json index 75d9e4f78f1..3b9bf192265 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -11,10 +11,17 @@ "eslint-plugin-unicorn/prefer-array-find": "error", "eslint/no-array-constructor": "error", "eslint/no-await-in-loop": "off", + "eslint/no-div-regex": "error", + "eslint/no-extra-label": "error", "eslint/no-empty-pattern": ["error", { "allowObjectPatternsAsParameters": false }], + "eslint/no-lone-blocks": "error", + "eslint/no-multi-str": "error", "eslint/no-new": "error", "eslint/no-object-constructor": "error", + "eslint/no-proto": "error", + "eslint/no-regex-spaces": "error", "eslint/no-return-assign": "error", + "eslint/no-sequences": "error", "eslint/no-shadow": "off", "eslint/no-useless-call": "error", "eslint/no-useless-computed-key": "error", @@ -22,6 +29,9 @@ "eslint/no-useless-constructor": "error", "eslint/no-warning-comments": "error", "eslint/no-unmodified-loop-condition": "error", + "eslint/prefer-exponentiation-operator": "error", + "eslint/prefer-numeric-literals": "error", + "eslint/unicode-bom": "error", "eslint-plugin-unicorn/prefer-set-size": "error", "oxc/no-accumulating-spread": "error", "oxc/no-async-endpoint-handlers": "error", @@ -31,6 +41,7 @@ "typescript/no-explicit-any": "error", "typescript/no-extraneous-class": "error", "typescript/no-meaningless-void-operator": "error", + "typescript/no-non-null-asserted-nullish-coalescing": "error", "typescript/no-unnecessary-type-assertion": "error", "typescript/no-unnecessary-type-arguments": "error", "typescript/no-unnecessary-type-constraint": "error", @@ -38,15 +49,20 @@ "typescript/no-unnecessary-type-parameters": "error", "typescript/no-unsafe-type-assertion": "off", "typescript/no-useless-default-assignment": "error", + "typescript/prefer-return-this-type": "error", "typescript/prefer-ts-expect-error": "error", "unicorn/consistent-function-scoping": "off", "unicorn/no-unnecessary-array-flat-depth": "error", "unicorn/no-unnecessary-array-splice-count": "error", "unicorn/no-unnecessary-slice-end": "error", + "unicorn/no-useless-error-capture-stack-trace": "error", "unicorn/no-useless-promise-resolve-reject": "error", "unicorn/prefer-date-now": "error", + "unicorn/prefer-optional-catch-binding": "error", "unicorn/prefer-set-size": "error", - "unicorn/require-post-message-target-origin": "error" + "unicorn/require-array-join-separator": "error", + "unicorn/require-post-message-target-origin": "error", + "unicorn/throw-new-error": "error" }, "ignorePatterns": [ "assets/", diff --git a/extensions/feishu/src/bot.ts b/extensions/feishu/src/bot.ts index fb80695af80..d377d3ea5b0 100644 --- a/extensions/feishu/src/bot.ts +++ b/extensions/feishu/src/bot.ts @@ -492,7 +492,6 @@ export async function handleFeishuMessage(params: { } return; } - } else { } try { diff --git a/extensions/mattermost/src/mattermost/client.ts b/extensions/mattermost/src/mattermost/client.ts index 3e1c3c93139..f668c0f14b2 100644 --- a/extensions/mattermost/src/mattermost/client.ts +++ b/extensions/mattermost/src/mattermost/client.ts @@ -329,7 +329,7 @@ export async function createMattermostDirectChannelWithRetry( // Calculate exponential backoff delay with full-jitter // Jitter is proportional to the exponential delay, not a fixed 1000ms // This ensures backoff behaves correctly for small delay configurations - const exponentialDelay = initialDelayMs * Math.pow(2, attempt); + const exponentialDelay = initialDelayMs * 2 ** attempt; const jitter = Math.random() * exponentialDelay; const delayMs = Math.min(exponentialDelay + jitter, maxDelayMs); diff --git a/extensions/nostr/src/nostr-bus.integration.test.ts b/extensions/nostr/src/nostr-bus.integration.test.ts index 9372cc3df61..8f677d95912 100644 --- a/extensions/nostr/src/nostr-bus.integration.test.ts +++ b/extensions/nostr/src/nostr-bus.integration.test.ts @@ -459,7 +459,7 @@ describe("Reconnect Backoff", () => { const JITTER = 0.3; for (let attempt = 0; attempt < 10; attempt++) { - const exponential = BASE * Math.pow(2, attempt); + const exponential = BASE * 2 ** attempt; const capped = Math.min(exponential, MAX); const minDelay = capped * (1 - JITTER); const maxDelay = capped * (1 + JITTER); diff --git a/extensions/qqbot/src/engine/api/retry.ts b/extensions/qqbot/src/engine/api/retry.ts index 4b466fb03e1..c7fdbf6f07b 100644 --- a/extensions/qqbot/src/engine/api/retry.ts +++ b/extensions/qqbot/src/engine/api/retry.ts @@ -84,9 +84,7 @@ export async function withRetry( // Schedule the next retry with the configured backoff. if (attempt < policy.maxRetries) { const delay = - policy.backoff === "exponential" - ? policy.baseDelayMs * Math.pow(2, attempt) - : policy.baseDelayMs; + policy.backoff === "exponential" ? policy.baseDelayMs * 2 ** attempt : policy.baseDelayMs; logger?.debug?.( `[qqbot:retry] Attempt ${attempt + 1} failed, retrying in ${delay}ms: ${lastError.message.slice(0, 100)}`, diff --git a/extensions/synology-chat/src/client.ts b/extensions/synology-chat/src/client.ts index 86b137e6037..6652bf29988 100644 --- a/extensions/synology-chat/src/client.ts +++ b/extensions/synology-chat/src/client.ts @@ -119,7 +119,7 @@ export async function sendMessage( } if (attempt < maxRetries - 1) { - await sleep(baseDelay * Math.pow(2, attempt)); + await sleep(baseDelay * 2 ** attempt); } } diff --git a/extensions/tlon/src/monitor/index.ts b/extensions/tlon/src/monitor/index.ts index b1d8d546248..3692daba2c0 100644 --- a/extensions/tlon/src/monitor/index.ts +++ b/extensions/tlon/src/monitor/index.ts @@ -104,7 +104,7 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise= maxAttempts) { throw error; } - const delay = Math.min(30000, 1000 * Math.pow(2, attempt - 1)); + const delay = Math.min(30000, 1000 * 2 ** (attempt - 1)); runtime.log?.(`[tlon] Retrying authentication in ${delay}ms...`); await new Promise((resolve, reject) => { const timer = setTimeout(resolve, delay); diff --git a/extensions/tlon/src/urbit/sse-client.ts b/extensions/tlon/src/urbit/sse-client.ts index 1a22b6ba851..2d12a6190ab 100644 --- a/extensions/tlon/src/urbit/sse-client.ts +++ b/extensions/tlon/src/urbit/sse-client.ts @@ -376,7 +376,7 @@ export class UrbitSSEClient { this.reconnectAttempts += 1; const delay = Math.min( - this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1), + this.reconnectDelay * 2 ** (this.reconnectAttempts - 1), this.maxReconnectDelay, ); diff --git a/src/agents/subagent-announce-queue.ts b/src/agents/subagent-announce-queue.ts index 725136ee759..556011540d2 100644 --- a/src/agents/subagent-announce-queue.ts +++ b/src/agents/subagent-announce-queue.ts @@ -189,7 +189,7 @@ function scheduleAnnounceDrain(key: string) { } catch (err) { queue.consecutiveFailures++; // Exponential backoff on consecutive failures: 2s, 4s, 8s, ... capped at 60s. - const errorBackoffMs = Math.min(1000 * Math.pow(2, queue.consecutiveFailures), 60_000); + const errorBackoffMs = Math.min(1000 * 2 ** queue.consecutiveFailures, 60_000); const retryDelayMs = Math.max(errorBackoffMs, queue.debounceMs); queue.lastEnqueuedAt = Date.now() + retryDelayMs - queue.debounceMs; defaultRuntime.error?.( diff --git a/src/channels/plugins/session-conversation.bundled-fallback.test.ts b/src/channels/plugins/session-conversation.bundled-fallback.test.ts index 73a8fd693e1..730436921bd 100644 --- a/src/channels/plugins/session-conversation.bundled-fallback.test.ts +++ b/src/channels/plugins/session-conversation.bundled-fallback.test.ts @@ -21,12 +21,12 @@ vi.mock("../../plugin-sdk/facade-runtime.js", async () => { ); return { ...actual, - tryLoadActivatedBundledPluginPublicSurfaceModuleSync: ({ dirName }: { dirName: string }) => ( - (fallbackState.loadCalls += 1), - dirName === fallbackState.activeDirName && fallbackState.resolveSessionConversation + tryLoadActivatedBundledPluginPublicSurfaceModuleSync: ({ dirName }: { dirName: string }) => { + fallbackState.loadCalls += 1; + return dirName === fallbackState.activeDirName && fallbackState.resolveSessionConversation ? { resolveSessionConversation: fallbackState.resolveSessionConversation } - : null - ), + : null; + }, }; }); diff --git a/src/docs/install-cloud-secrets.test.ts b/src/docs/install-cloud-secrets.test.ts index 1ff424edcaf..cf55fc247c2 100644 --- a/src/docs/install-cloud-secrets.test.ts +++ b/src/docs/install-cloud-secrets.test.ts @@ -31,10 +31,10 @@ describe("cloud install docs", () => { for (const password of KNOWN_WEAK_GATEWAY_PASSWORD_PLACEHOLDERS) { expect(markdown, docName).not.toContain(`OPENCLAW_GATEWAY_PASSWORD=${password}`); } - expect(markdown, docName).not.toMatch(/^ GOG_KEYRING_PASSWORD=change-me-now$/m); + expect(markdown, docName).not.toMatch(/^ {4}GOG_KEYRING_PASSWORD=change-me-now$/m); if (CLOUD_DOCKER_VM_INSTALL_DOCS.has(docName)) { - expect(markdown, docName).toMatch(/^ OPENCLAW_GATEWAY_TOKEN=[ \t]*\r?$/m); - expect(markdown, docName).toMatch(/^ GOG_KEYRING_PASSWORD=[ \t]*\r?$/m); + expect(markdown, docName).toMatch(/^ {4}OPENCLAW_GATEWAY_TOKEN=[ \t]*\r?$/m); + expect(markdown, docName).toMatch(/^ {4}GOG_KEYRING_PASSWORD=[ \t]*\r?$/m); expect(markdown, docName).toContain("openssl rand -hex 32"); } } diff --git a/src/gateway/openai-http.test.ts b/src/gateway/openai-http.test.ts index a829b4564d4..b1a753abd50 100644 --- a/src/gateway/openai-http.test.ts +++ b/src/gateway/openai-http.test.ts @@ -184,33 +184,27 @@ describe("OpenAI-compatible HTTP API (e2e)", () => { await res.text(); } - { - await expectAgentSessionKeyMatch({ - body: { model: "openclaw", messages: [{ role: "user", content: "hi" }] }, - headers: { "x-openclaw-agent-id": "beta" }, - matcher: /^agent:beta:/, - }); - } + await expectAgentSessionKeyMatch({ + body: { model: "openclaw", messages: [{ role: "user", content: "hi" }] }, + headers: { "x-openclaw-agent-id": "beta" }, + matcher: /^agent:beta:/, + }); - { - await expectAgentSessionKeyMatch({ - body: { - model: "openclaw/beta", - messages: [{ role: "user", content: "hi" }], - }, - matcher: /^agent:beta:/, - }); - } + await expectAgentSessionKeyMatch({ + body: { + model: "openclaw/beta", + messages: [{ role: "user", content: "hi" }], + }, + matcher: /^agent:beta:/, + }); - { - await expectAgentSessionKeyMatch({ - body: { - model: "openclaw/default", - messages: [{ role: "user", content: "hi" }], - }, - matcher: /^agent:main:/, - }); - } + await expectAgentSessionKeyMatch({ + body: { + model: "openclaw/default", + messages: [{ role: "user", content: "hi" }], + }, + matcher: /^agent:main:/, + }); { mockAgentOnce([{ text: "hello" }]); @@ -417,19 +411,17 @@ describe("OpenAI-compatible HTTP API (e2e)", () => { await res.text(); } - { - await expectInvalidRequestNoDispatch([ - { - role: "user", - content: [ - { - type: "image_url", - image_url: { url: "https://example.com/image.png" }, - }, - ], - }, - ]); - } + await expectInvalidRequestNoDispatch([ + { + role: "user", + content: [ + { + type: "image_url", + image_url: { url: "https://example.com/image.png" }, + }, + ], + }, + ]); { mockAgentOnce([{ text: "I can see the image" }]); @@ -522,19 +514,17 @@ describe("OpenAI-compatible HTTP API (e2e)", () => { await res.text(); } - { - await expectInvalidRequestNoDispatch([ - { - role: "user", - content: [ - { - type: "image_url", - image_url: { url: "data:application/pdf;base64,QUJDRA==" }, - }, - ], - }, - ]); - } + await expectInvalidRequestNoDispatch([ + { + role: "user", + content: [ + { + type: "image_url", + image_url: { url: "data:application/pdf;base64,QUJDRA==" }, + }, + ], + }, + ]); { const manyImageParts = Array.from({ length: 9 }).map(() => ({ diff --git a/src/logging/diagnostic-support-export.test.ts b/src/logging/diagnostic-support-export.test.ts index 552593e9223..e5dd9dc2c17 100644 --- a/src/logging/diagnostic-support-export.test.ts +++ b/src/logging/diagnostic-support-export.test.ts @@ -522,7 +522,7 @@ describe("diagnostic support export", () => { >; expect(Object.getPrototypeOf(snapshot)).toBe(null); - expect(snapshot.__proto__).toBeUndefined(); + expect(Object.hasOwn(snapshot, "__proto__")).toBe(false); expect(snapshot.constructor).toBeUndefined(); expect(snapshot.prototype).toBeUndefined(); expect(snapshot.field0000).toBe(0); diff --git a/src/logging/diagnostic-support-redaction.ts b/src/logging/diagnostic-support-redaction.ts index 28219e83923..caa926f8c7e 100644 --- a/src/logging/diagnostic-support-redaction.ts +++ b/src/logging/diagnostic-support-redaction.ts @@ -358,7 +358,7 @@ function sanitizeCommandArguments(args: unknown[], redaction: SupportRedactionCo if (!hasInlineValue) { redactNext = true; } - return hasInlineValue ? arg.replace(/=.*/u, "=") : arg; + return hasInlineValue ? arg.replace(/[=].*/u, "=") : arg; } return redactSupportString(arg, redaction); }); diff --git a/test/scripts/oxlint-config.test.ts b/test/scripts/oxlint-config.test.ts index 4df633b4b39..8e758c17f16 100644 --- a/test/scripts/oxlint-config.test.ts +++ b/test/scripts/oxlint-config.test.ts @@ -11,6 +11,25 @@ type OxlintTsconfig = { exclude?: string[]; }; +const ZERO_BASELINE_RULES = [ + "eslint/no-div-regex", + "eslint/no-extra-label", + "eslint/no-lone-blocks", + "eslint/no-multi-str", + "eslint/no-proto", + "eslint/no-regex-spaces", + "eslint/no-sequences", + "eslint/prefer-exponentiation-operator", + "eslint/prefer-numeric-literals", + "eslint/unicode-bom", + "typescript/no-non-null-asserted-nullish-coalescing", + "typescript/prefer-return-this-type", + "unicorn/no-useless-error-capture-stack-trace", + "unicorn/prefer-optional-catch-binding", + "unicorn/require-array-join-separator", + "unicorn/throw-new-error", +]; + function readJson(path: string): unknown { return JSON.parse(fs.readFileSync(path, "utf8")) as unknown; } @@ -68,4 +87,12 @@ describe("oxlint config", () => { { allowInterfaces: "with-single-extends" }, ]); }); + + it("enables clean zero-baseline lint rules", () => { + const config = readJson(".oxlintrc.json") as OxlintConfig; + + for (const rule of ZERO_BASELINE_RULES) { + expect(config.rules?.[rule]).toBe("error"); + } + }); }); diff --git a/ui/src/ui/controllers/config/form-utils.node.test.ts b/ui/src/ui/controllers/config/form-utils.node.test.ts index f76d3dda855..abd9256d663 100644 --- a/ui/src/ui/controllers/config/form-utils.node.test.ts +++ b/ui/src/ui/controllers/config/form-utils.node.test.ts @@ -139,7 +139,7 @@ describe("prototype pollution prevention", () => { const obj: Record = {}; setPathValue(obj, ["__proto__", "polluted"], true); expect(({} as Record).polluted).toBeUndefined(); - expect(obj.__proto__).toBe(Object.prototype); + expect(Object.getPrototypeOf(obj)).toBe(Object.prototype); }); it("setPathValue rejects constructor in path", () => {