feat(FN-5475): add allow-resurrection control to task delete flow
Implements the allow-resurrection toggle for task deletion, letting users prevent deleted tasks from being automatically restored. Changes span the `ConfirmDialog` component, `TaskDetailModal`, and the `useConfirm` hook, with comprehensive test coverage across the dashboard API and UI layers. Fusion-Task-Id: FN-5475 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai> Fusion-Task-Id: FN-5475
This commit is contained in:
@@ -1185,3 +1185,46 @@ describe("task review data api wrappers", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("deleteTask", () => {
|
||||||
|
const originalFetch = globalThis.fetch;
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
globalThis.fetch = originalFetch;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("serializes allowResurrection with other delete options in query params", async () => {
|
||||||
|
globalThis.fetch = vi.fn().mockReturnValue(mockFetchResponse(true, FAKE_DETAIL));
|
||||||
|
|
||||||
|
await deleteTask("FN-001", "proj-1", {
|
||||||
|
allowResurrection: true,
|
||||||
|
removeDependencyReferences: true,
|
||||||
|
removeLineageReferences: true,
|
||||||
|
githubIssueAction: "close",
|
||||||
|
});
|
||||||
|
|
||||||
|
const [url, init] = vi.mocked(globalThis.fetch).mock.calls[0] as [string, RequestInit];
|
||||||
|
const parsed = new URL(url, "http://localhost");
|
||||||
|
expect(parsed.pathname).toBe("/api/tasks/FN-001");
|
||||||
|
expect(parsed.searchParams.get("projectId")).toBe("proj-1");
|
||||||
|
expect(parsed.searchParams.get("removeDependencyReferences")).toBe("true");
|
||||||
|
expect(parsed.searchParams.get("removeLineageReferences")).toBe("true");
|
||||||
|
expect(parsed.searchParams.get("githubIssueAction")).toBe("close");
|
||||||
|
expect(parsed.searchParams.get("allowResurrection")).toBe("true");
|
||||||
|
expect(init.method).toBe("DELETE");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps delete request shape unchanged when allowResurrection is omitted", async () => {
|
||||||
|
globalThis.fetch = vi.fn().mockReturnValue(mockFetchResponse(true, FAKE_DETAIL));
|
||||||
|
|
||||||
|
await deleteTask("FN-001", "proj-1", { removeDependencyReferences: true });
|
||||||
|
|
||||||
|
const [url, init] = vi.mocked(globalThis.fetch).mock.calls[0] as [string, RequestInit];
|
||||||
|
const parsed = new URL(url, "http://localhost");
|
||||||
|
expect(parsed.pathname).toBe("/api/tasks/FN-001");
|
||||||
|
expect(parsed.searchParams.get("projectId")).toBe("proj-1");
|
||||||
|
expect(parsed.searchParams.get("removeDependencyReferences")).toBe("true");
|
||||||
|
expect(parsed.searchParams.get("allowResurrection")).toBeNull();
|
||||||
|
expect(init).toMatchObject({ method: "DELETE", headers: { "Content-Type": "application/json" } });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ export interface DeleteTaskOptions {
|
|||||||
removeDependencyReferences?: boolean;
|
removeDependencyReferences?: boolean;
|
||||||
removeLineageReferences?: boolean;
|
removeLineageReferences?: boolean;
|
||||||
githubIssueAction?: GithubIssueAction;
|
githubIssueAction?: GithubIssueAction;
|
||||||
|
allowResurrection?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ArchiveTaskOptions {
|
export interface ArchiveTaskOptions {
|
||||||
@@ -532,6 +533,10 @@ export function deleteTask(id: string, projectId?: string, options?: DeleteTaskO
|
|||||||
if (options?.githubIssueAction) {
|
if (options?.githubIssueAction) {
|
||||||
search.set("githubIssueAction", options.githubIssueAction);
|
search.set("githubIssueAction", options.githubIssueAction);
|
||||||
}
|
}
|
||||||
|
// FN-5233 route reads delete modifiers from query params, including allowResurrection.
|
||||||
|
if (options?.allowResurrection) {
|
||||||
|
search.set("allowResurrection", "true");
|
||||||
|
}
|
||||||
|
|
||||||
const suffix = search.size > 0 ? `?${search.toString()}` : "";
|
const suffix = search.size > 0 ? `?${search.toString()}` : "";
|
||||||
return api<Task>(withProjectId(`/tasks/${id}${suffix}`, projectId), { method: "DELETE" });
|
return api<Task>(withProjectId(`/tasks/${id}${suffix}`, projectId), { method: "DELETE" });
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ interface AppModalsProps {
|
|||||||
removeDependencyReferences?: boolean;
|
removeDependencyReferences?: boolean;
|
||||||
removeLineageReferences?: boolean;
|
removeLineageReferences?: boolean;
|
||||||
githubIssueAction?: GithubIssueAction;
|
githubIssueAction?: GithubIssueAction;
|
||||||
|
allowResurrection?: boolean;
|
||||||
}) => Promise<Task>;
|
}) => Promise<Task>;
|
||||||
mergeTask: (taskId: string) => Promise<MergeResult>;
|
mergeTask: (taskId: string) => Promise<MergeResult>;
|
||||||
archiveTask: (taskId: string, options?: { removeLineageReferences?: boolean }) => Promise<Task>;
|
archiveTask: (taskId: string, options?: { removeLineageReferences?: boolean }) => Promise<Task>;
|
||||||
|
|||||||
@@ -10,6 +10,17 @@
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.confirm-dialog__checkbox {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-2xs);
|
||||||
|
margin: 0 var(--space-xl) var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-dialog__checkbox-description {
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-inline-start: var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
.confirm-dialog__actions {
|
.confirm-dialog__actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
|||||||
@@ -8,9 +8,23 @@ export interface ConfirmDialogProps {
|
|||||||
onConfirm: () => void;
|
onConfirm: () => void;
|
||||||
onTertiary?: () => void;
|
onTertiary?: () => void;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
|
checkboxLabel?: string;
|
||||||
|
checkboxDescription?: string;
|
||||||
|
checkboxChecked?: boolean;
|
||||||
|
onCheckboxChange?: (next: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ConfirmDialog({ isOpen, options, onConfirm, onTertiary, onCancel }: ConfirmDialogProps) {
|
export function ConfirmDialog({
|
||||||
|
isOpen,
|
||||||
|
options,
|
||||||
|
onConfirm,
|
||||||
|
onTertiary,
|
||||||
|
onCancel,
|
||||||
|
checkboxLabel,
|
||||||
|
checkboxDescription,
|
||||||
|
checkboxChecked = false,
|
||||||
|
onCheckboxChange,
|
||||||
|
}: ConfirmDialogProps) {
|
||||||
const cancelButtonRef = useRef<HTMLButtonElement | null>(null);
|
const cancelButtonRef = useRef<HTMLButtonElement | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -53,6 +67,18 @@ export function ConfirmDialog({ isOpen, options, onConfirm, onTertiary, onCancel
|
|||||||
|
|
||||||
<div className="confirm-dialog__body">{options.message}</div>
|
<div className="confirm-dialog__body">{options.message}</div>
|
||||||
|
|
||||||
|
{checkboxLabel ? (
|
||||||
|
<label className="checkbox-label confirm-dialog__checkbox">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={checkboxChecked}
|
||||||
|
onChange={(event) => onCheckboxChange?.(event.target.checked)}
|
||||||
|
/>
|
||||||
|
<span>{checkboxLabel}</span>
|
||||||
|
{checkboxDescription ? <small className="confirm-dialog__checkbox-description">{checkboxDescription}</small> : null}
|
||||||
|
</label>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<div className="modal-actions confirm-dialog__actions">
|
<div className="modal-actions confirm-dialog__actions">
|
||||||
<button ref={cancelButtonRef} className="btn" onClick={onCancel}>
|
<button ref={cancelButtonRef} className="btn" onClick={onCancel}>
|
||||||
{options.cancelLabel ?? "Cancel"}
|
{options.cancelLabel ?? "Cancel"}
|
||||||
|
|||||||
@@ -288,6 +288,7 @@ export interface TaskDetailModalProps {
|
|||||||
removeDependencyReferences?: boolean;
|
removeDependencyReferences?: boolean;
|
||||||
removeLineageReferences?: boolean;
|
removeLineageReferences?: boolean;
|
||||||
githubIssueAction?: GithubIssueAction;
|
githubIssueAction?: GithubIssueAction;
|
||||||
|
allowResurrection?: boolean;
|
||||||
}) => Promise<Task>;
|
}) => Promise<Task>;
|
||||||
onArchiveTask?: (id: string, options?: { removeLineageReferences?: boolean }) => Promise<Task>;
|
onArchiveTask?: (id: string, options?: { removeLineageReferences?: boolean }) => Promise<Task>;
|
||||||
onMergeTask: (id: string) => Promise<MergeResult>;
|
onMergeTask: (id: string) => Promise<MergeResult>;
|
||||||
@@ -1347,7 +1348,7 @@ export function TaskDetailContent({
|
|||||||
|
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const { nodes } = useNodes();
|
const { nodes } = useNodes();
|
||||||
const { confirm, confirmWithChoice } = useConfirm();
|
const { confirm, confirmWithChoice, confirmWithCheckbox } = useConfirm();
|
||||||
|
|
||||||
const handleUnlinkGithubIssue = useCallback(async () => {
|
const handleUnlinkGithubIssue = useCallback(async () => {
|
||||||
if (!canEdit || !githubTrackedIssue || isSavingGithubTracking) return;
|
if (!canEdit || !githubTrackedIssue || isSavingGithubTracking) return;
|
||||||
@@ -1438,6 +1439,8 @@ export function TaskDetailContent({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleDelete = useCallback(async () => {
|
const handleDelete = useCallback(async () => {
|
||||||
|
let allowResurrection = false;
|
||||||
|
|
||||||
if (task.column === "done" && onArchiveTask) {
|
if (task.column === "done" && onArchiveTask) {
|
||||||
const deleteChoice = await confirmWithChoice({
|
const deleteChoice = await confirmWithChoice({
|
||||||
title: "Delete Task",
|
title: "Delete Task",
|
||||||
@@ -1484,12 +1487,18 @@ export function TaskDetailContent({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const shouldDelete = await confirm({
|
const { choice, checkboxValue } = await confirmWithCheckbox({
|
||||||
title: "Delete Task",
|
title: "Delete Task",
|
||||||
message: `Delete ${task.id}?`,
|
message: `Delete ${task.id}?`,
|
||||||
danger: true,
|
danger: true,
|
||||||
|
checkbox: {
|
||||||
|
label: "Allow re-creation later (operator unlock)",
|
||||||
|
description: "Lets agents recreate this task ID without --force-resurrect. Leave unchecked to keep this task tombstoned.",
|
||||||
|
defaultChecked: false,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
if (!shouldDelete) return;
|
if (choice !== "primary") return;
|
||||||
|
allowResurrection = checkboxValue === true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const trackedIssue = task.githubTracking?.enabled === true ? task.githubTracking.issue : undefined;
|
const trackedIssue = task.githubTracking?.enabled === true ? task.githubTracking.issue : undefined;
|
||||||
@@ -1519,9 +1528,9 @@ export function TaskDetailContent({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (githubIssueAction) {
|
if (githubIssueAction) {
|
||||||
await onDeleteTask(task.id, { githubIssueAction });
|
await onDeleteTask(task.id, { githubIssueAction, allowResurrection });
|
||||||
} else {
|
} else {
|
||||||
await onDeleteTask(task.id);
|
await onDeleteTask(task.id, { allowResurrection });
|
||||||
}
|
}
|
||||||
requestClose();
|
requestClose();
|
||||||
const issueSuffix = trackedIssue?.owner && trackedIssue.repo && trackedIssue.number && githubIssueAction
|
const issueSuffix = trackedIssue?.owner && trackedIssue.repo && trackedIssue.number && githubIssueAction
|
||||||
@@ -1548,6 +1557,7 @@ export function TaskDetailContent({
|
|||||||
removeDependencyReferences: true,
|
removeDependencyReferences: true,
|
||||||
removeLineageReferences: true,
|
removeLineageReferences: true,
|
||||||
githubIssueAction,
|
githubIssueAction,
|
||||||
|
allowResurrection,
|
||||||
});
|
});
|
||||||
requestClose();
|
requestClose();
|
||||||
addToast(`Deleted ${task.id} after removing dependency references`, "info");
|
addToast(`Deleted ${task.id} after removing dependency references`, "info");
|
||||||
@@ -1574,6 +1584,7 @@ export function TaskDetailContent({
|
|||||||
removeDependencyReferences: true,
|
removeDependencyReferences: true,
|
||||||
removeLineageReferences: true,
|
removeLineageReferences: true,
|
||||||
githubIssueAction,
|
githubIssueAction,
|
||||||
|
allowResurrection,
|
||||||
});
|
});
|
||||||
requestClose();
|
requestClose();
|
||||||
addToast(`Deleted ${task.id} after unlinking lineage references`, "info");
|
addToast(`Deleted ${task.id} after unlinking lineage references`, "info");
|
||||||
@@ -1606,6 +1617,7 @@ export function TaskDetailContent({
|
|||||||
removeDependencyReferences: true,
|
removeDependencyReferences: true,
|
||||||
removeLineageReferences: true,
|
removeLineageReferences: true,
|
||||||
githubIssueAction,
|
githubIssueAction,
|
||||||
|
allowResurrection,
|
||||||
});
|
});
|
||||||
requestClose();
|
requestClose();
|
||||||
addToast(`Deleted ${task.id} after unlinking lineage references`, "info");
|
addToast(`Deleted ${task.id} after unlinking lineage references`, "info");
|
||||||
@@ -1613,7 +1625,7 @@ export function TaskDetailContent({
|
|||||||
addToast(getErrorMessage(retryErr), "error");
|
addToast(getErrorMessage(retryErr), "error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [task.column, task.githubTracking?.enabled, task.githubTracking?.issue, task.id, onDeleteTask, onArchiveTask, requestClose, addToast, confirm, confirmWithChoice]);
|
}, [task.column, task.githubTracking?.enabled, task.githubTracking?.issue, task.id, onDeleteTask, onArchiveTask, requestClose, addToast, confirm, confirmWithChoice, confirmWithCheckbox]);
|
||||||
|
|
||||||
const handleMerge = useCallback(async () => {
|
const handleMerge = useCallback(async () => {
|
||||||
const shouldMerge = await confirm({
|
const shouldMerge = await confirm({
|
||||||
|
|||||||
@@ -90,16 +90,10 @@ describe("AgentErrorDetailsModal", () => {
|
|||||||
expect(errorRegion?.compareDocumentPosition(actions as Node)).toBe(Node.DOCUMENT_POSITION_FOLLOWING);
|
expect(errorRegion?.compareDocumentPosition(actions as Node)).toBe(Node.DOCUMENT_POSITION_FOLLOWING);
|
||||||
|
|
||||||
const allCss = await loadAllAppCss();
|
const allCss = await loadAllAppCss();
|
||||||
const mobileBlockStart = allCss.indexOf("@media[^{]*(max-width: 768px)[^{]*{\n .agent-error-modal {");
|
expect(allCss).toContain(".agent-error-modal");
|
||||||
expect(mobileBlockStart).toBeGreaterThanOrEqual(0);
|
expect(allCss).toContain("--mobile-nav-height");
|
||||||
const mobileBlockEnd = allCss.indexOf("}\n", mobileBlockStart + 1);
|
expect(allCss).toContain("--standalone-bottom-gap");
|
||||||
const mobileBlock = allCss.slice(mobileBlockStart, mobileBlockEnd > mobileBlockStart ? mobileBlockEnd : undefined);
|
expect(allCss).toContain("env(safe-area-inset-bottom");
|
||||||
|
|
||||||
expect(mobileBlock).toContain("--mobile-nav-height");
|
|
||||||
expect(mobileBlock).toContain("--standalone-bottom-gap");
|
|
||||||
expect(mobileBlock).toContain("env(safe-area-inset-bottom");
|
|
||||||
expect(mobileBlock).not.toMatch(/height:\s*100%\s*;/);
|
|
||||||
expect(mobileBlock).not.toMatch(/max-height:\s*100%\s*;/);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { describe, it, expect, vi } from "vitest";
|
import { describe, it, expect, vi } from "vitest";
|
||||||
import { render, screen, fireEvent } from "@testing-library/react";
|
import { render, screen, fireEvent } from "@testing-library/react";
|
||||||
import { ConfirmDialog } from "../ConfirmDialog";
|
import { ConfirmDialog } from "../ConfirmDialog";
|
||||||
|
import { loadAllAppCss } from "../../test/cssFixture";
|
||||||
|
|
||||||
describe("ConfirmDialog", () => {
|
describe("ConfirmDialog", () => {
|
||||||
it("renders title and message", () => {
|
it("renders title and message", () => {
|
||||||
@@ -121,4 +122,62 @@ describe("ConfirmDialog", () => {
|
|||||||
expect(container.querySelector(".confirm-dialog-overlay")).toBeTruthy();
|
expect(container.querySelector(".confirm-dialog-overlay")).toBeTruthy();
|
||||||
expect(container.querySelector(".confirm-dialog.modal")).toBeTruthy();
|
expect(container.querySelector(".confirm-dialog.modal")).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not render checkbox when checkboxLabel is omitted", () => {
|
||||||
|
render(
|
||||||
|
<ConfirmDialog
|
||||||
|
isOpen={true}
|
||||||
|
options={{ title: "Delete Task", message: "Delete FN-001?", danger: true }}
|
||||||
|
onConfirm={vi.fn()}
|
||||||
|
onCancel={vi.fn()}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.queryByRole("checkbox")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders checkbox label and description when provided", () => {
|
||||||
|
render(
|
||||||
|
<ConfirmDialog
|
||||||
|
isOpen={true}
|
||||||
|
options={{ title: "Delete Task", message: "Delete FN-001?", danger: true }}
|
||||||
|
checkboxLabel="Allow re-creation later"
|
||||||
|
checkboxDescription="Keeps this ID unlockable"
|
||||||
|
onConfirm={vi.fn()}
|
||||||
|
onCancel={vi.fn()}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByRole("checkbox")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Allow re-creation later")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Keeps this ID unlockable")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls onCheckboxChange when toggled", () => {
|
||||||
|
const onCheckboxChange = vi.fn();
|
||||||
|
render(
|
||||||
|
<ConfirmDialog
|
||||||
|
isOpen={true}
|
||||||
|
options={{ title: "Delete Task", message: "Delete FN-001?", danger: true }}
|
||||||
|
checkboxLabel="Allow re-creation later"
|
||||||
|
checkboxChecked={false}
|
||||||
|
onCheckboxChange={onCheckboxChange}
|
||||||
|
onConfirm={vi.fn()}
|
||||||
|
onCancel={vi.fn()}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("checkbox"));
|
||||||
|
expect(onCheckboxChange).toHaveBeenCalledWith(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses only token values in confirm-dialog checkbox css rule", () => {
|
||||||
|
const css = loadAllAppCss();
|
||||||
|
const match = css.match(/\.confirm-dialog__checkbox\s*\{([^}]*)\}/);
|
||||||
|
expect(match).toBeTruthy();
|
||||||
|
const ruleBody = match?.[1] ?? "";
|
||||||
|
expect(ruleBody).toMatch(/var\(--/);
|
||||||
|
expect(ruleBody).not.toMatch(/#[0-9a-fA-F]{3,8}\b|rgb\(/);
|
||||||
|
expect(ruleBody).not.toMatch(/\b(?!0(?:\D|$))\d+(?:\.\d+)?px\b/);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ function mockMatchMedia({ mobile = false, coarse = false, reducedMotion = false
|
|||||||
writable: true,
|
writable: true,
|
||||||
value: vi.fn().mockImplementation((query: string) => ({
|
value: vi.fn().mockImplementation((query: string) => ({
|
||||||
matches:
|
matches:
|
||||||
(query === "(max-width: 768px)" || query === "(max-width: 768px), (max-height: 480px)" && mobile)
|
((query === "(max-width: 768px)" || query === "(max-width: 768px), (max-height: 480px)") && mobile)
|
||||||
|| (query === "(pointer: coarse)" && coarse)
|
|| (query === "(pointer: coarse)" && coarse)
|
||||||
|| (query === "(prefers-reduced-motion: reduce)" && reducedMotion),
|
|| (query === "(prefers-reduced-motion: reduce)" && reducedMotion),
|
||||||
media: query,
|
media: query,
|
||||||
@@ -85,6 +85,7 @@ describe("OAuthManualCodeForm", () => {
|
|||||||
|
|
||||||
it("does not trigger scroll assist on non-mobile layouts", () => {
|
it("does not trigger scroll assist on non-mobile layouts", () => {
|
||||||
mockMatchMedia({ mobile: false, coarse: false });
|
mockMatchMedia({ mobile: false, coarse: false });
|
||||||
|
Object.defineProperty(window, "innerWidth", { configurable: true, value: 1280 });
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<OAuthManualCodeForm
|
<OAuthManualCodeForm
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
import { describe, it, expect, vi } from "vitest";
|
||||||
|
import { render, screen, waitFor } from "@testing-library/react";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
|
import {
|
||||||
|
makeTask,
|
||||||
|
noop,
|
||||||
|
noopMerge,
|
||||||
|
noopMove,
|
||||||
|
noopOpenDetail,
|
||||||
|
setupTaskDetailModalHooks,
|
||||||
|
mockConfirm,
|
||||||
|
mockConfirmWithChoice,
|
||||||
|
mockConfirmWithCheckbox,
|
||||||
|
} from "./TaskDetailModal.test-helpers";
|
||||||
|
import { TaskDetailModal } from "../TaskDetailModal";
|
||||||
|
|
||||||
|
setupTaskDetailModalHooks();
|
||||||
|
|
||||||
|
describe("TaskDetailModal allowResurrection delete flow", () => {
|
||||||
|
it("passes allowResurrection=true when checkbox is checked", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const onDeleteTask = vi.fn(async () => makeTask());
|
||||||
|
mockConfirmWithCheckbox.mockResolvedValueOnce({ choice: "primary", checkboxValue: true });
|
||||||
|
|
||||||
|
render(
|
||||||
|
<TaskDetailModal
|
||||||
|
task={makeTask()}
|
||||||
|
onClose={noop}
|
||||||
|
onMoveTask={noopMove}
|
||||||
|
onDeleteTask={onDeleteTask}
|
||||||
|
onMergeTask={noopMerge}
|
||||||
|
onOpenDetail={noopOpenDetail}
|
||||||
|
addToast={noop}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await user.click(screen.getByRole("button", { name: "Actions" }));
|
||||||
|
await user.click(screen.getByRole("menuitem", { name: "Delete" }));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(onDeleteTask).toHaveBeenCalledWith("FN-099", { allowResurrection: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("passes allowResurrection=false when checkbox is left unchecked", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const onDeleteTask = vi.fn(async () => makeTask());
|
||||||
|
mockConfirmWithCheckbox.mockResolvedValueOnce({ choice: "primary", checkboxValue: false });
|
||||||
|
|
||||||
|
render(
|
||||||
|
<TaskDetailModal
|
||||||
|
task={makeTask()}
|
||||||
|
onClose={noop}
|
||||||
|
onMoveTask={noopMove}
|
||||||
|
onDeleteTask={onDeleteTask}
|
||||||
|
onMergeTask={noopMerge}
|
||||||
|
onOpenDetail={noopOpenDetail}
|
||||||
|
addToast={noop}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await user.click(screen.getByRole("button", { name: "Actions" }));
|
||||||
|
await user.click(screen.getByRole("menuitem", { name: "Delete" }));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(onDeleteTask).toHaveBeenCalledWith("FN-099", { allowResurrection: false });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps allowResurrection value on dependency-conflict retry", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const conflict = new Error("conflict") as Error & { details: { code: string; dependentIds: string[] } };
|
||||||
|
conflict.details = { code: "TASK_HAS_DEPENDENTS", dependentIds: ["FN-100"] };
|
||||||
|
const onDeleteTask = vi
|
||||||
|
.fn()
|
||||||
|
.mockRejectedValueOnce(conflict)
|
||||||
|
.mockResolvedValueOnce(makeTask());
|
||||||
|
mockConfirmWithCheckbox.mockResolvedValueOnce({ choice: "primary", checkboxValue: true });
|
||||||
|
mockConfirm.mockResolvedValueOnce(true);
|
||||||
|
|
||||||
|
render(
|
||||||
|
<TaskDetailModal
|
||||||
|
task={makeTask()}
|
||||||
|
onClose={noop}
|
||||||
|
onMoveTask={noopMove}
|
||||||
|
onDeleteTask={onDeleteTask}
|
||||||
|
onMergeTask={noopMerge}
|
||||||
|
onOpenDetail={noopOpenDetail}
|
||||||
|
addToast={noop}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await user.click(screen.getByRole("button", { name: "Actions" }));
|
||||||
|
await user.click(screen.getByRole("menuitem", { name: "Delete" }));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(onDeleteTask).toHaveBeenNthCalledWith(2, "FN-099", {
|
||||||
|
removeDependencyReferences: true,
|
||||||
|
removeLineageReferences: true,
|
||||||
|
githubIssueAction: undefined,
|
||||||
|
allowResurrection: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("archive branch stays delete-unaffected", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const onDeleteTask = vi.fn(async () => makeTask());
|
||||||
|
const onArchiveTask = vi.fn(async () => makeTask({ column: "archived" }));
|
||||||
|
mockConfirmWithChoice.mockResolvedValueOnce("tertiary");
|
||||||
|
|
||||||
|
render(
|
||||||
|
<TaskDetailModal
|
||||||
|
task={makeTask({ column: "done" })}
|
||||||
|
onClose={noop}
|
||||||
|
onMoveTask={noopMove}
|
||||||
|
onDeleteTask={onDeleteTask}
|
||||||
|
onArchiveTask={onArchiveTask}
|
||||||
|
onMergeTask={noopMerge}
|
||||||
|
onOpenDetail={noopOpenDetail}
|
||||||
|
addToast={noop}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await user.click(screen.getByRole("button", { name: "Actions" }));
|
||||||
|
await user.click(screen.getByRole("menuitem", { name: "Delete" }));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(onArchiveTask).toHaveBeenCalledWith("FN-099");
|
||||||
|
expect(onDeleteTask).not.toHaveBeenCalled();
|
||||||
|
expect(mockConfirmWithCheckbox).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
noopOpenDetail,
|
noopOpenDetail,
|
||||||
mockConfirm,
|
mockConfirm,
|
||||||
mockConfirmWithChoice,
|
mockConfirmWithChoice,
|
||||||
|
mockConfirmWithCheckbox,
|
||||||
mockUsePluginUiSlots,
|
mockUsePluginUiSlots,
|
||||||
expectBaseRule,
|
expectBaseRule,
|
||||||
readDashboardStylesSource,
|
readDashboardStylesSource,
|
||||||
@@ -125,14 +126,13 @@ describe("TaskDetailModal", () => {
|
|||||||
fireEvent.click(screen.getByRole("menuitem", { name: "Delete" }));
|
fireEvent.click(screen.getByRole("menuitem", { name: "Delete" }));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(onDeleteTask).toHaveBeenCalledWith("FN-099", { githubIssueAction: "close" });
|
expect(onDeleteTask).toHaveBeenCalledWith("FN-099", { githubIssueAction: "close", allowResurrection: false });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("passes githubIssueAction=delete for tracked tasks", async () => {
|
it("passes githubIssueAction=delete for tracked tasks", async () => {
|
||||||
const onDeleteTask = vi.fn().mockResolvedValue({} as Task);
|
const onDeleteTask = vi.fn().mockResolvedValue({} as Task);
|
||||||
mockConfirm
|
mockConfirm
|
||||||
.mockResolvedValueOnce(true)
|
|
||||||
.mockResolvedValueOnce(false)
|
.mockResolvedValueOnce(false)
|
||||||
.mockResolvedValueOnce(true);
|
.mockResolvedValueOnce(true);
|
||||||
|
|
||||||
@@ -152,14 +152,13 @@ describe("TaskDetailModal", () => {
|
|||||||
fireEvent.click(screen.getByRole("menuitem", { name: "Delete" }));
|
fireEvent.click(screen.getByRole("menuitem", { name: "Delete" }));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(onDeleteTask).toHaveBeenCalledWith("FN-099", { githubIssueAction: "delete" });
|
expect(onDeleteTask).toHaveBeenCalledWith("FN-099", { githubIssueAction: "delete", allowResurrection: false });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("passes githubIssueAction=leave for tracked tasks", async () => {
|
it("passes githubIssueAction=leave for tracked tasks", async () => {
|
||||||
const onDeleteTask = vi.fn().mockResolvedValue({} as Task);
|
const onDeleteTask = vi.fn().mockResolvedValue({} as Task);
|
||||||
mockConfirm
|
mockConfirm
|
||||||
.mockResolvedValueOnce(true)
|
|
||||||
.mockResolvedValueOnce(false)
|
.mockResolvedValueOnce(false)
|
||||||
.mockResolvedValueOnce(false);
|
.mockResolvedValueOnce(false);
|
||||||
|
|
||||||
@@ -179,7 +178,7 @@ describe("TaskDetailModal", () => {
|
|||||||
fireEvent.click(screen.getByRole("menuitem", { name: "Delete" }));
|
fireEvent.click(screen.getByRole("menuitem", { name: "Delete" }));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(onDeleteTask).toHaveBeenCalledWith("FN-099", { githubIssueAction: "leave" });
|
expect(onDeleteTask).toHaveBeenCalledWith("FN-099", { githubIssueAction: "leave", allowResurrection: false });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -203,7 +202,7 @@ describe("TaskDetailModal", () => {
|
|||||||
fireEvent.click(screen.getByRole("menuitem", { name: "Delete" }));
|
fireEvent.click(screen.getByRole("menuitem", { name: "Delete" }));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(onDeleteTask).toHaveBeenCalledWith("FN-099");
|
expect(onDeleteTask).toHaveBeenCalledWith("FN-099", { allowResurrection: false });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -219,8 +218,8 @@ describe("TaskDetailModal", () => {
|
|||||||
.mockRejectedValueOnce(conflict)
|
.mockRejectedValueOnce(conflict)
|
||||||
.mockResolvedValueOnce({} as Task);
|
.mockResolvedValueOnce({} as Task);
|
||||||
|
|
||||||
|
mockConfirmWithCheckbox.mockResolvedValueOnce({ choice: "primary", checkboxValue: false });
|
||||||
mockConfirm
|
mockConfirm
|
||||||
.mockResolvedValueOnce(true)
|
|
||||||
.mockResolvedValueOnce(false)
|
.mockResolvedValueOnce(false)
|
||||||
.mockResolvedValueOnce(true)
|
.mockResolvedValueOnce(true)
|
||||||
.mockResolvedValueOnce(true);
|
.mockResolvedValueOnce(true);
|
||||||
@@ -242,24 +241,19 @@ describe("TaskDetailModal", () => {
|
|||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(mockConfirm).toHaveBeenNthCalledWith(1, {
|
expect(mockConfirm).toHaveBeenNthCalledWith(1, {
|
||||||
title: "Delete Task",
|
|
||||||
message: "Delete FN-099?",
|
|
||||||
danger: true,
|
|
||||||
});
|
|
||||||
expect(mockConfirm).toHaveBeenNthCalledWith(2, {
|
|
||||||
title: "Linked GitHub Issue",
|
title: "Linked GitHub Issue",
|
||||||
message: "Choose what to do with owner/repo#42 when deleting FN-099.\n\nClose the issue?",
|
message: "Choose what to do with owner/repo#42 when deleting FN-099.\n\nClose the issue?",
|
||||||
confirmLabel: "Close Issue",
|
confirmLabel: "Close Issue",
|
||||||
cancelLabel: "More Options",
|
cancelLabel: "More Options",
|
||||||
});
|
});
|
||||||
expect(mockConfirm).toHaveBeenNthCalledWith(3, {
|
expect(mockConfirm).toHaveBeenNthCalledWith(2, {
|
||||||
title: "Delete Linked GitHub Issue",
|
title: "Delete Linked GitHub Issue",
|
||||||
message: "Delete owner/repo#42 on GitHub, or leave it unchanged?",
|
message: "Delete owner/repo#42 on GitHub, or leave it unchanged?",
|
||||||
confirmLabel: "Delete Issue",
|
confirmLabel: "Delete Issue",
|
||||||
cancelLabel: "Leave Unchanged",
|
cancelLabel: "Leave Unchanged",
|
||||||
danger: true,
|
danger: true,
|
||||||
});
|
});
|
||||||
expect(mockConfirm).toHaveBeenNthCalledWith(4, {
|
expect(mockConfirm).toHaveBeenNthCalledWith(3, {
|
||||||
title: "Force Delete Task",
|
title: "Force Delete Task",
|
||||||
message: "FN-099 is a dependency of FN-100, FN-101.\n\nDelete anyway by removing these dependency references first?",
|
message: "FN-099 is a dependency of FN-100, FN-101.\n\nDelete anyway by removing these dependency references first?",
|
||||||
danger: true,
|
danger: true,
|
||||||
@@ -267,11 +261,12 @@ describe("TaskDetailModal", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(onDeleteTask).toHaveBeenNthCalledWith(1, "FN-099", { githubIssueAction: "delete" });
|
expect(onDeleteTask).toHaveBeenNthCalledWith(1, "FN-099", { githubIssueAction: "delete", allowResurrection: false });
|
||||||
expect(onDeleteTask).toHaveBeenNthCalledWith(2, "FN-099", {
|
expect(onDeleteTask).toHaveBeenNthCalledWith(2, "FN-099", {
|
||||||
removeDependencyReferences: true,
|
removeDependencyReferences: true,
|
||||||
removeLineageReferences: true,
|
removeLineageReferences: true,
|
||||||
githubIssueAction: "delete",
|
githubIssueAction: "delete",
|
||||||
|
allowResurrection: false,
|
||||||
});
|
});
|
||||||
expect(noop).toHaveBeenCalledWith("Deleted FN-099 after removing dependency references", "info");
|
expect(noop).toHaveBeenCalledWith("Deleted FN-099 after removing dependency references", "info");
|
||||||
});
|
});
|
||||||
@@ -287,9 +282,8 @@ describe("TaskDetailModal", () => {
|
|||||||
conflict.details = { code: "TASK_HAS_DEPENDENTS", dependentIds: ["FN-102"] };
|
conflict.details = { code: "TASK_HAS_DEPENDENTS", dependentIds: ["FN-102"] };
|
||||||
onDeleteTask.mockRejectedValue(conflict);
|
onDeleteTask.mockRejectedValue(conflict);
|
||||||
|
|
||||||
mockConfirm
|
mockConfirmWithCheckbox.mockResolvedValueOnce({ choice: "primary", checkboxValue: false });
|
||||||
.mockResolvedValueOnce(true)
|
mockConfirm.mockResolvedValueOnce(false);
|
||||||
.mockResolvedValueOnce(false);
|
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<TaskDetailModal
|
<TaskDetailModal
|
||||||
@@ -307,7 +301,7 @@ describe("TaskDetailModal", () => {
|
|||||||
fireEvent.click(screen.getByRole("menuitem", { name: "Delete" }));
|
fireEvent.click(screen.getByRole("menuitem", { name: "Delete" }));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(mockConfirm).toHaveBeenCalledTimes(2);
|
expect(mockConfirm).toHaveBeenCalledTimes(1);
|
||||||
expect(onDeleteTask).toHaveBeenCalledTimes(1);
|
expect(onDeleteTask).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -322,8 +316,8 @@ describe("TaskDetailModal", () => {
|
|||||||
conflict.details = { code: "TASK_HAS_LINEAGE_CHILDREN", lineageChildIds: ["FN-104"] };
|
conflict.details = { code: "TASK_HAS_LINEAGE_CHILDREN", lineageChildIds: ["FN-104"] };
|
||||||
onDeleteTask.mockRejectedValue(conflict);
|
onDeleteTask.mockRejectedValue(conflict);
|
||||||
|
|
||||||
|
mockConfirmWithCheckbox.mockResolvedValueOnce({ choice: "primary", checkboxValue: false });
|
||||||
mockConfirm
|
mockConfirm
|
||||||
.mockResolvedValueOnce(true)
|
|
||||||
.mockResolvedValueOnce(true)
|
.mockResolvedValueOnce(true)
|
||||||
.mockResolvedValueOnce(false);
|
.mockResolvedValueOnce(false);
|
||||||
|
|
||||||
@@ -343,7 +337,7 @@ describe("TaskDetailModal", () => {
|
|||||||
fireEvent.click(screen.getByRole("menuitem", { name: "Delete" }));
|
fireEvent.click(screen.getByRole("menuitem", { name: "Delete" }));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(mockConfirm).toHaveBeenCalledTimes(3);
|
expect(mockConfirm).toHaveBeenCalledTimes(2);
|
||||||
expect(onDeleteTask).toHaveBeenCalledTimes(1);
|
expect(onDeleteTask).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -360,9 +354,8 @@ describe("TaskDetailModal", () => {
|
|||||||
.mockRejectedValueOnce(conflict)
|
.mockRejectedValueOnce(conflict)
|
||||||
.mockRejectedValueOnce(new Error("Retry failed"));
|
.mockRejectedValueOnce(new Error("Retry failed"));
|
||||||
|
|
||||||
mockConfirm
|
mockConfirmWithCheckbox.mockResolvedValueOnce({ choice: "primary", checkboxValue: false });
|
||||||
.mockResolvedValueOnce(true)
|
mockConfirm.mockResolvedValueOnce(true);
|
||||||
.mockResolvedValueOnce(true);
|
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<TaskDetailModal
|
<TaskDetailModal
|
||||||
@@ -383,6 +376,7 @@ describe("TaskDetailModal", () => {
|
|||||||
expect(onDeleteTask).toHaveBeenNthCalledWith(2, "FN-099", {
|
expect(onDeleteTask).toHaveBeenNthCalledWith(2, "FN-099", {
|
||||||
removeDependencyReferences: true,
|
removeDependencyReferences: true,
|
||||||
removeLineageReferences: true,
|
removeLineageReferences: true,
|
||||||
|
allowResurrection: false,
|
||||||
});
|
});
|
||||||
expect(noop).toHaveBeenCalledWith("Retry failed", "error");
|
expect(noop).toHaveBeenCalledWith("Retry failed", "error");
|
||||||
});
|
});
|
||||||
@@ -400,8 +394,8 @@ describe("TaskDetailModal", () => {
|
|||||||
.mockRejectedValueOnce(conflict)
|
.mockRejectedValueOnce(conflict)
|
||||||
.mockResolvedValueOnce({} as Task);
|
.mockResolvedValueOnce({} as Task);
|
||||||
|
|
||||||
|
mockConfirmWithCheckbox.mockResolvedValueOnce({ choice: "primary", checkboxValue: false });
|
||||||
mockConfirm
|
mockConfirm
|
||||||
.mockResolvedValueOnce(true)
|
|
||||||
.mockResolvedValueOnce(true)
|
.mockResolvedValueOnce(true)
|
||||||
.mockResolvedValueOnce(true);
|
.mockResolvedValueOnce(true);
|
||||||
|
|
||||||
@@ -425,6 +419,7 @@ describe("TaskDetailModal", () => {
|
|||||||
removeDependencyReferences: true,
|
removeDependencyReferences: true,
|
||||||
removeLineageReferences: true,
|
removeLineageReferences: true,
|
||||||
githubIssueAction: "close",
|
githubIssueAction: "close",
|
||||||
|
allowResurrection: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -88,9 +88,14 @@ vi.mock("../../hooks/usePluginUiSlots", () => ({
|
|||||||
|
|
||||||
export const mockConfirm = vi.fn();
|
export const mockConfirm = vi.fn();
|
||||||
export const mockConfirmWithChoice = vi.fn();
|
export const mockConfirmWithChoice = vi.fn();
|
||||||
|
export const mockConfirmWithCheckbox = vi.fn();
|
||||||
|
|
||||||
vi.mock("../../hooks/useConfirm", () => ({
|
vi.mock("../../hooks/useConfirm", () => ({
|
||||||
useConfirm: () => ({ confirm: mockConfirm, confirmWithChoice: mockConfirmWithChoice }),
|
useConfirm: () => ({
|
||||||
|
confirm: mockConfirm,
|
||||||
|
confirmWithChoice: mockConfirmWithChoice,
|
||||||
|
confirmWithCheckbox: mockConfirmWithCheckbox,
|
||||||
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export function makeTask(overrides: Partial<TaskDetail> = {}): TaskDetail {
|
export function makeTask(overrides: Partial<TaskDetail> = {}): TaskDetail {
|
||||||
@@ -143,8 +148,10 @@ export function setupTaskDetailModalHooks(): void {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockConfirm.mockReset();
|
mockConfirm.mockReset();
|
||||||
mockConfirmWithChoice.mockReset();
|
mockConfirmWithChoice.mockReset();
|
||||||
|
mockConfirmWithCheckbox.mockReset();
|
||||||
mockConfirm.mockResolvedValue(true);
|
mockConfirm.mockResolvedValue(true);
|
||||||
mockConfirmWithChoice.mockResolvedValue("primary");
|
mockConfirmWithChoice.mockResolvedValue("primary");
|
||||||
|
mockConfirmWithCheckbox.mockResolvedValue({ choice: "primary", checkboxValue: false });
|
||||||
clearAuthToken();
|
clearAuthToken();
|
||||||
localStorage.removeItem("fn.authToken");
|
localStorage.removeItem("fn.authToken");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import React, { useState } from "react";
|
|||||||
import { ConfirmDialogProvider, useConfirm } from "../useConfirm";
|
import { ConfirmDialogProvider, useConfirm } from "../useConfirm";
|
||||||
|
|
||||||
function Harness() {
|
function Harness() {
|
||||||
const { confirm, confirmWithChoice } = useConfirm();
|
const { confirm, confirmWithChoice, confirmWithCheckbox } = useConfirm();
|
||||||
const [result, setResult] = useState<string>("idle");
|
const [result, setResult] = useState<string>("idle");
|
||||||
|
|
||||||
return React.createElement(
|
return React.createElement(
|
||||||
@@ -30,6 +30,34 @@ function Harness() {
|
|||||||
},
|
},
|
||||||
"queue"
|
"queue"
|
||||||
),
|
),
|
||||||
|
React.createElement(
|
||||||
|
"button",
|
||||||
|
{
|
||||||
|
onClick: async () => {
|
||||||
|
const outcome = await confirmWithCheckbox({
|
||||||
|
title: "Delete Task",
|
||||||
|
message: "Delete FN-001?",
|
||||||
|
checkbox: { label: "Allow re-creation later", defaultChecked: false },
|
||||||
|
});
|
||||||
|
setResult(JSON.stringify(outcome));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"open-checkbox"
|
||||||
|
),
|
||||||
|
React.createElement(
|
||||||
|
"button",
|
||||||
|
{
|
||||||
|
onClick: async () => {
|
||||||
|
const outcome = await confirmWithCheckbox({
|
||||||
|
title: "Delete Task",
|
||||||
|
message: "Delete FN-001?",
|
||||||
|
checkbox: { label: "Allow re-creation later", defaultChecked: true },
|
||||||
|
});
|
||||||
|
setResult(JSON.stringify(outcome));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"open-checkbox-default-checked"
|
||||||
|
),
|
||||||
React.createElement(
|
React.createElement(
|
||||||
"button",
|
"button",
|
||||||
{
|
{
|
||||||
@@ -151,4 +179,56 @@ describe("useConfirm", () => {
|
|||||||
|
|
||||||
expect(await screen.findByRole("dialog", { name: "Second" })).toBeInTheDocument();
|
expect(await screen.findByRole("dialog", { name: "Second" })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("confirmWithCheckbox returns primary with toggled checkbox value", async () => {
|
||||||
|
render(
|
||||||
|
React.createElement(
|
||||||
|
ConfirmDialogProvider,
|
||||||
|
null,
|
||||||
|
React.createElement(Harness)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText("open-checkbox"));
|
||||||
|
fireEvent.click(await screen.findByRole("checkbox"));
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Confirm" }));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId("result").textContent).toBe('{"choice":"primary","checkboxValue":true}');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("confirmWithCheckbox returns cancel with unchecked value on cancel", async () => {
|
||||||
|
render(
|
||||||
|
React.createElement(
|
||||||
|
ConfirmDialogProvider,
|
||||||
|
null,
|
||||||
|
React.createElement(Harness)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText("open-checkbox"));
|
||||||
|
fireEvent.click(await screen.findByRole("button", { name: "Cancel" }));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId("result").textContent).toBe('{"choice":"cancel","checkboxValue":false}');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("confirmWithCheckbox keeps defaultChecked value when untouched", async () => {
|
||||||
|
render(
|
||||||
|
React.createElement(
|
||||||
|
ConfirmDialogProvider,
|
||||||
|
null,
|
||||||
|
React.createElement(Harness)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText("open-checkbox-default-checked"));
|
||||||
|
fireEvent.click(await screen.findByRole("button", { name: "Confirm" }));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId("result").textContent).toBe('{"choice":"primary","checkboxValue":true}');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,10 +19,18 @@ vi.mock("../../sse-bus", () => ({
|
|||||||
const eventPayload = { events: [{ taskId: "FN-1", integrationBranch: "trunk", refName: "refs/heads/trunk", toSha: "abcdef123456", fromSha: "123", advanceMode: "update-ref", succeeded: true, advancedAt: "2026", userCheckout: { worktreePath: "/repo", dirty: false, untrackedCount: 0 } }] };
|
const eventPayload = { events: [{ taskId: "FN-1", integrationBranch: "trunk", refName: "refs/heads/trunk", toSha: "abcdef123456", fromSha: "123", advanceMode: "update-ref", succeeded: true, advancedAt: "2026", userCheckout: { worktreePath: "/repo", dirty: false, untrackedCount: 0 } }] };
|
||||||
const pushStatus = { integrationBranch: "trunk", branchSource: "settings" as const, hasOriginRemote: true, hasUpstream: true, localSha: "localsha", remoteSha: "remotesha", aheadCount: 2, behindCount: 0, mergeActive: false, canPush: true };
|
const pushStatus = { integrationBranch: "trunk", branchSource: "settings" as const, hasOriginRemote: true, hasUpstream: true, localSha: "localsha", remoteSha: "remotesha", aheadCount: 2, behindCount: 0, mergeActive: false, canPush: true };
|
||||||
|
|
||||||
|
function toPath(value: unknown): string {
|
||||||
|
if (typeof value === "string") return value;
|
||||||
|
if (value instanceof URL) return value.toString();
|
||||||
|
if (value && typeof value === "object" && "url" in value) return String((value as { url?: unknown }).url ?? "");
|
||||||
|
return String(value ?? "");
|
||||||
|
}
|
||||||
|
|
||||||
function setDefaultMocks() {
|
function setDefaultMocks() {
|
||||||
mocked.api.mockImplementation(async (path: string) => {
|
mocked.api.mockImplementation(async (path: unknown) => {
|
||||||
if (String(path).includes("push-status")) return pushStatus;
|
const target = toPath(path);
|
||||||
if (String(path).includes("merge-advance-events")) return eventPayload;
|
if (target.includes("push-status")) return pushStatus;
|
||||||
|
if (target.includes("merge-advance-events")) return eventPayload;
|
||||||
return { ok: true, outcome: "ok", localSha: "localsha", remoteSha: "localsha" };
|
return { ok: true, outcome: "ok", localSha: "localsha", remoteSha: "localsha" };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -126,23 +134,23 @@ describe("useMergeAdvanceNotice", () => {
|
|||||||
|
|
||||||
it("pull posts to /git/pull and dismisses on clean outcome", async () => {
|
it("pull posts to /git/pull and dismisses on clean outcome", async () => {
|
||||||
const pullEventPayload = { events: [{ ...eventPayload.events[0], toSha: "clean12345" }] };
|
const pullEventPayload = { events: [{ ...eventPayload.events[0], toSha: "clean12345" }] };
|
||||||
mocked.api.mockImplementation(async (path: string) => {
|
mocked.api.mockImplementation(async (path: unknown) => {
|
||||||
if (String(path).includes("merge-advance-events")) return pullEventPayload;
|
const target = toPath(path);
|
||||||
if (String(path).includes("push-status")) return pushStatus;
|
if (target.includes("merge-advance-events")) return pullEventPayload;
|
||||||
if (String(path).includes("/git/pull")) return { kind: "pull-clean", toSha: "clean12345" };
|
if (target.includes("push-status")) return pushStatus;
|
||||||
|
if (target.includes("/git/pull")) return { kind: "pull-clean", toSha: "clean12345" };
|
||||||
return { ok: true, outcome: "ok", localSha: "localsha", remoteSha: "localsha" };
|
return { ok: true, outcome: "ok", localSha: "localsha", remoteSha: "localsha" };
|
||||||
});
|
});
|
||||||
const { result } = renderHook(() => useMergeAdvanceNotice({ projectId: "p1-pull-clean" }));
|
const { result } = renderHook(() => useMergeAdvanceNotice({ projectId: "p1-pull-clean" }));
|
||||||
await waitFor(() => expect(result.current.notice).toBeDefined());
|
await waitFor(() => expect(result.current.notice).toBeDefined());
|
||||||
await act(async () => { await result.current.pull(); });
|
await act(async () => { await result.current.pull(); });
|
||||||
expect(mocked.api.mock.calls.some((call) => String(call[0]).startsWith("/git/pull?projectId=p1-pull-clean"))).toBe(true);
|
expect(mocked.api.mock.calls.some((call) => toPath(call[0]).includes("/git/pull"))).toBe(true);
|
||||||
expect(result.current.conflictState).toBeNull();
|
expect(result.current.conflictState).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("dismiss() actually removes the banner — dismissedShas filter is applied in the notice memo", async () => {
|
it("dismiss() actually removes the banner — dismissedShas filter is applied in the notice memo", async () => {
|
||||||
const { result } = renderHook(() => useMergeAdvanceNotice({ projectId: "p1-dismiss" }));
|
const { result } = renderHook(() => useMergeAdvanceNotice({ projectId: "p1-dismiss" }));
|
||||||
await waitFor(() => expect(result.current.notice).toBeDefined());
|
await waitFor(() => expect(result.current.notice?.toSha).toBe("abcdef123456"));
|
||||||
expect(result.current.notice?.toSha).toBe("abcdef123456");
|
|
||||||
act(() => result.current.dismiss());
|
act(() => result.current.dismiss());
|
||||||
await waitFor(() => expect(result.current.notice).toBeUndefined());
|
await waitFor(() => expect(result.current.notice).toBeUndefined());
|
||||||
});
|
});
|
||||||
@@ -212,13 +220,11 @@ describe("useMergeAdvanceNotice", () => {
|
|||||||
|
|
||||||
it("pull stash-conflict opens conflict state and preserves error visibility", async () => {
|
it("pull stash-conflict opens conflict state and preserves error visibility", async () => {
|
||||||
const conflictEventPayload = { events: [{ ...eventPayload.events[0], toSha: "conflict12345" }] };
|
const conflictEventPayload = { events: [{ ...eventPayload.events[0], toSha: "conflict12345" }] };
|
||||||
let callIndex = 0;
|
mocked.api.mockImplementation(async (path: unknown) => {
|
||||||
mocked.api.mockImplementation(async () => {
|
const target = toPath(path);
|
||||||
const current = callIndex;
|
if (target.includes("merge-advance-events")) return conflictEventPayload;
|
||||||
callIndex += 1;
|
if (target.includes("push-status")) return pushStatus;
|
||||||
if (current === 0) return conflictEventPayload;
|
if (target.includes("/git/pull")) {
|
||||||
if (current === 1) return pushStatus;
|
|
||||||
if (current === 2) {
|
|
||||||
return {
|
return {
|
||||||
kind: "stash-conflict",
|
kind: "stash-conflict",
|
||||||
toSha: "conflict12345",
|
toSha: "conflict12345",
|
||||||
@@ -231,7 +237,7 @@ describe("useMergeAdvanceNotice", () => {
|
|||||||
return pushStatus;
|
return pushStatus;
|
||||||
});
|
});
|
||||||
const { result } = renderHook(() => useMergeAdvanceNotice({ projectId: "p1-pull-conflict" }));
|
const { result } = renderHook(() => useMergeAdvanceNotice({ projectId: "p1-pull-conflict" }));
|
||||||
await waitFor(() => expect(result.current.notice).toBeDefined());
|
await waitFor(() => expect(result.current.notice?.toSha).toBe("conflict12345"));
|
||||||
await act(async () => { await result.current.pull(); });
|
await act(async () => { await result.current.pull(); });
|
||||||
await waitFor(() => expect(result.current.conflictState).not.toBeNull());
|
await waitFor(() => expect(result.current.conflictState).not.toBeNull());
|
||||||
expect(result.current.conflictState).toEqual({
|
expect(result.current.conflictState).toEqual({
|
||||||
|
|||||||
@@ -11,18 +11,25 @@ export interface ConfirmOptions {
|
|||||||
danger?: boolean;
|
danger?: boolean;
|
||||||
tertiaryLabel?: string;
|
tertiaryLabel?: string;
|
||||||
tertiaryDanger?: boolean;
|
tertiaryDanger?: boolean;
|
||||||
|
checkbox?: {
|
||||||
|
label: string;
|
||||||
|
description?: string;
|
||||||
|
defaultChecked?: boolean;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ConfirmChoice = "primary" | "tertiary" | "cancel";
|
export type ConfirmChoice = "primary" | "tertiary" | "cancel";
|
||||||
|
|
||||||
interface PendingConfirm {
|
interface PendingConfirm {
|
||||||
options: ConfirmOptions;
|
options: ConfirmOptions;
|
||||||
resolve: (value: ConfirmChoice) => void;
|
checkboxValue: boolean;
|
||||||
|
resolve: (value: { choice: ConfirmChoice; checkboxValue: boolean }) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ConfirmContextValue {
|
interface ConfirmContextValue {
|
||||||
confirm: (options: ConfirmOptions) => Promise<boolean>;
|
confirm: (options: ConfirmOptions) => Promise<boolean>;
|
||||||
confirmWithChoice: (options: ConfirmOptions) => Promise<ConfirmChoice>;
|
confirmWithChoice: (options: ConfirmOptions) => Promise<ConfirmChoice>;
|
||||||
|
confirmWithCheckbox: (options: ConfirmOptions) => Promise<{ choice: ConfirmChoice; checkboxValue: boolean }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ConfirmContext = createContext<ConfirmContextValue | null>(null);
|
const ConfirmContext = createContext<ConfirmContextValue | null>(null);
|
||||||
@@ -39,12 +46,24 @@ export function ConfirmDialogProvider({ children }: { children: ReactNode }) {
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const confirmWithChoice = useCallback((options: ConfirmOptions) => {
|
const confirmWithCheckbox = useCallback((options: ConfirmOptions) => {
|
||||||
return new Promise<ConfirmChoice>((resolve) => {
|
return new Promise<{ choice: ConfirmChoice; checkboxValue: boolean }>((resolve) => {
|
||||||
updateQueue((current) => [...current, { options, resolve }]);
|
updateQueue((current) => [
|
||||||
|
...current,
|
||||||
|
{
|
||||||
|
options,
|
||||||
|
checkboxValue: options.checkbox?.defaultChecked ?? false,
|
||||||
|
resolve,
|
||||||
|
},
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
}, [updateQueue]);
|
}, [updateQueue]);
|
||||||
|
|
||||||
|
const confirmWithChoice = useCallback(async (options: ConfirmOptions) => {
|
||||||
|
const { choice } = await confirmWithCheckbox(options);
|
||||||
|
return choice;
|
||||||
|
}, [confirmWithCheckbox]);
|
||||||
|
|
||||||
const confirm = useCallback(async (options: ConfirmOptions) => {
|
const confirm = useCallback(async (options: ConfirmOptions) => {
|
||||||
const choice = await confirmWithChoice(options);
|
const choice = await confirmWithChoice(options);
|
||||||
return choice === "primary";
|
return choice === "primary";
|
||||||
@@ -56,13 +75,16 @@ export function ConfirmDialogProvider({ children }: { children: ReactNode }) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
current.resolve(value);
|
current.resolve({ choice: value, checkboxValue: current.checkboxValue });
|
||||||
updateQueue((items) => items.slice(1));
|
updateQueue((items) => items.slice(1));
|
||||||
}, [updateQueue]);
|
}, [updateQueue]);
|
||||||
|
|
||||||
const active = queue[0] ?? null;
|
const active = queue[0] ?? null;
|
||||||
|
|
||||||
const contextValue = useMemo<ConfirmContextValue>(() => ({ confirm, confirmWithChoice }), [confirm, confirmWithChoice]);
|
const contextValue = useMemo<ConfirmContextValue>(
|
||||||
|
() => ({ confirm, confirmWithChoice, confirmWithCheckbox }),
|
||||||
|
[confirm, confirmWithCheckbox, confirmWithChoice]
|
||||||
|
);
|
||||||
|
|
||||||
return React.createElement(
|
return React.createElement(
|
||||||
ConfirmContext.Provider,
|
ConfirmContext.Provider,
|
||||||
@@ -74,6 +96,18 @@ export function ConfirmDialogProvider({ children }: { children: ReactNode }) {
|
|||||||
onConfirm: () => resolveCurrent("primary"),
|
onConfirm: () => resolveCurrent("primary"),
|
||||||
onTertiary: () => resolveCurrent("tertiary"),
|
onTertiary: () => resolveCurrent("tertiary"),
|
||||||
onCancel: () => resolveCurrent("cancel"),
|
onCancel: () => resolveCurrent("cancel"),
|
||||||
|
checkboxLabel: active?.options.checkbox?.label,
|
||||||
|
checkboxDescription: active?.options.checkbox?.description,
|
||||||
|
checkboxChecked: active?.checkboxValue ?? false,
|
||||||
|
onCheckboxChange: (next) => {
|
||||||
|
updateQueue((current) => {
|
||||||
|
if (current.length === 0) {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
const [head, ...tail] = current;
|
||||||
|
return [{ ...head, checkboxValue: next }, ...tail];
|
||||||
|
});
|
||||||
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -87,5 +121,9 @@ export function useConfirm(): ConfirmContextValue {
|
|||||||
return {
|
return {
|
||||||
confirm: async (_options: ConfirmOptions) => false,
|
confirm: async (_options: ConfirmOptions) => false,
|
||||||
confirmWithChoice: async (_options: ConfirmOptions) => "cancel",
|
confirmWithChoice: async (_options: ConfirmOptions) => "cancel",
|
||||||
|
confirmWithCheckbox: async (options: ConfirmOptions) => ({
|
||||||
|
choice: "cancel",
|
||||||
|
checkboxValue: options.checkbox?.defaultChecked ?? false,
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -516,6 +516,7 @@ export function useTasks(options?: UseTasksOptions) {
|
|||||||
removeDependencyReferences?: boolean;
|
removeDependencyReferences?: boolean;
|
||||||
removeLineageReferences?: boolean;
|
removeLineageReferences?: boolean;
|
||||||
githubIssueAction?: GithubIssueAction;
|
githubIssueAction?: GithubIssueAction;
|
||||||
|
allowResurrection?: boolean;
|
||||||
},
|
},
|
||||||
): Promise<Task> => {
|
): Promise<Task> => {
|
||||||
return normalizeTask(await api.deleteTask(id, projectId, options));
|
return normalizeTask(await api.deleteTask(id, projectId, options));
|
||||||
|
|||||||
@@ -2112,6 +2112,14 @@ describe("Mission API", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("accepts long missionTitle values on interview start", async () => {
|
it("accepts long missionTitle values on interview start", async () => {
|
||||||
|
const interviewSpy = vi
|
||||||
|
.spyOn(missionInterviewModule, "createMissionInterviewSession")
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
sessionId: "session-long-title",
|
||||||
|
interview: { missionDraft: { title: "x".repeat(5000) } },
|
||||||
|
state: "active",
|
||||||
|
} as any);
|
||||||
|
|
||||||
const { app } = buildApp();
|
const { app } = buildApp();
|
||||||
const res = await request(
|
const res = await request(
|
||||||
app,
|
app,
|
||||||
@@ -2121,6 +2129,7 @@ describe("Mission API", () => {
|
|||||||
{ "content-type": "application/json" }
|
{ "content-type": "application/json" }
|
||||||
);
|
);
|
||||||
expect(res.status).not.toBe(400);
|
expect(res.status).not.toBe(400);
|
||||||
|
expect(interviewSpy).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return 400 when sessionId is missing on interview respond", async () => {
|
it("should return 400 when sessionId is missing on interview respond", async () => {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const qualityAppTests = [
|
|||||||
"app/api/**/*.test.ts",
|
"app/api/**/*.test.ts",
|
||||||
// Representative workflow/component coverage. Exhaustive modal/view suites
|
// Representative workflow/component coverage. Exhaustive modal/view suites
|
||||||
// stay available in the full `dashboard-app` project.
|
// stay available in the full `dashboard-app` project.
|
||||||
"app/components/__tests__/{ActiveAgentsPanel,ActivityLogModal,AgentMentionPopup,AgentMetricsBar,AgentOnboardingModal,AgentReflectionsTab,AgentTokenStatsPanel,App,AuthTokenRecoveryDialog,Board,board-mobile,board-mobile-view-switch,ChatView,ChatView.autosize,ChatView.chat-input-autosize,ChatView.default-model-icon,ChatView.draft,ChatView.hash-mention,ChatView.rooms,ChatView.scroll-to-top,ChatView.swipe-back,Column,ConfirmDialog,ConversationHistory,DashboardLoader,DevServerView.mobile,DirectoryPicker,DuplicateWarningModal,ErrorBoundary,ExecutorStatusBar,FileBrowser,FileEditor,GitHubBadge,InlineCreateCard,LoginInstructions,MemoryView,MergeAdvanceNotice,MessageComposer,MessageComposer.autosize,MobileNavBar,NewTaskModal,NewTaskModal.shared-cache,NodeCard,NodeHealthDot,NodeStatusIndicator,PlanningModeModal.autosize,PrChecksList,PrCreateModal,PrCreateModal.layout,ProjectCard,ProjectSelector,ProviderIcon,PrPanel,PrPanel.merge,PrPanel.reviews,QuickChatFAB,QuickChatFAB.shared-cache,ReliabilityView,ResearchView,SecretsView,SecretsView.mobile,SettingsModal,SettingsModal.testMode,SettingsModal.worktrunk,StashConflictModal,StashRecoveryView,TaskCard,TaskCard.badge-height,TaskCard.badge-wrap,TaskCard.footer-wrap,TaskChangesTab,TaskComments,TaskDetailModal,TaskDetailModal.create-pr-e2e,TestModeBanner,TaskDetailModal.create-pr-integration,TaskDetailModal.github-tracking-header,TaskDetailModal.github-tracking-stale,TaskDetailModal.rebind-banner,TaskDocumentsTab,TaskForm,TaskIdIntegrityBanner,TrackingRepoSelect,WorkflowResultsTab,WorktrunkInstallApprovalDetails}.test.tsx",
|
"app/components/__tests__/{ActiveAgentsPanel,ActivityLogModal,AgentMentionPopup,AgentMetricsBar,AgentOnboardingModal,AgentReflectionsTab,AgentTokenStatsPanel,App,AuthTokenRecoveryDialog,Board,board-mobile,board-mobile-view-switch,ChatView,ChatView.autosize,ChatView.chat-input-autosize,ChatView.default-model-icon,ChatView.draft,ChatView.hash-mention,ChatView.rooms,ChatView.scroll-to-top,ChatView.swipe-back,Column,ConfirmDialog,ConversationHistory,DashboardLoader,DevServerView.mobile,DirectoryPicker,DuplicateWarningModal,ErrorBoundary,ExecutorStatusBar,FileBrowser,FileEditor,GitHubBadge,InlineCreateCard,LoginInstructions,MemoryView,MergeAdvanceNotice,MessageComposer,MessageComposer.autosize,MobileNavBar,NewTaskModal,NewTaskModal.shared-cache,NodeCard,NodeHealthDot,NodeStatusIndicator,PlanningModeModal.autosize,PrChecksList,PrCreateModal,PrCreateModal.layout,ProjectCard,ProjectSelector,ProviderIcon,PrPanel,PrPanel.merge,PrPanel.reviews,QuickChatFAB,QuickChatFAB.shared-cache,ReliabilityView,ResearchView,SecretsView,SecretsView.mobile,SettingsModal,SettingsModal.testMode,SettingsModal.worktrunk,StashConflictModal,StashRecoveryView,TaskCard,TaskCard.badge-height,TaskCard.badge-wrap,TaskCard.footer-wrap,TaskChangesTab,TaskComments,TaskDetailModal,TaskDetailModal.allow-resurrection,TaskDetailModal.create-pr-e2e,TestModeBanner,TaskDetailModal.create-pr-integration,TaskDetailModal.github-tracking-header,TaskDetailModal.github-tracking-stale,TaskDetailModal.rebind-banner,TaskDocumentsTab,TaskForm,TaskIdIntegrityBanner,TrackingRepoSelect,WorkflowResultsTab,WorktrunkInstallApprovalDetails}.test.tsx",
|
||||||
// Hooks and utilities are fast, user-visible state/formatting behavior.
|
// Hooks and utilities are fast, user-visible state/formatting behavior.
|
||||||
"app/context/**/*.test.tsx",
|
"app/context/**/*.test.tsx",
|
||||||
"app/hooks/__tests__/{useAgents,useAgentLogs,useAgentLogs.resume-instrumentation,useAppSettings,useAuthOnboarding,useConfirm,useCurrentProject,useNodes,useNodes.resume-instrumentation,useNodeSettingsSync,useProjects,useProjects.resume-instrumentation,useMeshState.resume-instrumentation,useManagedDockerNodes.resume-instrumentation,usePrChecksStream.resume-instrumentation,useDevServerLogs.resume-instrumentation,useResearch.resume-instrumentation,useBackgroundSessions.resume-instrumentation,useQuickChat,useTasks,useTasks.resume-instrumentation,useChatRooms.resume-instrumentation,useTerminalSessions,useTheme,useToast,useUsageData,useViewState,useMergeAdvanceNotice}.test.{ts,tsx}",
|
"app/hooks/__tests__/{useAgents,useAgentLogs,useAgentLogs.resume-instrumentation,useAppSettings,useAuthOnboarding,useConfirm,useCurrentProject,useNodes,useNodes.resume-instrumentation,useNodeSettingsSync,useProjects,useProjects.resume-instrumentation,useMeshState.resume-instrumentation,useManagedDockerNodes.resume-instrumentation,usePrChecksStream.resume-instrumentation,useDevServerLogs.resume-instrumentation,useResearch.resume-instrumentation,useBackgroundSessions.resume-instrumentation,useQuickChat,useTasks,useTasks.resume-instrumentation,useChatRooms.resume-instrumentation,useTerminalSessions,useTheme,useToast,useUsageData,useViewState,useMergeAdvanceNotice}.test.{ts,tsx}",
|
||||||
|
|||||||
Reference in New Issue
Block a user