diff --git a/backend/.sqlx/query-081dc94a7d0fdaade77cfb593a025d8c48d7eab3dbb30ca0b43fb1ef45d8d8bd.json b/backend/.sqlx/query-6bde827da007b470b9d0acccfc3e00ce6aac650b9138a236f34c614eed753849.json similarity index 60% rename from backend/.sqlx/query-081dc94a7d0fdaade77cfb593a025d8c48d7eab3dbb30ca0b43fb1ef45d8d8bd.json rename to backend/.sqlx/query-6bde827da007b470b9d0acccfc3e00ce6aac650b9138a236f34c614eed753849.json index e303ef51b7..cf8050b606 100644 --- a/backend/.sqlx/query-081dc94a7d0fdaade77cfb593a025d8c48d7eab3dbb30ca0b43fb1ef45d8d8bd.json +++ b/backend/.sqlx/query-6bde827da007b470b9d0acccfc3e00ce6aac650b9138a236f34c614eed753849.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "INSERT INTO flow (\n workspace_id, path, summary, description,\n dependency_job, lock_error_logs, draft_only, tag,\n dedicated_worker, visible_to_runner_only, on_behalf_of_email,\n value, schema, edited_by, edited_at\n ) VALUES (\n $1, $2, $3, $4,\n NULL, '', $5, $6,\n $7, $8, $9,\n $10, $11::text::json, $12, now()\n )", + "query": "INSERT INTO flow (\n workspace_id, path, summary, description,\n dependency_job, lock_error_logs, draft_only, tag,\n dedicated_worker, visible_to_runner_only, on_behalf_of_email,\n ws_error_handler_muted,\n value, schema, edited_by, edited_at\n ) VALUES (\n $1, $2, $3, $4,\n NULL, '', $5, $6,\n $7, $8, $9,\n $10,\n $11, $12::text::json, $13, now()\n )", "describe": { "columns": [], "parameters": { @@ -14,6 +14,7 @@ "Bool", "Bool", "Text", + "Bool", "Jsonb", "Text", "Varchar" @@ -21,5 +22,5 @@ }, "nullable": [] }, - "hash": "081dc94a7d0fdaade77cfb593a025d8c48d7eab3dbb30ca0b43fb1ef45d8d8bd" + "hash": "6bde827da007b470b9d0acccfc3e00ce6aac650b9138a236f34c614eed753849" } diff --git a/backend/.sqlx/query-207a0721b6f0b8b6ddd4120343eba524a2bc1e9047bdde5f568af4d993dbb74c.json b/backend/.sqlx/query-77ac7257be02fb04c4b3213e2221e6f60621b4b2909d770de744ef5671e12ed9.json similarity index 69% rename from backend/.sqlx/query-207a0721b6f0b8b6ddd4120343eba524a2bc1e9047bdde5f568af4d993dbb74c.json rename to backend/.sqlx/query-77ac7257be02fb04c4b3213e2221e6f60621b4b2909d770de744ef5671e12ed9.json index b0cf45284a..b61fdcb8ed 100644 --- a/backend/.sqlx/query-207a0721b6f0b8b6ddd4120343eba524a2bc1e9047bdde5f568af4d993dbb74c.json +++ b/backend/.sqlx/query-77ac7257be02fb04c4b3213e2221e6f60621b4b2909d770de744ef5671e12ed9.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n UPDATE\n flow\n SET\n path = $1,\n summary = $2,\n description = $3,\n dependency_job = NULL,\n lock_error_logs = '',\n draft_only = NULL,\n tag = $4,\n dedicated_worker = $5,\n visible_to_runner_only = $6,\n on_behalf_of_email = $7,\n value = $8,\n schema = $9::text::json,\n edited_by = $10,\n edited_at = now()\n WHERE\n path = $11 AND workspace_id = $12", + "query": "\n UPDATE\n flow\n SET\n path = $1,\n summary = $2,\n description = $3,\n dependency_job = NULL,\n lock_error_logs = '',\n draft_only = NULL,\n tag = $4,\n dedicated_worker = $5,\n visible_to_runner_only = $6,\n on_behalf_of_email = $7,\n ws_error_handler_muted = $8,\n value = $9,\n schema = $10::text::json,\n edited_by = $11,\n edited_at = now()\n WHERE\n path = $12 AND workspace_id = $13", "describe": { "columns": [], "parameters": { @@ -12,6 +12,7 @@ "Bool", "Bool", "Text", + "Bool", "Jsonb", "Text", "Varchar", @@ -21,5 +22,5 @@ }, "nullable": [] }, - "hash": "207a0721b6f0b8b6ddd4120343eba524a2bc1e9047bdde5f568af4d993dbb74c" + "hash": "77ac7257be02fb04c4b3213e2221e6f60621b4b2909d770de744ef5671e12ed9" } diff --git a/backend/windmill-api/src/flows.rs b/backend/windmill-api/src/flows.rs index 8959ac9a60..fbfd64e2ef 100644 --- a/backend/windmill-api/src/flows.rs +++ b/backend/windmill-api/src/flows.rs @@ -473,12 +473,14 @@ async fn create_flow( workspace_id, path, summary, description, dependency_job, lock_error_logs, draft_only, tag, dedicated_worker, visible_to_runner_only, on_behalf_of_email, + ws_error_handler_muted, value, schema, edited_by, edited_at ) VALUES ( $1, $2, $3, $4, NULL, '', $5, $6, $7, $8, $9, - $10, $11::text::json, $12, now() + $10, + $11, $12::text::json, $13, now() )"#, w_id, nf.path, @@ -489,6 +491,7 @@ async fn create_flow( nf.dedicated_worker, nf.visible_to_runner_only.unwrap_or(false), nf.on_behalf_of_email.and(Some(&authed.email)), + nf.ws_error_handler_muted.unwrap_or(false), sqlx::types::Json(&nf.value) as _, schema_str, &authed.username, @@ -897,12 +900,13 @@ async fn update_flow( dedicated_worker = $5, visible_to_runner_only = $6, on_behalf_of_email = $7, - value = $8, - schema = $9::text::json, - edited_by = $10, + ws_error_handler_muted = $8, + value = $9, + schema = $10::text::json, + edited_by = $11, edited_at = now() WHERE - path = $11 AND workspace_id = $12", + path = $12 AND workspace_id = $13", if is_new_path { flow_path } else { &nf.path }, nf.summary, nf.description.as_deref().unwrap_or(""), @@ -910,6 +914,7 @@ async fn update_flow( nf.dedicated_worker, nf.visible_to_runner_only.unwrap_or(false), nf.on_behalf_of_email.and(Some(&authed.email)), + nf.ws_error_handler_muted.unwrap_or(false), sqlx::types::Json(&nf.value) as _, schema_str, authed.username, diff --git a/cli/test/sync_pull_push.test.ts b/cli/test/sync_pull_push.test.ts index 03f5f678a4..fdbb65cccc 100644 --- a/cli/test/sync_pull_push.test.ts +++ b/cli/test/sync_pull_push.test.ts @@ -906,7 +906,9 @@ Deno.test("readDirRecursive reads all files correctly", async () => { // Integration Tests (use withTestBackend for automated backend setup) // ============================================================================= +import { yamlParseFile } from "../deps.ts"; import { withTestBackend } from "./test_backend.ts"; +import { shouldSkipOnCI } from "./cargo_backend.ts"; Deno.test({ name: "Integration: Pull creates correct local structure", @@ -1950,3 +1952,212 @@ excludes: [] }); }, }); + +// ============================================================================= +// ws_error_handler_muted Persistence Tests +// ============================================================================= + +Deno.test({ + name: "Integration: Script ws_error_handler_muted is persisted through push/pull", + ignore: shouldSkipOnCI(), // Requires EE features + sanitizeResources: false, + sanitizeOps: false, + async fn() { + await withTestBackend(async (backend, tempDir) => { + await Deno.writeTextFile( + `${tempDir}/wmill.yaml`, + `defaultTs: bun +includes: + - "**" +excludes: [] +`, + ); + + const uniqueId = Date.now(); + await ensureDir(`${tempDir}/f/test`); + + // Create a script with ws_error_handler_muted: true + const scriptName = `f/test/muted_script_${uniqueId}`; + const script = createScriptFixture(scriptName, "deno"); + await Deno.writeTextFile(`${tempDir}/${script.contentFile.path}`, script.contentFile.content); + // Add ws_error_handler_muted to the metadata + const metadataWithMuted = script.metadataFile.content + `ws_error_handler_muted: true\n`; + await Deno.writeTextFile(`${tempDir}/${script.metadataFile.path}`, metadataWithMuted); + + // Push + const pushResult = await backend.runCLICommand( + ["sync", "push", "--yes", "--includes", `f/test/muted_script_${uniqueId}**`], + tempDir, + ); + assertEquals( + pushResult.code, + 0, + `Push should succeed.\nstdout: ${pushResult.stdout}\nstderr: ${pushResult.stderr}`, + ); + + // Verify via API that ws_error_handler_muted was persisted + const apiResp = await backend.apiRequest!( + `/api/w/${backend.workspace}/scripts/get/p/${scriptName}`, + ); + assertEquals(apiResp.status, 200, "API should return the script"); + const scriptData = await apiResp.json(); + assertEquals( + scriptData.ws_error_handler_muted, + true, + "API should return ws_error_handler_muted: true for the pushed script", + ); + + // Pull into a fresh directory and verify the field round-trips + const pullDir = await Deno.makeTempDir({ prefix: "wmill_muted_script_pull_" }); + try { + await Deno.writeTextFile( + `${pullDir}/wmill.yaml`, + `defaultTs: bun +includes: + - "f/test/muted_script_${uniqueId}**" +excludes: [] +`, + ); + + const pullResult = await backend.runCLICommand(["sync", "pull", "--yes"], pullDir); + assertEquals( + pullResult.code, + 0, + `Pull should succeed.\nstdout: ${pullResult.stdout}\nstderr: ${pullResult.stderr}`, + ); + + // Verify ws_error_handler_muted is in the pulled metadata + const pulledMetadata = await Deno.readTextFile(`${pullDir}/${script.metadataFile.path}`); + assertStringIncludes( + pulledMetadata, + "ws_error_handler_muted: true", + "Pulled script metadata should contain ws_error_handler_muted: true", + ); + + // Verify push from pulled dir is idempotent (no changes) + const push2 = await backend.runCLICommand( + ["sync", "push", "--dry-run", "--includes", `f/test/muted_script_${uniqueId}**`], + pullDir, + ); + assertEquals(push2.code, 0, `Second push dry-run should succeed: ${push2.stderr}`); + const output = (push2.stdout + push2.stderr).toLowerCase(); + assert( + output.includes("0 change") || output.includes("no change") || output.includes("nothing"), + `Should have no changes after push-pull cycle for script with ws_error_handler_muted. Output: ${output}`, + ); + } finally { + await Deno.remove(pullDir, { recursive: true }).catch(() => {}); + } + }); + }, +}); + +Deno.test({ + name: "Integration: Flow ws_error_handler_muted is persisted through push/pull", + ignore: shouldSkipOnCI(), // Requires EE features + sanitizeResources: false, + sanitizeOps: false, + async fn() { + await withTestBackend(async (backend, tempDir) => { + await Deno.writeTextFile( + `${tempDir}/wmill.yaml`, + `defaultTs: bun +includes: + - "**" +excludes: [] +`, + ); + + const uniqueId = Date.now(); + const flowName = `f/test/muted_flow_${uniqueId}`; + const flowFixture = createFlowFixture(flowName); + + // Create flow directory and files + await ensureDir(`${tempDir}/f/test/muted_flow_${uniqueId}${getFolderSuffix("flow")}`); + for (const [key, file] of Object.entries(flowFixture)) { + if (key === "metadata") { + // Add ws_error_handler_muted to flow metadata + const contentWithMuted = file.content + `ws_error_handler_muted: true\n`; + await Deno.writeTextFile(`${tempDir}/${file.path}`, contentWithMuted); + } else { + await Deno.writeTextFile(`${tempDir}/${file.path}`, file.content); + } + } + + // Push + const pushResult = await backend.runCLICommand( + ["sync", "push", "--yes", "--includes", `f/test/muted_flow_${uniqueId}*/**`], + tempDir, + ); + assertEquals( + pushResult.code, + 0, + `Push should succeed.\nstdout: ${pushResult.stdout}\nstderr: ${pushResult.stderr}`, + ); + + // Verify via API that ws_error_handler_muted was persisted + const apiResp = await backend.apiRequest!( + `/api/w/${backend.workspace}/flows/get/${flowName}`, + ); + assertEquals(apiResp.status, 200, "API should return the flow"); + const flowData = await apiResp.json(); + assertEquals( + flowData.ws_error_handler_muted, + true, + "API should return ws_error_handler_muted: true for the pushed flow", + ); + + // Pull into a fresh directory and verify the field round-trips + const pullDir = await Deno.makeTempDir({ prefix: "wmill_muted_flow_pull_" }); + try { + await Deno.writeTextFile( + `${pullDir}/wmill.yaml`, + `defaultTs: bun +includes: + - "f/test/muted_flow_${uniqueId}*/**" +excludes: [] +`, + ); + + const pullResult = await backend.runCLICommand(["sync", "pull", "--yes"], pullDir); + assertEquals( + pullResult.code, + 0, + `Pull should succeed.\nstdout: ${pullResult.stdout}\nstderr: ${pullResult.stderr}`, + ); + + // Verify ws_error_handler_muted is in the pulled flow.yaml + const flowYamlPath = `${pullDir}/${flowFixture.metadata.path}`; + const pulledFlowYaml = await Deno.readTextFile(flowYamlPath); + assertStringIncludes( + pulledFlowYaml, + "ws_error_handler_muted: true", + "Pulled flow.yaml should contain ws_error_handler_muted: true", + ); + + // Parse the YAML to confirm it's a proper boolean value + // deno-lint-ignore no-explicit-any + const parsed = await yamlParseFile(flowYamlPath) as any; + assertEquals( + parsed.ws_error_handler_muted, + true, + "ws_error_handler_muted should be boolean true in parsed flow YAML", + ); + + // Verify push from pulled dir is idempotent (no changes) + const push2 = await backend.runCLICommand( + ["sync", "push", "--dry-run", "--includes", `f/test/muted_flow_${uniqueId}*/**`], + pullDir, + ); + assertEquals(push2.code, 0, `Second push dry-run should succeed: ${push2.stderr}`); + const output = (push2.stdout + push2.stderr).toLowerCase(); + assert( + output.includes("0 change") || output.includes("no change") || output.includes("nothing"), + `Should have no changes after push-pull cycle for flow with ws_error_handler_muted. Output: ${output}`, + ); + } finally { + await Deno.remove(pullDir, { recursive: true }).catch(() => {}); + } + }); + }, +});