Files
buzz/desktop/tests/e2e/project-issue-comments.spec.ts
thomaspblock de188ebf06 feat(projects): add agent and CLI project-home support (#6590)
## Summary
- inject bounded project-home identity and repository context into
managed agent sessions
- add project-aware CLI flows for creating projects, repositories,
issues, and related channels
- match project homes through the existing relay query surface, then
filter channel metadata client-side

This is Part 1 of the channel-first Projects stack. Part 2 contains
project creation and model foundations.

## Testing
- `cargo fmt --all -- --check`
- `cargo clippy -p buzz-cli -p buzz-acp --all-targets -- -D warnings`
- `cargo test -p buzz-cli -p buzz-acp` — 1,184 tests passed, 1 doc test
ignored
- full pre-push gate passed

## Post-Deploy Monitoring & Validation
- validate project-home agent context and project-aware CLI commands
against a staging relay
- healthy signals: project context matches the active channel, explicit
repo coordinates remain stable, and normal channels receive no project
block
- failure signals: cross-channel project context, duplicate project
creation, or commands targeting an unrelated repository; mitigate by
reverting this PR

---------

Signed-off-by: Thomas Petersen <thomasp@squareup.com>
Signed-off-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz>
Co-authored-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz>
2026-08-26 12:09:06 -07:00

251 lines
9.1 KiB
TypeScript

import { expect, test } from "@playwright/test";
import { installMockBridge } from "../helpers/bridge";
const ISSUE_COMMENTS = [
"First issue comment",
"Second issue comment",
"Third issue comment",
"Fourth issue comment",
];
const DEFAULT_MOCK_PUBKEY = "deadbeef".repeat(8);
async function openBuzzProject(page: import("@playwright/test").Page) {
await page.goto("/", { waitUntil: "domcontentloaded" });
await page.getByTestId("open-projects-view").click();
await page.getByTestId("projects-section-projects").click();
const projectEntry = page
.locator(
'[data-testid="project-card-buzz"], [data-testid="project-row-buzz"]',
)
.first();
await expect(projectEntry).toBeVisible({ timeout: 10_000 });
await projectEntry.click();
await page.getByTestId("project-home-context-repo-buzz").click();
}
test("issue detail can open agent chat or seed a channel question", async ({
page,
}) => {
await installMockBridge(page);
await openBuzzProject(page);
await page.getByRole("tab", { name: "Tasks", exact: true }).click();
const issueRow = page.getByTestId("project-issue-row").first();
await expect(issueRow).toBeVisible({ timeout: 10_000 });
await issueRow.getByRole("button", { name: /^#/ }).click();
const communication = page.getByTestId(
"project-context-communication-actions",
);
await expect(communication).toBeVisible();
const contextPanel = page.getByTestId("project-repository-actions-panel");
await expect(
contextPanel.getByRole("heading", { name: "Actions", exact: true }),
).toHaveCount(0);
await expect(
contextPanel.getByRole("heading", { name: "Details", exact: true }),
).toBeVisible();
await expect(
contextPanel.getByRole("heading", { name: "Assignment", exact: true }),
).toHaveCount(0);
await expect(
contextPanel.getByRole("heading", { name: "Discussion", exact: true }),
).toHaveCount(0);
await expect(
contextPanel.getByTestId("project-repository-people"),
).toHaveCount(0);
await page.getByTestId("project-context-chat-agent").click();
await expect(page.getByTestId("project-agent-chat-panel")).toBeVisible();
await expect(page.getByTestId("projects-agent-selection-item")).toHaveCount(
1,
);
await page.getByRole("button", { name: "Close agent chat" }).click();
await expect(
page.getByTestId("project-right-panel-repository-tab"),
).toHaveAttribute("aria-pressed", "false");
await page.getByTestId("project-right-panel-repository-tab").click();
await page.getByTestId("project-context-discuss").click();
await expect(
page.getByTestId("project-context-channel-choices"),
).toBeVisible();
await page.getByTestId("project-context-related-channel").first().click();
await expect(page.getByTestId("message-input")).toContainText(
"Let's talk about this task:",
);
});
test("issue discussion ignores an author-claimed origin channel", async ({
page,
}) => {
const forgedIssueId = "f".repeat(64);
await page.addInitScript(
({ issueId, owner }) => {
window.__BUZZ_E2E_EXTRA_PROJECT_EVENTS__ = [
{
id: issueId,
kind: 1621,
pubkey: owner,
created_at: Math.floor(Date.now() / 1000) + 10,
content: "This task claims an unrelated visible channel.",
tags: [
["a", `30617:${owner}:buzz`],
["subject", "Forged origin task"],
["h", "9dae0116-799b-5071-a0a8-fdd30a91a35d"],
],
},
];
},
{ issueId: forgedIssueId, owner: DEFAULT_MOCK_PUBKEY },
);
await installMockBridge(page);
await openBuzzProject(page);
await page.getByRole("tab", { name: "Tasks", exact: true }).click();
const issueRow = page
.getByTestId("project-issue-row")
.filter({ hasText: "Forged origin task" });
await expect(issueRow).toBeVisible();
await issueRow.getByRole("button", { name: /^#/ }).click();
await page.getByTestId("project-context-discuss").click();
const channelChoices = page.getByTestId("project-context-channel-choices");
const relatedChannel = channelChoices.getByTestId(
"project-context-related-channel",
);
await expect(relatedChannel).toHaveCount(1);
await expect(relatedChannel).toContainText("#buzz");
await expect(channelChoices).not.toContainText("#random");
await relatedChannel.click();
await expect(page.getByTestId("chat-title")).toHaveText("buzz");
const issueDraftChip = page
.getByTestId("message-input")
.locator('[data-composer-buzz-link=""]', {
hasText: "buzz",
});
await expect(issueDraftChip).toHaveAttribute(
"data-href",
new RegExp(`id=${forgedIssueId}`),
);
await page.getByTestId("channel-random").click();
await expect(
page.getByTestId("message-input").locator('[data-composer-buzz-link=""]'),
).toHaveCount(0);
});
test("issue comments use the project activity timeline", async ({ page }) => {
await installMockBridge(page);
await openBuzzProject(page);
await page.getByRole("tab", { name: "Tasks", exact: true }).click();
const issueRow = page.getByTestId("project-issue-row").first();
await expect(issueRow).toBeVisible({ timeout: 10_000 });
await issueRow.getByRole("button", { name: /^#/ }).click();
const composer = page.getByTestId("project-issue-comment-composer");
await expect(composer).toBeVisible();
for (const comment of ISSUE_COMMENTS) {
await composer.locator('[contenteditable="true"]').fill(comment);
await composer.getByRole("button", { name: "Send message" }).click();
await expect(page.getByText(comment, { exact: true })).toBeVisible({
timeout: 10_000,
});
}
const timelineRows = page.getByTestId("project-issue-comment-timeline-row");
const earlierComments = page.getByTestId("project-issue-earlier-comments");
const historyToggle = page.getByTestId(
"project-issue-comment-history-toggle",
);
await expect(timelineRows).toHaveCount(3);
await expect(earlierComments).toContainText("Show 1 earlier comment");
await expect(
timelineRows.filter({ hasText: "First issue comment" }),
).toHaveCount(0);
await expect(
timelineRows.filter({ hasText: "Fourth issue comment" }),
).toHaveCount(1);
await earlierComments.click();
await expect(timelineRows).toHaveCount(4);
for (const comment of ISSUE_COMMENTS) {
await expect(timelineRows.filter({ hasText: comment })).toHaveCount(1);
}
await historyToggle.click();
await expect(timelineRows).toHaveCount(0);
await expect(historyToggle).toContainText("Show 4 earlier comments");
await historyToggle.click();
await expect(timelineRows).toHaveCount(4);
});
test("issue assignees can be assigned and unassigned", async ({ page }) => {
await installMockBridge(page);
await openBuzzProject(page);
await page.getByRole("tab", { name: "Tasks", exact: true }).click();
const issueRow = page.getByTestId("project-issue-row").first();
await expect(issueRow).toBeVisible({ timeout: 10_000 });
await issueRow.getByRole("button", { name: /^#/ }).click();
const issueHeader = page
.getByTestId("project-issue-detail")
.locator("header")
.first();
await expect(issueHeader).toContainText("Task created");
await expect(issueHeader).not.toContainText("alice");
await expect(issueHeader.locator("img")).toHaveCount(0);
const contextAssignment = page.getByTestId("project-context-task-assignment");
await expect(contextAssignment).toBeVisible();
await expect(contextAssignment.locator("img")).toHaveCount(0);
const selfAssign = page.getByTestId("project-context-issue-self-assign");
await expect(selfAssign).toBeVisible();
await expect(page.getByTestId("project-context-issue-assign")).toBeVisible();
const createTask = page
.getByTestId("project-repository-actions-panel")
.getByRole("button", { name: "Create task", exact: true });
await expect(createTask).toBeVisible();
await expect(selfAssign.locator("svg")).toHaveCount(1);
const actionGeometry = await Promise.all(
[selfAssign, createTask].map((action) =>
action.evaluate((element) => {
const bounds = element.getBoundingClientRect();
return {
height: bounds.height,
left: bounds.left,
width: bounds.width,
};
}),
),
);
expect(actionGeometry[0]).toEqual(actionGeometry[1]);
await selfAssign.click();
await expect(contextAssignment).toContainText("Assigned to me");
await expect(page.getByTestId("project-detail-section").first()).toHaveCSS(
"border-top-width",
"0px",
);
await page.getByTestId("project-issue-assign").click();
const candidate = page
.locator('[data-testid^="project-assignee-result-"]')
.first();
await expect(candidate).toBeVisible();
const candidateTestId = await candidate.getAttribute("data-testid");
const assignee = candidateTestId?.replace("project-assignee-result-", "");
if (!assignee) throw new Error("Assignee result is missing its pubkey.");
expect(assignee).toMatch(/^[0-9a-f]{64}$/);
await candidate.click();
const unassign = page.getByTestId(`project-issue-unassign-${assignee}`);
await expect(unassign).toBeVisible({ timeout: 10_000 });
await unassign.click();
await expect(page.getByText("Task unassigned.")).toBeVisible();
await expect(unassign).toHaveCount(0, { timeout: 10_000 });
});