mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 21:07:01 -06:00
fix(ui): preserve cloud worker provider-owned settings (#129846)
This commit is contained in:
committed by
GitHub
parent
263d2a2a91
commit
ced723c2a8
@@ -399,7 +399,7 @@ suite.define(() => {
|
||||
}
|
||||
});
|
||||
|
||||
it("deletes a profile and its project defaults only after confirmation", async () => {
|
||||
it("preserves provider-owned editors and deletes profiles plus their project defaults", async () => {
|
||||
const context = await suite.browser.newContext({ locale: "en-US", serviceWorkers: "block" });
|
||||
const page = await context.newPage();
|
||||
const pending = configuredCloudWorkerProfile();
|
||||
@@ -416,9 +416,28 @@ suite.define(() => {
|
||||
},
|
||||
};
|
||||
const gateway = await installMockGateway(page, {
|
||||
featureMethods: ["config.patch", "environments.list"],
|
||||
featureMethods: ["config.patch", "config.schema", "environments.list"],
|
||||
methodResponses: {
|
||||
"config.get": configResponse(initialConfig, "cloud-workers-delete-1"),
|
||||
"config.schema": {
|
||||
version: "e2e",
|
||||
generatedAt: "2026-08-25T00:00:00.000Z",
|
||||
uiHints: {},
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
cloudWorkers: {
|
||||
type: "object",
|
||||
properties: {
|
||||
profiles: {
|
||||
type: "object",
|
||||
additionalProperties: { type: "object" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"config.patch": {
|
||||
ok: true,
|
||||
hash: "cloud-workers-delete-2",
|
||||
@@ -435,6 +454,57 @@ suite.define(() => {
|
||||
const pendingRow = page.locator(".settings-row").filter({
|
||||
has: page.locator("code", { hasText: /^pending$/ }),
|
||||
});
|
||||
await pendingRow.getByRole("button", { name: "Edit" }).click();
|
||||
const editor = page.locator(".settings-section", {
|
||||
has: page.getByRole("heading", { name: "Edit profile", exact: true }),
|
||||
});
|
||||
await expect.poll(() => page.getByLabel("Crabbox backend").inputValue()).toBe("aws");
|
||||
|
||||
const replacement = {
|
||||
provider: "static-ssh",
|
||||
install: "bundle",
|
||||
settings: {
|
||||
host: "worker.example.test",
|
||||
user: "openclaw",
|
||||
keyRef: { source: "env", provider: "default", id: "QA_PRIVATE_KEY" },
|
||||
},
|
||||
};
|
||||
const replacedConfig = {
|
||||
cloudWorkers: {
|
||||
profiles: { pending: replacement, retained },
|
||||
projectProfiles: initialConfig.cloudWorkers.projectProfiles,
|
||||
},
|
||||
};
|
||||
const configGetCount = (await gateway.getRequests("config.get")).length;
|
||||
await gateway.setMethodResponse(
|
||||
"config.get",
|
||||
configResponse(replacedConfig, "cloud-workers-provider-replaced"),
|
||||
);
|
||||
await gateway.emitGatewayEvent("config.changed", {
|
||||
path: "/tmp/openclaw.json",
|
||||
hash: "cloud-workers-provider-replaced",
|
||||
ts: Date.now(),
|
||||
});
|
||||
await gateway.waitForRequest("config.get", { after: configGetCount });
|
||||
await pendingRow.getByText("Provider: static-ssh", { exact: true }).waitFor();
|
||||
const saveButton = editor.getByRole("button", { name: "Save" });
|
||||
await expect.poll(() => saveButton.isEnabled()).toBe(true);
|
||||
await saveButton.click();
|
||||
await expect
|
||||
.poll(() => editor.getByRole("alert").textContent())
|
||||
.toBe("This profile changed or was removed. Reload the page and try again.");
|
||||
expect(await gateway.getRequests("config.patch")).toHaveLength(0);
|
||||
await editor.getByRole("button", { name: "Cancel" }).click();
|
||||
|
||||
await pendingRow.getByRole("button", { name: "Edit" }).click();
|
||||
await expect.poll(() => new URL(page.url()).pathname).toBe("/settings/advanced");
|
||||
await expect.poll(() => new URL(page.url()).searchParams.get("section")).toBe("cloudWorkers");
|
||||
await gateway.waitForRequest("config.schema");
|
||||
await page.locator(".page-title").getByText("Advanced", { exact: true }).waitFor();
|
||||
expect(await gateway.getRequests("config.patch")).toHaveLength(0);
|
||||
await page.goBack();
|
||||
await pendingRow.getByText("Provider: static-ssh", { exact: true }).waitFor();
|
||||
|
||||
await pendingRow.getByRole("button", { name: "Delete" }).click();
|
||||
const confirmation = await waitForConfirmModal(page);
|
||||
await expect.poll(() => confirmation.textContent()).toContain("Delete profile pending?");
|
||||
|
||||
@@ -2584,7 +2584,7 @@ export const en: TranslationMap = {
|
||||
intro: "Run agent sessions on ephemeral cloud machines instead of this gateway.",
|
||||
documentation: "Cloud worker documentation",
|
||||
sectionTitle: "Profiles",
|
||||
sectionDescription: "Each profile defines how Crabbox provisions and retires a worker.",
|
||||
sectionDescription: "Each profile defines how its provider provisions and retires a worker.",
|
||||
empty: "No cloud worker profiles are configured.",
|
||||
addProfile: "Add profile",
|
||||
editProfile: "Edit profile",
|
||||
|
||||
@@ -18,6 +18,7 @@ const configuredProfile = {
|
||||
ttl: "24h",
|
||||
idleTimeout: "60m",
|
||||
setup: "install-node",
|
||||
setupEnv: ["OPENCLAW_WORKER_ARTIFACT_TOKEN"],
|
||||
desktop: true,
|
||||
binary: "/opt/crabbox",
|
||||
region: "eu-west-1",
|
||||
@@ -91,6 +92,7 @@ describe("cloud worker settings state", () => {
|
||||
ttl: "8h",
|
||||
idleTimeout: "45m",
|
||||
setup: null,
|
||||
setupEnv: null,
|
||||
desktop: null,
|
||||
binary: null,
|
||||
region: "eu-west-1",
|
||||
@@ -102,6 +104,79 @@ describe("cloud worker settings state", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves forwarded setup environment while its setup command remains configured", () => {
|
||||
const config = { cloudWorkers: { profiles: { production: configuredProfile } } };
|
||||
const draft = createCloudWorkerDraft(readCloudWorkerProfiles(config)[0]);
|
||||
|
||||
expect(buildCloudWorkerUpsertPatch(config, draft, "production")).toMatchObject({
|
||||
patch: {
|
||||
cloudWorkers: {
|
||||
profiles: {
|
||||
production: {
|
||||
settings: {
|
||||
setup: "install-node",
|
||||
setupEnv: ["OPENCLAW_WORKER_ARTIFACT_TOKEN"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it.each([undefined, []])("keeps empty setup environment unchanged (%j)", (setupEnv) => {
|
||||
const existingSettings = Object.fromEntries(
|
||||
Object.entries(configuredProfile.settings).filter(
|
||||
([key]) => key !== "setupEnv" || setupEnv !== undefined,
|
||||
),
|
||||
);
|
||||
if (setupEnv) {
|
||||
existingSettings.setupEnv = setupEnv;
|
||||
}
|
||||
const profile = { ...configuredProfile, settings: existingSettings };
|
||||
const config = { cloudWorkers: { profiles: { production: profile } } };
|
||||
const draft = { ...createCloudWorkerDraft(readCloudWorkerProfiles(config)[0]), setup: "" };
|
||||
|
||||
expect(buildCloudWorkerUpsertPatch(config, draft, "production")).toEqual({
|
||||
patch: {
|
||||
cloudWorkers: {
|
||||
profiles: {
|
||||
production: { ...profile, settings: { ...existingSettings, setup: null } },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects an edit after its authoritative profile changes provider", () => {
|
||||
const config = {
|
||||
cloudWorkers: {
|
||||
profiles: {
|
||||
production: {
|
||||
provider: "static-ssh",
|
||||
settings: { host: "worker.example.test", user: "openclaw" },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const draft = createCloudWorkerDraft({
|
||||
id: "production",
|
||||
providerId: "crabbox",
|
||||
install: "bundle",
|
||||
backend: "aws",
|
||||
machineClass: "standard",
|
||||
ttl: "8h",
|
||||
idleTimeout: "45m",
|
||||
setup: "",
|
||||
desktop: false,
|
||||
binary: "",
|
||||
});
|
||||
|
||||
expect(buildCloudWorkerUpsertPatch(config, draft, "production")).toEqual({
|
||||
error: "profileMissing",
|
||||
});
|
||||
});
|
||||
|
||||
it("builds add and delete payloads against the complete profile record", () => {
|
||||
const config = { cloudWorkers: { profiles: { production: configuredProfile } } };
|
||||
const draft = {
|
||||
|
||||
@@ -153,6 +153,9 @@ export function buildCloudWorkerUpsertPatch(
|
||||
}
|
||||
const id = editingId ?? draft.id;
|
||||
const existing = isRecord(profiles[id]) ? profiles[id] : {};
|
||||
if (editingId && normalizeOptionalString(existing.provider) !== "crabbox") {
|
||||
return { error: "profileMissing" };
|
||||
}
|
||||
const existingSettings = profileSettings(existing);
|
||||
const settings = {
|
||||
...existingSettings,
|
||||
@@ -161,6 +164,11 @@ export function buildCloudWorkerUpsertPatch(
|
||||
ttl: draft.ttl.trim(),
|
||||
idleTimeout: draft.idleTimeout.trim(),
|
||||
setup: draft.setup.trim() || null,
|
||||
...(draft.setup.trim() ||
|
||||
!Array.isArray(existingSettings.setupEnv) ||
|
||||
existingSettings.setupEnv.length === 0
|
||||
? {}
|
||||
: { setupEnv: null }),
|
||||
desktop: draft.desktop ? true : null,
|
||||
binary: draft.binary.trim() || null,
|
||||
};
|
||||
|
||||
@@ -163,6 +163,10 @@ class CloudWorkersPage extends OpenClawLightDomElement {
|
||||
if (!this.canManage()) {
|
||||
return;
|
||||
}
|
||||
if (profile.providerId !== "crabbox") {
|
||||
this.context.navigate("advanced", { search: "?section=cloudWorkers" });
|
||||
return;
|
||||
}
|
||||
this.editor = { kind: "edit", profileId: profile.id };
|
||||
this.draft = createCloudWorkerDraft(profile);
|
||||
this.formError = null;
|
||||
|
||||
Reference in New Issue
Block a user