Files
buzz/desktop/tests/e2e/persistent-agent-audience.spec.ts
Logan Johnson 091670ff77 fix(e2e): close bootstrap checker receiver gaps
Preserve unsafe bootstrap events through supported parenthesized forms and grant loop bootstrap credit only when syntax proves an enumerable runtime iteration.

Co-authored-by: Larry <larry@buzz.local>
Co-authored-by: Brother Darryl <darryl@buzz.local>
Signed-off-by: Logan Johnson <loganj@squareup.com>
2026-08-21 00:34:00 +00:00

305 lines
9.7 KiB
TypeScript

import { expect, test, type Page, bootstrapE2ePage } from "../helpers/test";
import { waitForAnimations } from "../helpers/animations";
import { installMockBridge } from "../helpers/bridge";
const SHOTS = "test-results/persistent-agent-audience";
const OWNER = "deadbeef".repeat(8);
const CHANNEL_ID = "9a1657ac-f7aa-5db0-b632-d8bbeb6dfb50";
const AGENT_A = "a".repeat(64);
const AGENT_B = "b".repeat(64);
const THREAD_ROOT_ID = "mock-general-welcome";
const SCOPE = `${OWNER}:${CHANNEL_ID}:thread:${THREAD_ROOT_ID}`;
async function seedAudience(page: Page, pubkeys: string[], theme = "buzz") {
await page.addInitScript(
({ audience, scope, selectedTheme }) => {
window.localStorage.setItem("buzz:keep-addressed-agents-active", "1");
window.localStorage.setItem(
"buzz:persistent-agent-audiences:v2",
JSON.stringify({ [scope]: audience }),
);
window.localStorage.setItem("buzz-theme", selectedTheme);
},
{ audience: pubkeys, scope: SCOPE, selectedTheme: theme },
);
}
async function openGeneral(page: Page) {
await bootstrapE2ePage(page, `/#/channels/${CHANNEL_ID}`, {
waitUntil: "domcontentloaded",
});
await expect(page.getByTestId("chat-title")).toHaveText("general");
}
async function openThread(page: Page, threadRootId = THREAD_ROOT_ID) {
await bootstrapE2ePage(
page,
`/#/channels/${CHANNEL_ID}?messageId=${threadRootId}&thread=${threadRootId}`,
{ waitUntil: "domcontentloaded" },
);
await expect(page.getByTestId("message-thread-panel")).toBeVisible();
}
async function emitRootMessage(
page: Page,
content: string,
mentionPubkeys: string[],
) {
const event = await page.evaluate(
({ message, pubkeys }) =>
(
window as Window & {
__BUZZ_E2E_EMIT_MOCK_MESSAGE__?: (input: {
channelName: string;
content: string;
mentionPubkeys: string[];
}) => { id: string };
}
).__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({
channelName: "general",
content: message,
mentionPubkeys: pubkeys,
}),
{ message: content, pubkeys: mentionPubkeys },
);
if (!event) throw new Error("Mock message emitter is not installed");
return event;
}
function channelComposer(page: Page) {
return page.getByTestId("channel-composer-overlay");
}
function threadComposer(page: Page) {
return page.getByTestId("thread-composer-overlay");
}
async function installAudienceFixtures(
page: Page,
options: { sendMessageDelayMs?: number } = {},
) {
await installMockBridge(page, {
...options,
managedAgents: [
{
pubkey: AGENT_A,
name: "Morgarita",
status: "running",
channelNames: ["general"],
},
{
pubkey: AGENT_B,
name: "Vogue",
status: "running",
channelNames: ["general"],
},
],
});
}
test("first thread open inherits explicitly addressed agents in authored order", async ({
page,
}) => {
await page.addInitScript(() => {
window.localStorage.setItem("buzz:keep-addressed-agents-active", "1");
});
await installAudienceFixtures(page);
await openGeneral(page);
const root = await emitRootMessage(
page,
"@Vogue please pair with @Morgarita",
// Event tag order deliberately opposes authored mention order.
[AGENT_A, AGENT_B],
);
await openThread(page, root.id);
const input = threadComposer(page).getByTestId("message-input");
await expect(input).toHaveText("@Vogue @Morgarita ");
await expect(input.locator(".agent-mention-highlight")).toHaveCount(2);
await expect
.poll(() =>
page.evaluate(
({ owner, channelId, rootId }) => {
const stored = JSON.parse(
localStorage.getItem("buzz:persistent-agent-audiences:v2") ?? "{}",
);
return stored[`${owner}:${channelId}:thread:${rootId}`] ?? null;
},
{ owner: OWNER, channelId: CHANNEL_ID, rootId: root.id },
),
)
.toEqual([AGENT_B, AGENT_A]);
});
test("persistent agents transition atomically before Enter-send resolves", async ({
page,
}) => {
await seedAudience(page, [AGENT_A]);
await installAudienceFixtures(page, { sendMessageDelayMs: 1_500 });
await openThread(page);
const composer = threadComposer(page);
const input = composer.getByTestId("message-input");
const send = composer.getByTestId("send-message");
await input.fill("@Morgarita hello");
await input.press("Enter");
// The network send is still pending, so this is the first observable
// post-submit editor state rather than the later success hydration pass.
await expect(input).toHaveText("@Morgarita ", { timeout: 500 });
await expect(input.locator(".agent-mention-highlight")).toHaveCount(1, {
timeout: 500,
});
await expect(input).toBeFocused();
await page.waitForTimeout(200);
await expect(composer.getByTestId("mention-autocomplete")).toHaveCount(0);
await expect(send).toBeEnabled();
await expect
.poll(() =>
input.evaluate((element) => {
const selection = window.getSelection();
const viewDesc = (
element as HTMLElement & {
pmViewDesc?: {
posFromDOM: (node: Node, offset: number, bias: number) => number;
size: number;
};
}
).pmViewDesc;
if (!selection?.anchorNode || !viewDesc) return null;
const position = viewDesc.posFromDOM(
selection.anchorNode,
selection.anchorOffset,
1,
);
// The root view desc includes the document's two boundary tokens,
// while posFromDOM is relative to the editable root. Converting both
// to ProseMirror coordinates proves selection.from/to === doc.content.size.
return {
empty: selection.isCollapsed,
text: element.textContent,
endsWithSpace: element.textContent?.endsWith(" ") ?? false,
atDocumentEnd: position + 1 === viewDesc.size - 2,
};
}),
)
.toEqual({
empty: true,
text: "@Morgarita ",
endsWithSpace: true,
atDocumentEnd: true,
});
// Exercise the reported contract, not just its selection prerequisites:
// immediate typing after restoration must remain outside the mention chip.
await input.pressSequentially("testing");
await expect(input).toHaveText("@Morgarita testing");
await expect(input.locator(".agent-mention-highlight")).toHaveCount(1);
});
test("timeline agent send remains one-shot and returns to the placeholder", async ({
page,
}) => {
await seedAudience(page, [AGENT_A]);
await installAudienceFixtures(page, { sendMessageDelayMs: 1_500 });
await openGeneral(page);
const composer = channelComposer(page);
const input = composer.getByTestId("message-input");
await input.fill("@Mor");
await composer
.getByTestId("mention-autocomplete")
.getByText("Morgarita", { exact: true })
.click();
await input.pressSequentially("hello");
await expect(input).toHaveText("@Morgarita hello");
await input.press("Enter");
await expect(input).toHaveText("", { timeout: 500 });
await expect(input.locator("[data-placeholder]").first()).toHaveAttribute(
"data-placeholder",
"Message #general",
{ timeout: 500 },
);
await expect(input).toBeFocused();
await expect
.poll(() =>
input.evaluate((element) => {
const selection = window.getSelection();
return {
collapsed: selection?.isCollapsed ?? false,
inside: Boolean(
selection?.anchorNode && element.contains(selection.anchorNode),
),
};
}),
)
.toEqual({ collapsed: true, inside: true });
});
test("persistent agents restore through the native inline mention UI", async ({
page,
}) => {
await seedAudience(page, [AGENT_B, AGENT_A]);
await installAudienceFixtures(page);
await openThread(page);
const composer = threadComposer(page);
const input = composer.getByTestId("message-input");
await expect(input).toHaveText("@Vogue @Morgarita ");
await expect(page.getByText("Talking to", { exact: true })).toHaveCount(0);
await expect(input.locator(".agent-mention-highlight")).toHaveCount(2);
await input.fill("@Morgarita hello");
await expect
.poll(() =>
page.evaluate(
({ scope }) => {
const stored = JSON.parse(
localStorage.getItem("buzz:persistent-agent-audiences:v2") ?? "{}",
);
return stored[scope] ?? [];
},
{ scope: SCOPE },
),
)
.toEqual([AGENT_A]);
await composer.getByTestId("send-message").click();
await expect(input).toContainText("@Morgarita");
await expect(input).not.toContainText("@Vogue");
await expect(input.locator(".agent-mention-highlight")).toHaveCount(1);
});
for (const theme of ["buzz", "buzz-dark"]) {
test(`captures native persistent mentions in ${theme}`, async ({ page }) => {
await seedAudience(page, [AGENT_A, AGENT_B], theme);
await installAudienceFixtures(page);
await openThread(page);
const overlay = threadComposer(page);
const composer = overlay.getByTestId("message-composer");
await overlay.getByTestId("message-input").focus();
await waitForAnimations(page);
await composer.screenshot({
path: `${SHOTS}/${theme}-native-mentions.png`,
});
});
}
test("native persistent mentions fit the narrow composer", async ({ page }) => {
await page.setViewportSize({ width: 700, height: 760 });
await seedAudience(page, [AGENT_A, AGENT_B]);
await installAudienceFixtures(page);
await openThread(page);
const overlay = threadComposer(page);
const composer = overlay.getByTestId("message-composer");
await expect(overlay.getByTestId("message-input")).toContainText(
"@Morgarita",
);
await waitForAnimations(page);
await composer.screenshot({ path: `${SHOTS}/narrow-native-mentions.png` });
});