From 995c75baa00ebd380e77b2c8b3d0a86e39e624eb Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 13 Jul 2026 13:54:03 -0700 Subject: [PATCH] fix(ci): restore current-main quality gates (#106751) * fix(ci): restore Control UI quality gates * fix(ci): repair dead export cleanup fallout * fix(ci): repair latest main quality gates * fix(ci): satisfy current-main gate contracts * docs: format Cloud Workers guide * test: isolate context lookup module --- docs/gateway/cloud-workers.md | 26 ++++++++-------- extensions/bonjour/src/advertiser.ts | 8 ++--- .../googlechat/src/google-auth.runtime.ts | 2 +- scripts/check-kysely-guardrails.mjs | 2 ++ src/agents/bash-tools.exec-runtime.test.ts | 3 +- src/agents/context.lookup.test.ts | 8 +++-- src/commands/agent.test.ts | 6 +--- .../auth-choice.plugin-providers.test.ts | 31 +++++++++++++------ src/config/includes.test.ts | 10 +++--- .../server-methods/models-auth-status.test.ts | 20 ++++++------ .../session-transcript-readers.test.ts | 4 +-- ...espects-ackmaxchars-heartbeat-acks.test.ts | 1 + src/infra/state-migrations.ts | 4 +-- ui/config/control-ui-chunking.ts | 11 +++++++ ui/src/api/gateway.node.test.ts | 10 ++++-- ui/src/app/control-ui-chunking.test.ts | 4 +++ ui/src/lib/channels/index.ts | 5 +-- ui/src/pages/model-providers/data.ts | 8 ++--- ui/src/pages/plugins/view.test.ts | 6 +--- 19 files changed, 95 insertions(+), 74 deletions(-) diff --git a/docs/gateway/cloud-workers.md b/docs/gateway/cloud-workers.md index a1aa6d5c19ec..8657b13bd91e 100644 --- a/docs/gateway/cloud-workers.md +++ b/docs/gateway/cloud-workers.md @@ -16,13 +16,13 @@ Cloud workers are opt-in and invisible until you configure a profile. Unconfigur ## What runs where -| Concern | Location | -| --- | --- | -| Agent loop + tools (`exec`, `read`, `write`, `edit`, …) | Cloud worker box | -| Model inference and provider credentials | Gateway (proxied by `{provider, model}` reference) | -| Transcript (durable, session store) | Gateway | -| Live streaming into the sidebar | Gateway fanout, fed by the worker's replayable event stream | -| Workspace git history | Authored on the box credential-free; the Gateway adopts commits and owns push/PR | +| Concern | Location | +| ------------------------------------------------------- | -------------------------------------------------------------------------------- | +| Agent loop + tools (`exec`, `read`, `write`, `edit`, …) | Cloud worker box | +| Model inference and provider credentials | Gateway (proxied by `{provider, model}` reference) | +| Transcript (durable, session store) | Gateway | +| Live streaming into the sidebar | Gateway fanout, fed by the worker's replayable event stream | +| Workspace git history | Authored on the box credential-free; the Gateway adopts commits and owns push/PR | The box needs no inbound ports except `sshd` and no egress beyond what your setup command uses: the Gateway connects out via SSH and a reverse tunnel carries the worker's WebSocket back. No Tailscale or VPN required. @@ -58,12 +58,12 @@ Add a profile under `cloudWorkers.profiles` in `openclaw.json`: Profile fields: -| Key | Meaning | -| --- | --- | -| `provider` | Worker provider id registered by a plugin (`crabbox` for the bundled plugin). | -| `install` | `bundle` (default) ships the running Gateway's build; `npm` installs the exact released Gateway version with pinned integrity. `npm` requires the Gateway to run from a packaged release. | -| `settings` | Provider-owned JSON. For crabbox: `provider` (backend), `class` (machine class), `ttl`, `idleTimeout` (Go durations), optional `setup` and absolute `binary` path. | -| `lifetime` | Optional stored policy (`idleTimeoutMinutes`, `maxLifetimeMinutes`). | +| Key | Meaning | +| ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `provider` | Worker provider id registered by a plugin (`crabbox` for the bundled plugin). | +| `install` | `bundle` (default) ships the running Gateway's build; `npm` installs the exact released Gateway version with pinned integrity. `npm` requires the Gateway to run from a packaged release. | +| `settings` | Provider-owned JSON. For crabbox: `provider` (backend), `class` (machine class), `ttl`, `idleTimeout` (Go durations), optional `setup` and absolute `binary` path. | +| `lifetime` | Optional stored policy (`idleTimeoutMinutes`, `maxLifetimeMinutes`). | ### The setup command diff --git a/extensions/bonjour/src/advertiser.ts b/extensions/bonjour/src/advertiser.ts index fc5ba0cb321f..86139ee970c9 100644 --- a/extensions/bonjour/src/advertiser.ts +++ b/extensions/bonjour/src/advertiser.ts @@ -466,10 +466,10 @@ export async function startGatewayBonjourAdvertiser( `bonjour: ${label} name conflict resolved; newName=${JSON.stringify(name)}`, ); }); - svc.on("hostname-change", (hostname) => { + svc.on("hostname-change", (nextHostname) => { markConflictObserved(label, svc); logger.warn( - `bonjour: ${label} hostname conflict resolved; newHostname=${JSON.stringify(hostname)}`, + `bonjour: ${label} hostname conflict resolved; newHostname=${JSON.stringify(nextHostname)}`, ); }); } catch (err) { @@ -542,7 +542,7 @@ export async function startGatewayBonjourAdvertiser( const updateStateTrackers = (services: BonjourCycle) => { const now = Date.now(); for (const { label, svc } of services) { - const nextState = svc.serviceState; + const nextState: string = svc.serviceState; const current = stateTracker.get(label); const nextEnteredAt = current && current.state !== "announced" && nextState !== "announced" @@ -629,7 +629,7 @@ export async function startGatewayBonjourAdvertiser( updateStateTrackers(cycle); for (const { label, svc } of cycle) { const now = Date.now(); - const state = svc.serviceState; + const state: string = svc.serviceState; if (state === "announced") { consecutiveRestarts = 0; consecutiveStuckStateRestarts = 0; diff --git a/extensions/googlechat/src/google-auth.runtime.ts b/extensions/googlechat/src/google-auth.runtime.ts index f122a35389d1..8d9241d15d33 100644 --- a/extensions/googlechat/src/google-auth.runtime.ts +++ b/extensions/googlechat/src/google-auth.runtime.ts @@ -493,7 +493,7 @@ async function readGoogleAuthResponseBytes(response: Response): Promise { - googleAuthRuntimePromise ??= import("google-auth-library").catch((error) => { + googleAuthRuntimePromise ??= import("google-auth-library").catch((error: unknown) => { googleAuthRuntimePromise = null; throw error; }); diff --git a/scripts/check-kysely-guardrails.mjs b/scripts/check-kysely-guardrails.mjs index f22498d163c3..df43d0eac85b 100644 --- a/scripts/check-kysely-guardrails.mjs +++ b/scripts/check-kysely-guardrails.mjs @@ -39,6 +39,7 @@ const rawSqliteAllowPathGroups = { "src/infra/sqlite-wal.ts", "src/state/openclaw-agent-db-session-migrations.ts", "src/state/openclaw-agent-db.ts", + "src/state/openclaw-state-db-schema-helpers.ts", "src/state/openclaw-state-db.ts", "src/state/sqlite-schema-shape.test-support.ts", ], @@ -64,6 +65,7 @@ const rawSqliteAllowPathGroups = { "src/infra/state-migrations.storage.ts", "src/infra/state-migrations.cron-run-logs.ts", "src/infra/state-migrations.debug-proxy.ts", + "src/infra/state-migrations.task-sidecar-rows.ts", ], "shared database stores with direct DatabaseSync access": ["src/proxy-capture/store.sqlite.ts"], "Kysely-backed stores that own a DatabaseSync boundary": [ diff --git a/src/agents/bash-tools.exec-runtime.test.ts b/src/agents/bash-tools.exec-runtime.test.ts index 079d1bcab563..447e8cfee88c 100644 --- a/src/agents/bash-tools.exec-runtime.test.ts +++ b/src/agents/bash-tools.exec-runtime.test.ts @@ -48,8 +48,7 @@ beforeAll(async () => { prepareGatewaySuspend, resetGatewaySuspendCoordinatorForLifecycleRestart, resumeGatewaySuspend, - } = - await import("../infra/gateway-suspend-coordinator.js")); + } = await import("../infra/gateway-suspend-coordinator.js")); }); beforeEach(() => { diff --git a/src/agents/context.lookup.test.ts b/src/agents/context.lookup.test.ts index f8a5916b7fae..69f39596076a 100644 --- a/src/agents/context.lookup.test.ts +++ b/src/agents/context.lookup.test.ts @@ -2,7 +2,6 @@ // model resolution. import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/types.openclaw.js"; -import { lookupCachedContextWindow, providerContextTokenCacheKey } from "./context-cache.js"; import { CONTEXT_WINDOW_RUNTIME_STATE } from "./context-runtime-state.js"; type DiscoveredModel = { @@ -122,7 +121,7 @@ async function importResolveContextTokensForModel() { describe("lookupContextTokens", () => { beforeAll(async () => { - contextModule = await import("./context.js"); + contextModule = await importFreshContextModule(); }); beforeEach(() => { @@ -381,7 +380,10 @@ describe("lookupContextTokens", () => { await contextModule.ensureContextWindowCacheLoaded(); expect( - lookupCachedContextWindow(providerContextTokenCacheKey("fresh-provider", "fresh-model")), + contextModule.lookupContextTokens("fresh-model", { + allowAsyncLoad: false, + skipRuntimeConfigLoad: true, + }), ).toBe(123_456); expect(CONTEXT_WINDOW_RUNTIME_STATE.loadPromise).not.toBe(legacyLoadPromise); expect(CONTEXT_WINDOW_RUNTIME_STATE.loadGeneration).toBe( diff --git a/src/commands/agent.test.ts b/src/commands/agent.test.ts index 952696cda1dd..a21bea3bd9ed 100644 --- a/src/commands/agent.test.ts +++ b/src/commands/agent.test.ts @@ -26,11 +26,7 @@ import { parseSqliteSessionFileMarker } from "../config/sessions/sqlite-marker.j import { clearSessionStoreCacheForTest } from "../config/sessions/store.js"; import type { SessionEntry } from "../config/sessions/types.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; -import { - emitAgentEvent, - onAgentEvent, - resetAgentEventsForTest, -} from "../infra/agent-events.js"; +import { emitAgentEvent, onAgentEvent, resetAgentEventsForTest } from "../infra/agent-events.js"; import type { PluginProviderRegistration } from "../plugins/registry.test-fixtures.js"; import { resetPluginRuntimeStateForTest, setActivePluginRegistry } from "../plugins/runtime.js"; import type { RuntimeEnv } from "../runtime.js"; diff --git a/src/commands/onboard-non-interactive/local/auth-choice.plugin-providers.test.ts b/src/commands/onboard-non-interactive/local/auth-choice.plugin-providers.test.ts index f2f49a725956..790c381b2587 100644 --- a/src/commands/onboard-non-interactive/local/auth-choice.plugin-providers.test.ts +++ b/src/commands/onboard-non-interactive/local/auth-choice.plugin-providers.test.ts @@ -3,23 +3,34 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../../../config/config.js"; import { applyNonInteractivePluginProviderChoice } from "./auth-choice.plugin-providers.js"; +type RuntimePluginInstallResult = { + cfg: OpenClawConfig; + required: boolean; + installed: boolean; + status?: "installed" | "skipped" | "failed" | "timed_out"; +}; + const ensureCodexRuntimePluginForModelSelection = vi.hoisted(() => - vi.fn(async ({ cfg }: { cfg: OpenClawConfig }) => ({ - cfg, - required: false, - installed: false, - })), + vi.fn( + async ({ cfg }: { cfg: OpenClawConfig }): Promise => ({ + cfg, + required: false, + installed: false, + }), + ), ); vi.mock("../../codex-runtime-plugin-install.js", () => ({ CODEX_RUNTIME_PLUGIN_ID: "codex", ensureCodexRuntimePluginForModelSelection, })); const ensureCopilotRuntimePluginForModelSelection = vi.hoisted(() => - vi.fn(async ({ cfg }: { cfg: OpenClawConfig }) => ({ - cfg, - required: false, - installed: false, - })), + vi.fn( + async ({ cfg }: { cfg: OpenClawConfig }): Promise => ({ + cfg, + required: false, + installed: false, + }), + ), ); vi.mock("../../copilot-runtime-plugin-install.js", () => ({ ensureCopilotRuntimePluginForModelSelection, diff --git a/src/config/includes.test.ts b/src/config/includes.test.ts index 1181cc8fd42b..d1d9a3011eab 100644 --- a/src/config/includes.test.ts +++ b/src/config/includes.test.ts @@ -754,7 +754,10 @@ describe("security: path traversal protection (CWE-22)", () => { }); it("rejects include paths at or over the platform-safe maximum", () => { - expectResolveIncludeError(() => resolve({ $include: "a".repeat(4096) }, {}), /maximum length/); + expectResolveIncludeError( + () => resolve({ $include: "a".repeat(4096) }, {}), + /maximum length/, + ); expectResolveIncludeError( () => resolve({ $include: "b".repeat(4097) }, {}), /maximum length/, @@ -863,10 +866,7 @@ describe("security: path traversal protection (CWE-22)", () => { ); expect(() => - resolveConfigIncludes( - { $include: "./big.json5" }, - path.join(configDir, "openclaw.json"), - ), + resolveConfigIncludes({ $include: "./big.json5" }, path.join(configDir, "openclaw.json")), ).toThrow(/security checks|max/i); }); }); diff --git a/src/gateway/server-methods/models-auth-status.test.ts b/src/gateway/server-methods/models-auth-status.test.ts index cfd8f0a025d7..b0984ccc79f2 100644 --- a/src/gateway/server-methods/models-auth-status.test.ts +++ b/src/gateway/server-methods/models-auth-status.test.ts @@ -380,7 +380,7 @@ describe("models.authStatus", () => { mocks.getRuntimeConfig.mockReturnValue({ models: { providers: { - openrouter: { ...Object.fromEntries([["apiKey", profileId]]) }, + openrouter: Object.fromEntries([["apiKey", profileId]]), }, }, }); @@ -458,11 +458,9 @@ describe("models.authStatus", () => { mocks.getRuntimeConfig.mockReturnValue({ models: { providers: { - openai: { - ...Object.fromEntries([ - ["apiKey", { source: "file", provider: "mounted-json", id: "model-provider-key" }], - ]), - }, + openai: Object.fromEntries([ + ["apiKey", { source: "file", provider: "mounted-json", id: "model-provider-key" }], + ]), }, }, }); @@ -484,7 +482,7 @@ describe("models.authStatus", () => { mocks.getRuntimeConfig.mockReturnValue({ models: { providers: { - anthropic: { ...Object.fromEntries([["apiKey", "ANTHROPIC_API_KEY"]]) }, + anthropic: Object.fromEntries([["apiKey", "ANTHROPIC_API_KEY"]]), }, }, }); @@ -506,7 +504,7 @@ describe("models.authStatus", () => { mocks.getRuntimeConfig.mockReturnValue({ models: { providers: { - anthropic: { ...Object.fromEntries([["apiKey", "ANTHROPIC_API_KEY"]]) }, + anthropic: Object.fromEntries([["apiKey", "ANTHROPIC_API_KEY"]]), }, }, }); @@ -527,7 +525,7 @@ describe("models.authStatus", () => { mocks.getRuntimeConfig.mockReturnValue({ models: { providers: { - ollama: { ...Object.fromEntries([["apiKey", "ollama-local"]]) }, + ollama: Object.fromEntries([["apiKey", "ollama-local"]]), }, }, }); @@ -546,7 +544,7 @@ describe("models.authStatus", () => { const profileId = "anthropic:saved"; mocks.getRuntimeConfig.mockReturnValue({ models: { - providers: { anthropic: { ...Object.fromEntries([["apiKey", profileId]]) } }, + providers: { anthropic: Object.fromEntries([["apiKey", profileId]]) }, }, }); mocks.ensureAuthProfileStore.mockReturnValue({ @@ -1140,7 +1138,7 @@ describe("models.authLogout", () => { mocks.getRuntimeConfig.mockReturnValue({ models: { providers: { - openrouter: { ...Object.fromEntries([["apiKey", profileId]]) }, + openrouter: Object.fromEntries([["apiKey", profileId]]), }, }, }); diff --git a/src/gateway/session-transcript-readers.test.ts b/src/gateway/session-transcript-readers.test.ts index 4085ebd280a0..5a76c6241486 100644 --- a/src/gateway/session-transcript-readers.test.ts +++ b/src/gateway/session-transcript-readers.test.ts @@ -354,10 +354,10 @@ describe("session transcript reader facade", () => { expect(messages).toMatchObject([{ content: "branch prompt" }, { content: "active branch" }]); expect( - messages.map((message) => (message as { __openclaw?: { id?: string } }).__openclaw?.id), + messages.map((message) => (message as { __openclaw?: { id?: string } })["__openclaw"]?.id), ).toEqual(["root", "active"]); expect( - messages.map((message) => (message as { __openclaw?: { seq?: number } }).__openclaw?.seq), + messages.map((message) => (message as { __openclaw?: { seq?: number } })["__openclaw"]?.seq), ).toEqual([2, 4]); await expect(readSessionMessageCountAsync(scope)).resolves.toBe(2); }); diff --git a/src/infra/heartbeat-runner.respects-ackmaxchars-heartbeat-acks.test.ts b/src/infra/heartbeat-runner.respects-ackmaxchars-heartbeat-acks.test.ts index 13608f51a8d3..e9566848c0ed 100644 --- a/src/infra/heartbeat-runner.respects-ackmaxchars-heartbeat-acks.test.ts +++ b/src/infra/heartbeat-runner.respects-ackmaxchars-heartbeat-acks.test.ts @@ -109,6 +109,7 @@ describe("runHeartbeatOnce ack handling", () => { cfg: params.cfg, accountId: undefined, audioAsVoice: undefined, + deliveryPartIndex: 0, deliveryQueueId: undefined, forceDocument: undefined, formatting: undefined, diff --git a/src/infra/state-migrations.ts b/src/infra/state-migrations.ts index 0c53dda4dcac..56d0a0f37040 100644 --- a/src/infra/state-migrations.ts +++ b/src/infra/state-migrations.ts @@ -8,9 +8,7 @@ export { runLegacyStateMigrations, } from "./state-migrations.doctor.js"; export { migrateLegacyAgentDir } from "./state-migrations.legacy-sessions.js"; -export { - migrateOrphanedSessionKeys, -} from "./state-migrations.session-store.js"; +export { migrateOrphanedSessionKeys } from "./state-migrations.session-store.js"; export { autoMigrateLegacyStateDir, autoMigrateLegacyTaskStateSidecars, diff --git a/ui/config/control-ui-chunking.ts b/ui/config/control-ui-chunking.ts index 6ac6b81dde46..fb38dab08ade 100644 --- a/ui/config/control-ui-chunking.ts +++ b/ui/config/control-ui-chunking.ts @@ -12,6 +12,17 @@ function moduleIdIncludesPackage(id: string, packageName: string): boolean { } export function controlUiManualChunk(id: string): string | undefined { + const normalized = normalizeModuleId(id); + + // These entry-and-route helpers must stay together; separate shared chunks + // turn small route-graph changes into extra startup preload requests. + if ( + normalized.endsWith("/ui/src/components/config-form.shared.ts") || + normalized.endsWith("/ui/src/lib/clipboard.ts") + ) { + return "control-ui-shared"; + } + if ( moduleIdIncludesPackage(id, "lit") || moduleIdIncludesPackage(id, "lit-html") || diff --git a/ui/src/api/gateway.node.test.ts b/ui/src/api/gateway.node.test.ts index 229af643270b..86157fc3817a 100644 --- a/ui/src/api/gateway.node.test.ts +++ b/ui/src/api/gateway.node.test.ts @@ -1404,7 +1404,13 @@ describe("GatewayBrowserClient", () => { vi.useRealTimers(); }); - it("does not auto-reconnect on AUTH_TOKEN_MISSING", async () => { + it.each([ + ConnectErrorDetailCodes.AUTH_TOKEN_MISSING, + ConnectErrorDetailCodes.AUTH_BOOTSTRAP_TOKEN_INVALID, + ConnectErrorDetailCodes.AUTH_PASSWORD_MISSING, + ConnectErrorDetailCodes.AUTH_RATE_LIMITED, + ConnectErrorDetailCodes.PAIRING_REQUIRED, + ])("does not auto-reconnect on %s", async (detailCode) => { useNodeFakeTimers(); localStorage.clear(); @@ -1421,7 +1427,7 @@ describe("GatewayBrowserClient", () => { error: { code: "INVALID_REQUEST", message: "unauthorized", - details: { code: "AUTH_TOKEN_MISSING" }, + details: { code: detailCode }, }, }); await expectSocketClosed(ws1); diff --git a/ui/src/app/control-ui-chunking.test.ts b/ui/src/app/control-ui-chunking.test.ts index d1b704c56c7b..826cda4c7272 100644 --- a/ui/src/app/control-ui-chunking.test.ts +++ b/ui/src/app/control-ui-chunking.test.ts @@ -19,6 +19,10 @@ describe("Control UI build chunking", () => { expect(controlUiManualChunk("/tmp/openclaw-pnpm-node-modules/json5/dist/index.js")).toBe( "config-runtime", ); + expect(controlUiManualChunk("/repo/ui/src/components/config-form.shared.ts")).toBe( + "control-ui-shared", + ); + expect(controlUiManualChunk("/repo/ui/src/lib/clipboard.ts")).toBe("control-ui-shared"); expect(controlUiManualChunk("/tmp/openclaw-pnpm-node-modules/@noble/ed25519/index.js")).toBe( "gateway-runtime", ); diff --git a/ui/src/lib/channels/index.ts b/ui/src/lib/channels/index.ts index 3c533212f7a1..aa517b60b97f 100644 --- a/ui/src/lib/channels/index.ts +++ b/ui/src/lib/channels/index.ts @@ -221,10 +221,7 @@ async function startWhatsAppLogin( return true; } -async function waitWhatsAppLogin( - state: ChannelsState, - accountId?: string, -): Promise { +async function waitWhatsAppLogin(state: ChannelsState, accountId?: string): Promise { const operation = beginWhatsAppOperation(state); if (!operation) { return false; diff --git a/ui/src/pages/model-providers/data.ts b/ui/src/pages/model-providers/data.ts index da9fab769184..b54d21e48f32 100644 --- a/ui/src/pages/model-providers/data.ts +++ b/ui/src/pages/model-providers/data.ts @@ -328,10 +328,10 @@ export function buildModelProviderCards(input: ModelProviderCardsInput): ModelPr ) .map((draft) => { const apiKeySupported = apiKeyCapabilities.get(draft.card.id); - return { - ...draft.card, - ...(apiKeySupported === undefined ? {} : { apiKeySupported }), - }; + if (apiKeySupported !== undefined) { + draft.card.apiKeySupported = apiKeySupported; + } + return draft.card; }) .toSorted((a, b) => a.displayName.localeCompare(b.displayName)); } diff --git a/ui/src/pages/plugins/view.test.ts b/ui/src/pages/plugins/view.test.ts index b4cf13598715..f6a658079373 100644 --- a/ui/src/pages/plugins/view.test.ts +++ b/ui/src/pages/plugins/view.test.ts @@ -528,11 +528,7 @@ describe("renderPlugins", () => { expect(normalizedText(row.querySelector(".settings-row__title"))).toBe("Calendar Plus"); expect(row.querySelector(".plugins-install")).toBeNull(); actionButton(row, "Disable")?.click(); - expect(onSetEnabled).toHaveBeenCalledWith( - "calendar-runtime", - false, - clawHubKey(packageName), - ); + expect(onSetEnabled).toHaveBeenCalledWith("calendar-runtime", false, clawHubKey(packageName)); }); it("does not present an empty catalog alongside an initial list failure", () => {