taskgenius_taskgenius-plugin/src/__tests__/MoveActionExecutor.canvas.test.ts
Quorafind f28ea8e9c0 refactor(v10): modularize entry + gantt year-view and project grouping fixes
Decompose the monolithic plugin entry into bootstrap modules and extracted
pure functions (src/bootstrap, src/modules), advance dataflow project
resolution, and reconcile the Phase 0 contract tests.

Fixes bundled in:
- gantt: fix year-zoom timeline so year/month labels render and a full year
  fits on screen. Major-label gate no longer keyed on single-day width; add
  a Year minor-label branch; formatMinorTick('Year') returns a real month
  name; lower MIN_DAY_WIDTH to 2; drop per-render logging; register the
  offscreen-indicator click handlers once to stop a listener leak.
- project: stop the Projects view fragmenting into one task per file.
  determineTgProject() gains an applyDefaultNaming option (default true,
  preserving the public API / File Source / unit tests); the dataflow cache
  and worker-sync paths pass false so they match ProjectData.worker.
- build: restore RegExpCursor in progress-bar-widget (the SearchCursor
  migration was incomplete -- SearchCursor cannot do regex search).

Pre-commit hook (npm run build) verified manually; hook could not spawn on
this Windows Git Bash setup. Tests: 1718 passing; production build green.
2026-06-28 16:09:40 +08:00

473 lines
13 KiB
TypeScript

/**
* MoveActionExecutor Canvas Tests
*
* Tests for Canvas task movement functionality including:
* - Moving Canvas tasks between Canvas files
* - Moving Canvas tasks to Markdown files
* - Cross-format task movement
* - Error handling and validation
*/
import { MoveActionExecutor } from "../executors/completion/move-executor";
import {
OnCompletionActionType,
OnCompletionExecutionContext,
OnCompletionMoveConfig,
} from "../types/onCompletion";
import { Task, CanvasTaskMetadata } from "../types/task";
import { createMockPlugin, createMockApp } from "./mockUtils";
// Mock Canvas task updater
const mockCanvasTaskUpdater = {
moveCanvasTask: jest.fn(),
deleteCanvasTask: jest.fn(),
};
// Mock plugin
const mockPlugin = {
...createMockPlugin(),
} as any;
mockPlugin.writeAPI.canvasTaskUpdater = mockCanvasTaskUpdater;
// Mock vault
const mockVault = {
getAbstractFileByPath: jest.fn(),
getFileByPath: jest.fn(),
read: jest.fn(),
modify: jest.fn(),
create: jest.fn(),
};
const mockApp = {
...createMockApp(),
vault: mockVault,
} as any;
describe("MoveActionExecutor - Canvas Tasks", () => {
let executor: MoveActionExecutor;
let mockContext: OnCompletionExecutionContext;
beforeEach(() => {
executor = new MoveActionExecutor();
// Reset mocks
jest.clearAllMocks();
// Reset all vault method mocks to default behavior
mockVault.getAbstractFileByPath.mockReset();
mockVault.getFileByPath.mockReset();
mockVault.read.mockReset();
mockVault.modify.mockReset();
mockVault.create.mockReset();
// Reset Canvas task updater mocks
mockCanvasTaskUpdater.moveCanvasTask.mockReset();
mockCanvasTaskUpdater.deleteCanvasTask.mockReset();
});
describe("Canvas to Canvas Movement", () => {
it("should successfully move Canvas task to another Canvas file", async () => {
const canvasTask: Task<CanvasTaskMetadata> = {
id: "canvas-task-1",
content: "Test Canvas task",
filePath: "source.canvas",
line: 0,
completed: true,
status: "x",
originalMarkdown: "- [x] Test Canvas task",
metadata: {
sourceType: "canvas",
canvasNodeId: "node-1",
tags: [],
children: [],
},
};
const moveConfig: OnCompletionMoveConfig = {
type: OnCompletionActionType.MOVE,
targetFile: "target.canvas",
targetSection: "Completed Tasks",
};
mockContext = {
task: canvasTask,
plugin: mockPlugin as any,
app: mockApp as any,
};
// Mock successful move
mockCanvasTaskUpdater.moveCanvasTask.mockResolvedValue({
success: true,
});
const result = await executor.execute(mockContext, moveConfig);
expect(result.success).toBe(true);
expect(result.message).toContain(
"Task moved to Canvas file target.canvas"
);
expect(result.message).toContain("section: Completed Tasks");
expect(mockCanvasTaskUpdater.moveCanvasTask).toHaveBeenCalledWith(
canvasTask,
"target.canvas",
undefined,
"Completed Tasks"
);
});
it("should handle Canvas to Canvas move failure", async () => {
const canvasTask: Task<CanvasTaskMetadata> = {
id: "canvas-task-2",
content: "Test Canvas task",
filePath: "source.canvas",
line: 0,
completed: true,
status: "x",
originalMarkdown: "- [x] Test Canvas task",
metadata: {
sourceType: "canvas",
canvasNodeId: "node-1",
tags: [],
children: [],
},
};
const moveConfig: OnCompletionMoveConfig = {
type: OnCompletionActionType.MOVE,
targetFile: "target.canvas",
};
mockContext = {
task: canvasTask,
plugin: mockPlugin as any,
app: mockApp as any,
};
// Mock move failure
mockCanvasTaskUpdater.moveCanvasTask.mockResolvedValue({
success: false,
error: "Target Canvas file not found",
});
const result = await executor.execute(mockContext, moveConfig);
expect(result.success).toBe(false);
expect(result.error).toContain("Target Canvas file not found");
});
});
describe("Canvas to Markdown Movement", () => {
it("should successfully move Canvas task to Markdown file", async () => {
const canvasTask: Task<CanvasTaskMetadata> = {
id: "canvas-task-3",
content: "Test Canvas task",
filePath: "source.canvas",
line: 0,
completed: true,
status: "x",
originalMarkdown: "- [x] Test Canvas task #project/test",
metadata: {
sourceType: "canvas",
canvasNodeId: "node-1",
tags: ["#project/test"],
children: [],
},
};
const moveConfig: OnCompletionMoveConfig = {
type: OnCompletionActionType.MOVE,
targetFile: "target.md",
targetSection: "Completed Tasks",
};
mockContext = {
task: canvasTask,
plugin: mockPlugin as any,
app: mockApp as any,
};
// Mock successful Canvas deletion
mockCanvasTaskUpdater.deleteCanvasTask.mockResolvedValue({
success: true,
});
// Mock target file exists
const mockTargetFile = { path: "target.md" };
mockVault.getFileByPath.mockReturnValue(mockTargetFile);
mockVault.getAbstractFileByPath.mockReturnValue(mockTargetFile);
mockVault.read.mockResolvedValue(
"# Target File\n\n## Completed Tasks\n\n"
);
mockVault.modify.mockResolvedValue(undefined);
const result = await executor.execute(mockContext, moveConfig);
expect(result.success).toBe(true);
expect(result.message).toContain(
"Task moved from Canvas to target.md"
);
expect(result.message).toContain("section: Completed Tasks");
expect(mockCanvasTaskUpdater.deleteCanvasTask).toHaveBeenCalledWith(
canvasTask
);
expect(mockVault.modify).toHaveBeenCalled();
});
it("should create target Markdown file if it does not exist", async () => {
const canvasTask: Task<CanvasTaskMetadata> = {
id: "canvas-task-4",
content: "Test Canvas task",
filePath: "source.canvas",
line: 0,
completed: true,
status: "x",
originalMarkdown: "- [x] Test Canvas task",
metadata: {
sourceType: "canvas",
canvasNodeId: "node-1",
tags: [],
children: [],
},
};
const moveConfig: OnCompletionMoveConfig = {
type: OnCompletionActionType.MOVE,
targetFile: "new-target.md",
};
mockContext = {
task: canvasTask,
plugin: mockPlugin as any,
app: mockApp as any,
};
// Mock successful Canvas deletion
mockCanvasTaskUpdater.deleteCanvasTask.mockResolvedValue({
success: true,
});
// Mock target file does not exist initially, then gets created
const mockCreatedFile = { path: "new-target.md" };
mockVault.getFileByPath
.mockReturnValueOnce(null) // File doesn't exist initially
.mockReturnValueOnce(mockCreatedFile); // File exists after creation
mockVault.getAbstractFileByPath.mockReturnValue(null);
mockVault.create.mockResolvedValue(mockCreatedFile);
mockVault.read.mockResolvedValue("");
mockVault.modify.mockResolvedValue(undefined);
const result = await executor.execute(mockContext, moveConfig);
expect(result.success).toBe(true);
expect(mockVault.create).toHaveBeenCalledWith("new-target.md", "");
expect(mockVault.modify).toHaveBeenCalled();
});
it("should preserve task when target file creation fails", async () => {
const canvasTask: Task<CanvasTaskMetadata> = {
id: "canvas-task-preserve",
content: "Test Canvas task",
filePath: "source.canvas",
line: 0,
completed: true,
status: "x",
originalMarkdown: "- [x] Test Canvas task",
metadata: {
sourceType: "canvas",
canvasNodeId: "node-preserve",
tags: [],
children: [],
},
};
const moveConfig: OnCompletionMoveConfig = {
type: OnCompletionActionType.MOVE,
targetFile: "invalid/path/target.md",
};
mockContext = {
task: canvasTask,
plugin: mockPlugin as any,
app: mockApp as any,
};
// Mock target file does not exist and creation fails
mockVault.getFileByPath.mockReturnValue(null);
mockVault.getAbstractFileByPath.mockReturnValue(null);
mockVault.create.mockRejectedValue(new Error("Invalid path"));
const result = await executor.execute(mockContext, moveConfig);
expect(result.success).toBe(false);
expect(result.error).toContain(
"Failed to create target file: invalid/path/target.md"
);
// Verify that deleteCanvasTask was NOT called since move failed
expect(
mockCanvasTaskUpdater.deleteCanvasTask
).not.toHaveBeenCalled();
});
it("should handle Canvas deletion failure after successful move", async () => {
const canvasTask: Task<CanvasTaskMetadata> = {
id: "canvas-task-5",
content: "Test Canvas task",
filePath: "source.canvas",
line: 0,
completed: true,
status: "x",
originalMarkdown: "- [x] Test Canvas task",
metadata: {
sourceType: "canvas",
canvasNodeId: "node-1",
tags: [],
children: [],
},
};
const moveConfig: OnCompletionMoveConfig = {
type: OnCompletionActionType.MOVE,
targetFile: "target.md",
};
mockContext = {
task: canvasTask,
plugin: mockPlugin as any,
app: mockApp as any,
};
// Mock successful target file operations but Canvas deletion failure
const mockTargetFile = { path: "target.md" };
mockVault.getFileByPath.mockReturnValue(mockTargetFile);
mockVault.getAbstractFileByPath.mockReturnValue(mockTargetFile);
mockVault.read.mockResolvedValue("# Target File\n\n");
mockVault.modify.mockResolvedValue(undefined);
mockCanvasTaskUpdater.deleteCanvasTask.mockResolvedValue({
success: false,
error: "Canvas node not found",
});
const result = await executor.execute(mockContext, moveConfig);
expect(result.success).toBe(false);
expect(result.error).toContain(
"Task moved successfully to target.md, but failed to remove from Canvas: Canvas node not found"
);
// Verify that target file was modified first
expect(mockVault.modify).toHaveBeenCalled();
});
it("should handle target file creation failure", async () => {
const canvasTask: Task<CanvasTaskMetadata> = {
id: "canvas-task-6",
content: "Test Canvas task",
filePath: "source.canvas",
line: 0,
completed: true,
status: "x",
originalMarkdown: "- [x] Test Canvas task",
metadata: {
sourceType: "canvas",
canvasNodeId: "node-1",
tags: [],
children: [],
},
};
const moveConfig: OnCompletionMoveConfig = {
type: OnCompletionActionType.MOVE,
targetFile: "invalid/path/target.md",
};
mockContext = {
task: canvasTask,
plugin: mockPlugin as any,
app: mockApp as any,
};
// Mock target file does not exist and creation fails
mockVault.getFileByPath.mockReturnValue(null);
mockVault.getAbstractFileByPath.mockReturnValue(null);
mockVault.create.mockRejectedValue(new Error("Invalid path"));
const result = await executor.execute(mockContext, moveConfig);
expect(result.success).toBe(false);
expect(result.error).toContain(
"Failed to create target file: invalid/path/target.md"
);
});
});
describe("Configuration Validation", () => {
it("should validate correct move configuration", () => {
const validConfig: OnCompletionMoveConfig = {
type: OnCompletionActionType.MOVE,
targetFile: "target.canvas",
};
const isValid = executor["validateConfig"](validConfig);
expect(isValid).toBe(true);
});
it("should reject invalid configuration", async () => {
const invalidConfig = {
type: OnCompletionActionType.MOVE,
targetFile: "", // Empty target file
} as OnCompletionMoveConfig;
const canvasTask: Task<CanvasTaskMetadata> = {
id: "canvas-task-7",
content: "Test task",
filePath: "source.canvas",
line: 0,
completed: true,
status: "x",
originalMarkdown: "- [x] Test task",
metadata: {
sourceType: "canvas",
canvasNodeId: "node-1",
tags: [],
children: [],
},
};
mockContext = {
task: canvasTask,
plugin: mockPlugin as any,
app: mockApp as any,
};
const result = await executor.execute(mockContext, invalidConfig);
expect(result.success).toBe(false);
expect(result.error).toContain("Invalid configuration");
});
});
describe("Description Generation", () => {
it("should generate correct description with section", () => {
const config: OnCompletionMoveConfig = {
type: OnCompletionActionType.MOVE,
targetFile: "archive.canvas",
targetSection: "Completed Tasks",
};
const description = executor.getDescription(config);
expect(description).toBe(
"Move task to archive.canvas (section: Completed Tasks)"
);
});
it("should generate correct description without section", () => {
const config: OnCompletionMoveConfig = {
type: OnCompletionActionType.MOVE,
targetFile: "archive.md",
};
const description = executor.getDescription(config);
expect(description).toBe("Move task to archive.md");
});
});
});