Files
fusion/packages/engine/src/__tests__/executor-soft-delete-guard.test.ts
gsxdsm 845d82ec67 fix(ci): restore full-suite after verification request + lucide mock gaps (#2332)
## Summary

Main Full Suite shards have been red after recent landings. Root causes:

1. **Executor tests** — `execute()` now polls
`getTaskVerificationRequestAsync` (chat-enqueued verification). Shared
`createMockStore()` (and soft-delete inline store) lacked the method, so
nearly every execute-path suite failed with `is not a function`.
2. **TaskDetailModal suites** — `NativeStructurePreview` imports `Map` /
`Lightbulb` / `BarChart3` / `Target` / `CircleAlert` from lucide; the
shared TaskDetail lucide mock omitted them, so suites failed at import.
3. **Grok process-lifecycle** — 15s bound stress timed out under
full-suite load without product-bug evidence → quarantined on sight per
AGENTS.md.

## Test plan

- [x] `executor-task-done-blocked`, `executor-fast-mode-workflows`,
concurrent-execute race
- [x] `executor-step-session`, plan-only scope leak, review-step
indexing
- [x] `TaskDetailModal.create-pr` + `TaskDetail.mobile-transition`
- [ ] Full Suite CI on this PR

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Improvements**
* Added `html2canvas` support in the dashboard to enable HTML-to-canvas
rendering needed for visual structure previews.
* **Tests**
* Updated task execution test mocks to handle task verification-request
flows reliably.
* Improved task deletion safeguard coverage and related execution
behavior checks.
* Enhanced test stubs to support structure preview rendering elements
during modal-related tests.
* **Chores**
* Quarantined a timing-sensitive process lifecycle test and refreshed
quarantine tracking to improve full-suite stability.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-19 09:50:22 -07:00

123 lines
4.6 KiB
TypeScript

import "./executor-test-helpers.js";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { Task } from "@fusion/core";
import { TaskExecutor } from "../executor.js";
import { executingTaskLock } from "../active-session-registry.js";
import { executorLog } from "../logger.js";
import * as childProcess from "node:child_process";
import { resetExecutorMocks } from "./executor-test-helpers.js";
function makeTask(overrides: Partial<Task> & Pick<Task, "id">): Task {
return {
id: overrides.id,
title: overrides.title ?? null,
description: overrides.description ?? "desc",
status: overrides.status ?? "open",
column: overrides.column ?? "in-progress",
createdAt: overrides.createdAt ?? "2026-01-01T00:00:00.000Z",
updatedAt: overrides.updatedAt ?? "2026-01-01T00:00:00.000Z",
dependencies: overrides.dependencies ?? [],
comments: overrides.comments ?? [],
steps: overrides.steps ?? [],
currentStep: overrides.currentStep ?? 0,
log: overrides.log ?? [],
assignedAgentId: overrides.assignedAgentId,
checkedOutBy: overrides.checkedOutBy,
checkoutLeaseEpoch: overrides.checkoutLeaseEpoch,
paused: overrides.paused,
deletedAt: overrides.deletedAt,
} as unknown as Task;
}
function createStore(overrides?: { tasks?: Task[] }) {
const listeners = new Map<string, ((payload: unknown) => void)[]>();
const store = {
on: vi.fn((event: string, listener: (payload: unknown) => void) => {
const existing = listeners.get(event) ?? [];
existing.push(listener);
listeners.set(event, existing);
}),
off: vi.fn(),
getSettings: vi.fn().mockResolvedValue({ globalPause: false, enginePaused: false }),
listTasks: vi.fn().mockResolvedValue(overrides?.tasks ?? []),
/*
FNXC:EngineTests 2026-07-17-06:20:
Graph entry captures a tool-failure log cursor (getAgentLogCount + updateTask) before the
soft-delete short-circuit. Stub both so execute() can reach the deletedAt refuse path.
*/
getAgentLogCount: vi.fn().mockResolvedValue(0),
getAgentLogs: vi.fn().mockResolvedValue([]),
// FNXC:TaskVerificationRequest 2026-07-19-04:30: execute() polls pending verification requests.
getTaskVerificationRequestAsync: vi.fn().mockResolvedValue(null),
claimTaskVerificationRequest: vi.fn().mockResolvedValue(null),
finishTaskVerificationRequest: vi.fn().mockResolvedValue(undefined),
// FNXC:TaskVerificationRequest 2026-07-19-12:00: match createMockStore() in executor-test-helpers.ts.
createTaskVerificationRequest: vi.fn().mockResolvedValue(undefined),
updateTask: vi.fn().mockResolvedValue({}),
} as any;
return store;
}
describe("TaskExecutor soft-delete guards", () => {
beforeEach(() => {
resetExecutorMocks();
});
it("refuses to execute soft-deleted tasks even when checked out and releases execution lock", async () => {
const store = createStore();
const executor = new TaskExecutor(store, "/tmp/test");
const warnSpy = vi.spyOn(executorLog, "warn");
const execSyncSpy = vi.spyOn(childProcess, "execSync");
const task = makeTask({
id: "FN-5137",
checkedOutBy: "agent-1",
checkoutLeaseEpoch: 2,
deletedAt: "2026-01-02T00:00:00.000Z",
});
await executor.execute(task);
expect(execSyncSpy).not.toHaveBeenCalled();
expect(warnSpy).toHaveBeenCalledWith("FN-5137: refusing execute — task is soft-deleted");
expect(executingTaskLock.tryClaim(task.id)).toBe(true);
executingTaskLock.release(task.id);
});
it("resumeTaskForAgent skips checked-out soft-deleted tasks", async () => {
const deletedTask = makeTask({
id: "FN-assigned-deleted",
column: "in-progress",
assignedAgentId: "agent-1",
checkedOutBy: "agent-1",
checkoutLeaseEpoch: 3,
paused: false,
deletedAt: "2026-01-03T00:00:00.000Z",
});
const store = createStore({ tasks: [deletedTask] });
const executor = new TaskExecutor(store, "/tmp/test");
const executeSpy = vi.spyOn(executor, "execute");
await executor.resumeTaskForAgent("agent-1");
expect(executeSpy).not.toHaveBeenCalled();
});
it("resumeOrphaned skips checked-out soft-deleted in-progress tasks", async () => {
const deletedTask = makeTask({
id: "FN-deleted",
column: "in-progress",
checkedOutBy: "agent-1",
checkoutLeaseEpoch: 4,
paused: false,
deletedAt: "2026-01-03T00:00:00.000Z",
});
const store = createStore({ tasks: [deletedTask] });
const executor = new TaskExecutor(store, "/tmp/test");
const executeSpy = vi.spyOn(executor, "execute");
await executor.resumeOrphaned();
expect(executeSpy).not.toHaveBeenCalled();
});
});