857 lines
27 KiB
TypeScript
857 lines
27 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
|
import { apiPathPattern, mockApi, primeMockSession } from "./support/network.js";
|
|
|
|
const cronTasks = [
|
|
{
|
|
id: "economic.transfer_queue",
|
|
name: "Process e-conomic transfer queue",
|
|
description: "Processes pending e-conomic transfer queue jobs.",
|
|
module: "economic",
|
|
schedule: { type: "interval", seconds: 30 },
|
|
default_schedule: { type: "interval", seconds: 30 },
|
|
enabled: true,
|
|
default_enabled: true,
|
|
estimated_duration_ms: 2400,
|
|
next_run_at: "2026-07-09 12:01:00",
|
|
last_status: "succeeded",
|
|
due: true,
|
|
},
|
|
{
|
|
id: "system.sync_logs",
|
|
name: "Sync logs",
|
|
description: "Flushes application logs.",
|
|
module: "system",
|
|
schedule: { type: "interval", seconds: 300 },
|
|
default_schedule: { type: "interval", seconds: 300 },
|
|
enabled: false,
|
|
default_enabled: true,
|
|
estimated_duration_ms: 900,
|
|
next_run_at: "2026-07-09 12:05:00",
|
|
last_status: "failed",
|
|
last_error: "Redis unavailable",
|
|
due: false,
|
|
},
|
|
];
|
|
|
|
function cronListPayload(tasks = cronTasks) {
|
|
return {
|
|
success: true,
|
|
data: {
|
|
tasks,
|
|
summary: {
|
|
total: tasks.length,
|
|
enabled: tasks.filter((task) => task.enabled).length,
|
|
due: tasks.filter((task) => task.enabled && task.due).length,
|
|
},
|
|
},
|
|
meta: {},
|
|
includes: {},
|
|
};
|
|
}
|
|
|
|
test.describe("Superuser cron operations", () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: [
|
|
"superuser",
|
|
"superuser_cron_view",
|
|
"SUPERUSER_RUN_CRON",
|
|
"superuser_cron_manage",
|
|
"superuser_coolify_manage",
|
|
],
|
|
});
|
|
await primeMockSession(page, { token: "superuser-cron-token", bootPath: null });
|
|
});
|
|
|
|
test("superusers can inspect, run, toggle, and reschedule cron tasks", async ({ page }, testInfo) => {
|
|
const patchPayloads: Array<Record<string, unknown>> = [];
|
|
const runPayloads: Array<Record<string, unknown>> = [];
|
|
const deployPayloads: Array<Record<string, unknown>> = [];
|
|
let currentTasks = cronTasks.map((task) => ({ ...task, schedule: { ...task.schedule } }));
|
|
let runHistory = [
|
|
{
|
|
id: 41,
|
|
task_id: "economic.transfer_queue",
|
|
source: "automatic",
|
|
status: "succeeded",
|
|
started_at: "2026-07-09 12:00:00",
|
|
duration_ms: 2100,
|
|
},
|
|
];
|
|
|
|
await page.route(apiPathPattern("/superuser/cron"), async (route) => {
|
|
if (route.request().method() !== "GET") {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
await route.fulfill({
|
|
status: 202,
|
|
contentType: "application/json",
|
|
body: JSON.stringify(cronListPayload(currentTasks)),
|
|
});
|
|
});
|
|
|
|
await page.route(apiPathPattern("/superuser/cron/runs"), async (route) => {
|
|
await route.fulfill({
|
|
status: 202,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({
|
|
success: true,
|
|
data: {
|
|
runs: runHistory,
|
|
},
|
|
meta: {},
|
|
includes: {},
|
|
}),
|
|
});
|
|
});
|
|
|
|
await page.route(apiPathPattern("/superuser/cron/workers"), async (route) => {
|
|
if (route.request().method() !== "GET") {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({
|
|
success: true,
|
|
data: {
|
|
state: "healthy",
|
|
desired_workers: 1,
|
|
channel: {
|
|
id: 1,
|
|
slug: "stable",
|
|
name: "Stable",
|
|
},
|
|
channels: [
|
|
{
|
|
id: 1,
|
|
slug: "stable",
|
|
name: "Stable",
|
|
default_channel: true,
|
|
},
|
|
],
|
|
api_target: {
|
|
id: 17,
|
|
app: "api",
|
|
},
|
|
cron_target: {
|
|
id: 71,
|
|
app: "cron",
|
|
coolify_service_uuid: "cron-worker-uuid",
|
|
auto_deploy: false,
|
|
},
|
|
workers: [
|
|
{
|
|
worker_id: "release-stable-cron-worker",
|
|
name: "release-stable-cron-worker",
|
|
hostname: "coolify-cron-1",
|
|
source: "coolify_worker",
|
|
status: "running",
|
|
stale: false,
|
|
last_heartbeat_at: "2026-07-09 12:01:30",
|
|
last_run_count: 1,
|
|
commit_sha: "abcdef1234567890",
|
|
release_channel_id: 1,
|
|
release_target_id: 71,
|
|
coolify_resource_uuid: "cron-worker-uuid",
|
|
},
|
|
],
|
|
summary: {
|
|
total: 1,
|
|
running: 1,
|
|
stale: 0,
|
|
desired: 1,
|
|
state: "healthy",
|
|
},
|
|
latest_deployment: {
|
|
id: 88,
|
|
app: "cron",
|
|
status: "deployed",
|
|
provider_operation_id: "deploy-88",
|
|
started_at: "2026-07-09 12:01:00",
|
|
},
|
|
recent_deployments: [
|
|
{
|
|
id: 88,
|
|
app: "cron",
|
|
status: "deployed",
|
|
provider_operation_id: "deploy-88",
|
|
started_at: "2026-07-09 12:01:00",
|
|
},
|
|
],
|
|
issues: [],
|
|
deployment: {
|
|
ok: true,
|
|
state: "healthy",
|
|
channel: {
|
|
id: 1,
|
|
slug: "stable",
|
|
name: "Stable",
|
|
},
|
|
api_target: {
|
|
id: 17,
|
|
app: "api",
|
|
},
|
|
target: {
|
|
id: 71,
|
|
app: "cron",
|
|
coolify_service_uuid: "cron-worker-uuid",
|
|
auto_deploy: false,
|
|
},
|
|
latest_deployment: {
|
|
id: 88,
|
|
app: "cron",
|
|
status: "deployed",
|
|
provider_operation_id: "deploy-88",
|
|
started_at: "2026-07-09 12:01:00",
|
|
},
|
|
issues: [],
|
|
},
|
|
},
|
|
meta: {},
|
|
includes: {},
|
|
}),
|
|
});
|
|
});
|
|
|
|
await page.route(apiPathPattern("/superuser/cron/workers/deploy"), async (route) => {
|
|
deployPayloads.push(route.request().postDataJSON() as Record<string, unknown>);
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({
|
|
success: true,
|
|
data: {
|
|
ok: true,
|
|
deployment: {
|
|
id: 89,
|
|
app: "cron",
|
|
status: "deployed",
|
|
provider_operation_id: "deploy-89",
|
|
},
|
|
target: {
|
|
id: 71,
|
|
app: "cron",
|
|
coolify_service_uuid: "cron-worker-uuid",
|
|
auto_deploy: false,
|
|
},
|
|
worker_status: {
|
|
state: "waiting_for_heartbeat",
|
|
channel: {
|
|
id: 1,
|
|
slug: "stable",
|
|
name: "Stable",
|
|
},
|
|
channels: [
|
|
{
|
|
id: 1,
|
|
slug: "stable",
|
|
name: "Stable",
|
|
default_channel: true,
|
|
},
|
|
],
|
|
workers: [],
|
|
summary: {
|
|
total: 0,
|
|
running: 0,
|
|
stale: 0,
|
|
desired: 1,
|
|
state: "waiting_for_heartbeat",
|
|
},
|
|
deployment: {
|
|
state: "waiting_for_heartbeat",
|
|
target: {
|
|
id: 71,
|
|
app: "cron",
|
|
coolify_service_uuid: "cron-worker-uuid",
|
|
auto_deploy: false,
|
|
},
|
|
},
|
|
latest_deployment: {
|
|
id: 89,
|
|
status: "deployed",
|
|
provider_operation_id: "deploy-89",
|
|
},
|
|
issues: [
|
|
{
|
|
code: "waiting_for_first_heartbeat",
|
|
severity: "info",
|
|
message: "Coolify accepted the deployment; waiting for the worker to write its first heartbeat.",
|
|
},
|
|
],
|
|
},
|
|
},
|
|
meta: {},
|
|
includes: {},
|
|
}),
|
|
});
|
|
});
|
|
|
|
await page.route(apiPathPattern("/superuser/cron/run"), async (route) => {
|
|
runPayloads.push(route.request().postDataJSON() as Record<string, unknown>);
|
|
runHistory = [
|
|
{
|
|
id: 42,
|
|
task_id: "economic.transfer_queue",
|
|
source: "manual",
|
|
status: "queued",
|
|
scheduled_for: "2026-07-09 12:02:00",
|
|
created_at: "2026-07-09 12:02:00",
|
|
duration_ms: null,
|
|
},
|
|
...runHistory,
|
|
];
|
|
await route.fulfill({
|
|
status: 202,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({
|
|
success: true,
|
|
data: {
|
|
id: 42,
|
|
task_id: "economic.transfer_queue",
|
|
source: "manual",
|
|
status: "queued",
|
|
scheduled_for: "2026-07-09 12:02:00",
|
|
created_at: "2026-07-09 12:02:00",
|
|
duration_ms: null,
|
|
},
|
|
meta: {},
|
|
includes: {},
|
|
}),
|
|
});
|
|
});
|
|
|
|
await page.route(apiPathPattern("/superuser/cron/config"), async (route) => {
|
|
const payload = route.request().postDataJSON() as Record<string, unknown>;
|
|
patchPayloads.push(payload);
|
|
currentTasks = currentTasks.map((task) => {
|
|
if (task.id !== payload.task_id) {
|
|
return task;
|
|
}
|
|
return {
|
|
...task,
|
|
...(typeof payload.enabled === "boolean" ? { enabled: payload.enabled } : {}),
|
|
...(payload.schedule ? { schedule: payload.schedule as { type: string; seconds: number } } : {}),
|
|
};
|
|
});
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify(cronListPayload(currentTasks)),
|
|
});
|
|
});
|
|
|
|
await page.goto("/superuser/system/cron", { waitUntil: "domcontentloaded" });
|
|
|
|
await expect(page.getByTestId("superuser-cron-page")).toBeVisible();
|
|
await expect(page.getByTestId("cron-task-row-economic.transfer_queue")).toContainText(
|
|
"Process e-conomic transfer queue"
|
|
);
|
|
await expect(page.getByTestId("cron-total-tasks")).toContainText("2");
|
|
if (!/mobile/i.test(testInfo.project.name)) {
|
|
await expect(page.getByTestId("cron-worker-panel")).toContainText("release-stable-cron-worker");
|
|
await expect(page.getByTestId("cron-worker-running")).toContainText("1");
|
|
await expect(page.getByTestId("cron-worker-target")).toContainText("cron-worker-uuid");
|
|
await expect(page.getByTestId("cron-worker-deploy")).toHaveText("Update Coolify deployment");
|
|
|
|
await Promise.all([
|
|
page.waitForResponse(
|
|
(response) =>
|
|
response.url().includes("/superuser/cron/workers/deploy") && response.request().method() === "POST"
|
|
),
|
|
page.getByTestId("cron-worker-deploy").click(),
|
|
]);
|
|
expect(deployPayloads).toEqual([{ channel_id: 1 }]);
|
|
await expect(page.getByTestId("cron-run-queued")).toContainText("deployment update");
|
|
}
|
|
|
|
await Promise.all([
|
|
page.waitForResponse(
|
|
(response) => response.url().includes("/superuser/cron/run") && response.request().method() === "POST"
|
|
),
|
|
page.getByTestId("cron-task-run-economic.transfer_queue").click(),
|
|
]);
|
|
|
|
expect(runPayloads).toEqual([{ task_id: "economic.transfer_queue", force: false }]);
|
|
await expect(page.getByTestId("cron-run-history")).toContainText("manual");
|
|
await expect(page.getByTestId("cron-run-history")).toContainText("Queued");
|
|
await expect(page.getByTestId("cron-run-queued")).toContainText("was queued");
|
|
|
|
await page.getByTestId("cron-task-toggle-system.sync_logs").click();
|
|
expect(patchPayloads.at(-1)).toMatchObject({ task_id: "system.sync_logs", enabled: true });
|
|
|
|
await page.getByTestId("cron-task-interval-economic.transfer_queue").fill("120");
|
|
await page.getByTestId("cron-task-save-economic.transfer_queue").click();
|
|
expect(patchPayloads.at(-1)).toMatchObject({
|
|
task_id: "economic.transfer_queue",
|
|
schedule: { type: "interval", seconds: 120 },
|
|
});
|
|
});
|
|
|
|
test("superusers can deploy the cron worker to Coolify when no deployment target exists", async ({ page }) => {
|
|
const deployPayloads: Array<Record<string, unknown>> = [];
|
|
let deploymentTarget: Record<string, unknown> | null = null;
|
|
|
|
await page.route(apiPathPattern("/superuser/cron"), async (route) => {
|
|
if (route.request().method() !== "GET") {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify(cronListPayload()),
|
|
});
|
|
});
|
|
|
|
await page.route(apiPathPattern("/superuser/cron/runs"), async (route) => {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({
|
|
success: true,
|
|
data: { runs: [] },
|
|
meta: {},
|
|
includes: {},
|
|
}),
|
|
});
|
|
});
|
|
|
|
await page.route(apiPathPattern("/superuser/cron/workers"), async (route) => {
|
|
if (route.request().method() !== "GET") {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({
|
|
success: true,
|
|
data: {
|
|
state: deploymentTarget ? "waiting_for_heartbeat" : "needs_deploy",
|
|
desired_workers: 1,
|
|
channel: {
|
|
id: 1,
|
|
slug: "stable",
|
|
name: "Stable",
|
|
},
|
|
channels: [
|
|
{
|
|
id: 1,
|
|
slug: "stable",
|
|
name: "Stable",
|
|
default_channel: true,
|
|
},
|
|
],
|
|
api_target: {
|
|
id: 17,
|
|
app: "api",
|
|
},
|
|
cron_target: deploymentTarget,
|
|
workers: [],
|
|
summary: {
|
|
total: 0,
|
|
running: 0,
|
|
stale: 0,
|
|
desired: 1,
|
|
state: deploymentTarget ? "waiting_for_heartbeat" : "needs_deploy",
|
|
},
|
|
latest_deployment: deploymentTarget
|
|
? {
|
|
id: 72,
|
|
app: "cron",
|
|
status: "deployed",
|
|
provider_operation_id: "deploy-72",
|
|
started_at: "2026-07-09 12:01:00",
|
|
}
|
|
: null,
|
|
recent_deployments: deploymentTarget
|
|
? [
|
|
{
|
|
id: 72,
|
|
app: "cron",
|
|
status: "deployed",
|
|
provider_operation_id: "deploy-72",
|
|
started_at: "2026-07-09 12:01:00",
|
|
},
|
|
]
|
|
: [],
|
|
issues: [
|
|
deploymentTarget
|
|
? {
|
|
code: "waiting_for_first_heartbeat",
|
|
severity: "info",
|
|
message: "Coolify accepted the deployment; waiting for the worker to write its first heartbeat.",
|
|
}
|
|
: {
|
|
code: "missing_cron_target",
|
|
severity: "warning",
|
|
message: "No cron worker Coolify target exists for this release channel.",
|
|
},
|
|
],
|
|
deployment: {
|
|
ok: true,
|
|
state: deploymentTarget ? "waiting_for_heartbeat" : "needs_deploy",
|
|
channel: {
|
|
id: 1,
|
|
slug: "stable",
|
|
name: "Stable",
|
|
},
|
|
api_target: {
|
|
id: 17,
|
|
app: "api",
|
|
},
|
|
target: deploymentTarget,
|
|
latest_deployment: deploymentTarget
|
|
? {
|
|
id: 72,
|
|
app: "cron",
|
|
status: "deployed",
|
|
provider_operation_id: "deploy-72",
|
|
started_at: "2026-07-09 12:01:00",
|
|
}
|
|
: null,
|
|
},
|
|
},
|
|
meta: {},
|
|
includes: {},
|
|
}),
|
|
});
|
|
});
|
|
|
|
await page.route(apiPathPattern("/superuser/cron/workers/deploy"), async (route) => {
|
|
deployPayloads.push(route.request().postDataJSON() as Record<string, unknown>);
|
|
deploymentTarget = {
|
|
id: 71,
|
|
app: "cron",
|
|
coolify_service_uuid: "cron-worker-uuid",
|
|
auto_deploy: false,
|
|
};
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({
|
|
success: true,
|
|
data: {
|
|
ok: true,
|
|
deployment: {
|
|
id: 72,
|
|
app: "cron",
|
|
status: "deployed",
|
|
provider_operation_id: "deploy-72",
|
|
},
|
|
target: deploymentTarget,
|
|
worker_status: {
|
|
state: "waiting_for_heartbeat",
|
|
channel: {
|
|
id: 1,
|
|
slug: "stable",
|
|
name: "Stable",
|
|
},
|
|
channels: [
|
|
{
|
|
id: 1,
|
|
slug: "stable",
|
|
name: "Stable",
|
|
default_channel: true,
|
|
},
|
|
],
|
|
cron_target: deploymentTarget,
|
|
workers: [],
|
|
summary: {
|
|
total: 0,
|
|
running: 0,
|
|
stale: 0,
|
|
desired: 1,
|
|
state: "waiting_for_heartbeat",
|
|
},
|
|
deployment: {
|
|
state: "waiting_for_heartbeat",
|
|
target: deploymentTarget,
|
|
},
|
|
latest_deployment: {
|
|
id: 72,
|
|
status: "deployed",
|
|
provider_operation_id: "deploy-72",
|
|
},
|
|
issues: [
|
|
{
|
|
code: "waiting_for_first_heartbeat",
|
|
severity: "info",
|
|
message: "Coolify accepted the deployment; waiting for the worker to write its first heartbeat.",
|
|
},
|
|
],
|
|
},
|
|
},
|
|
meta: {},
|
|
includes: {},
|
|
}),
|
|
});
|
|
});
|
|
|
|
await page.goto("/superuser/system/cron", { waitUntil: "domcontentloaded" });
|
|
|
|
await expect(page.getByTestId("cron-worker-deploy")).toHaveText("Deploy to Coolify");
|
|
await Promise.all([
|
|
page.waitForResponse(
|
|
(response) =>
|
|
response.url().includes("/superuser/cron/workers/deploy") && response.request().method() === "POST"
|
|
),
|
|
page.getByTestId("cron-worker-deploy").click(),
|
|
]);
|
|
|
|
expect(deployPayloads).toEqual([{ channel_id: 1 }]);
|
|
await expect(page.getByTestId("cron-run-queued")).toContainText("deployment was queued");
|
|
await expect(page.getByTestId("cron-worker-state")).toContainText("Waiting for heartbeat");
|
|
await expect(page.getByTestId("cron-worker-issues")).toContainText("waiting for the worker");
|
|
await expect(page.getByTestId("cron-worker-target")).toContainText("cron-worker-uuid");
|
|
await expect(page.getByTestId("cron-worker-deploy")).toHaveText("Update Coolify deployment");
|
|
});
|
|
|
|
test("repairs an orphaned Coolify cron worker target", async ({ page }) => {
|
|
const deployPayloads: Array<Record<string, unknown>> = [];
|
|
let repaired = false;
|
|
const oldTarget = {
|
|
id: 71,
|
|
app: "cron",
|
|
coolify_service_uuid: "missing-cron-worker-uuid",
|
|
auto_deploy: false,
|
|
};
|
|
const repairedTarget = {
|
|
id: 71,
|
|
app: "cron",
|
|
coolify_service_uuid: "cron-worker-uuid",
|
|
auto_deploy: false,
|
|
};
|
|
|
|
await page.route(apiPathPattern("/superuser/cron"), async (route) => {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify(cronListPayload()),
|
|
});
|
|
});
|
|
|
|
await page.route(apiPathPattern("/superuser/cron/runs"), async (route) => {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({
|
|
success: true,
|
|
data: { runs: [] },
|
|
meta: {},
|
|
includes: {},
|
|
}),
|
|
});
|
|
});
|
|
|
|
await page.route(apiPathPattern("/superuser/cron/workers"), async (route) => {
|
|
const target = repaired ? repairedTarget : oldTarget;
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({
|
|
success: true,
|
|
data: {
|
|
state: repaired ? "waiting_for_heartbeat" : "failed",
|
|
desired_workers: 1,
|
|
channel: {
|
|
id: 1,
|
|
slug: "stable",
|
|
name: "Stable",
|
|
},
|
|
channels: [
|
|
{
|
|
id: 1,
|
|
slug: "stable",
|
|
name: "Stable",
|
|
default_channel: true,
|
|
},
|
|
],
|
|
api_target: null,
|
|
cron_target: target,
|
|
workers: [],
|
|
summary: {
|
|
total: 0,
|
|
running: 0,
|
|
stale: 0,
|
|
desired: 1,
|
|
state: repaired ? "waiting_for_heartbeat" : "failed",
|
|
},
|
|
latest_deployment: repaired
|
|
? {
|
|
id: 73,
|
|
app: "cron",
|
|
status: "deployed",
|
|
provider_operation_id: "deploy-73",
|
|
}
|
|
: null,
|
|
recent_deployments: [],
|
|
provider: {
|
|
configured: true,
|
|
resource_uuid: target.coolify_service_uuid,
|
|
resource_type: "application",
|
|
checked: true,
|
|
missing: !repaired,
|
|
resource: repaired
|
|
? {
|
|
ok: true,
|
|
uuid: "cron-worker-uuid",
|
|
name: "release-stable-cron-worker",
|
|
}
|
|
: {
|
|
ok: false,
|
|
missing: true,
|
|
error: "Coolify API request failed: HTTP 404",
|
|
},
|
|
},
|
|
issues: repaired
|
|
? [
|
|
{
|
|
code: "waiting_for_first_heartbeat",
|
|
severity: "info",
|
|
message: "Coolify accepted the deployment; waiting for the worker to write its first heartbeat.",
|
|
},
|
|
]
|
|
: [
|
|
{
|
|
code: "no_worker_heartbeat",
|
|
severity: "danger",
|
|
message: "Cron worker target exists, but no worker heartbeat has been recorded.",
|
|
},
|
|
{
|
|
code: "missing_coolify_worker_resource",
|
|
severity: "danger",
|
|
message: "The stored Coolify cron worker resource was not found and must be recreated.",
|
|
},
|
|
{
|
|
code: "repairable_cron_target",
|
|
severity: "info",
|
|
message: "No API target is configured, but the cron target has enough deployment context to repair itself.",
|
|
},
|
|
],
|
|
deployment: {
|
|
ok: true,
|
|
state: repaired ? "waiting_for_heartbeat" : "failed",
|
|
channel: {
|
|
id: 1,
|
|
slug: "stable",
|
|
name: "Stable",
|
|
},
|
|
api_target: null,
|
|
target,
|
|
latest_deployment: repaired
|
|
? {
|
|
id: 73,
|
|
app: "cron",
|
|
status: "deployed",
|
|
provider_operation_id: "deploy-73",
|
|
}
|
|
: null,
|
|
provider: {
|
|
missing: !repaired,
|
|
},
|
|
action: repaired ? "update" : "repair",
|
|
can_deploy: true,
|
|
issues: [],
|
|
},
|
|
},
|
|
meta: {},
|
|
includes: {},
|
|
}),
|
|
});
|
|
});
|
|
|
|
await page.route(apiPathPattern("/superuser/cron/workers/deploy"), async (route) => {
|
|
deployPayloads.push(route.request().postDataJSON() as Record<string, unknown>);
|
|
repaired = true;
|
|
await route.fulfill({
|
|
status: 202,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({
|
|
success: true,
|
|
data: {
|
|
ok: true,
|
|
deployment: {
|
|
id: 73,
|
|
app: "cron",
|
|
status: "deployed",
|
|
provider_operation_id: "deploy-73",
|
|
},
|
|
target: repairedTarget,
|
|
worker_status: {
|
|
state: "waiting_for_heartbeat",
|
|
channel: {
|
|
id: 1,
|
|
slug: "stable",
|
|
name: "Stable",
|
|
},
|
|
channels: [
|
|
{
|
|
id: 1,
|
|
slug: "stable",
|
|
name: "Stable",
|
|
default_channel: true,
|
|
},
|
|
],
|
|
api_target: null,
|
|
cron_target: repairedTarget,
|
|
workers: [],
|
|
summary: {
|
|
total: 0,
|
|
running: 0,
|
|
stale: 0,
|
|
desired: 1,
|
|
state: "waiting_for_heartbeat",
|
|
},
|
|
deployment: {
|
|
state: "waiting_for_heartbeat",
|
|
target: repairedTarget,
|
|
action: "update",
|
|
can_deploy: true,
|
|
},
|
|
latest_deployment: {
|
|
id: 73,
|
|
status: "deployed",
|
|
provider_operation_id: "deploy-73",
|
|
},
|
|
issues: [
|
|
{
|
|
code: "waiting_for_first_heartbeat",
|
|
severity: "info",
|
|
message: "Coolify accepted the deployment; waiting for the worker to write its first heartbeat.",
|
|
},
|
|
],
|
|
},
|
|
},
|
|
meta: {},
|
|
includes: {},
|
|
}),
|
|
});
|
|
});
|
|
|
|
await page.goto("/superuser/system/cron", { waitUntil: "domcontentloaded" });
|
|
|
|
await expect(page.getByTestId("cron-worker-api-target")).toContainText("--");
|
|
await expect(page.getByTestId("cron-worker-target")).toContainText("missing-cron-worker-uuid");
|
|
await expect(page.getByTestId("cron-worker-issues")).toContainText("must be recreated");
|
|
await expect(page.getByTestId("cron-worker-deploy")).toHaveText("Repair Coolify deployment");
|
|
|
|
await Promise.all([
|
|
page.waitForResponse(
|
|
(response) =>
|
|
response.url().includes("/superuser/cron/workers/deploy") && response.request().method() === "POST"
|
|
),
|
|
page.getByTestId("cron-worker-deploy").click(),
|
|
]);
|
|
|
|
expect(deployPayloads).toEqual([{ channel_id: 1 }]);
|
|
await expect(page.getByTestId("cron-worker-state")).toContainText("Waiting for heartbeat");
|
|
await expect(page.getByTestId("cron-worker-target")).toContainText("cron-worker-uuid");
|
|
await expect(page.getByTestId("cron-worker-deploy")).toHaveText("Update Coolify deployment");
|
|
});
|
|
});
|