Files
buzz/desktop/tests/e2e/workflows.spec.ts
Wes 5079c770fe fix(workflows): make deletion persistent and retryable (#7735)
## Summary

Fix workflow deletion leaving its definition queryable, and make public
deletion acceptance agree atomically with executable/definition state.

- Delete the executable workflow and soft-delete its definition under
the existing coordinate lock, in the same transaction as kind-5
acceptance and mention indexing. Failed deletion leaves no public
accepted event; identical retries can succeed. Legacy already-stored
failed requests still repair.
- Choose one committed dispatch/audit owner for concurrent identical
requests. Completed duplicates remain silent. Preserve owner/community
isolation, newer revisions, orphan cleanup, and intentional backdated
recreation.
- Align Desktop confirmation and architecture documentation with
recreation semantics. Add deterministic PostgreSQL, live WebSocket, and
Desktop regressions. No schema migration or new retry/delivery
subsystem.

## Current validation

Head: `40d86ddab0853ffb292316653b22c3fd4f983f0c`.

- **Exact clean head:** live relay workflow-deletion E2E **4/4 passed**.
All applicable normal pre-push hooks passed: Rust unit suites, full
Desktop unit suite, Desktop lint/typecheck, Tauri clippy/tests, and
file-size/branch checks. No hook bypass.
- **Pre-commit implementation tree, then committed at this head:** full
discovered DB/relay PostgreSQL lane **351/351 passed**; built Desktop
deletion Playwright smoke **3/3 passed** (success, failure preserving
editor/draft, corrected confirmation copy).
- **Deterministic mutation evidence:** unchanged previous relay
`659b79282` fails both new regressions: concurrent identical requests
produce two normal/dispatch-owner responses, and rejected deletion
leaves one public event. Both controls pass. Fixed relay passes all
four.
- **Independent source review:** transaction/authorization/lock-order
and regression/cleanup lanes clear. Reviewers did not run tests
independently.

## Honest limits / merge gates

The broad pre-commit DB/relay/test-client selection passed
**1,215/1,216**; the untouched mesh-demo echo test returned HTTP 504
instead of 200. It remains outside this repair and is not counted green.
Initial hook attempts also exposed an untouched Desktop inventory
ordering assertion and unidentified ACP/agent test failures; standalone
package reruns and the final complete hooks passed without changing
those packages. Causes are not established.

The previous Desktop CI shard failed before tests on a Hermit `just`
download (HTTP 500). New-head CI/security review and reviewer
disposition remain required. No deployment, fresh interactive Desktop
session, full local `just ci`, or multi-replica runtime validation
claimed. Postgres/Redis validation used task-owned local containers,
fresh identities, and disposable fixtures.

## Compatibility

No automatic cleanup of existing workflows: repeating deletion repairs
definitions left by the old path. Canonical UUID coordinates and legacy
name fallback (`LIMIT 1` for duplicate names) are unchanged. Backdated
recreation remains intentional per [Tyler’s
decision](https://github.com/block/buzz/pull/7735#issuecomment-5729881295),
not an irreversible tombstone. Mention-index failure now rolls back the
complete deletion rather than best-effort accepting partial work.

Detailed reviewer disposition and evidence:
https://github.com/block/buzz/pull/7735#issuecomment-5737367825

---------

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz>
Co-authored-by: Mordecai <f314b6033b6e13df94c8e9e1187d3666b6b9ce54e66ee986e31cdaa909e8dcbc@buzz.block.builderlab.xyz>
2026-09-21 08:37:14 -06:00

1087 lines
40 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { expect, test } from "@playwright/test";
import { parse as parseYaml } from "yaml";
import { installMockBridge } from "../helpers/bridge";
import { waitForAnimations } from "../helpers/animations";
test.beforeEach(async ({ page }) => {
await installMockBridge(page);
});
async function navigateToWorkflows(page: import("@playwright/test").Page) {
await page.goto("/");
await page.getByTestId("open-workflows-view").click();
await expect(page).toHaveURL(/#\/workflows$/);
await expect(page.getByTestId("workflows-view")).toBeVisible();
}
async function selectWorkflowChannel(
page: import("@playwright/test").Page,
dialog: import("@playwright/test").Locator,
) {
const channelList = page.getByTestId("channel-combobox-list");
if (!(await channelList.isVisible())) {
await dialog.getByRole("combobox", { name: "Channel" }).click();
}
await expect(channelList).toBeVisible();
await waitForAnimations(page);
await channelList
.getByRole("option", { name: "agents", exact: true })
.click();
}
async function editWorkflowName(
dialog: import("@playwright/test").Locator,
name: string,
) {
await dialog.getByRole("button", { name: "Edit workflow name" }).click();
const input = dialog.getByRole("textbox", { name: "Workflow name" });
await input.fill(name);
await dialog.getByRole("button", { name: "Save workflow name" }).click();
await expect(input).not.toBeVisible();
await expect(dialog).toContainText(name);
await expect(
dialog.page().getByTestId("channel-combobox-list"),
).not.toBeVisible();
}
async function createWorkflow(
page: import("@playwright/test").Page,
name: string,
options?: {
description?: string;
enabled?: boolean;
trigger?: string;
stepConditionText?: string;
stepName?: string;
stepTimeoutSecs?: string;
},
) {
await page.getByRole("button", { name: "Create Workflow" }).click();
const dialog = page.getByRole("dialog", { name: "Create workflow" });
await expect(dialog).toBeVisible();
await selectWorkflowChannel(page, dialog);
await editWorkflowName(dialog, name);
if (options?.description) {
await dialog.getByRole("tab", { name: "YAML" }).click();
const yamlEditor = dialog.getByRole("textbox", { name: "Workflow YAML" });
const yaml = await yamlEditor.inputValue();
await yamlEditor.fill(
yaml.replace(
`name: ${name}`,
`name: ${name}\ndescription: ${JSON.stringify(options.description)}`,
),
);
await dialog.getByRole("tab", { name: "Form" }).click();
}
if (options?.trigger) {
await dialog
.getByRole("button", { name: "Trigger: Message Posted" })
.click();
await dialog.getByRole("button", { name: "Trigger event" }).click();
await page
.getByRole("menuitem", {
name:
options.trigger === "diff_posted"
? "Diff Posted"
: options.trigger === "reaction_added"
? "Reaction Added"
: options.trigger === "webhook"
? "Webhook"
: "Message Posted",
})
.click();
}
await dialog.getByRole("button", { name: "Add step", exact: true }).click();
await page.getByRole("menuitem", { name: "Send Message" }).click();
await dialog.getByLabel("Message text").fill("Workflow notification");
if (options?.stepName) {
await dialog.getByRole("button", { name: "Step details" }).click();
await dialog.getByLabel("Name (optional)").fill(options.stepName);
}
if (options?.stepConditionText || options?.stepTimeoutSecs) {
await dialog.getByRole("button", { name: "Run controls" }).click();
}
if (options?.stepConditionText) {
await dialog.getByLabel("Text to match").fill(options.stepConditionText);
}
if (options?.stepTimeoutSecs) {
await dialog
.getByRole("textbox", { name: "Timeout", exact: true })
.fill(options.stepTimeoutSecs);
}
await dialog.getByRole("button", { name: "Create" }).click();
if (!options?.trigger || options.trigger === "message_posted") {
const activationConfirmation = page.getByRole("alertdialog", {
name: "This workflow may run often",
});
await activationConfirmation
.getByRole("button", {
name: options?.enabled === false ? "Keep off" : "Turn on",
})
.click();
}
await expect(
page.getByRole("heading", { name: "Create Workflow" }),
).not.toBeVisible();
}
test("navigates to workflows view and shows the empty create tile", async ({
page,
}) => {
await navigateToWorkflows(page);
await expect(page.getByTestId("new-workflow-card")).toBeVisible();
await expect(page.locator('[data-testid^="workflow-card-"]')).toHaveCount(0);
});
test("creates a narrowly triggered workflow without an activation warning", async ({
page,
}) => {
await navigateToWorkflows(page);
await createWorkflow(page, `safe_webhook_${Date.now()}`, {
trigger: "webhook",
});
await expect(
page.getByTestId("workflow-activation-confirmation"),
).toHaveCount(0);
const yaml = await page.evaluate(() => {
const call = [...(window.__BUZZ_E2E_COMMAND_PAYLOADS__ ?? [])]
.reverse()
.find((candidate) => candidate.command === "create_workflow");
return (call?.payload as { yamlDefinition?: string } | undefined)
?.yamlDefinition;
});
expect(parseYaml(yaml ?? "").enabled).not.toBe(false);
});
test("creation reveals the trigger pane only after a one-shot channel pick", async ({
page,
}) => {
await navigateToWorkflows(page);
await page.getByRole("button", { name: "Create Workflow" }).click();
const dialog = page.getByRole("dialog", { name: "Create workflow" });
const channelList = page.getByTestId("channel-combobox-list");
await expect(channelList).toBeVisible();
await expect(dialog.getByTestId("workflow-node-inspector")).toHaveCount(0);
await page.keyboard.press("Escape");
await expect(channelList).not.toBeVisible();
await expect
.poll(() => dialog.getByText("Untitled workflow", { exact: true }).count())
.toBe(0);
await expect(channelList).not.toBeVisible();
await dialog.getByRole("combobox", { name: "Channel" }).click();
await channelList
.getByRole("option", { name: "agents", exact: true })
.click();
await expect(dialog.getByTestId("workflow-node-inspector")).toBeVisible();
await expect(
dialog.getByRole("button", { name: "Trigger event" }),
).toBeVisible();
});
test("Escape closes the selected inspector before the workflow modal", async ({
page,
}) => {
for (const width of [760, 1280]) {
await page.setViewportSize({ width, height: 820 });
await page.goto(
"/#/workflows?view=create&channel=94a444a4-c0a3-5966-ab05-530c6ddc2301&pane=trigger",
);
const dialog = page.getByRole("dialog", { name: "Create workflow" });
const inspector = dialog.getByTestId("workflow-node-inspector");
await expect(dialog).toBeVisible();
await expect(inspector).toBeVisible();
await page.keyboard.press("Escape");
await expect(inspector).not.toBeVisible();
await expect(dialog).toBeVisible();
await expect(page).toHaveURL(/view=create/);
await page.keyboard.press("Escape");
await expect(dialog).not.toBeVisible();
await expect(page).toHaveURL(/#\/workflows$/);
}
});
test("creates a workflow via the form builder", async ({ page }) => {
const workflowName = `test_workflow_${Date.now()}`;
await navigateToWorkflows(page);
await createWorkflow(page, workflowName);
// Verify workflow appears in the list
await expect(page.getByTestId("workflows-view")).toContainText(workflowName);
});
test("accepts the generated name through the first form mutation", async ({
page,
}) => {
await navigateToWorkflows(page);
await page.getByRole("button", { name: "Create Workflow" }).click();
const dialog = page.getByRole("dialog", { name: "Create workflow" });
await selectWorkflowChannel(page, dialog);
const generatedName = (
await dialog
.getByRole("button", { name: "Edit workflow name" })
.evaluate((button) => button.previousElementSibling?.textContent ?? "")
).trim();
expect(generatedName).toMatch(/^[a-z]+-[a-z]+-[a-z]+$/);
await dialog.getByRole("button", { name: "Add first step" }).click();
await dialog.getByRole("button", { name: "Create workflow" }).click();
const activationConfirmation = page.getByRole("alertdialog", {
name: "This workflow may run often",
});
await activationConfirmation.getByRole("button", { name: "Turn on" }).click();
await expect(page.getByTestId("workflows-view")).toContainText(generatedName);
});
test("disables autocapitalization in the workflow form", async ({ page }) => {
await navigateToWorkflows(page);
await page.getByRole("button", { name: "Create Workflow" }).click();
const dialog = page.getByRole("dialog", { name: "Create workflow" });
await selectWorkflowChannel(page, dialog);
await dialog.getByRole("button", { name: "Edit workflow name" }).click();
await expect(
dialog.getByRole("textbox", { name: "Workflow name" }),
).toHaveAttribute("autocapitalize", "off");
await dialog.getByRole("button", { name: "Add step", exact: true }).click();
await page.getByRole("menuitem", { name: "Send Message" }).click();
await dialog.getByRole("button", { name: "Step details" }).click();
await expect(dialog.getByLabel("Name (optional)")).toHaveAttribute(
"autocapitalize",
"off",
);
});
test("shows executable guidance for diff trigger conditions", async ({
page,
}) => {
await navigateToWorkflows(page);
await page.getByRole("button", { name: "Create Workflow" }).click();
const dialog = page.getByRole("dialog", { name: "Create workflow" });
await selectWorkflowChannel(page, dialog);
await dialog.getByRole("button", { name: "Trigger event" }).click();
await page
.getByRole("menuitem", { name: "Diff Posted", exact: true })
.click();
await expect(dialog.getByLabel("Diff text")).toHaveAttribute(
"placeholder",
"e.g. deploy",
);
});
test("captures workflow library across responsive viewports", async ({
page,
}) => {
await navigateToWorkflows(page);
await createWorkflow(page, "Notify reviewers when source files change", {
description: "Watches diff events for src/ changes",
enabled: false,
trigger: "diff_posted",
});
await createWorkflow(page, "Post the daily standup reminder to the team", {
description: "Keeps the team aligned every morning",
trigger: "message_posted",
});
await createWorkflow(
page,
"Request approval before deploying to production",
{
description: "Requires a final review before release",
trigger: "reaction_added",
},
);
for (const viewport of [
{ width: 800, height: 720, name: "narrow" },
{ width: 1024, height: 720, name: "medium" },
{ width: 1280, height: 720, name: "wide" },
]) {
await page.setViewportSize(viewport);
await page.screenshot({
animations: "disabled",
path: `test-results/workflow-library-${viewport.name}.png`,
});
}
await page.setViewportSize({ width: 1280, height: 720 });
const firstCard = page.locator('[data-testid^="workflow-card-"]').first();
await firstCard.getByRole("button", { name: "Workflow actions" }).click();
await page.screenshot({
animations: "disabled",
path: "test-results/workflow-library-wide-actions.png",
});
});
test("captures disabled diff workflows in the list UI", async ({ page }) => {
const workflowName = `diff_workflow_${Date.now()}`;
await navigateToWorkflows(page);
await createWorkflow(page, workflowName, {
enabled: false,
trigger: "diff_posted",
stepName: "Notify reviewers",
stepConditionText: "src/",
stepTimeoutSecs: "45s",
});
const card = page
.locator('[data-testid^="workflow-card-"]')
.filter({ hasText: workflowName })
.first();
await expect(card.locator("h3")).toHaveText(
"When a diff is posted, send “Workflow notification”",
);
await expect(card).toContainText(workflowName);
await expect(
card.getByRole("switch", { name: "Disable workflow" }),
).toBeChecked();
});
test("enables and disables a workflow from its card status toggle", async ({
page,
}) => {
const workflowName = `toggle_workflow_${Date.now()}`;
await navigateToWorkflows(page);
await createWorkflow(page, workflowName);
const workflowCard = () =>
page
.locator('[data-testid^="workflow-card-"]')
.filter({ hasText: workflowName })
.first();
const disable = workflowCard().getByRole("switch", {
name: "Disable workflow",
});
await expect(disable).toBeChecked();
await disable.click();
const enable = workflowCard().getByRole("switch", {
name: "Enable workflow",
});
await expect(enable).not.toBeChecked();
await enable.click();
const activationConfirmation = page.getByRole("alertdialog", {
name: "This workflow may run often",
});
await expect(activationConfirmation).toBeVisible();
await activationConfirmation.getByRole("button", { name: "Turn on" }).click();
await expect(
workflowCard().getByRole("switch", { name: "Disable workflow" }),
).toBeChecked();
await workflowCard()
.getByRole("button", { name: "Workflow actions" })
.click();
await expect(
page.getByRole("menuitemcheckbox", { name: "Enable" }),
).toHaveCount(0);
});
test("rejects a stale card toggle without overwriting a newer edit", async ({
page,
}) => {
const workflowName = `stale_toggle_${Date.now()}`;
await navigateToWorkflows(page);
await createWorkflow(page, workflowName);
const workflowCard = page
.locator('[data-testid^="workflow-card-"]')
.filter({ hasText: workflowName })
.first();
await page.evaluate(async (name) => {
const invoke = window.__BUZZ_E2E_INVOKE_MOCK_COMMAND__;
if (!invoke) throw new Error("mock command bridge unavailable");
const createCall = [...(window.__BUZZ_E2E_COMMAND_PAYLOADS__ ?? [])]
.reverse()
.find((call) => call.command === "create_workflow");
const channelId = (
createCall?.payload as { channelId?: string } | undefined
)?.channelId;
if (!channelId) throw new Error("create workflow channel unavailable");
const workflows = (await invoke("get_channels_workflows", {
channelIds: [channelId],
})) as Array<{
id: string;
revision: string;
definition: Record<string, unknown>;
}>;
const workflow = workflows.find(
(candidate) => candidate.definition.name === name,
);
if (!workflow) throw new Error("created workflow unavailable");
await invoke("update_workflow", {
workflowId: workflow.id,
expectedRevision: workflow.revision,
yamlDefinition: `name: ${name} edited elsewhere\nenabled: true\ntrigger:\n on: message_posted\nsteps:\n - id: step_1\n action: post_message\n`,
});
}, workflowName);
await workflowCard.getByRole("switch", { name: "Disable workflow" }).click();
await expect(
page
.locator("[data-sonner-toast][data-removed='false']")
.filter({ hasText: "workflow changed since it was loaded" }),
).toBeVisible();
const authoritativeName = await page.evaluate(async () => {
const invoke = window.__BUZZ_E2E_INVOKE_MOCK_COMMAND__;
if (!invoke) throw new Error("mock command bridge unavailable");
const createCall = [...(window.__BUZZ_E2E_COMMAND_PAYLOADS__ ?? [])]
.reverse()
.find((call) => call.command === "create_workflow");
const channelId = (
createCall?.payload as { channelId?: string } | undefined
)?.channelId;
if (!channelId) throw new Error("create workflow channel unavailable");
const workflows = (await invoke("get_channels_workflows", {
channelIds: [channelId],
})) as Array<{ name: string }>;
return workflows[0]?.name;
});
expect(authoritativeName).toBe(`${workflowName} edited elsewhere`);
});
test("reports a rejected workflow status change", async ({ page }) => {
const workflowName = `rejected_toggle_${Date.now()}`;
await navigateToWorkflows(page);
await createWorkflow(page, workflowName);
await page.evaluate(() => {
window.__BUZZ_E2E__ ??= {};
window.__BUZZ_E2E__.mock ??= {};
window.__BUZZ_E2E__.mock.workflowUpdateError = "relay refused the update";
});
const workflowCard = page
.locator('[data-testid^="workflow-card-"]')
.filter({ hasText: workflowName })
.first();
await workflowCard.getByRole("switch", { name: "Disable workflow" }).click();
const errorToast = page
.locator("[data-sonner-toast][data-removed='false']")
.filter({ hasText: "Couldn’t change workflow status" });
await expect(errorToast).toContainText("relay refused the update");
await expect(
workflowCard.getByRole("switch", { name: "Disable workflow" }),
).toBeChecked();
});
test("shows the webhook secret dialog after saving a webhook workflow", async ({
page,
}) => {
const workflowName = `webhook_workflow_${Date.now()}`;
await navigateToWorkflows(page);
await createWorkflow(page, workflowName, {
trigger: "webhook",
});
await expect(page.getByText("Webhook ready")).toBeVisible();
await expect(page.getByRole("button", { name: "Copy URL" })).toBeVisible();
await expect(page.getByRole("button", { name: "Copy Secret" })).toBeVisible();
await expect(page.getByText(/•{24}/)).toBeVisible();
await page.goBack();
const confirmation = page.getByRole("alertdialog", {
name: "Continue without this secret?",
});
await expect(confirmation).toBeVisible();
await confirmation.getByRole("button", { name: "Go back" }).click();
await expect(page.getByText("Webhook ready")).toBeVisible();
await page.getByRole("button", { name: "Continue", exact: true }).click();
await confirmation.getByRole("button", { name: "Continue" }).click();
await expect(page.getByText("Webhook ready")).not.toBeVisible();
});
test("edits an existing workflow", async ({ page }) => {
const originalName = `edit_test_${Date.now()}`;
const updatedName = `${originalName}_updated`;
await navigateToWorkflows(page);
await createWorkflow(page, originalName);
// Verify it exists
await expect(page.getByTestId("workflows-view")).toContainText(originalName);
// Open the dropdown menu and click Edit
await page.getByRole("button", { name: "Workflow actions" }).first().click();
await page.getByRole("menuitem", { name: "Edit" }).click();
// Dialog should open in edit mode
const dialog = page.getByRole("dialog", { name: "Edit workflow" });
await expect(dialog).toBeVisible();
await expect(dialog.getByText("Edit workflow")).toBeVisible();
// Change the name
await dialog.getByRole("button", { name: "Edit workflow name" }).click();
const nameInput = dialog
.getByRole("textbox", { name: "Workflow name" })
.first();
await nameInput.clear();
await nameInput.fill(updatedName);
await dialog.getByRole("button", { name: "Save workflow name" }).click();
// Save
await page.getByRole("button", { name: "Save" }).click();
await expect(page.getByRole("dialog")).not.toBeVisible();
// Verify the updated name appears
await expect(page.getByTestId("workflows-view")).toContainText(updatedName);
});
test("duplicates a workflow", async ({ page }) => {
const originalName = `dup_test_${Date.now()}`;
await navigateToWorkflows(page);
await createWorkflow(page, originalName);
// Open the dropdown menu and click Duplicate
await page.getByRole("button", { name: "Workflow actions" }).first().click();
await page.getByRole("menuitem", { name: "Duplicate" }).click();
// Dialog should open in duplicate mode with "(copy)" suffix
await expect(page.getByRole("dialog")).toBeVisible();
await expect(page.getByText("Duplicate Workflow")).toBeVisible();
// Submit the duplicate after deliberately choosing its destination channel.
const dialog = page.getByRole("dialog", { name: "Duplicate workflow" });
await selectWorkflowChannel(page, dialog);
await dialog.getByRole("button", { name: "Create copy" }).click();
const activationConfirmation = page.getByRole("alertdialog", {
name: "This workflow may run often",
});
await expect(activationConfirmation).toBeVisible();
await activationConfirmation.getByRole("button", { name: "Back" }).click();
await expect(dialog).toBeVisible();
await expect
.poll(() =>
page.evaluate(
() =>
(window.__BUZZ_E2E_COMMAND_PAYLOADS__ ?? []).filter(
(call) => call.command === "create_workflow",
).length,
),
)
.toBe(1);
await dialog.getByRole("button", { name: "Create copy" }).click();
await activationConfirmation
.getByRole("button", { name: "Keep off" })
.click();
await expect(page.getByRole("dialog")).not.toBeVisible();
const copiedYaml = await page.evaluate(() => {
const calls = (window.__BUZZ_E2E_COMMAND_PAYLOADS__ ?? []).filter(
(call) => call.command === "create_workflow",
);
return (calls.at(-1)?.payload as { yamlDefinition?: string } | undefined)
?.yamlDefinition;
});
expect(parseYaml(copiedYaml ?? "").enabled).toBe(false);
// Both the original and copy should exist
await expect(page.getByTestId("workflows-view")).toContainText(originalName);
});
test("deletes a workflow with confirmation", async ({ page }) => {
const workflowName = `delete_test_${Date.now()}`;
await navigateToWorkflows(page);
await createWorkflow(page, workflowName);
// Verify it exists
await expect(page.getByTestId("workflows-view")).toContainText(workflowName);
// Open the dropdown menu and click Delete
await page.getByRole("button", { name: "Workflow actions" }).first().click();
await page.getByRole("menuitem", { name: "Delete" }).click();
// Confirmation dialog should appear with workflow name
await expect(page.getByRole("alertdialog")).toBeVisible();
await expect(page.getByRole("alertdialog")).toContainText(workflowName);
// Confirm deletion
await page.getByRole("button", { name: "Delete" }).click();
await expect(page.getByRole("alertdialog")).not.toBeVisible();
// Verify workflow is gone — back to the empty create tile.
await expect(page.getByTestId("new-workflow-card")).toBeVisible();
await expect(page.locator('[data-testid^="workflow-card-"]')).toHaveCount(0);
});
test("deleting the open workflow closes its editor", async ({ page }) => {
const workflowName = `delete_open_${Date.now()}`;
await navigateToWorkflows(page);
await createWorkflow(page, workflowName);
await page.getByRole("button", { name: `View ${workflowName}` }).click();
const detailDialog = page.getByRole("dialog", { name: "Edit workflow" });
await expect(detailDialog).toBeVisible();
await detailDialog.getByRole("button", { name: "Workflow actions" }).click();
await page.getByRole("menuitem", { name: "Delete" }).click();
const confirmation = page.getByRole("alertdialog");
await expect(confirmation).toContainText(workflowName);
await expect(confirmation).toContainText(
"Publishing its definition again, even an older version, can recreate it.",
);
await expect(confirmation).not.toContainText("permanently");
await confirmation.getByRole("button", { name: "Delete" }).click();
await expect(detailDialog).toHaveCount(0);
await expect(page).toHaveURL(/#\/workflows(?:\?|$)/);
await expect(page.getByTestId("new-workflow-card")).toBeVisible();
await expect(page.locator('[data-testid^="workflow-card-"]')).toHaveCount(0);
});
test("rejected deletion keeps the confirmation, editor, and draft", async ({
page,
}) => {
const workflowName = `delete_rejected_${Date.now()}`;
await navigateToWorkflows(page);
await createWorkflow(page, workflowName);
await page.getByRole("button", { name: `View ${workflowName}` }).click();
const editor = page.getByRole("dialog", { name: "Edit workflow" });
await editor.getByRole("tab", { name: "YAML" }).click();
const yamlEditor = editor.getByRole("textbox", { name: "Workflow YAML" });
await yamlEditor.fill(
(await yamlEditor.inputValue()).replace(
workflowName,
`${workflowName}_draft`,
),
);
await page.evaluate(() => {
window.__BUZZ_E2E__ ??= {};
window.__BUZZ_E2E__.mock ??= {};
window.__BUZZ_E2E__.mock.workflowDeleteError = "relay refused deletion";
});
await editor.getByRole("button", { name: "Workflow actions" }).click();
await page.getByRole("menuitem", { name: "Delete" }).click();
const confirmation = page.getByRole("alertdialog", {
name: "Delete workflow?",
});
await confirmation.getByRole("button", { name: "Delete" }).click();
await expect(confirmation).toBeVisible();
await expect(confirmation.getByRole("alert")).toContainText(
"relay refused deletion",
);
await confirmation.getByRole("button", { name: "Cancel" }).click();
await expect(editor).toBeVisible();
await expect(yamlEditor).toContainText(`${workflowName}_draft`);
});
test("captures the built editor at desktop and narrow widths", async ({
page,
}) => {
await navigateToWorkflows(page);
await page.getByRole("button", { name: "Create Workflow" }).click();
const dialog = page.getByRole("dialog", { name: "Create workflow" });
await selectWorkflowChannel(page, dialog);
await editWorkflowName(dialog, "editor_screenshot");
await dialog.getByRole("button", { name: "Add step", exact: true }).click();
await page.getByRole("menuitem", { name: "Send Message" }).click();
await dialog.getByLabel("Message text").fill("Notify the workflow channel");
const inspector = dialog.getByTestId("workflow-node-inspector");
for (const viewport of [
{ height: 900, name: "wide", width: 1440 },
{ height: 820, name: "narrow", width: 760 },
]) {
await page.setViewportSize(viewport);
await inspector.getByRole("button", { name: "Run controls" }).click();
await waitForAnimations(page);
await page.screenshot({
path: `test-results/workflow-editor-${viewport.name}-run-controls.png`,
});
await inspector.getByRole("button", { name: "Step details" }).click();
await waitForAnimations(page);
await page.screenshot({
path: `test-results/workflow-editor-${viewport.name}-step-details.png`,
});
await inspector.getByRole("button", { name: "Step details" }).click();
}
});
test("preserves final sequence affordances and responsive inspector behavior", async ({
page,
}) => {
await navigateToWorkflows(page);
await page.getByRole("button", { name: "Create Workflow" }).click();
const dialog = page.getByRole("dialog", { name: "Create workflow" });
await selectWorkflowChannel(page, dialog);
await editWorkflowName(dialog, "sequence_parity_test");
await dialog.getByRole("button", { name: "Add step", exact: true }).click();
await page.getByRole("menuitem", { name: "Send Message" }).click();
const inspector = dialog.getByTestId("workflow-node-inspector");
const runControls = inspector.getByRole("button", { name: "Run controls" });
const stepDetails = inspector.getByRole("button", { name: "Step details" });
await expect(runControls).toHaveAttribute("aria-expanded", "false");
await expect(stepDetails).toHaveAttribute("aria-expanded", "false");
await expect(inspector.getByLabel("Text to match")).toHaveCount(0);
await expect(inspector.getByLabel("Name (optional)")).toHaveCount(0);
await runControls.click();
await expect(runControls).toHaveAttribute("aria-expanded", "true");
await inspector.getByLabel("Text to match").fill("deploy");
const timeoutField = inspector.getByRole("textbox", {
name: "Timeout",
exact: true,
});
const timeoutSlider = inspector.getByRole("slider", {
name: "Timeout slider",
});
await expect(timeoutSlider).toHaveAttribute("aria-valuetext", "5m");
await timeoutField.fill("1h 2s");
await expect(timeoutSlider).toHaveAttribute("aria-valuetext", "1h");
await timeoutField.fill("45s");
await expect(runControls).toContainText("Conditional · 45s");
await stepDetails.click();
await expect(runControls).toHaveAttribute("aria-expanded", "false");
await expect(stepDetails).toHaveAttribute("aria-expanded", "true");
await expect(inspector.getByLabel("Text to match")).toHaveCount(0);
await inspector.getByLabel("Name (optional)").fill("Notify deployers");
await expect(stepDetails).toContainText("Notify deployers");
await expect(inspector.getByLabel("Step ID")).toHaveValue("step_1");
await expect(dialog.getByText("End", { exact: true })).toHaveCount(0);
const ingresses = dialog.getByTestId("workflow-node-ingress");
await expect(ingresses).toHaveCount(2);
await expect(
ingresses.first().locator("svg.lucide-arrow-down"),
).toBeVisible();
await expect(ingresses.last().locator("svg.lucide-arrow-down")).toHaveCount(
0,
);
await expect(
ingresses.last().getByRole("button", { name: "Add after Step 1" }),
).toBeVisible();
const stepNode = dialog.getByRole("button", { name: /^Step 1:/ });
const removeStep = dialog.getByRole("button", { name: "Remove Step 1" });
await expect(removeStep).toHaveClass(/-right-8/);
await stepNode.hover();
await expect(removeStep).toBeVisible();
await page.setViewportSize({ width: 760, height: 820 });
await expect(
dialog.getByTestId("workflow-node-inspector-backdrop"),
).toBeVisible();
await dialog.getByTestId("workflow-node-inspector-backdrop").click({
position: { x: 20, y: 20 },
});
await expect(dialog.getByTestId("workflow-node-inspector")).not.toBeVisible();
});
test("pane routes use stable IDs and Form/YAML changes stay synchronized", async ({
page,
}) => {
await navigateToWorkflows(page);
await page.getByRole("button", { name: "Create Workflow" }).click();
const dialog = page.getByRole("dialog", { name: "Create workflow" });
await selectWorkflowChannel(page, dialog);
await editWorkflowName(dialog, "pane_sync_test");
await dialog.getByRole("button", { name: "Add step", exact: true }).click();
await page.getByRole("menuitem", { name: "Send Message" }).click();
await dialog.getByLabel("Message text").fill("first message");
await expect(page).toHaveURL(/pane=step%3Astep_1/);
await dialog.getByRole("button", { name: "Add after Step 1" }).click();
await page.getByRole("menuitem", { name: "Delay" }).click();
await dialog.getByLabel("Duration").fill("5m");
await expect(page).toHaveURL(/pane=step%3Astep_2/);
await dialog.getByRole("button", { name: /^Step 1:/ }).click();
await expect(page).toHaveURL(/pane=step%3Astep_1/);
await dialog
.getByTestId("workflow-node-inspector")
.getByRole("button", { name: "Remove step", exact: true })
.click();
await expect(page).toHaveURL(/pane=step%3Astep_2/);
await dialog.getByRole("tab", { name: "YAML" }).click();
const yamlEditor = dialog.getByRole("textbox", { name: "Workflow YAML" });
await expect(yamlEditor).toContainText("id: step_2");
await expect(yamlEditor).not.toContainText("id: step_1");
const yaml = await yamlEditor.inputValue();
await yamlEditor.fill(yaml.replace("duration: 5m", "duration: 10m"));
await dialog.getByRole("tab", { name: "Form" }).click();
await dialog.getByRole("button", { name: /^Step 1:/ }).click();
await expect(dialog.getByLabel("Duration")).toHaveValue("10m");
});
test("clean editor close respects in-app versus direct-entry provenance", async ({
page,
}) => {
await navigateToWorkflows(page);
await page.getByRole("button", { name: "Create Workflow" }).click();
await expect(page).toHaveURL(/view=create/);
await page
.getByRole("dialog", { name: "Create workflow" })
.getByRole("button", {
name: "Close",
exact: true,
})
.click();
await expect(page).toHaveURL(/#\/workflows$/);
await page.goto("/#/workflows?view=create");
await page
.getByRole("dialog", { name: "Create workflow" })
.getByRole("button", {
name: "Close",
exact: true,
})
.click();
await expect(page).toHaveURL(/#\/workflows$/);
await page.goBack();
await expect(page).toHaveURL(/#\/workflows$/);
});
test("direct workflow routes survive refresh and invalid view opens detail", async ({
page,
}) => {
const workflowName = `route_test_${Date.now()}`;
await navigateToWorkflows(page);
await createWorkflow(page, workflowName);
await page.getByRole("button", { name: "Workflow actions" }).first().click();
await page.getByRole("menuitem", { name: "Edit" }).click();
await expect(page).toHaveURL(/#\/workflows\/[^?]+\?.*view=edit/);
const workflowId = new URL(page.url()).hash.match(/workflows\/([^?]+)/)?.[1];
expect(workflowId).toBeTruthy();
await page.goto(`/#/workflows/${workflowId}?view=invalid`);
const detailDialog = page.getByRole("dialog", { name: "Edit workflow" });
await expect(detailDialog).toBeVisible();
await expect(detailDialog).toContainText(workflowName);
await expect(
detailDialog.getByRole("button", { name: "Run history" }),
).toHaveCount(0);
await page.goto("/#/workflows?view=create");
await page.reload();
await expect(
page.getByRole("dialog", { name: "Create workflow" }),
).toBeVisible();
});
test("dirty create close is blocked and keep editing preserves the draft", async ({
page,
}) => {
await navigateToWorkflows(page);
await page.getByRole("button", { name: "Create Workflow" }).click();
const dialog = page.getByRole("dialog", { name: "Create workflow" });
await selectWorkflowChannel(page, dialog);
await editWorkflowName(dialog, "preserved_dirty_draft");
await dialog.getByRole("button", { name: "Close", exact: true }).click();
const confirmation = page.getByRole("alertdialog", {
name: "Discard changes?",
});
await expect(confirmation).toBeVisible();
await confirmation.getByRole("button", { name: "Keep editing" }).click();
await expect(dialog).toContainText("preserved_dirty_draft");
});
test("stale editor save preserves the local draft and reports the conflict", async ({
page,
}) => {
const workflowName = `stale_editor_${Date.now()}`;
await navigateToWorkflows(page);
await createWorkflow(page, workflowName);
await page.getByRole("button", { name: "Workflow actions" }).first().click();
await page.getByRole("menuitem", { name: "Edit" }).click();
const dialog = page.getByRole("dialog", { name: "Edit workflow" });
await dialog.getByRole("button", { name: "Edit workflow name" }).click();
const nameInput = dialog
.getByRole("textbox", { name: "Workflow name" })
.first();
await nameInput.fill(`${workflowName}_local`);
await dialog.getByRole("button", { name: "Save workflow name" }).click();
await page.evaluate(async () => {
const invoke = window.__BUZZ_E2E_INVOKE_MOCK_COMMAND__;
if (!invoke) throw new Error("mock command bridge unavailable");
const workflowId = new URL(window.location.href).hash.match(
/workflows\/([^?]+)/,
)?.[1];
if (!workflowId) throw new Error("workflow id unavailable");
const workflow = (await invoke("get_workflow", { workflowId })) as {
revision: string;
};
await invoke("update_workflow", {
expectedRevision: workflow.revision,
workflowId,
yamlDefinition:
"name: authoritative_remote\ntrigger:\n on: message_posted\nsteps:\n - id: step_1\n action: send_message\n text: remote\n",
});
});
await dialog.getByRole("button", { name: "Save changes" }).click();
await expect(dialog).toContainText("workflow changed since it was loaded");
await expect(dialog).toContainText(`${workflowName}_local`);
await expect(dialog).toBeVisible();
});
test("unsupported workflow opens canonical YAML without a fabricated sequence", async ({
page,
}) => {
const workflowName = `unsupported_detail_${Date.now()}`;
await navigateToWorkflows(page);
const workflowId = await page.evaluate(async (name) => {
const invoke = window.__BUZZ_E2E_INVOKE_MOCK_COMMAND__;
if (!invoke) throw new Error("mock command bridge unavailable");
const workflow = (await invoke("create_workflow", {
channelId: "94a444a4-c0a3-5966-ab05-530c6ddc2301",
yamlDefinition: `name: ${name}\ntrigger:\n on: message_posted\n legacy_filter:\n author: alice\nsteps:\n - id: legacy_step\n action: send_message\n text: preserve me\n`,
})) as { id: string };
return workflow.id;
}, workflowName);
await page.goto(`/#/workflows/${workflowId}`);
const detailDialog = page.getByRole("dialog", { name: "Edit workflow" });
await expect(detailDialog).toBeVisible();
await expect(detailDialog).toContainText(workflowName);
await expect(detailDialog.getByRole("tab", { name: "YAML" })).toHaveAttribute(
"aria-selected",
"true",
);
const yamlEditor = detailDialog.getByRole("textbox", {
name: "Workflow YAML",
});
await expect(yamlEditor).toContainText("legacy_filter:");
await expect(yamlEditor).toContainText("author: alice");
await expect(yamlEditor).toContainText("id: legacy_step");
await expect(yamlEditor).toContainText("text: preserve me");
await expect(
detailDialog.getByRole("button", { name: "Trigger: Message Posted" }),
).toHaveCount(0);
await expect(
detailDialog.getByRole("button", { name: "Step 1: Send Message" }),
).toHaveCount(0);
});
test("card click opens the animated trigger inspector, triggers a run, and hides unsupported run history", async ({
page,
}) => {
const workflowName = `card_editor_${Date.now()}`;
await navigateToWorkflows(page);
await createWorkflow(page, workflowName);
const inspectorEntryAnimation = page.evaluate(
() =>
new Promise<boolean>((resolve) => {
const timeoutId = window.setTimeout(() => {
observer.disconnect();
resolve(false);
}, 1_000);
const observer = new MutationObserver(() => {
const inspector = document.querySelector<HTMLElement>(
'[data-testid="workflow-node-inspector"]',
);
if (!inspector) return;
observer.disconnect();
window.clearTimeout(timeoutId);
const initialRect = inspector.getBoundingClientRect();
const initialStyles = getComputedStyle(inspector);
const initial = {
opacity: initialStyles.opacity,
transform: initialStyles.transform,
width: initialRect.width,
};
requestAnimationFrame(() => {
requestAnimationFrame(() => {
const nextRect = inspector.getBoundingClientRect();
const nextStyles = getComputedStyle(inspector);
resolve(
nextRect.width !== initial.width ||
nextStyles.opacity !== initial.opacity ||
nextStyles.transform !== initial.transform,
);
});
});
});
observer.observe(document.body, { childList: true, subtree: true });
}),
);
await page.getByRole("button", { name: `View ${workflowName}` }).click();
await expect(page).toHaveURL(/#\/workflows\/[^?]+\?pane=trigger$/);
const detailDialog = page.getByRole("dialog", { name: "Edit workflow" });
await expect(detailDialog).toBeVisible();
await expect(detailDialog).toContainText(workflowName);
await expect(inspectorEntryAnimation).resolves.toBe(true);
await expect(page.getByTestId("workflow-node-inspector")).toBeVisible();
await expect(
detailDialog.getByRole("button", { name: "Trigger: Message Posted" }),
).toHaveAttribute("aria-pressed", "true");
const triggerCallsBefore = await page.evaluate(
() =>
window.__BUZZ_E2E_COMMAND_PAYLOADS__?.filter(
(call) => call.command === "trigger_workflow",
).length ?? 0,
);
await detailDialog.getByRole("button", { name: "Workflow actions" }).click();
await page.getByRole("menuitem", { name: "Trigger" }).click();
await expect
.poll(async () =>
page.evaluate(
() =>
window.__BUZZ_E2E_COMMAND_PAYLOADS__?.filter(
(call) => call.command === "trigger_workflow",
).length ?? 0,
),
)
.toBe(triggerCallsBefore + 1);
await expect(page.getByTestId("workflow-detail-panel")).toHaveCount(0);
await expect(
detailDialog.getByRole("button", { name: "Run history" }),
).toHaveCount(0);
});
test("missing workflow routes show an unavailable modal with close and retry", async ({
page,
}) => {
await navigateToWorkflows(page);
await page.goto("/#/workflows/missing-workflow?view=edit");
const unavailable = page.getByRole("dialog", {
name: "Workflow unavailable",
});
await expect(unavailable).toBeVisible();
await expect(
unavailable.getByRole("button", { name: "Retry" }),
).toBeVisible();
await unavailable.getByRole("button", { name: "Close" }).click();
await expect(page).toHaveURL(/#\/workflows$/);
});