mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 21:07:01 -06:00
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
This commit is contained in:
committed by
GitHub
parent
b086cb3110
commit
995c75baa0
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -493,7 +493,7 @@ async function readGoogleAuthResponseBytes(response: Response): Promise<Uint8Arr
|
||||
}
|
||||
|
||||
export async function loadGoogleAuthRuntime(): Promise<GoogleAuthRuntime> {
|
||||
googleAuthRuntimePromise ??= import("google-auth-library").catch((error) => {
|
||||
googleAuthRuntimePromise ??= import("google-auth-library").catch((error: unknown) => {
|
||||
googleAuthRuntimePromise = null;
|
||||
throw error;
|
||||
});
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -48,8 +48,7 @@ beforeAll(async () => {
|
||||
prepareGatewaySuspend,
|
||||
resetGatewaySuspendCoordinatorForLifecycleRestart,
|
||||
resumeGatewaySuspend,
|
||||
} =
|
||||
await import("../infra/gateway-suspend-coordinator.js"));
|
||||
} = await import("../infra/gateway-suspend-coordinator.js"));
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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<RuntimePluginInstallResult> => ({
|
||||
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<RuntimePluginInstallResult> => ({
|
||||
cfg,
|
||||
required: false,
|
||||
installed: false,
|
||||
}),
|
||||
),
|
||||
);
|
||||
vi.mock("../../copilot-runtime-plugin-install.js", () => ({
|
||||
ensureCopilotRuntimePluginForModelSelection,
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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]]),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -109,6 +109,7 @@ describe("runHeartbeatOnce ack handling", () => {
|
||||
cfg: params.cfg,
|
||||
accountId: undefined,
|
||||
audioAsVoice: undefined,
|
||||
deliveryPartIndex: 0,
|
||||
deliveryQueueId: undefined,
|
||||
forceDocument: undefined,
|
||||
formatting: undefined,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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") ||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
);
|
||||
|
||||
@@ -221,10 +221,7 @@ async function startWhatsAppLogin(
|
||||
return true;
|
||||
}
|
||||
|
||||
async function waitWhatsAppLogin(
|
||||
state: ChannelsState,
|
||||
accountId?: string,
|
||||
): Promise<boolean> {
|
||||
async function waitWhatsAppLogin(state: ChannelsState, accountId?: string): Promise<boolean> {
|
||||
const operation = beginWhatsAppOperation(state);
|
||||
if (!operation) {
|
||||
return false;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
Reference in New Issue
Block a user