When the engine restarts mid-step, an in-progress step may have already passed plan + code review but not yet been flipped to done by the agent's next task_update call. Previously, the next executor pass re-entered the step and replayed both reviews — measured at 5-20 min of pure waste per restart (observed in FN-2215 Step 1 and FN-2207 Step 6). recoverApprovedStepsOnResume scans the task log for any in-progress step whose most recent "code review Step N: APPROVE" entry is newer than its most recent "Step N → pending" transition, and marks those steps done before execute() runs. Safely skips steps that were reset after approval (e.g. by a workflow revision) or only received REVISE verdicts. Called from both the engine-restart path (resumeOrphaned) and the unpause path, matching the two places the task log shows as vulnerable to this race. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
238 lines
7.2 KiB
TypeScript
238 lines
7.2 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import type { MenuItemConstructorOptions } from "electron";
|
|
|
|
const mocks = vi.hoisted(() => {
|
|
const menuInstance = { id: "app-menu" };
|
|
|
|
return {
|
|
Menu: {
|
|
buildFromTemplate: vi.fn(() => menuInstance),
|
|
setApplicationMenu: vi.fn(),
|
|
},
|
|
shell: {
|
|
openExternal: vi.fn(() => Promise.resolve()),
|
|
},
|
|
menuInstance,
|
|
};
|
|
});
|
|
|
|
vi.mock("electron", () => ({
|
|
Menu: mocks.Menu,
|
|
shell: mocks.shell,
|
|
}));
|
|
|
|
function createMainWindowMock() {
|
|
return {
|
|
webContents: {
|
|
reload: vi.fn(),
|
|
reloadIgnoringCache: vi.fn(),
|
|
toggleDevTools: vi.fn(),
|
|
getZoomLevel: vi.fn(() => 1),
|
|
setZoomLevel: vi.fn(),
|
|
},
|
|
};
|
|
}
|
|
|
|
function findMenuItem(
|
|
template: MenuItemConstructorOptions[],
|
|
label: string,
|
|
): MenuItemConstructorOptions | undefined {
|
|
for (const item of template) {
|
|
if (item.label === label) {
|
|
return item;
|
|
}
|
|
|
|
if (Array.isArray(item.submenu)) {
|
|
const nested = findMenuItem(item.submenu, label);
|
|
if (nested) {
|
|
return nested;
|
|
}
|
|
}
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
function collectAccelerators(template: MenuItemConstructorOptions[]): string[] {
|
|
const accelerators: string[] = [];
|
|
|
|
for (const item of template) {
|
|
if (typeof item.accelerator === "string") {
|
|
accelerators.push(item.accelerator);
|
|
}
|
|
|
|
if (Array.isArray(item.submenu)) {
|
|
accelerators.push(...collectAccelerators(item.submenu));
|
|
}
|
|
}
|
|
|
|
return accelerators;
|
|
}
|
|
|
|
describe("application menu", () => {
|
|
const originalPlatform = process.platform;
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
Object.defineProperty(process, "platform", {
|
|
value: "darwin",
|
|
configurable: true,
|
|
});
|
|
});
|
|
|
|
afterEach(() => {
|
|
Object.defineProperty(process, "platform", {
|
|
value: originalPlatform,
|
|
configurable: true,
|
|
});
|
|
});
|
|
|
|
it("macOS template includes App menu with About, Preferences and Quit", async () => {
|
|
const { buildMenuTemplate } = await import("../menu.ts");
|
|
const template = buildMenuTemplate({
|
|
mainWindow: createMainWindowMock() as never,
|
|
appName: "Fusion",
|
|
});
|
|
|
|
expect(template[0]?.label).toBe("Fusion");
|
|
|
|
const appMenu = template[0]?.submenu as MenuItemConstructorOptions[];
|
|
expect(appMenu.some((item) => item.label === "About Fusion")).toBe(true);
|
|
expect(appMenu.some((item) => item.label === "Preferences")).toBe(true);
|
|
|
|
const preferences = appMenu.find((item) => item.label === "Preferences");
|
|
const quit = appMenu.find((item) => item.label === "Quit Fusion");
|
|
|
|
expect(preferences?.accelerator).toBe("CmdOrCtrl+,");
|
|
expect(quit?.accelerator).toBe("CmdOrCtrl+Q");
|
|
});
|
|
|
|
it("Edit menu includes standard editing shortcuts", async () => {
|
|
const { buildMenuTemplate } = await import("../menu.ts");
|
|
const template = buildMenuTemplate({
|
|
mainWindow: createMainWindowMock() as never,
|
|
appName: "Fusion",
|
|
});
|
|
|
|
const editMenu = findMenuItem(template, "Edit");
|
|
const editItems = (editMenu?.submenu ?? []) as MenuItemConstructorOptions[];
|
|
|
|
expect(editItems.some((item) => item.label === "Undo" && item.accelerator === "CmdOrCtrl+Z")).toBe(
|
|
true,
|
|
);
|
|
expect(editItems.some((item) => item.label === "Cut" && item.accelerator === "CmdOrCtrl+X")).toBe(true);
|
|
expect(editItems.some((item) => item.label === "Copy" && item.accelerator === "CmdOrCtrl+C")).toBe(true);
|
|
expect(editItems.some((item) => item.label === "Paste" && item.accelerator === "CmdOrCtrl+V")).toBe(
|
|
true,
|
|
);
|
|
expect(
|
|
editItems.some((item) => item.label === "Select All" && item.accelerator === "CmdOrCtrl+A"),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("View menu includes zoom controls and dev tools shortcut", async () => {
|
|
const { buildMenuTemplate } = await import("../menu.ts");
|
|
const template = buildMenuTemplate({
|
|
mainWindow: createMainWindowMock() as never,
|
|
appName: "Fusion",
|
|
});
|
|
|
|
const viewMenu = findMenuItem(template, "View");
|
|
const viewItems = (viewMenu?.submenu ?? []) as MenuItemConstructorOptions[];
|
|
|
|
expect(
|
|
viewItems.some((item) => item.label === "Zoom In" && item.accelerator === "CmdOrCtrl+Plus"),
|
|
).toBe(true);
|
|
expect(viewItems.some((item) => item.label === "Zoom Out" && item.accelerator === "CmdOrCtrl+-")).toBe(
|
|
true,
|
|
);
|
|
expect(viewItems.some((item) => item.label === "Reset Zoom" && item.accelerator === "CmdOrCtrl+0")).toBe(
|
|
true,
|
|
);
|
|
expect(
|
|
viewItems.some(
|
|
(item) => item.label === "Toggle Dev Tools" && item.accelerator === "Alt+CmdOrCtrl+I",
|
|
),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("Window menu includes Minimize and Close shortcuts", async () => {
|
|
const { buildMenuTemplate } = await import("../menu.ts");
|
|
const template = buildMenuTemplate({
|
|
mainWindow: createMainWindowMock() as never,
|
|
appName: "Fusion",
|
|
});
|
|
|
|
const windowMenu = findMenuItem(template, "Window");
|
|
const windowItems = (windowMenu?.submenu ?? []) as MenuItemConstructorOptions[];
|
|
|
|
expect(
|
|
windowItems.some((item) => item.label === "Minimize" && item.accelerator === "CmdOrCtrl+M"),
|
|
).toBe(true);
|
|
expect(windowItems.some((item) => item.label === "Close" && item.accelerator === "CmdOrCtrl+W")).toBe(
|
|
true,
|
|
);
|
|
});
|
|
|
|
it("non-macOS template omits App menu and app-specific labels", async () => {
|
|
Object.defineProperty(process, "platform", {
|
|
value: "win32",
|
|
configurable: true,
|
|
});
|
|
|
|
const { buildMenuTemplate } = await import("../menu.ts");
|
|
const template = buildMenuTemplate({
|
|
mainWindow: createMainWindowMock() as never,
|
|
appName: "Fusion",
|
|
});
|
|
|
|
expect(template[0]?.label).toBe("Edit");
|
|
expect(findMenuItem(template, "About Fusion")).toBeUndefined();
|
|
expect(findMenuItem(template, "Hide Fusion")).toBeUndefined();
|
|
});
|
|
|
|
it("Help menu contains Fusion Documentation link", async () => {
|
|
const { buildMenuTemplate } = await import("../menu.ts");
|
|
const template = buildMenuTemplate({
|
|
mainWindow: createMainWindowMock() as never,
|
|
appName: "Fusion",
|
|
});
|
|
|
|
const docsItem = findMenuItem(template, "Fusion Documentation");
|
|
|
|
expect(docsItem).toBeDefined();
|
|
docsItem?.click?.({} as never, {} as never, {} as never);
|
|
expect(mocks.shell.openExternal).toHaveBeenCalledWith(
|
|
"https://github.com/Runfusion/Fusion#readme",
|
|
);
|
|
});
|
|
|
|
it("all keyboard shortcuts use CmdOrCtrl prefix convention", async () => {
|
|
const { buildMenuTemplate } = await import("../menu.ts");
|
|
const template = buildMenuTemplate({
|
|
mainWindow: createMainWindowMock() as never,
|
|
appName: "Fusion",
|
|
});
|
|
|
|
const accelerators = collectAccelerators(template);
|
|
|
|
for (const accelerator of accelerators) {
|
|
expect(accelerator).not.toMatch(/(^|[+])Cmd\+/);
|
|
expect(accelerator).not.toMatch(/(^|[+])Ctrl\+/);
|
|
}
|
|
});
|
|
|
|
it("buildAppMenu builds and sets the application menu", async () => {
|
|
const { buildAppMenu } = await import("../menu.ts");
|
|
|
|
const menu = buildAppMenu({
|
|
mainWindow: createMainWindowMock() as never,
|
|
appName: "Fusion",
|
|
});
|
|
|
|
expect(mocks.Menu.buildFromTemplate).toHaveBeenCalledTimes(1);
|
|
expect(mocks.Menu.setApplicationMenu).toHaveBeenCalledWith(mocks.menuInstance);
|
|
expect(menu).toBe(mocks.menuInstance);
|
|
});
|
|
});
|