mirror of
https://github.com/block/buzz.git
synced 2026-11-05 00:41:28 +00:00
Allow task reassignment and color review results
Signed-off-by: John Tennant <jtennant@squareup.com>
This commit is contained in:
@@ -21,16 +21,16 @@ test("assignment preserves unrelated frontmatter, comments and body", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("retry retains operation and refuses another assignee", () => {
|
||||
test("retry retains operation and reassignment replaces notification state", () => {
|
||||
const pending = assignmentCanvas(canvas, pubkey, "operation", "pending");
|
||||
assert.equal(
|
||||
taskAssignee(assignmentCanvas(pending, pubkey, "operation", "sent"))
|
||||
.notification.status,
|
||||
"sent",
|
||||
);
|
||||
assert.throws(
|
||||
() => assignmentCanvas(pending, "b".repeat(64), "new", "pending"),
|
||||
/already assigned/,
|
||||
assert.deepEqual(
|
||||
taskAssignee(assignmentCanvas(pending, "b".repeat(64), "new", "pending")),
|
||||
{ pubkey: "b".repeat(64), notification: { id: "new", status: "pending" } },
|
||||
);
|
||||
assert.throws(
|
||||
() => assignmentCanvas(canvas, "bad", "new", "pending"),
|
||||
|
||||
@@ -32,8 +32,6 @@ export function assignmentCanvas(
|
||||
): string {
|
||||
if (!/^[a-f0-9]{64}$/i.test(pubkey)) throw new Error("Invalid agent pubkey");
|
||||
const existing = taskAssignee(content);
|
||||
if (existing && existing.pubkey !== pubkey)
|
||||
throw new Error("Task is already assigned");
|
||||
const { doc, body } = document(content);
|
||||
if (!existing) doc.set("assignee", doc.createNode({}));
|
||||
doc.setIn(["assignee", "pubkey"], pubkey);
|
||||
|
||||
@@ -38,7 +38,11 @@ export function AssignTask({
|
||||
setPending(true);
|
||||
setError("");
|
||||
try {
|
||||
const operationId = assignee?.notification?.id ?? crypto.randomUUID();
|
||||
const operationId =
|
||||
assignee?.pubkey === pubkey &&
|
||||
assignee.notification?.status === "pending"
|
||||
? assignee.notification.id
|
||||
: crypto.randomUUID();
|
||||
const result = await invokeTauri<{
|
||||
assigned: boolean;
|
||||
notified: boolean;
|
||||
@@ -72,13 +76,20 @@ export function AssignTask({
|
||||
|
||||
return (
|
||||
<div className="ml-auto flex items-center gap-2 text-xs">
|
||||
{assignee ? (
|
||||
{assignee && !choosing ? (
|
||||
<>
|
||||
<span title={assignee.pubkey}>
|
||||
<button
|
||||
type="button"
|
||||
title="Change assignee"
|
||||
aria-label="Change assignee"
|
||||
disabled={pending}
|
||||
onClick={() => setChoosing(true)}
|
||||
className="rounded px-2 py-1 hover:bg-accent disabled:opacity-50"
|
||||
>
|
||||
Assigned to{" "}
|
||||
{agents.data?.find((agent) => agent.pubkey === assignee.pubkey)
|
||||
?.name ?? "agent"}
|
||||
</span>
|
||||
</button>
|
||||
{assignee.notification?.status === "pending" && (
|
||||
<button
|
||||
type="button"
|
||||
@@ -92,6 +103,11 @@ export function AssignTask({
|
||||
</>
|
||||
) : choosing ? (
|
||||
<>
|
||||
{assignee && (
|
||||
<span className="text-muted-foreground">
|
||||
Reassigning does not stop the previous agent.
|
||||
</span>
|
||||
)}
|
||||
<select
|
||||
aria-label="Assign task to agent"
|
||||
disabled={pending}
|
||||
@@ -103,7 +119,11 @@ export function AssignTask({
|
||||
{agents.isLoading ? "Loading agents…" : "Choose agent…"}
|
||||
</option>
|
||||
{agents.data?.map((agent) => (
|
||||
<option key={agent.pubkey} value={agent.pubkey}>
|
||||
<option
|
||||
key={agent.pubkey}
|
||||
value={agent.pubkey}
|
||||
disabled={agent.pubkey === assignee?.pubkey}
|
||||
>
|
||||
{agent.name}
|
||||
</option>
|
||||
))}
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import { useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { ClipboardCheck } from "lucide-react";
|
||||
import {
|
||||
CircleCheck,
|
||||
CircleHelp,
|
||||
CircleX,
|
||||
ClipboardCheck,
|
||||
Clock3,
|
||||
TriangleAlert,
|
||||
} from "lucide-react";
|
||||
import type { ChannelBackedTask } from "../lib/channelBackedTask";
|
||||
import { useTaskBranchStatus } from "../useTaskBranchStatus";
|
||||
import { invokeTauri } from "@/shared/api/tauri";
|
||||
@@ -80,6 +87,29 @@ export function TaskReviewModal({
|
||||
<p>No attributable local runs found for this branch.</p>
|
||||
)}
|
||||
{[...latest].map(([name, { run, review }]) => {
|
||||
const count =
|
||||
review.finding_count ?? review.verdict?.findings?.length;
|
||||
const failed = review.invocation_ok === false;
|
||||
const clear = review.invocation_ok === true && count === 0;
|
||||
const ResultIcon = failed
|
||||
? CircleX
|
||||
: count && count > 0
|
||||
? TriangleAlert
|
||||
: clear
|
||||
? CircleCheck
|
||||
: CircleHelp;
|
||||
const resultColor = failed
|
||||
? "text-red-600 dark:text-red-400"
|
||||
: count && count > 0
|
||||
? "text-amber-600 dark:text-amber-400"
|
||||
: clear
|
||||
? "text-green-600 dark:text-green-400"
|
||||
: "text-muted-foreground";
|
||||
const resultLabel = failed
|
||||
? "Run failed"
|
||||
: count !== undefined
|
||||
? `${count} ${count === 1 ? "finding" : "findings"}`
|
||||
: "Result unknown";
|
||||
const freshness =
|
||||
name === "pr-template"
|
||||
? typeof run.pr_metadata?.title !== "string" ||
|
||||
@@ -98,15 +128,16 @@ export function TaskReviewModal({
|
||||
return (
|
||||
<details key={name} className="rounded-lg border p-3 text-sm">
|
||||
<summary className="cursor-pointer">
|
||||
<ResultIcon
|
||||
aria-hidden="true"
|
||||
data-testid={`review-result-${name}`}
|
||||
className={`mr-2 inline-block size-4 align-text-bottom ${resultColor}`}
|
||||
/>
|
||||
<span className="font-medium">{name}</span>
|
||||
<span className="ml-2 text-muted-foreground">
|
||||
{review.invocation_ok === false
|
||||
? "Run failed"
|
||||
: review.finding_count !== undefined ||
|
||||
review.verdict?.findings
|
||||
? `${review.finding_count ?? review.verdict?.findings?.length} findings`
|
||||
: "Result unknown"}{" "}
|
||||
· {freshness}
|
||||
<span className={`ml-2 ${resultColor}`}>{resultLabel}</span>
|
||||
<span className="mt-1 flex items-center gap-1 pl-6 text-xs text-muted-foreground">
|
||||
<Clock3 aria-hidden="true" className="size-3" />
|
||||
{freshness}
|
||||
</span>
|
||||
</summary>
|
||||
<div className="mt-3 space-y-2">
|
||||
|
||||
@@ -87,7 +87,19 @@ test("review modal shows latest intent results and conservative freshness", asyn
|
||||
await expect(dialog).toBeVisible();
|
||||
await expect(
|
||||
dialog.locator("summary").filter({ hasText: "builderbot" }),
|
||||
).toHaveText("builderbot1 findings · Same commit");
|
||||
).toContainText("1 finding");
|
||||
await expect(
|
||||
dialog.locator("summary").filter({ hasText: "builderbot" }),
|
||||
).toContainText("Same commit");
|
||||
await expect(dialog.getByTestId("review-result-builderbot")).toHaveClass(
|
||||
/text-amber-600/,
|
||||
);
|
||||
await expect(dialog.getByTestId("review-result-pr-template")).toHaveClass(
|
||||
/text-green-600/,
|
||||
);
|
||||
await expect(dialog.getByTestId("review-result-dead-code")).toHaveClass(
|
||||
/text-red-600/,
|
||||
);
|
||||
await expect(
|
||||
dialog.locator("summary").filter({ hasText: "pr-template" }),
|
||||
).toContainText("PR metadata changed");
|
||||
@@ -120,7 +132,10 @@ test("assign saves the task and retains notification retry after navigating away
|
||||
}) => {
|
||||
await installMockBridge(page, {
|
||||
canvasContent: TASK_CANVAS,
|
||||
managedAgents: [{ pubkey: "a".repeat(64), name: "Sol", status: "stopped" }],
|
||||
managedAgents: [
|
||||
{ pubkey: "a".repeat(64), name: "Sol", status: "stopped" },
|
||||
{ pubkey: "b".repeat(64), name: "Luna", status: "stopped" },
|
||||
],
|
||||
});
|
||||
await page.goto("/");
|
||||
await expect(page.getByTestId("channel-engineering")).toBeVisible();
|
||||
@@ -134,16 +149,19 @@ test("assign saves the task and retains notification retry after navigating away
|
||||
) => Promise<unknown>;
|
||||
};
|
||||
__ASSIGN_CALLS__: number;
|
||||
__ASSIGN_OPERATIONS__: string[];
|
||||
};
|
||||
const original = w.__TAURI_INTERNALS__.invoke.bind(w.__TAURI_INTERNALS__);
|
||||
let content: string | undefined;
|
||||
let channel: string | undefined;
|
||||
w.__ASSIGN_CALLS__ = 0;
|
||||
w.__ASSIGN_OPERATIONS__ = [];
|
||||
w.__TAURI_INTERNALS__.invoke = async (command, payload, options) => {
|
||||
if (command === "get_canvas" && content && payload.channelId === channel)
|
||||
return { content };
|
||||
if (command === "assign_task_channel") {
|
||||
w.__ASSIGN_CALLS__++;
|
||||
w.__ASSIGN_OPERATIONS__.push(payload.operationId);
|
||||
channel = payload.channelId;
|
||||
content =
|
||||
w.__ASSIGN_CALLS__ === 1
|
||||
@@ -185,6 +203,20 @@ test("assign saves the task and retains notification retry after navigating away
|
||||
.__ASSIGN_CALLS__,
|
||||
),
|
||||
).toBe(2);
|
||||
await page.getByRole("button", { name: "Change assignee" }).click();
|
||||
await page
|
||||
.getByRole("combobox", { name: "Assign task to agent" })
|
||||
.selectOption("b".repeat(64));
|
||||
await expect(page.getByTestId("task-overview")).toContainText(
|
||||
"Assigned to Luna",
|
||||
);
|
||||
const operations = await page.evaluate(
|
||||
() =>
|
||||
(window as typeof window & { __ASSIGN_OPERATIONS__: string[] })
|
||||
.__ASSIGN_OPERATIONS__,
|
||||
);
|
||||
expect(operations[0]).toBe(operations[1]);
|
||||
expect(operations[2]).not.toBe(operations[0]);
|
||||
});
|
||||
|
||||
test("loading a corrected canvas refreshes the sidebar task metadata", async ({
|
||||
|
||||
Reference in New Issue
Block a user