122 lines
4.9 KiB
TypeScript
122 lines
4.9 KiB
TypeScript
import { expect, type Page } from "@playwright/test";
|
|
import { missingE2eInputs, skipWhenE2eOperatorOptInMissing } from "./readiness";
|
|
|
|
/**
|
|
* Shared helpers for asserting that a workflow execution reaches a terminal
|
|
* status after the worker drains the outbox. Extracted from the workflow
|
|
* builder-run spec so other readiness slices (e.g. plugin or subscription
|
|
* flows that kick off workflows) can reuse the same polling contract.
|
|
*/
|
|
|
|
export const workflowTerminalStatuses = new Set<string>(["success", "error"]);
|
|
|
|
const workflowExecutionStatusAliases = new Map([
|
|
["success", "success"],
|
|
["error", "error"],
|
|
["running", "running"],
|
|
["waiting", "waiting"],
|
|
["waitingblock", "waitingBlock"],
|
|
["incomplete", "incomplete"],
|
|
]);
|
|
|
|
export const workflowOutboxDrainRequiredInputs = ["E2E_TENANT_EMAIL", "E2E_TENANT_PASSWORD", "E2E_WORKFLOW_RUN_MANUAL_PATH"] as const;
|
|
|
|
/**
|
|
* Operator gate for the outbox-drain readiness smoke. Skips when the operator
|
|
* opt-in is missing, and fails loudly when the opt-in is set but its required
|
|
* fixture inputs are absent — so a required readiness lane can never go green
|
|
* by silently skipping.
|
|
*/
|
|
export function requireWorkflowOutboxDrainReadiness(env: NodeJS.ProcessEnv = process.env): void {
|
|
if (env.E2E_REQUIRE_WORKFLOW_OUTBOX_DRAIN !== "1") {
|
|
skipWhenE2eOperatorOptInMissing("E2E_REQUIRE_WORKFLOW_OUTBOX_DRAIN", "worker/outbox drain readiness is operator-gated");
|
|
return;
|
|
}
|
|
|
|
const missing = missingE2eInputs(workflowOutboxDrainRequiredInputs, env);
|
|
if (missing.length > 0) {
|
|
throw new Error(`E2E workflow outbox-drain readiness opt-in missing ${missing.join(", ")} while E2E_REQUIRE_WORKFLOW_OUTBOX_DRAIN=1.`);
|
|
}
|
|
}
|
|
|
|
export function getWorkflowOutboxDrainPollTimeoutMs(env: NodeJS.ProcessEnv = process.env): number {
|
|
const parsed = Number.parseInt(env.E2E_WORKFLOW_OUTBOX_DRAIN_TIMEOUT_MS ?? "", 10);
|
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : 90_000;
|
|
}
|
|
|
|
export function getWorkflowOutboxDrainExpectedStatus(env: NodeJS.ProcessEnv = process.env): "success" | "error" {
|
|
const raw = env.E2E_WORKFLOW_OUTBOX_DRAIN_EXPECTED_STATUS?.trim().toLowerCase() || "success";
|
|
if (raw === "success" || raw === "error") return raw;
|
|
throw new Error("E2E_WORKFLOW_OUTBOX_DRAIN_EXPECTED_STATUS must be either success or error.");
|
|
}
|
|
|
|
export function normalizeWorkflowExecutionStatus(value: string | undefined): string | null {
|
|
if (!value) return null;
|
|
const normalized = value.trim().replace(/\s+/gu, "").toLowerCase();
|
|
return workflowExecutionStatusAliases.get(normalized) ?? null;
|
|
}
|
|
|
|
export function extractWorkflowExecutionStatusFromText(text: string): string | null {
|
|
const lines = text
|
|
.split(/\r?\n/u)
|
|
.map((line) => line.trim())
|
|
.filter(Boolean);
|
|
|
|
for (const [index, line] of lines.entries()) {
|
|
const inlineStatus = line.match(/^status\s+([a-z]+)$/iu)?.[1];
|
|
const normalizedInlineStatus = normalizeWorkflowExecutionStatus(inlineStatus);
|
|
if (normalizedInlineStatus) return normalizedInlineStatus;
|
|
|
|
if (/^status$/iu.test(line)) {
|
|
for (const candidate of lines.slice(index + 1, index + 4)) {
|
|
const normalizedCandidate = normalizeWorkflowExecutionStatus(candidate);
|
|
if (normalizedCandidate) return normalizedCandidate;
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
async function readWorkflowExecutionStatus(page: Page, executionDetailsHref: string, pollIndex: number): Promise<string | null> {
|
|
const pollUrl = new URL(executionDetailsHref, page.url());
|
|
pollUrl.searchParams.set("e2ePoll", String(pollIndex));
|
|
await page.goto(pollUrl.toString(), { waitUntil: "domcontentloaded" });
|
|
await page
|
|
.getByText(/^status$/i)
|
|
.first()
|
|
.waitFor({ state: "visible", timeout: 5_000 })
|
|
.catch(() => undefined);
|
|
return extractWorkflowExecutionStatusFromText(await page.locator("body").innerText());
|
|
}
|
|
|
|
/**
|
|
* Polls the execution details view until the workflow reaches the expected
|
|
* terminal status (success/error) after the worker drains the outbox, or fails
|
|
* once the drain timeout elapses. Returns the observed terminal status.
|
|
*/
|
|
export async function waitForWorkflowExecutionTerminal(page: Page, executionDetailsHref: string): Promise<"success" | "error"> {
|
|
const expectedStatus = getWorkflowOutboxDrainExpectedStatus();
|
|
let pollIndex = 0;
|
|
let observedStatus: string | null = null;
|
|
await expect
|
|
.poll(
|
|
async () => {
|
|
pollIndex += 1;
|
|
observedStatus = await readWorkflowExecutionStatus(page, executionDetailsHref, pollIndex);
|
|
return observedStatus ?? "unknown";
|
|
},
|
|
{
|
|
message: `workflow execution reached expected ${expectedStatus} status after worker/outbox drain`,
|
|
timeout: getWorkflowOutboxDrainPollTimeoutMs(),
|
|
intervals: [1_000, 2_000, 5_000],
|
|
}
|
|
)
|
|
.toBe(expectedStatus);
|
|
|
|
// The poll only resolves once the observed status equals the expected terminal
|
|
// status, so the observed value is guaranteed to be a terminal status here.
|
|
expect(observedStatus).toBe(expectedStatus);
|
|
return expectedStatus;
|
|
}
|