test: fix error handler test flake and merge handler test binaries (#11850)

* test: fix error handler test flake and merge handler test binaries

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

* test: tighten workspace handler test comments

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Ruben Fiszel
2026-10-17 22:04:45 +02:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 4ed6c1a6ce
commit 6a12e7ea49
29 changed files with 422 additions and 1181 deletions
@@ -1,14 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock, ws_error_handler_muted)\n VALUES ('test-workspace', $1, 'f/test/muted_failing_script', 'export function main() { throw new Error(\"fail\"); }', 'bun', 'script', 'test-user', '{}', '', '', E'{}\\n//bun.lock\\n<empty>', true)\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": []
},
"hash": "009fd85c1aa55ea36454eafffbf62505461eb1cc8b9af85a2e2ab6e7dbe950c6"
}
@@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock)\n VALUES ('test-workspace', $1, 'f/test/simple_script', $2, 'bun', 'script', 'test-user', '{}', 'Simple test script', 'A simple test script', E'{}\\n//bun.lock\\n<empty>')\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8",
"Text"
]
},
"nullable": []
},
"hash": "1043947475822c27f73b5357e6ec0d527d999f2865ff5de75186b49004a3565e"
}
@@ -1,20 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT error_handler->>'path' FROM workspace_settings WHERE workspace_id = 'test-workspace'",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "?column?",
"type_info": "Text"
}
],
"parameters": {
"Left": []
},
"nullable": [
null
]
},
"hash": "12c3101e02f197ad731b66f539fe677ad25520fcc9c5a2378a293122956bed4c"
}
@@ -1,12 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE workspace_settings\n SET error_handler = '{\"path\": \"script/f/test/error_handler\", \"extra_args\": {\"notify\": true}, \"muted_on_cancel\": true, \"muted_on_user_path\": false}'::jsonb\n WHERE workspace_id = 'test-workspace'\n ",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "18d1a6ac94f2c87d5ea8c48a228f061135be4065dca33fbc429d8b0186c5ccb3"
}
@@ -1,20 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT (error_handler->>'muted_on_cancel')::boolean FROM workspace_settings WHERE workspace_id = 'test-workspace'",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "bool",
"type_info": "Bool"
}
],
"parameters": {
"Left": []
},
"nullable": [
null
]
},
"hash": "25bf3956b4365a4d4f96368bcc6e33817ae284ab26c195eee53d988d74263e86"
}
@@ -1,20 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT (error_handler->>'muted_on_user_path')::boolean FROM workspace_settings WHERE workspace_id = 'test-workspace'",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "bool",
"type_info": "Bool"
}
],
"parameters": {
"Left": []
},
"nullable": [
null
]
},
"hash": "28b8264a427e8c19be823d2b8e40736a7b7675780672b503d91737cd0c617657"
}
@@ -1,12 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE workspace_settings\n SET success_handler = '{\"path\": \"script/f/test/success_handler\", \"extra_args\": {\"key\": \"value\"}}'::jsonb\n WHERE workspace_id = 'test-workspace'\n ",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "28f3dfb70aed943ad3d64da608168c027c645afb73dfc2360245b68ba593cab4"
}
@@ -1,12 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE workspace_settings\n SET success_handler = NULL\n WHERE workspace_id = 'test-workspace'\n ",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "2c2ed5c91a4893966e94305df8b9f8ac904c8d0781a83fdea66c918320fb5601"
}
@@ -1,12 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock)\n VALUES ('test-workspace', 3333333333, 'f/test/error_handler', 'export function main() { return \"handled\"; }', 'bun', 'script', 'test-user', '{}', '', '', E'{}\\n//bun.lock\\n<empty>')\n ",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "344c08104d62d9bcd3f9117abb574e73a6fb364e4ab3e3916958c2e493673b1d"
}
@@ -1,12 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO group_ (workspace_id, name, summary, extra_perms)\n VALUES ('test-workspace', 'success_handler', 'The group the success handler acts on behalf of', '{\"u/test-user\": true}')\n ON CONFLICT DO NOTHING\n ",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "3a71ab02239d8dd4e2f440a35155459a7d5091c125a0998e79d89f8c560a3fd3"
}
@@ -1,14 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock)\n VALUES ('test-workspace', 1234567890, 'f/test/success_handler', $1, 'bun', 'script', 'test-user', '{}', 'Success handler script', 'Handles successful job completions', E'{}\\n//bun.lock\\n<empty>')\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text"
]
},
"nullable": []
},
"hash": "42250637493a623850ca76fe5a705fe4acef039f38a5e76da8d09f2f7e755779"
}
@@ -1,12 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO group_ (workspace_id, name, summary, extra_perms)\n VALUES ('test-workspace', 'error_handler', 'The group the error handler acts on behalf of', '{\"u/test-user\": true}')\n ON CONFLICT DO NOTHING\n ",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "5f5bdd667f699a5a1c05e07d805a97afb06252541d06b4785063819c3893b674"
}
@@ -1,12 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE workspace_settings\n SET success_handler = '{\"path\": \"script/f/test/success_handler\"}'::jsonb\n WHERE workspace_id = 'test-workspace'\n ",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "65016ea993fb40d4ab6b0a6f15c2082e8dfbaada01243bae07065c089bc42fa9"
}
@@ -1,14 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock)\n VALUES ('test-workspace', 1111111111, 'f/test/error_handler', $1, 'bun', 'script', 'test-user', '{}', 'Error handler script', 'Handles failed job completions', E'{}\\n//bun.lock\\n<empty>')\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text"
]
},
"nullable": []
},
"hash": "69e1c04c246c42a3a1d7202b6fb349d0ac779431993e225945ddfb91489cb927"
}
@@ -1,12 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE workspace_settings\n SET error_handler = NULL\n WHERE workspace_id = 'test-workspace'\n ",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "79648f03eb50385183c34d57bda1ac984cf6502609df9ee51b7d684b585b8447"
}
@@ -1,14 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock)\n VALUES ('test-workspace', $1, 'f/test/success_script', 'export function main() { return \"ok\"; }', 'bun', 'script', 'test-user', '{}', '', '', E'{}\\n//bun.lock\\n<empty>')\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8"
]
},
"nullable": []
},
"hash": "8511e096394edb1433f6572d50b66d8558866af53d829d83fa634f12a90733c0"
}
@@ -1,20 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT success_handler->'extra_args' FROM workspace_settings WHERE workspace_id = 'test-workspace'",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "?column?",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": []
},
"nullable": [
null
]
},
"hash": "87044e1466cd114cbef336620e96dc29f3f930149fedc240d44c614dfc794ff7"
}
@@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock)\n VALUES ('test-workspace', $1, 'f/test/failing_script', $2, 'bun', 'script', 'test-user', '{}', 'Failing test script', 'A script that always fails', E'{}\\n//bun.lock\\n<empty>')\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int8",
"Text"
]
},
"nullable": []
},
"hash": "b0c10d17ed684af4dc79db7a199d4438ea003b5762178f25ff12ef23c1a8f46a"
}
@@ -1,12 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO group_ (workspace_id, name, summary, extra_perms)\n VALUES ('test-workspace', 'error_handler', 'Error handler group', '{\"u/test-user\": true}')\n ON CONFLICT DO NOTHING\n ",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "ba0c3dad5d2a77922821c9c6d58e06a65e6666e197f3e59b880470df35c27bec"
}
@@ -1,12 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE workspace_settings\n SET error_handler = '{\"path\": \"script/f/test/error_handler\"}'::jsonb\n WHERE workspace_id = 'test-workspace'\n ",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "bb4cde84157126f29680cb36075848e7cde2ab7c6b7f0861becd4c0b8c330a8d"
}
@@ -1,12 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE workspace_settings\n SET success_handler = '{\"path\": \"flow/f/test/success_flow\"}'::jsonb\n WHERE workspace_id = 'test-workspace'\n ",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "c2f271bcea1f11b2979152cc743559236fd875192b7b7f4824c63675fdc6a551"
}
@@ -1,12 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock)\n VALUES ('test-workspace', 5555555555, 'f/test/error_handler', 'export function main() { return \"handled\"; }', 'bun', 'script', 'test-user', '{}', '', '', E'{}\\n//bun.lock\\n<empty>')\n ",
"describe": {
"columns": [],
"parameters": {
"Left": []
},
"nullable": []
},
"hash": "cc719da4da98b6fe9612a4cc2f42dd19834a3e0d0be593ea6bc1239fe5453a74"
}
@@ -1,20 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT error_handler->'extra_args' FROM workspace_settings WHERE workspace_id = 'test-workspace'",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "?column?",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": []
},
"nullable": [
null
]
},
"hash": "d5f1a2bfbac206a33b991d58f9ff4b8e8e4a3840ea1c65d409576379dede41ed"
}
@@ -1,20 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT id\n FROM v2_job\n WHERE workspace_id = 'test-workspace'\n AND permissioned_as_email = 'error_handler@windmill.dev'\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
}
],
"parameters": {
"Left": []
},
"nullable": [
false
]
},
"hash": "e4c9f3bca6bd636c3ee31fc643182ed5ca48a45da895fb992d820682f5169f7c"
}
@@ -1,20 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT success_handler->>'path' FROM workspace_settings WHERE workspace_id = 'test-workspace'",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "?column?",
"type_info": "Text"
}
],
"parameters": {
"Left": []
},
"nullable": [
null
]
},
"hash": "e9808cc152e1ce4717260ebb42f203ca16321a4550073258734a6208b83c00c2"
}
@@ -1,38 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n id,\n runnable_path,\n permissioned_as_email,\n parent_job\n FROM v2_job\n WHERE workspace_id = 'test-workspace'\n AND permissioned_as_email = 'error_handler@windmill.dev'\n ORDER BY created_at DESC\n LIMIT 1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "runnable_path",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "permissioned_as_email",
"type_info": "Varchar"
},
{
"ordinal": 3,
"name": "parent_job",
"type_info": "Uuid"
}
],
"parameters": {
"Left": []
},
"nullable": [
false,
true,
false,
true
]
},
"hash": "fdf0a16a72bc2eca2025d64750e2e6a7e7ff90b5ad4b42e6aab53992b447a20e"
}
-480
View File
@@ -1,480 +0,0 @@
use sqlx::{Pool, Postgres};
use windmill_test_utils::*;
/// Test that workspace error handler can be set and removed via database operations
#[cfg(feature = "deno_core")]
#[sqlx::test(fixtures("base"))]
async fn test_error_handler_settings(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
let _server = ApiServer::start(db.clone()).await?;
// Initially error_handler should be NULL
let initial = sqlx::query_scalar!(
r#"SELECT error_handler->>'path' FROM workspace_settings WHERE workspace_id = 'test-workspace'"#
)
.fetch_one(&db)
.await?;
assert!(initial.is_none());
// Set error handler with all options
sqlx::query!(
r#"
UPDATE workspace_settings
SET error_handler = '{"path": "script/f/test/error_handler", "extra_args": {"notify": true}, "muted_on_cancel": true, "muted_on_user_path": false}'::jsonb
WHERE workspace_id = 'test-workspace'
"#
)
.execute(&db)
.await?;
let after_set = sqlx::query_scalar!(
r#"SELECT error_handler->>'path' FROM workspace_settings WHERE workspace_id = 'test-workspace'"#
)
.fetch_one(&db)
.await?;
assert_eq!(after_set, Some("script/f/test/error_handler".to_string()));
// Verify extra_args
let extra_args = sqlx::query_scalar!(
r#"SELECT error_handler->'extra_args' FROM workspace_settings WHERE workspace_id = 'test-workspace'"#
)
.fetch_one(&db)
.await?;
assert!(extra_args.is_some());
// Verify muted_on_cancel
let muted_on_cancel = sqlx::query_scalar!(
r#"SELECT (error_handler->>'muted_on_cancel')::boolean FROM workspace_settings WHERE workspace_id = 'test-workspace'"#
)
.fetch_one(&db)
.await?;
assert_eq!(muted_on_cancel, Some(true));
// Verify muted_on_user_path
let muted_on_user_path = sqlx::query_scalar!(
r#"SELECT (error_handler->>'muted_on_user_path')::boolean FROM workspace_settings WHERE workspace_id = 'test-workspace'"#
)
.fetch_one(&db)
.await?;
assert_eq!(muted_on_user_path, Some(false));
// Remove error handler
sqlx::query!(
r#"
UPDATE workspace_settings
SET error_handler = NULL
WHERE workspace_id = 'test-workspace'
"#
)
.execute(&db)
.await?;
let after_remove = sqlx::query_scalar!(
r#"SELECT error_handler->>'path' FROM workspace_settings WHERE workspace_id = 'test-workspace'"#
)
.fetch_one(&db)
.await?;
assert!(after_remove.is_none());
Ok(())
}
/// Test that a failed job triggers the workspace error handler
#[cfg(all(feature = "deno_core", feature = "enterprise", feature = "private"))]
#[sqlx::test(fixtures("base"))]
async fn test_error_handler_triggered_on_failure(db: Pool<Postgres>) -> anyhow::Result<()> {
use windmill_common::jobs::JobPayload;
use windmill_common::runnable_settings::{ConcurrencySettings, DebouncingSettings};
use windmill_common::scripts::{ScriptHash, ScriptLang};
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
// Create the error handler script
let error_handler_code = r#"
export async function main(path: string, email: string, job_id: string, is_flow: boolean, workspace_id: string, error: any) {
console.log("Error handler called for job:", job_id);
return { handled: true, original_path: path };
}
"#;
sqlx::query!(
r#"
INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock)
VALUES ('test-workspace', 1111111111, 'f/test/error_handler', $1, 'bun', 'script', 'test-user', '{}', 'Error handler script', 'Handles failed job completions', E'{}\n//bun.lock\n<empty>')
"#,
error_handler_code
)
.execute(&db)
.await?;
// Create a script that will fail
let failing_script_code = "export function main() { throw new Error('intentional failure'); }";
let failing_script_hash: i64 = 2222222222;
sqlx::query!(
r#"
INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock)
VALUES ('test-workspace', $1, 'f/test/failing_script', $2, 'bun', 'script', 'test-user', '{}', 'Failing test script', 'A script that always fails', E'{}\n//bun.lock\n<empty>')
"#,
failing_script_hash,
failing_script_code
)
.execute(&db)
.await?;
// Set up the error handler in workspace_settings
sqlx::query!(
r#"
UPDATE workspace_settings
SET error_handler = '{"path": "script/f/test/error_handler"}'::jsonb
WHERE workspace_id = 'test-workspace'
"#
)
.execute(&db)
.await?;
// Create the error_handler group
sqlx::query!(
r#"
INSERT INTO group_ (workspace_id, name, summary, extra_perms)
VALUES ('test-workspace', 'error_handler', 'The group the error handler acts on behalf of', '{"u/test-user": true}')
ON CONFLICT DO NOTHING
"#
)
.execute(&db)
.await?;
// Run the failing script
let completed_job = RunJob::from(JobPayload::ScriptHash {
hash: ScriptHash(failing_script_hash),
path: "f/test/failing_script".to_string(),
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: None,
language: ScriptLang::Bun,
priority: None,
apply_preprocessor: false,
concurrency_settings: ConcurrencySettings::default(),
debouncing_settings: DebouncingSettings::default(),
labels: None,
job_token_scopes: None,
})
.run_until_complete(&db, false, server.addr.port())
.await;
// Verify the job actually failed
assert!(!completed_job.success, "Job should have failed");
let main_job_id = completed_job.id;
// Wait for the error handler job to be created
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
// Verify the error handler job was created
let error_handler_job = sqlx::query!(
r#"
SELECT
id,
runnable_path,
permissioned_as_email,
parent_job
FROM v2_job
WHERE workspace_id = 'test-workspace'
AND permissioned_as_email = 'error_handler@windmill.dev'
ORDER BY created_at DESC
LIMIT 1
"#
)
.fetch_optional(&db)
.await?;
assert!(
error_handler_job.is_some(),
"Error handler job should have been created"
);
let handler_job = error_handler_job.unwrap();
assert_eq!(
handler_job.runnable_path.as_deref(),
Some("f/test/error_handler"),
"Error handler should run the configured script"
);
assert_eq!(
handler_job.permissioned_as_email.as_str(),
"error_handler@windmill.dev",
"Error handler should run as error_handler user"
);
assert_eq!(
handler_job.parent_job,
Some(main_job_id),
"Error handler should have the failed job as parent"
);
Ok(())
}
/// Test that error handler is NOT triggered when ws_error_handler_muted is set on the script
#[cfg(all(feature = "deno_core", feature = "enterprise", feature = "private"))]
#[sqlx::test(fixtures("base"))]
async fn test_error_handler_muted_on_script(db: Pool<Postgres>) -> anyhow::Result<()> {
use windmill_common::jobs::JobPayload;
use windmill_common::runnable_settings::{ConcurrencySettings, DebouncingSettings};
use windmill_common::scripts::{ScriptHash, ScriptLang};
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
// Create the error handler script
sqlx::query!(
r#"
INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock)
VALUES ('test-workspace', 3333333333, 'f/test/error_handler', 'export function main() { return "handled"; }', 'bun', 'script', 'test-user', '{}', '', '', E'{}\n//bun.lock\n<empty>')
"#,
)
.execute(&db)
.await?;
// Create a failing script with ws_error_handler_muted = true
let failing_script_hash: i64 = 4444444444;
sqlx::query!(
r#"
INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock, ws_error_handler_muted)
VALUES ('test-workspace', $1, 'f/test/muted_failing_script', 'export function main() { throw new Error("fail"); }', 'bun', 'script', 'test-user', '{}', '', '', E'{}\n//bun.lock\n<empty>', true)
"#,
failing_script_hash,
)
.execute(&db)
.await?;
// Set up the error handler
sqlx::query!(
r#"
UPDATE workspace_settings
SET error_handler = '{"path": "script/f/test/error_handler"}'::jsonb
WHERE workspace_id = 'test-workspace'
"#
)
.execute(&db)
.await?;
sqlx::query!(
r#"
INSERT INTO group_ (workspace_id, name, summary, extra_perms)
VALUES ('test-workspace', 'error_handler', 'Error handler group', '{"u/test-user": true}')
ON CONFLICT DO NOTHING
"#
)
.execute(&db)
.await?;
// Run the muted failing script
let completed_job = RunJob::from(JobPayload::ScriptHash {
hash: ScriptHash(failing_script_hash),
path: "f/test/muted_failing_script".to_string(),
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: None,
language: ScriptLang::Bun,
priority: None,
apply_preprocessor: false,
concurrency_settings: ConcurrencySettings::default(),
debouncing_settings: DebouncingSettings::default(),
labels: None,
job_token_scopes: None,
})
.run_until_complete(&db, false, server.addr.port())
.await;
assert!(!completed_job.success, "Job should have failed");
// Wait and check that NO error handler job was created
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
let error_handler_job = sqlx::query_scalar!(
r#"
SELECT id
FROM v2_job
WHERE workspace_id = 'test-workspace'
AND permissioned_as_email = 'error_handler@windmill.dev'
"#
)
.fetch_optional(&db)
.await?;
assert!(
error_handler_job.is_none(),
"Error handler should NOT have been triggered for a muted script"
);
Ok(())
}
/// Test that error handler is NOT triggered on successful job completion
#[cfg(all(feature = "deno_core", feature = "enterprise", feature = "private"))]
#[sqlx::test(fixtures("base"))]
async fn test_error_handler_not_triggered_on_success(db: Pool<Postgres>) -> anyhow::Result<()> {
use windmill_common::jobs::JobPayload;
use windmill_common::runnable_settings::{ConcurrencySettings, DebouncingSettings};
use windmill_common::scripts::{ScriptHash, ScriptLang};
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
// Create the error handler script
sqlx::query!(
r#"
INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock)
VALUES ('test-workspace', 5555555555, 'f/test/error_handler', 'export function main() { return "handled"; }', 'bun', 'script', 'test-user', '{}', '', '', E'{}\n//bun.lock\n<empty>')
"#,
)
.execute(&db)
.await?;
// Create a successful script
let success_script_hash: i64 = 6666666666;
sqlx::query!(
r#"
INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock)
VALUES ('test-workspace', $1, 'f/test/success_script', 'export function main() { return "ok"; }', 'bun', 'script', 'test-user', '{}', '', '', E'{}\n//bun.lock\n<empty>')
"#,
success_script_hash,
)
.execute(&db)
.await?;
// Set up the error handler
sqlx::query!(
r#"
UPDATE workspace_settings
SET error_handler = '{"path": "script/f/test/error_handler"}'::jsonb
WHERE workspace_id = 'test-workspace'
"#
)
.execute(&db)
.await?;
sqlx::query!(
r#"
INSERT INTO group_ (workspace_id, name, summary, extra_perms)
VALUES ('test-workspace', 'error_handler', 'Error handler group', '{"u/test-user": true}')
ON CONFLICT DO NOTHING
"#
)
.execute(&db)
.await?;
// Run the successful script
let completed_job = RunJob::from(JobPayload::ScriptHash {
hash: ScriptHash(success_script_hash),
path: "f/test/success_script".to_string(),
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: None,
language: ScriptLang::Bun,
priority: None,
apply_preprocessor: false,
concurrency_settings: ConcurrencySettings::default(),
debouncing_settings: DebouncingSettings::default(),
labels: None,
job_token_scopes: None,
})
.run_until_complete(&db, false, server.addr.port())
.await;
assert!(completed_job.success, "Job should have succeeded");
// Wait and check that NO error handler job was created
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
let error_handler_job = sqlx::query_scalar!(
r#"
SELECT id
FROM v2_job
WHERE workspace_id = 'test-workspace'
AND permissioned_as_email = 'error_handler@windmill.dev'
"#
)
.fetch_optional(&db)
.await?;
assert!(
error_handler_job.is_none(),
"Error handler should NOT have been triggered for a successful job"
);
Ok(())
}
/// A hub script run at the top level (a schedule, a trigger, the API) is a `script_hub` job,
/// and its failure must reach the workspace error handler like a workspace script's.
#[cfg(all(feature = "enterprise", feature = "private"))]
#[sqlx::test(fixtures("base"))]
async fn test_error_handler_triggered_on_hub_script_failure(
db: Pool<Postgres>,
) -> anyhow::Result<()> {
use windmill_common::jobs::JobPayload;
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
let version = "990000002";
let hub_dir = &*windmill_common::worker::HUB_CACHE_DIR;
tokio::fs::create_dir_all(hub_dir).await?;
tokio::fs::write(
format!("{hub_dir}/{version}"),
r#"{"content":"exit 1","lockfile":null,"language":"bash","schema":{},"summary":null}"#,
)
.await?;
// The workspace handler settings are cached per workspace id across the tests of this
// binary, so this reuses the handler path every other test here configures.
sqlx::query(
"INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock)
VALUES ('test-workspace', 1111111112, 'f/test/error_handler', 'echo handled', 'bash', 'script', 'test-user', '{}', '', '', '')",
)
.execute(&db)
.await?;
sqlx::query(
r#"UPDATE workspace_settings SET error_handler = '{"path": "script/f/test/error_handler"}'::jsonb
WHERE workspace_id = 'test-workspace'"#,
)
.execute(&db)
.await?;
let completed_job = RunJob::from(JobPayload::ScriptHub {
path: format!("hub/{version}/test/fail"),
apply_preprocessor: false,
})
.run_until_complete(&db, false, server.addr.port())
.await;
assert!(!completed_job.success, "Hub script should have failed");
let mut handler_parent = None;
for _ in 0..50 {
handler_parent = sqlx::query_scalar::<_, Option<uuid::Uuid>>(
"SELECT parent_job FROM v2_job
WHERE workspace_id = 'test-workspace' AND permissioned_as_email = 'error_handler@windmill.dev'",
)
.fetch_optional(&db)
.await?;
if handler_parent.is_some() {
break;
}
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
}
assert_eq!(
handler_parent,
Some(Some(completed_job.id)),
"Workspace error handler should handle the failed hub script"
);
Ok(())
}
-293
View File
@@ -1,293 +0,0 @@
use sqlx::{Pool, Postgres};
use windmill_test_utils::*;
/// Both job-running tests seed `f/test/success_handler` with a different hash, each in its own
/// database, while the deployed-script caches are process-global and keyed by workspace and
/// path. With them on, one test resolves the handler to the other's hash and the handler push
/// 404s whenever that hash's info is not cached yet.
#[cfg(all(feature = "enterprise", feature = "private"))]
fn disable_deployed_script_cache() {
windmill_common::DEPLOYED_SCRIPT_CACHE_DISABLED
.store(true, std::sync::atomic::Ordering::Relaxed);
}
/// The handler job is pushed after the job it handles is reported complete.
#[cfg(all(feature = "enterprise", feature = "private"))]
async fn wait_for_success_handler_job(
db: &Pool<Postgres>,
) -> anyhow::Result<Option<(Option<String>, Option<uuid::Uuid>)>> {
for _ in 0..100 {
let job = sqlx::query_as::<_, (Option<String>, Option<uuid::Uuid>)>(
"SELECT runnable_path, parent_job FROM v2_job
WHERE workspace_id = 'test-workspace' AND permissioned_as_email = 'success_handler@windmill.dev'",
)
.fetch_optional(db)
.await?;
if job.is_some() {
return Ok(job);
}
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
}
Ok(None)
}
/// Test that the workspace success handler cache works correctly with 60s TTL
#[cfg(feature = "deno_core")]
#[sqlx::test(fixtures("base"))]
async fn test_success_handler_cache(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
// First, create a success handler script
let _server = ApiServer::start(db.clone()).await?;
// Set up a success handler in workspace_settings using new JSONB column
sqlx::query!(
r#"
UPDATE workspace_settings
SET success_handler = '{"path": "script/f/test/success_handler", "extra_args": {"key": "value"}}'::jsonb
WHERE workspace_id = 'test-workspace'
"#
)
.execute(&db)
.await?;
// Verify the success handler was set
let result = sqlx::query_scalar!(
r#"SELECT success_handler->>'path' FROM workspace_settings WHERE workspace_id = 'test-workspace'"#
)
.fetch_one(&db)
.await?;
assert_eq!(result, Some("script/f/test/success_handler".to_string()));
// Verify extra args were set
let extra_args = sqlx::query_scalar!(
r#"SELECT success_handler->'extra_args' FROM workspace_settings WHERE workspace_id = 'test-workspace'"#
)
.fetch_one(&db)
.await?;
assert!(extra_args.is_some());
Ok(())
}
/// Test that success handler can be set and removed via database operations
#[cfg(feature = "deno_core")]
#[sqlx::test(fixtures("base"))]
async fn test_success_handler_settings(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
let _server = ApiServer::start(db.clone()).await?;
// Initially success_handler should be NULL
let initial = sqlx::query_scalar!(
r#"SELECT success_handler->>'path' FROM workspace_settings WHERE workspace_id = 'test-workspace'"#
)
.fetch_one(&db)
.await?;
assert!(initial.is_none());
// Set success handler
sqlx::query!(
r#"
UPDATE workspace_settings
SET success_handler = '{"path": "flow/f/test/success_flow"}'::jsonb
WHERE workspace_id = 'test-workspace'
"#
)
.execute(&db)
.await?;
let after_set = sqlx::query_scalar!(
r#"SELECT success_handler->>'path' FROM workspace_settings WHERE workspace_id = 'test-workspace'"#
)
.fetch_one(&db)
.await?;
assert_eq!(after_set, Some("flow/f/test/success_flow".to_string()));
// Remove success handler
sqlx::query!(
r#"
UPDATE workspace_settings
SET success_handler = NULL
WHERE workspace_id = 'test-workspace'
"#
)
.execute(&db)
.await?;
let after_remove = sqlx::query_scalar!(
r#"SELECT success_handler->>'path' FROM workspace_settings WHERE workspace_id = 'test-workspace'"#
)
.fetch_one(&db)
.await?;
assert!(after_remove.is_none());
Ok(())
}
/// Test that a successful job completion triggers the success handler
#[cfg(all(feature = "deno_core", feature = "enterprise", feature = "private"))]
#[sqlx::test(fixtures("base"))]
async fn test_success_handler_triggered_on_success(db: Pool<Postgres>) -> anyhow::Result<()> {
use serde_json::json;
use windmill_common::jobs::JobPayload;
use windmill_common::runnable_settings::{ConcurrencySettings, DebouncingSettings};
use windmill_common::scripts::{ScriptHash, ScriptLang};
initialize_tracing().await;
disable_deployed_script_cache();
let server = ApiServer::start(db.clone()).await?;
// Create a simple success handler script
// Note: lock must be non-null for script to be considered "deployed"
let success_handler_code = r#"
export async function main(path: string, email: string, job_id: string, is_flow: boolean, workspace_id: string, result: any) {
console.log("Success handler called for job:", job_id);
return { handled: true, original_path: path };
}
"#;
sqlx::query!(
r#"
INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock)
VALUES ('test-workspace', 1234567890, 'f/test/success_handler', $1, 'bun', 'script', 'test-user', '{}', 'Success handler script', 'Handles successful job completions', E'{}\n//bun.lock\n<empty>')
"#,
success_handler_code
)
.execute(&db)
.await?;
// Create a simple test script that we'll run (needs to be JobKind::Script, not Preview)
// Note: lock must be non-null for script to be considered "deployed"
let test_script_code = "export function main() { return 'success'; }";
let test_script_hash: i64 = 9876543210;
sqlx::query!(
r#"
INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock)
VALUES ('test-workspace', $1, 'f/test/simple_script', $2, 'bun', 'script', 'test-user', '{}', 'Simple test script', 'A simple test script', E'{}\n//bun.lock\n<empty>')
"#,
test_script_hash,
test_script_code
)
.execute(&db)
.await?;
// Set up the success handler in workspace_settings
sqlx::query!(
r#"
UPDATE workspace_settings
SET success_handler = '{"path": "script/f/test/success_handler"}'::jsonb
WHERE workspace_id = 'test-workspace'
"#
)
.execute(&db)
.await?;
// Create the success_handler group
sqlx::query!(
r#"
INSERT INTO group_ (workspace_id, name, summary, extra_perms)
VALUES ('test-workspace', 'success_handler', 'The group the success handler acts on behalf of', '{"u/test-user": true}')
ON CONFLICT DO NOTHING
"#
)
.execute(&db)
.await?;
// Run a script using ScriptHash (produces JobKind::Script, not Preview)
let completed_job = RunJob::from(JobPayload::ScriptHash {
hash: ScriptHash(test_script_hash),
path: "f/test/simple_script".to_string(),
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: None,
language: ScriptLang::Bun,
priority: None,
apply_preprocessor: false,
concurrency_settings: ConcurrencySettings::default(),
debouncing_settings: DebouncingSettings::default(),
labels: None,
job_token_scopes: None,
})
.run_until_complete(&db, false, server.addr.port())
.await;
let result = completed_job.json_result().unwrap();
assert_eq!(result, json!("success"));
let main_job_id = completed_job.id;
let handler_job = wait_for_success_handler_job(&db).await?;
assert_eq!(
handler_job,
Some((
Some("f/test/success_handler".to_string()),
Some(main_job_id)
)),
"Success handler should run the configured script with the main job as parent"
);
// Note: root_job may be None when it equals parent_job (optimization in push function)
Ok(())
}
/// A hub script run at the top level (a schedule, a trigger, the API) is a `script_hub` job,
/// and its success must reach the workspace success handler like a workspace script's.
#[cfg(all(feature = "enterprise", feature = "private"))]
#[sqlx::test(fixtures("base"))]
async fn test_success_handler_triggered_on_hub_script_success(
db: Pool<Postgres>,
) -> anyhow::Result<()> {
use windmill_common::jobs::JobPayload;
initialize_tracing().await;
disable_deployed_script_cache();
let server = ApiServer::start(db.clone()).await?;
let version = "990000003";
let hub_dir = &*windmill_common::worker::HUB_CACHE_DIR;
tokio::fs::create_dir_all(hub_dir).await?;
tokio::fs::write(
format!("{hub_dir}/{version}"),
r#"{"content":"echo ok","lockfile":null,"language":"bash","schema":{},"summary":null}"#,
)
.await?;
// The workspace handler settings are cached per workspace id across the tests of this
// binary, so this reuses the handler path every other test here configures.
sqlx::query(
"INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock)
VALUES ('test-workspace', 1234567891, 'f/test/success_handler', 'echo handled', 'bash', 'script', 'test-user', '{}', '', '', '')",
)
.execute(&db)
.await?;
sqlx::query(
r#"UPDATE workspace_settings SET success_handler = '{"path": "script/f/test/success_handler"}'::jsonb
WHERE workspace_id = 'test-workspace'"#,
)
.execute(&db)
.await?;
let completed_job = RunJob::from(JobPayload::ScriptHub {
path: format!("hub/{version}/test/ok"),
apply_preprocessor: false,
})
.run_until_complete(&db, false, server.addr.port())
.await;
assert!(completed_job.success, "Hub script should have succeeded");
let handler_job = wait_for_success_handler_job(&db).await?;
assert_eq!(
handler_job.map(|(_, parent)| parent),
Some(Some(completed_job.id)),
"Workspace success handler should handle the successful hub script"
);
Ok(())
}
+422
View File
@@ -0,0 +1,422 @@
#![cfg(all(feature = "enterprise", feature = "private"))]
use sqlx::{Pool, Postgres};
use windmill_test_utils::*;
const ERROR_HANDLER_EMAIL: &str = "error_handler@windmill.dev";
const SUCCESS_HANDLER_EMAIL: &str = "success_handler@windmill.dev";
/// Every test seeds `f/test/error_handler` or `f/test/success_handler` with a different hash,
/// each in its own database, while the deployed-script caches are process-global and keyed by
/// workspace and path. With them on, one test resolves the handler to another's hash and the
/// handler push 404s whenever that hash's info is not cached yet.
fn disable_deployed_script_cache() {
windmill_common::DEPLOYED_SCRIPT_CACHE_DISABLED
.store(true, std::sync::atomic::Ordering::Relaxed);
}
/// Polls so the assertion does not depend on the handler being pushed before
/// `run_until_complete` returns, which only holds while the worker drains its completions on exit.
async fn wait_for_handler_job(
db: &Pool<Postgres>,
handler_email: &str,
) -> anyhow::Result<Option<(Option<String>, Option<uuid::Uuid>)>> {
for _ in 0..100 {
let job = sqlx::query_as::<_, (Option<String>, Option<uuid::Uuid>)>(
"SELECT runnable_path, parent_job FROM v2_job
WHERE workspace_id = 'test-workspace' AND permissioned_as_email = $1",
)
.bind(handler_email)
.fetch_optional(db)
.await?;
if job.is_some() {
return Ok(job);
}
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
}
Ok(None)
}
#[cfg(feature = "deno_core")]
async fn insert_bun_script(
db: &Pool<Postgres>,
hash: i64,
path: &str,
content: &str,
ws_error_handler_muted: bool,
) -> anyhow::Result<()> {
// The lock must be non-null for the script to count as deployed.
sqlx::query(
r#"INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock, ws_error_handler_muted)
VALUES ('test-workspace', $1, $2, $3, 'bun', 'script', 'test-user', '{}', '', '', E'{}\n//bun.lock\n<empty>', $4)"#,
)
.bind(hash)
.bind(path)
.bind(content)
.bind(ws_error_handler_muted)
.execute(db)
.await?;
Ok(())
}
/// Configures `f/test/error_handler` and `f/test/success_handler` as the workspace handlers.
/// Both settings are cached per workspace id across the tests of this binary, and any test's
/// top-level job can fill either cache, so every test configures both, with the same paths.
async fn configure_workspace_handlers(db: &Pool<Postgres>) -> anyhow::Result<()> {
sqlx::query(
r#"UPDATE workspace_settings
SET error_handler = '{"path": "script/f/test/error_handler"}'::jsonb,
success_handler = '{"path": "script/f/test/success_handler"}'::jsonb
WHERE workspace_id = 'test-workspace'"#,
)
.execute(db)
.await?;
sqlx::query(
r#"INSERT INTO group_ (workspace_id, name, summary, extra_perms)
VALUES ('test-workspace', 'error_handler', '', '{"u/test-user": true}'),
('test-workspace', 'success_handler', '', '{"u/test-user": true}')
ON CONFLICT DO NOTHING"#,
)
.execute(db)
.await?;
Ok(())
}
/// Runs a deployed script by hash, which makes a `script` job rather than a preview.
#[cfg(feature = "deno_core")]
async fn run_script_hash(db: &Pool<Postgres>, port: u16, hash: i64, path: &str) -> CompletedJob {
use windmill_common::jobs::JobPayload;
use windmill_common::runnable_settings::{ConcurrencySettings, DebouncingSettings};
use windmill_common::scripts::{ScriptHash, ScriptLang};
RunJob::from(JobPayload::ScriptHash {
hash: ScriptHash(hash),
path: path.to_string(),
cache_ttl: None,
cache_ignore_s3_path: None,
dedicated_worker: None,
language: ScriptLang::Bun,
priority: None,
apply_preprocessor: false,
concurrency_settings: ConcurrencySettings::default(),
debouncing_settings: DebouncingSettings::default(),
labels: None,
job_token_scopes: None,
})
.run_until_complete(db, false, port)
.await
}
/// A job's handler, if any, is pushed before the worker that ran it exits, so once a control
/// job has been handled, every handler job that the jobs run before it would produce exists.
#[cfg(feature = "deno_core")]
async fn assert_only_handled(
db: &Pool<Postgres>,
control_job: uuid::Uuid,
message: &str,
) -> anyhow::Result<()> {
assert!(
wait_for_handler_job(db, ERROR_HANDLER_EMAIL)
.await?
.is_some(),
"Error handler should have handled the control failure"
);
let handled = sqlx::query_scalar::<_, Option<uuid::Uuid>>(
"SELECT parent_job FROM v2_job
WHERE workspace_id = 'test-workspace' AND permissioned_as_email = $1",
)
.bind(ERROR_HANDLER_EMAIL)
.fetch_all(db)
.await?;
assert_eq!(handled, vec![Some(control_job)], "{message}");
Ok(())
}
/// Test that a failed job triggers the workspace error handler
#[cfg(feature = "deno_core")]
#[sqlx::test(fixtures("base"))]
async fn test_error_handler_triggered_on_failure(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
disable_deployed_script_cache();
let server = ApiServer::start(db.clone()).await?;
insert_bun_script(
&db,
1111111111,
"f/test/error_handler",
r#"
export async function main(path: string, email: string, job_id: string, is_flow: boolean, workspace_id: string, error: any) {
console.log("Error handler called for job:", job_id);
return { handled: true, original_path: path };
}
"#,
false,
)
.await?;
insert_bun_script(
&db,
2222222222,
"f/test/failing_script",
"export function main() { throw new Error('intentional failure'); }",
false,
)
.await?;
configure_workspace_handlers(&db).await?;
let completed_job =
run_script_hash(&db, server.addr.port(), 2222222222, "f/test/failing_script").await;
assert!(!completed_job.success, "Job should have failed");
assert_eq!(
wait_for_handler_job(&db, ERROR_HANDLER_EMAIL).await?,
Some((
Some("f/test/error_handler".to_string()),
Some(completed_job.id)
)),
"Error handler should run the configured script with the failed job as parent"
);
Ok(())
}
/// Test that error handler is NOT triggered when ws_error_handler_muted is set on the script
#[cfg(feature = "deno_core")]
#[sqlx::test(fixtures("base"))]
async fn test_error_handler_muted_on_script(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
disable_deployed_script_cache();
let server = ApiServer::start(db.clone()).await?;
let port = server.addr.port();
insert_bun_script(
&db,
3333333333,
"f/test/error_handler",
r#"export function main() { return "handled"; }"#,
false,
)
.await?;
let failing = r#"export function main() { throw new Error("fail"); }"#;
insert_bun_script(
&db,
4444444444,
"f/test/muted_failing_script",
failing,
true,
)
.await?;
insert_bun_script(&db, 4444444445, "f/test/failing_script", failing, false).await?;
configure_workspace_handlers(&db).await?;
let muted_job = run_script_hash(&db, port, 4444444444, "f/test/muted_failing_script").await;
assert!(!muted_job.success, "Job should have failed");
let control_job = run_script_hash(&db, port, 4444444445, "f/test/failing_script").await;
assert!(!control_job.success, "Job should have failed");
assert_only_handled(
&db,
control_job.id,
"Error handler should NOT have been triggered for a muted script",
)
.await
}
/// Test that error handler is NOT triggered on successful job completion
#[cfg(feature = "deno_core")]
#[sqlx::test(fixtures("base"))]
async fn test_error_handler_not_triggered_on_success(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
disable_deployed_script_cache();
let server = ApiServer::start(db.clone()).await?;
let port = server.addr.port();
insert_bun_script(
&db,
5555555555,
"f/test/error_handler",
r#"export function main() { return "handled"; }"#,
false,
)
.await?;
insert_bun_script(
&db,
6666666666,
"f/test/success_script",
r#"export function main() { return "ok"; }"#,
false,
)
.await?;
insert_bun_script(
&db,
6666666667,
"f/test/failing_script",
r#"export function main() { throw new Error("fail"); }"#,
false,
)
.await?;
configure_workspace_handlers(&db).await?;
let success_job = run_script_hash(&db, port, 6666666666, "f/test/success_script").await;
assert!(success_job.success, "Job should have succeeded");
let control_job = run_script_hash(&db, port, 6666666667, "f/test/failing_script").await;
assert!(!control_job.success, "Job should have failed");
assert_only_handled(
&db,
control_job.id,
"Error handler should NOT have been triggered for a successful job",
)
.await
}
/// A hub script run at the top level (a schedule, a trigger, the API) is a `script_hub` job,
/// and its failure must reach the workspace error handler like a workspace script's.
#[sqlx::test(fixtures("base"))]
async fn test_error_handler_triggered_on_hub_script_failure(
db: Pool<Postgres>,
) -> anyhow::Result<()> {
use windmill_common::jobs::JobPayload;
initialize_tracing().await;
disable_deployed_script_cache();
let server = ApiServer::start(db.clone()).await?;
let version = "990000002";
let hub_dir = &*windmill_common::worker::HUB_CACHE_DIR;
tokio::fs::create_dir_all(hub_dir).await?;
tokio::fs::write(
format!("{hub_dir}/{version}"),
r#"{"content":"exit 1","lockfile":null,"language":"bash","schema":{},"summary":null}"#,
)
.await?;
sqlx::query(
"INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock)
VALUES ('test-workspace', 1111111112, 'f/test/error_handler', 'echo handled', 'bash', 'script', 'test-user', '{}', '', '', '')",
)
.execute(&db)
.await?;
configure_workspace_handlers(&db).await?;
let completed_job = RunJob::from(JobPayload::ScriptHub {
path: format!("hub/{version}/test/fail"),
apply_preprocessor: false,
})
.run_until_complete(&db, false, server.addr.port())
.await;
assert!(!completed_job.success, "Hub script should have failed");
let handler_job = wait_for_handler_job(&db, ERROR_HANDLER_EMAIL).await?;
assert_eq!(
handler_job.map(|(_, parent)| parent),
Some(Some(completed_job.id)),
"Workspace error handler should handle the failed hub script"
);
Ok(())
}
/// Test that a successful job completion triggers the success handler
#[cfg(feature = "deno_core")]
#[sqlx::test(fixtures("base"))]
async fn test_success_handler_triggered_on_success(db: Pool<Postgres>) -> anyhow::Result<()> {
use serde_json::json;
initialize_tracing().await;
disable_deployed_script_cache();
let server = ApiServer::start(db.clone()).await?;
insert_bun_script(
&db,
1234567890,
"f/test/success_handler",
r#"
export async function main(path: string, email: string, job_id: string, is_flow: boolean, workspace_id: string, result: any) {
console.log("Success handler called for job:", job_id);
return { handled: true, original_path: path };
}
"#,
false,
)
.await?;
insert_bun_script(
&db,
9876543210,
"f/test/simple_script",
"export function main() { return 'success'; }",
false,
)
.await?;
configure_workspace_handlers(&db).await?;
let completed_job =
run_script_hash(&db, server.addr.port(), 9876543210, "f/test/simple_script").await;
assert_eq!(completed_job.json_result().unwrap(), json!("success"));
assert_eq!(
wait_for_handler_job(&db, SUCCESS_HANDLER_EMAIL).await?,
Some((
Some("f/test/success_handler".to_string()),
Some(completed_job.id)
)),
"Success handler should run the configured script with the main job as parent"
);
Ok(())
}
/// A hub script run at the top level (a schedule, a trigger, the API) is a `script_hub` job,
/// and its success must reach the workspace success handler like a workspace script's.
#[sqlx::test(fixtures("base"))]
async fn test_success_handler_triggered_on_hub_script_success(
db: Pool<Postgres>,
) -> anyhow::Result<()> {
use windmill_common::jobs::JobPayload;
initialize_tracing().await;
disable_deployed_script_cache();
let server = ApiServer::start(db.clone()).await?;
let version = "990000003";
let hub_dir = &*windmill_common::worker::HUB_CACHE_DIR;
tokio::fs::create_dir_all(hub_dir).await?;
tokio::fs::write(
format!("{hub_dir}/{version}"),
r#"{"content":"echo ok","lockfile":null,"language":"bash","schema":{},"summary":null}"#,
)
.await?;
sqlx::query(
"INSERT INTO script (workspace_id, hash, path, content, language, kind, created_by, schema, summary, description, lock)
VALUES ('test-workspace', 1234567891, 'f/test/success_handler', 'echo handled', 'bash', 'script', 'test-user', '{}', '', '', '')",
)
.execute(&db)
.await?;
configure_workspace_handlers(&db).await?;
let completed_job = RunJob::from(JobPayload::ScriptHub {
path: format!("hub/{version}/test/ok"),
apply_preprocessor: false,
})
.run_until_complete(&db, false, server.addr.port())
.await;
assert!(completed_job.success, "Hub script should have succeeded");
let handler_job = wait_for_handler_job(&db, SUCCESS_HANDLER_EMAIL).await?;
assert_eq!(
handler_job.map(|(_, parent)| parent),
Some(Some(completed_job.id)),
"Workspace success handler should handle the successful hub script"
);
Ok(())
}