Files
windmill/cli/test/push_workspace_settings_identity_unit.test.ts
T
Ruben FiszelandClaude Opus 5.5 9a5ec223bc feat: workspace settings as one document, applied server-side (#11740)
* feat: add the workspace settings document, applied server-side

* feat: add wmill workspace get-settings and apply-settings

* fix: warn on unknown settings keys and refuse ee-only ones in the dry run

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: stub the settings document endpoint in the push settings unit tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: refuse mistyped settings and keep 4xx status on a partial settings apply

Reject non-boolean switches and non-string Slack credentials instead of
coercing them into destructive writes, preserve the failing endpoint's
status when wrapping a partial-apply error, keep the stored Slack secret
when an older CLI pushes one encrypted under another workspace's key,
print the plan's warnings in apply-settings, and state the admin check
load_settings_document leaves to its caller.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: compare handler arguments verbatim and type-check scalar settings

A handler's extra_args is stored as sent and its nulls reach the job as
arguments, so an added or removed null argument is now a change. Scalar
settings are type-checked in the plan so a dry run never reports a change
the endpoint then refuses, and an error handler's omitted mute flags
compare as the false the endpoint stores.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: require a 32-bit integer rate limit in the settings plan

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat: data table permissions and protection rules in the settings document

The settings document gains datatable_permissions, each data table's roles
named by their Postgres role name and written by catalog id, and
protection_rules, keyed by rule name. Both are managed only when stated and
stated as a whole. The dev workspace lock stays with the dev workspace
setting.

wmill sync carries datatable_permissions only with includeDatatablePermissions
(or --include-datatable-permissions), since tenants and roles are specific to
a workspace and an instance. protection_rules stay out of settings.yaml:
protection-rules.yaml syncs them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: validate the default data table role and plan permissions against a replaced datatable

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 12:35:08 +00:00

122 lines
4.2 KiB
TypeScript

/**
* Regression guard: `sync push` (pushWorkspaceSettings) must never apply the
* workspace display name from settings.yaml, and must apply the color only when
* the file carries one. Rationale lives at the apply sites in settings.ts.
*/
import { expect, test, describe, beforeEach } from "bun:test";
import { mockServices } from "./mock_services.ts";
let changeWorkspaceNameCalls: unknown[] = [];
let changeWorkspaceColorCalls: unknown[] = [];
let editWebhookCalls: unknown[] = [];
let remoteName = "";
let remoteColor: string | undefined = undefined;
let remoteWebhook: string | undefined = undefined;
// Every wmill.* call reachable from pushWorkspaceSettings is stubbed so the
// function runs without a backend; only the three we assert on record calls.
mockServices({
// A server without the settings document, so the per-setting endpoints run.
applyWorkspaceSettingsDocument: async () => {
throw { status: 404 };
},
getSettings: async (_a: { workspace: string }) => ({
webhook: remoteWebhook,
color: remoteColor,
}),
getWorkspaceName: async (_a: { workspace: string }) => remoteName,
changeWorkspaceName: async (a: unknown) => {
changeWorkspaceNameCalls.push(a);
},
changeWorkspaceColor: async (a: unknown) => {
changeWorkspaceColorCalls.push(a);
},
editWebhook: async (a: unknown) => {
editWebhookCalls.push(a);
},
editAutoInvite: async () => {},
editInstanceGroups: async () => {},
editErrorHandler: async () => {},
editSuccessHandler: async () => {},
editCopilotConfig: async () => {},
editLargeFileStorageConfig: async () => {},
editWorkspaceGitSyncConfig: async () => {},
editWorkspaceDefaultApp: async () => {},
editDefaultScripts: async () => {},
workspaceMuteCriticalAlertsUi: async () => {},
updateOperatorSettings: async () => {},
editDataTableConfig: async () => {},
editSlackCommand: async () => {},
setWorkspaceSlackOauthConfig: async () => {},
deleteWorkspaceSlackOauthConfig: async () => {},
});
const { pushWorkspaceSettings } = await import("../src/core/settings.ts");
describe("pushWorkspaceSettings workspace identity", () => {
const ws = "phoenix";
beforeEach(() => {
changeWorkspaceNameCalls = [];
changeWorkspaceColorCalls = [];
editWebhookCalls = [];
remoteName = "phoenix";
remoteColor = undefined;
remoteWebhook = undefined;
});
test("a differing name in settings.yaml is not applied to the workspace", async () => {
// Another setting also differs so the function proceeds past its no-op early
// return; only that setting must be applied, never the name.
remoteWebhook = "https://old";
await pushWorkspaceSettings(ws, "settings", undefined, {
name: "phoenix-staging",
webhook: "https://new",
});
expect(editWebhookCalls.length).toBe(1);
expect(changeWorkspaceNameCalls.length).toBe(0);
});
test("a name-only difference is a complete no-op", async () => {
await pushWorkspaceSettings(ws, "settings", undefined, {
name: "phoenix-staging",
});
expect(editWebhookCalls.length).toBe(0);
expect(changeWorkspaceNameCalls.length).toBe(0);
});
test("a settings.yaml without a color key does not clear the workspace color", async () => {
remoteColor = "#ff0000";
remoteWebhook = "https://old";
await pushWorkspaceSettings(ws, "settings", undefined, {
name: "phoenix",
webhook: "https://new",
});
expect(editWebhookCalls.length).toBe(1);
expect(changeWorkspaceColorCalls.length).toBe(0);
});
test("a color in settings.yaml is applied when it differs from the workspace", async () => {
remoteColor = "#ff0000";
await pushWorkspaceSettings(ws, "settings", undefined, {
name: "phoenix",
color: "#00ff00",
});
expect(editWebhookCalls.length).toBe(0);
expect(changeWorkspaceColorCalls).toEqual([
{ workspace: ws, requestBody: { color: "#00ff00" } },
]);
});
test("a color matching the workspace is a complete no-op", async () => {
remoteColor = "#ff0000";
await pushWorkspaceSettings(ws, "settings", undefined, {
name: "phoenix",
color: "#ff0000",
});
expect(editWebhookCalls.length).toBe(0);
expect(changeWorkspaceColorCalls.length).toBe(0);
});
});