From 284032ac747b89e22c9284cd808cd04c9640d66c Mon Sep 17 00:00:00 2001 From: Khushboo Verma Date: Wed, 23 Jul 2025 12:50:49 +0530 Subject: [PATCH 1/6] Add execution delay headers to async executions --- app/controllers/general.php | 3 ++ app/controllers/shared/api.php | 3 ++ app/init/constants.php | 2 +- src/Appwrite/Event/Func.php | 2 +- .../Functions/Http/Executions/Create.php | 2 + src/Appwrite/Platform/Workers/Functions.php | 11 +++- .../Functions/FunctionsCustomServerTest.php | 50 +++++++++++++++++++ tests/resources/functions/basic/index.js | 18 +++++++ 8 files changed, 88 insertions(+), 3 deletions(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index 42e26b6fbe..ad6b61d6b8 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -57,6 +57,7 @@ Config::setParam('cookieSamesite', Response::COOKIE_SAMESITE_NONE); function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, string $previewHostname, ?Key $apiKey) { + // add headers here as well $host = $request->getHostname() ?? ''; if (!empty($previewHostname)) { $host = $previewHostname; @@ -370,6 +371,8 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw $headers['x-appwrite-key'] = API_KEY_DYNAMIC . '_' . $jwtKey; $headers['x-appwrite-trigger'] = 'http'; $headers['x-appwrite-user-jwt'] = ''; + // add here + // in API - for sync executions, hard code delay to 0 $ip = $headers['x-real-ip'] ?? ''; if (!empty($ip)) { diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 86fb1e5822..04e5c41ba5 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -713,7 +713,10 @@ App::shutdown() $queueForEvents->setPayload($responsePayload); } + $scheduledAt = new \DateTime(); + $queueForFunctions + ->setHeaders(['x-appwrite-scheduled-at' => $scheduledAt->format('Y-m-d\TH:i:s.v\Z')]) ->from($queueForEvents) ->trigger(); diff --git a/app/init/constants.php b/app/init/constants.php index ebf79086a7..d58cd60560 100644 --- a/app/init/constants.php +++ b/app/init/constants.php @@ -142,7 +142,7 @@ const APP_AUTH_TYPE_ADMIN = 'Admin'; // Response related const MAX_OUTPUT_CHUNK_SIZE = 10 * 1024 * 1024; // 10MB // Function headers -const FUNCTION_ALLOWLIST_HEADERS_REQUEST = ['content-type', 'agent', 'content-length', 'host']; +const FUNCTION_ALLOWLIST_HEADERS_REQUEST = ['content-type', 'agent', 'content-length', 'host', 'x-appwrite-scheduled-at']; const FUNCTION_ALLOWLIST_HEADERS_RESPONSE = ['content-type', 'content-length']; // Message types const MESSAGE_TYPE_EMAIL = 'email'; diff --git a/src/Appwrite/Event/Func.php b/src/Appwrite/Event/Func.php index ae316c84e5..b6a51e3bc7 100644 --- a/src/Appwrite/Event/Func.php +++ b/src/Appwrite/Event/Func.php @@ -161,7 +161,7 @@ class Func extends Event /** * Sets custom headers for the function event. * - * @param string $headers + * @param array $headers * @return self */ public function setHeaders(array $headers): self diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php index d502a78e07..689ab9a67c 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php @@ -221,6 +221,8 @@ class Create extends Base $headers['x-appwrite-country-code'] = ''; $headers['x-appwrite-continent-code'] = ''; $headers['x-appwrite-continent-eu'] = 'false'; + $currentTime = new \DateTime(); + $headers['x-appwrite-scheduled-at'] = $currentTime->format('Y-m-d\TH:i:s.v\Z'); $ip = $headers['x-real-ip'] ?? ''; if (!empty($ip)) { diff --git a/src/Appwrite/Platform/Workers/Functions.php b/src/Appwrite/Platform/Workers/Functions.php index 2c9ca16b0c..1335d03aea 100644 --- a/src/Appwrite/Platform/Workers/Functions.php +++ b/src/Appwrite/Platform/Workers/Functions.php @@ -405,6 +405,11 @@ class Functions extends Action $headers['x-appwrite-country-code'] = ''; $headers['x-appwrite-continent-code'] = ''; $headers['x-appwrite-continent-eu'] = 'false'; + $scheduledAt = $headers['x-appwrite-scheduled-at'] ?? ''; + $executedAt = new \DateTime(); + $headers['x-appwrite-executed-at'] = $executedAt->format('Y-m-d\TH:i:s.v\Z'); + $delay = $executedAt->getTimestamp() - (new \DateTime($scheduledAt))->getTimestamp(); + $headers['x-appwrite-execution-delay'] = (string)floor($delay); /** Create execution or update execution status */ $execution = $dbForProject->getDocument('executions', $executionId ?? ''); @@ -447,8 +452,12 @@ class Functions extends Action } if ($execution->getAttribute('status') !== 'processing') { + $headersFromExecution = $execution->getAttribute('requestHeaders', []); $execution->setAttribute('status', 'processing'); - + $execution->setAttribute('requestHeaders', \array_merge($headersFromExecution, [ + ['name' => 'x-appwrite-executed-at', 'value' => $headers['x-appwrite-executed-at']], + ['name' => 'x-appwrite-execution-delay', 'value' => $headers['x-appwrite-execution-delay'], + ]])); $execution = $dbForProject->updateDocument('executions', $executionId, $execution); } diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index ff99033fdf..8ba4d39a74 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -2282,4 +2282,54 @@ class FunctionsCustomServerTest extends Scope $this->cleanupFunction($functionId); } + + public function testExecutionHeaders() + { + $functionId = $this->setupFunction([ + 'functionId' => ID::unique(), + 'name' => 'Test execution headers', + 'execute' => [Role::any()->toString()], + 'runtime' => 'node-22', + 'entrypoint' => 'index.js', + 'timeout' => 10, + ]); + + $deploymentId = $this->setupDeployment($functionId, [ + 'code' => $this->packageFunction('basic'), + 'activate' => true, + ]); + + $deployment = $this->getDeployment($functionId, $deploymentId); + $this->assertEquals(200, $deployment['headers']['status-code']); + + $execution = $this->createExecution($functionId, [ + 'async' => true, + ]); + + $this->assertEquals(202, $execution['headers']['status-code']); + $this->assertNotEmpty($execution['body']['$id']); + + $executionId = $execution['body']['$id'] ?? ''; + + sleep(5); + + $execution = $this->getExecution($functionId, $executionId); + $this->assertEquals(200, $execution['headers']['status-code']); + $this->assertEquals('completed', $execution['body']['status']); + $this->assertEquals(200, $execution['body']['responseStatusCode']); + $this->assertGreaterThan(0, $execution['body']['duration']); + $this->assertNotEmpty($execution['body']['logs']); + $this->assertNotEmpty($execution['body']['responseHeaders']); + $requestHeaders = array_column($execution['body']['requestHeaders'], 'value', 'name'); + $this->assertArrayHasKey('x-appwrite-scheduled-at', $requestHeaders); + $this->assertArrayHasKey('x-appwrite-executed-at', $requestHeaders); + $this->assertArrayHasKey('x-appwrite-execution-delay', $requestHeaders); + $this->assertIsNumeric($requestHeaders['x-appwrite-execution-delay']); + $this->assertGreaterThan($requestHeaders['x-appwrite-scheduled-at'], $requestHeaders['x-appwrite-executed-at']); + $this->assertStringContainsString('execution-delay-is-valid', $execution['body']['logs']); + $this->assertStringContainsString('scheduled-at-is-valid', $execution['body']['logs']); + $this->assertStringContainsString('executed-at-is-valid', $execution['body']['logs']); + + $this->cleanupFunction($functionId); + } } diff --git a/tests/resources/functions/basic/index.js b/tests/resources/functions/basic/index.js index 1eb9d38c58..60c7cbb692 100644 --- a/tests/resources/functions/basic/index.js +++ b/tests/resources/functions/basic/index.js @@ -7,6 +7,24 @@ module.exports = async(context) => { } else { context.log('jwt-is-invalid'); } + + if (context.req.headers["x-appwrite-execution-delay"]) { + context.log("execution-delay-is-valid"); + } else { + context.log("execution-delay-is-invalid"); + } + + if (context.req.headers["x-appwrite-scheduled-at"]) { + context.log("scheduled-at-is-valid"); + } else { + context.log("scheduled-at-is-invalid"); + } + + if (context.req.headers["x-appwrite-executed-at"]) { + context.log("executed-at-is-valid"); + } else { + context.log("executed-at-is-invalid"); + } if(context.req.path === '/custom-response') { const code = +(context.req.query['code'] || '200'); From 3e74e39d1dd74d5b7c48923bcce5bcc9fdf8d259 Mon Sep 17 00:00:00 2001 From: Khushboo Verma Date: Mon, 28 Jul 2025 10:26:20 +0530 Subject: [PATCH 2/6] Add headers for event triggered executions --- app/controllers/shared/api.php | 6 +++--- app/init/constants.php | 2 +- src/Appwrite/Platform/Workers/Functions.php | 3 ++- .../e2e/Services/Functions/FunctionsCustomServerTest.php | 8 ++++++++ 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 04e5c41ba5..5f88440c2c 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -70,8 +70,11 @@ $eventDatabaseListener = function (Document $project, Document $document, Respon ->setParam('userId', $document->getId()) ->setPayload($response->output($document, Response::MODEL_USER)); + $scheduledAt = new \DateTime(); + // Trigger functions, webhooks, and realtime events $queueForFunctions + ->setHeaders(['x-appwrite-scheduled-at' => $scheduledAt->format('Y-m-d\TH:i:s.v\Z')]) ->from($queueForEvents) ->trigger(); @@ -713,10 +716,7 @@ App::shutdown() $queueForEvents->setPayload($responsePayload); } - $scheduledAt = new \DateTime(); - $queueForFunctions - ->setHeaders(['x-appwrite-scheduled-at' => $scheduledAt->format('Y-m-d\TH:i:s.v\Z')]) ->from($queueForEvents) ->trigger(); diff --git a/app/init/constants.php b/app/init/constants.php index d58cd60560..7678f5e0b6 100644 --- a/app/init/constants.php +++ b/app/init/constants.php @@ -142,7 +142,7 @@ const APP_AUTH_TYPE_ADMIN = 'Admin'; // Response related const MAX_OUTPUT_CHUNK_SIZE = 10 * 1024 * 1024; // 10MB // Function headers -const FUNCTION_ALLOWLIST_HEADERS_REQUEST = ['content-type', 'agent', 'content-length', 'host', 'x-appwrite-scheduled-at']; +const FUNCTION_ALLOWLIST_HEADERS_REQUEST = ['content-type', 'agent', 'content-length', 'host', 'x-appwrite-scheduled-at', 'x-appwrite-execution-delay', 'x-appwrite-executed-at']; const FUNCTION_ALLOWLIST_HEADERS_RESPONSE = ['content-type', 'content-length']; // Message types const MESSAGE_TYPE_EMAIL = 'email'; diff --git a/src/Appwrite/Platform/Workers/Functions.php b/src/Appwrite/Platform/Workers/Functions.php index 1335d03aea..bf80f94942 100644 --- a/src/Appwrite/Platform/Workers/Functions.php +++ b/src/Appwrite/Platform/Workers/Functions.php @@ -164,7 +164,8 @@ class Functions extends Action method: 'POST', headers: [ 'user-agent' => 'Appwrite/' . APP_VERSION_STABLE, - 'content-type' => 'application/json' + 'content-type' => 'application/json', + 'x-appwrite-scheduled-at' => $headers['x-appwrite-scheduled-at'] ?? '', ], data: null, user: $user, diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index 8ba4d39a74..3acbcae0ee 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -1508,6 +1508,14 @@ class FunctionsCustomServerTest extends Scope $this->assertEquals(204, $lastExecution['responseStatusCode']); $this->assertStringContainsString($userId, $lastExecution['logs']); $this->assertStringContainsString('Event User', $lastExecution['logs']); + + $requestHeaders = array_column($lastExecution['requestHeaders'], 'value', 'name'); + $this->assertArrayHasKey('x-appwrite-scheduled-at', $requestHeaders); + $this->assertArrayHasKey('x-appwrite-executed-at', $requestHeaders); + $this->assertArrayHasKey('x-appwrite-execution-delay', $requestHeaders); + $this->assertIsNumeric($requestHeaders['x-appwrite-execution-delay']); + $this->assertGreaterThan($requestHeaders['x-appwrite-scheduled-at'], $requestHeaders['x-appwrite-executed-at']); + }, 10000, 500); $this->cleanupFunction($functionId); From bc383fa3ec009a1c2cd42de0ecd3ad0ed9bf5054 Mon Sep 17 00:00:00 2001 From: Khushboo Verma Date: Mon, 28 Jul 2025 10:33:14 +0530 Subject: [PATCH 3/6] Add safety check for empty header --- src/Appwrite/Platform/Workers/Functions.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Appwrite/Platform/Workers/Functions.php b/src/Appwrite/Platform/Workers/Functions.php index bf80f94942..c5d1c7a976 100644 --- a/src/Appwrite/Platform/Workers/Functions.php +++ b/src/Appwrite/Platform/Workers/Functions.php @@ -407,10 +407,12 @@ class Functions extends Action $headers['x-appwrite-continent-code'] = ''; $headers['x-appwrite-continent-eu'] = 'false'; $scheduledAt = $headers['x-appwrite-scheduled-at'] ?? ''; - $executedAt = new \DateTime(); - $headers['x-appwrite-executed-at'] = $executedAt->format('Y-m-d\TH:i:s.v\Z'); - $delay = $executedAt->getTimestamp() - (new \DateTime($scheduledAt))->getTimestamp(); - $headers['x-appwrite-execution-delay'] = (string)floor($delay); + if (!empty($scheduledAt)) { + $executedAt = new \DateTime(); + $headers['x-appwrite-executed-at'] = $executedAt->format('Y-m-d\TH:i:s.v\Z'); + $delay = $executedAt->getTimestamp() - (new \DateTime($scheduledAt))->getTimestamp(); + $headers['x-appwrite-execution-delay'] = (string)floor(max(0, $delay)); + } /** Create execution or update execution status */ $execution = $dbForProject->getDocument('executions', $executionId ?? ''); From bd1b548cd8b8a69b209a854f3f25fc4d13a26ee7 Mon Sep 17 00:00:00 2001 From: Khushboo Verma Date: Mon, 28 Jul 2025 10:44:40 +0530 Subject: [PATCH 4/6] Add logs to event-handler function --- .../Functions/FunctionsCustomServerTest.php | 3 +++ .../resources/functions/event-handler/index.js | 17 +++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index 3acbcae0ee..eec0d6a236 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -1515,6 +1515,9 @@ class FunctionsCustomServerTest extends Scope $this->assertArrayHasKey('x-appwrite-execution-delay', $requestHeaders); $this->assertIsNumeric($requestHeaders['x-appwrite-execution-delay']); $this->assertGreaterThan($requestHeaders['x-appwrite-scheduled-at'], $requestHeaders['x-appwrite-executed-at']); + $this->assertStringContainsString('execution-delay-is-valid', $lastExecution['logs']); + $this->assertStringContainsString('scheduled-at-is-valid', $lastExecution['logs']); + $this->assertStringContainsString('executed-at-is-valid', $lastExecution['logs']); }, 10000, 500); diff --git a/tests/resources/functions/event-handler/index.js b/tests/resources/functions/event-handler/index.js index 6df3b7fb35..3ffcc7a1d5 100644 --- a/tests/resources/functions/event-handler/index.js +++ b/tests/resources/functions/event-handler/index.js @@ -1,5 +1,22 @@ module.exports = async(context) => { context.log(context.req.body.$id); context.log(context.req.body.name); + if (context.req.headers["x-appwrite-execution-delay"]) { + context.log("execution-delay-is-valid"); + } else { + context.log("execution-delay-is-invalid"); + } + + if (context.req.headers["x-appwrite-scheduled-at"]) { + context.log("scheduled-at-is-valid"); + } else { + context.log("scheduled-at-is-invalid"); + } + + if (context.req.headers["x-appwrite-executed-at"]) { + context.log("executed-at-is-valid"); + } else { + context.log("executed-at-is-invalid"); + } return context.res.empty(); }; From dd2a04b36228c5dbe70c9dbe18e8e76e8e407542 Mon Sep 17 00:00:00 2001 From: Khushboo Verma Date: Mon, 28 Jul 2025 10:58:36 +0530 Subject: [PATCH 5/6] Add headers for sync executions --- app/controllers/general.php | 8 ++-- .../Functions/Http/Executions/Create.php | 6 +++ .../Functions/FunctionsCustomServerTest.php | 42 ++++++++++++++++++- 3 files changed, 52 insertions(+), 4 deletions(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index ad6b61d6b8..446ffd9058 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -57,7 +57,6 @@ Config::setParam('cookieSamesite', Response::COOKIE_SAMESITE_NONE); function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, string $previewHostname, ?Key $apiKey) { - // add headers here as well $host = $request->getHostname() ?? ''; if (!empty($previewHostname)) { $host = $previewHostname; @@ -371,8 +370,11 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw $headers['x-appwrite-key'] = API_KEY_DYNAMIC . '_' . $jwtKey; $headers['x-appwrite-trigger'] = 'http'; $headers['x-appwrite-user-jwt'] = ''; - // add here - // in API - for sync executions, hard code delay to 0 + // add headers for sync executions + $currentTime = new \DateTime(); + $headers['x-appwrite-scheduled-at'] = $currentTime->format('Y-m-d\TH:i:s.v\Z'); + $headers['x-appwrite-executed-at'] = $currentTime->format('Y-m-d\TH:i:s.v\Z'); + $headers['x-appwrite-execution-delay'] = '0'; $ip = $headers['x-real-ip'] ?? ''; if (!empty($ip)) { diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php index b5af34823a..ad1e711ab7 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Executions/Create.php @@ -231,6 +231,12 @@ class Create extends Base $currentTime = new \DateTime(); $headers['x-appwrite-scheduled-at'] = $currentTime->format('Y-m-d\TH:i:s.v\Z'); + if (!$async) { + $executedAt = new \DateTime(); + $headers['x-appwrite-executed-at'] = $executedAt->format('Y-m-d\TH:i:s.v\Z'); + $headers['x-appwrite-execution-delay'] = '0'; + } + $ip = $headers['x-real-ip'] ?? ''; if (!empty($ip)) { $record = $geodb->get($ip); diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index eec0d6a236..365be656c7 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -2294,7 +2294,7 @@ class FunctionsCustomServerTest extends Scope $this->cleanupFunction($functionId); } - public function testExecutionHeaders() + public function testAsyncExecutionHeaders() { $functionId = $this->setupFunction([ 'functionId' => ID::unique(), @@ -2343,4 +2343,44 @@ class FunctionsCustomServerTest extends Scope $this->cleanupFunction($functionId); } + + public function testSyncExecutionHeaders() + { + $functionId = $this->setupFunction([ + 'functionId' => ID::unique(), + 'name' => 'Test sync execution headers', + 'execute' => [Role::any()->toString()], + 'runtime' => 'node-22', + 'entrypoint' => 'index.js', + 'timeout' => 10, + ]); + + $deploymentId = $this->setupDeployment($functionId, [ + 'code' => $this->packageFunction('basic'), + 'activate' => true, + ]); + + $deployment = $this->getDeployment($functionId, $deploymentId); + $this->assertEquals(200, $deployment['headers']['status-code']); + + $execution = $this->createExecution($functionId, [ + 'async' => false, + ]); + + $this->assertEquals(201, $execution['headers']['status-code']); + $this->assertEquals('completed', $execution['body']['status']); + $this->assertEquals(200, $execution['body']['responseStatusCode']); + $this->assertGreaterThan(0, $execution['body']['duration']); + $this->assertNotEmpty($execution['body']['logs']); + $this->assertNotEmpty($execution['body']['responseHeaders']); + + $requestHeaders = array_column($execution['body']['requestHeaders'], 'value', 'name'); + $this->assertArrayHasKey('x-appwrite-scheduled-at', $requestHeaders); + $this->assertArrayHasKey('x-appwrite-executed-at', $requestHeaders); + $this->assertArrayHasKey('x-appwrite-execution-delay', $requestHeaders); + $this->assertEquals('0', $requestHeaders['x-appwrite-execution-delay']); + $this->assertGreaterThanOrEqual($requestHeaders['x-appwrite-scheduled-at'], $requestHeaders['x-appwrite-executed-at']); + + $this->cleanupFunction($functionId); + } } From ba69d3397b4058ef2652177745e5b7365d10ce04 Mon Sep 17 00:00:00 2001 From: Khushboo Verma Date: Mon, 28 Jul 2025 11:19:34 +0530 Subject: [PATCH 6/6] Update test for header --- tests/e2e/Services/FunctionsSchedule/FunctionsScheduleTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/Services/FunctionsSchedule/FunctionsScheduleTest.php b/tests/e2e/Services/FunctionsSchedule/FunctionsScheduleTest.php index c3dd2c7fc8..1080a24be7 100644 --- a/tests/e2e/Services/FunctionsSchedule/FunctionsScheduleTest.php +++ b/tests/e2e/Services/FunctionsSchedule/FunctionsScheduleTest.php @@ -118,7 +118,7 @@ class FunctionsScheduleTest extends Scope $this->assertEquals('scheduled', $execution['body']['status']); $this->assertEquals('PATCH', $execution['body']['requestMethod']); $this->assertEquals('/custom-path', $execution['body']['requestPath']); - $this->assertCount(0, $execution['body']['requestHeaders']); + $this->assertCount(1, $execution['body']['requestHeaders']); \sleep(120);