Track HiveGrid Cloud planning reference
This commit is contained in:
1 parent
b3d5ea6764
commit
e341c5938a
5784 files changed
+1104929
-3
No files matched your search
@@ -0,0 +1,121 @@
|
||||
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;
|
||||
}
|
||||
Reference in new issue
Block a user