mirror of
https://github.com/block/buzz.git
synced 2026-10-27 04:30:52 +00:00
Fixes the CI flakes that failed most often across `main` and unrelated branches from Oct 5 to Oct 14. The counts come from 150 failed `CI` runs. Every change except the nextest config is test-only. | Flake | Runs hit | Root cause | Fix | |---|---|---|---| | `probe_codex_acp_version_…descendant_holds_pipe_open` | 5 | The test writes its script in-process and then execs it. If a sibling test spawns a process in that window, the child inherits the open write fd and Linux rejects the exec with `ETXTBSY`. The probe swallows that error as `None`. | Write probe scripts from a short-lived `/bin/sh` (`write_executable_script`) | | `empty-edit-delete.spec.ts` (`:51`, `:74`, `:95`) | 11 | The seeded `mock-general-welcome` id isn't hex, so `formatTimelineMessages` can't overlay its edit. The post-subscribe window refetch then reverts the row. | Emit a per-test message with a real id after the exact channel subscription is live | | `huddle-transcription.spec.ts:798` | 9 | The composer's delayed initial autofocus (TipTap `focus()` runs in rAF) lands after the voice popover opens and dismisses it. | Wait for `message-input` focus before opening the popover | | `voice-note.spec.ts:906` | 6 | The fixture `ping.mp3` is about 1s long, so playback can end before `paused === false` is observed. | Loop the fixture audio | | `workflows.spec.ts:291` | 5 | The `Message text` fill runs before the inspector swaps to the new step, so it fills the trigger's condition instead. | Scope the fill to the step inspector after its `Action` menu appears | | `cluster_global_probe_commits_tokens_and_sessions_prove_coverage` | 3 | The `max-threads = 1` group only serializes these tests with each other. Other tests' sessions still mask cluster-wide `pg_stat_activity` (`MaskedActivity`). | `threads-required = "num-test-threads"` | | `test_usage_metrics_lock_has_single_owner_and_releases_on_drop` | 1 (this PR) | The test retries the lock right after dropping the leader. Postgres releases a session advisory lock only after the backend notices the disconnect, so the retry can run first. | Poll for the successor, bounded at 5s | ### Validation - **ETXTBSY:** a Linux experiment with Rust 1.95 using the same write→chmod→spawn pattern under concurrent spawns hit `ETXTBSY` 4–7% of the time. Using the `sh` writer, it hit 0 of 6,000. I did not catch it inside the full suite locally, so this fix is based on the mechanism, not an in-suite repro. All 11 probe tests pass on Linux and macOS. - **empty-edit-delete:** the baseline failed 16 of 45 runs (`--repeat-each`, 6 workers, no retries). The spec fix alone brings that to 4/45. The remaining failures come from a separate product race, described below. - **huddle:** the baseline failed 5 of 10 runs. With the fix, 60 of 60 passed without retries. - **nextest:** checked with a throwaway crate on 0.9.136. A grouped test with `num-test-threads` ran alone, even with `max-threads = 1`. - I have not stress-run the voice-note and workflows fixes; they rely on CI. ### Not in this PR - **Product bug in `useChannelLinks`:** the debounced `#channel` autocomplete close lets Enter accept a stale suggestion for about 120ms. That accounts for the remaining `empty-edit-delete` failures, and it affects users too. With the spec fix plus a synchronous close, the spec passed 120 of 120 runs. This needs a product decision, so it's held for a separate PR. - **Composer autofocus race:** the delayed autofocus can close an overlay the user just opened. The real fix is to revalidate when the delayed focus runs. Overlaps #6530. - **`thread-head-stale-edit.spec.ts:55`:** a forced click on a pointer-events-none action bar is suspected, but I couldn't reproduce it. - **Remaining ETXTBSY exposure:** about 40 other Rust tests write and then exec scripts, and 17 specs still edit the non-hex seed message. Root-cause lanes by Princess Donut, Mongo and Mordecai. Carl integrated and verified them. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz> Co-authored-by: Princess Donut <5d97ac8c272fa949af56c71e586146b1706e5c4c3daa125db5b4625e12e0686c@buzz.block.builderlab.xyz> Co-authored-by: Mongo <81cabd2ca1792494372ed410283c00770c0cf35afca052e940fa522883212b45@buzz.block.builderlab.xyz> Co-authored-by: Mordecai <f314b6033b6e13df94c8e9e1187d3666b6b9ce54e66ee986e31cdaa909e8dcbc@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
159 lines
6.1 KiB
TypeScript
159 lines
6.1 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
|
|
|
import { installMockBridge } from "../helpers/bridge";
|
|
|
|
// A message the mock identity authors in #general. Editing/deleting one's own
|
|
// message is exactly Sam's workflow: "delete a message by clearing its edit."
|
|
// It is emitted per test rather than reusing the seeded `mock-general-welcome`:
|
|
// edits and deletions only overlay a target with a real 64-hex event id, so
|
|
// that seed keeps its edit only until the post-subscribe window refetch.
|
|
const ORIGINAL_CONTENT = "Ship notes for #general";
|
|
const RENDERED_ORIGINAL_CONTENT = "Ship notes for general";
|
|
let OWN_MESSAGE_ID: string;
|
|
|
|
// Open the more-actions menu for a message row and wait for the menu to mount.
|
|
async function openMoreActionsMenu(
|
|
page: import("@playwright/test").Page,
|
|
messageId: string,
|
|
) {
|
|
const row = page.locator(`[data-message-id="${messageId}"]`);
|
|
await row.hover();
|
|
await page.getByTestId(`more-actions-${messageId}`).click();
|
|
await expect(page.locator('[role="menuitem"]').first()).toBeVisible({
|
|
timeout: 5_000,
|
|
});
|
|
}
|
|
|
|
// Enter edit mode for a message, clear it to empty, and submit — the gesture
|
|
// that triggers the empty-edit delete confirmation.
|
|
async function submitEmptyEdit(
|
|
page: import("@playwright/test").Page,
|
|
messageId: string,
|
|
) {
|
|
await openMoreActionsMenu(page, messageId);
|
|
await page.getByTestId(`edit-message-${messageId}`).click();
|
|
await expect(page.getByTestId("edit-target")).toBeVisible({ timeout: 5_000 });
|
|
// Edit mode sets the editor content via Tiptap's async transaction pipeline;
|
|
// wait for it to populate before we clear it.
|
|
const input = page.getByTestId("message-input");
|
|
await expect(input).not.toBeEmpty({ timeout: 5_000 });
|
|
await input.click();
|
|
await page.keyboard.press("ControlOrMeta+A");
|
|
await page.keyboard.press("Backspace");
|
|
await expect(input).toBeEmpty();
|
|
await page.keyboard.press("Enter");
|
|
}
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
await installMockBridge(page);
|
|
await page.goto("/");
|
|
await page.getByTestId("channel-general").click();
|
|
await expect(page.getByTestId("chat-title")).toHaveText("general");
|
|
await expect
|
|
.poll(() =>
|
|
page.evaluate(
|
|
() =>
|
|
(
|
|
window as Window & {
|
|
__BUZZ_E2E_HAS_MOCK_LIVE_SUBSCRIPTION__?: (input: {
|
|
channelName: string;
|
|
kind: number;
|
|
exactChannel: boolean;
|
|
}) => boolean;
|
|
}
|
|
).__BUZZ_E2E_HAS_MOCK_LIVE_SUBSCRIPTION__?.({
|
|
channelName: "general",
|
|
kind: 9,
|
|
exactChannel: true,
|
|
}) ?? false,
|
|
),
|
|
)
|
|
.toBe(true);
|
|
const emitted = await page.evaluate(
|
|
(content) =>
|
|
(
|
|
window as Window & {
|
|
__BUZZ_E2E_EMIT_MOCK_MESSAGE__?: (input: {
|
|
channelName: string;
|
|
content: string;
|
|
}) => { id: string } | undefined;
|
|
}
|
|
).__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({ channelName: "general", content }),
|
|
ORIGINAL_CONTENT,
|
|
);
|
|
if (!emitted?.id) throw new Error("mock message emit did not return an id");
|
|
OWN_MESSAGE_ID = emitted.id;
|
|
});
|
|
|
|
test("clearing an edit to empty prompts to delete, then deletes on confirm", async ({
|
|
page,
|
|
}) => {
|
|
const row = page.locator(`[data-message-id="${OWN_MESSAGE_ID}"]`);
|
|
await expect(row).toBeVisible({ timeout: 10_000 });
|
|
|
|
await submitEmptyEdit(page, OWN_MESSAGE_ID);
|
|
|
|
// The same "Delete message?" confirmation the Delete menu action shows — an
|
|
// empty edit is routed through it, not silently deleted.
|
|
const dialog = page.getByRole("alertdialog");
|
|
await expect(dialog).toBeVisible({ timeout: 10_000 });
|
|
await expect(dialog).toContainText("Delete message?");
|
|
// Edit mode stays active while the dialog is open — it exits only on confirm.
|
|
await expect(page.getByTestId("edit-target")).toBeVisible();
|
|
|
|
// Confirm → the message row is removed and edit mode has exited.
|
|
await dialog.getByRole("button", { name: "Delete" }).click();
|
|
await expect(dialog).toBeHidden({ timeout: 5_000 });
|
|
await expect(page.getByTestId("edit-target")).toBeHidden();
|
|
await expect(row).toBeHidden({ timeout: 5_000 });
|
|
});
|
|
|
|
test("cancelling the empty-edit delete keeps the message", async ({ page }) => {
|
|
const row = page.locator(`[data-message-id="${OWN_MESSAGE_ID}"]`);
|
|
await expect(row).toBeVisible({ timeout: 10_000 });
|
|
|
|
await submitEmptyEdit(page, OWN_MESSAGE_ID);
|
|
|
|
const dialog = page.getByRole("alertdialog");
|
|
await expect(dialog).toBeVisible({ timeout: 10_000 });
|
|
|
|
// Cancel → nothing is deleted, the original message survives, and the user is
|
|
// left in edit mode (the editing session is preserved, not discarded).
|
|
await dialog.getByRole("button", { name: "Cancel" }).click();
|
|
await expect(dialog).toBeHidden({ timeout: 5_000 });
|
|
await expect(page.getByTestId("edit-target")).toBeVisible();
|
|
await expect(row).toBeVisible();
|
|
await expect(page.getByTestId("message-timeline")).toContainText(
|
|
RENDERED_ORIGINAL_CONTENT,
|
|
);
|
|
await expect(row.getByLabel("Open channel general")).toBeVisible();
|
|
});
|
|
|
|
test("a non-empty edit still edits and never deletes", async ({ page }) => {
|
|
const row = page.locator(`[data-message-id="${OWN_MESSAGE_ID}"]`);
|
|
await expect(row).toBeVisible({ timeout: 10_000 });
|
|
|
|
await openMoreActionsMenu(page, OWN_MESSAGE_ID);
|
|
await page.getByTestId(`edit-message-${OWN_MESSAGE_ID}`).click();
|
|
await expect(page.getByTestId("edit-target")).toBeVisible({ timeout: 5_000 });
|
|
const input = page.getByTestId("message-input");
|
|
await expect(input).not.toBeEmpty({ timeout: 5_000 });
|
|
const editedContent = `Edited, not deleted ${Date.now()}`;
|
|
|
|
await input.click();
|
|
await page.keyboard.press("ControlOrMeta+A");
|
|
await page.keyboard.type(editedContent);
|
|
await page.keyboard.press("Enter");
|
|
|
|
// No delete confirmation, edit mode exits, the row survives with new text.
|
|
await expect(page.getByRole("alertdialog")).toHaveCount(0);
|
|
await expect(page.getByTestId("edit-target")).toBeHidden({ timeout: 10_000 });
|
|
await expect(row).toBeVisible();
|
|
await expect(page.getByTestId("message-timeline")).toContainText(
|
|
editedContent,
|
|
);
|
|
await expect(page.getByTestId("message-timeline")).not.toContainText(
|
|
RENDERED_ORIGINAL_CONTENT,
|
|
);
|
|
});
|