199 lines
6.2 KiB
TypeScript
199 lines
6.2 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, spyOn } from "bun:test";
|
|
import { buildLocalBranchTaskIndex, loadLocalBranchTasks } from "../core/task-loader.ts";
|
|
import type { GitOperations } from "../git/operations.ts";
|
|
import type { Task } from "../types/index.ts";
|
|
|
|
// Mock GitOperations for testing
|
|
class MockGitOperations implements Partial<GitOperations> {
|
|
private currentBranch = "main";
|
|
|
|
async getCurrentBranch(): Promise<string> {
|
|
return this.currentBranch;
|
|
}
|
|
|
|
async listRecentBranches(_daysAgo: number): Promise<string[]> {
|
|
return ["main", "feature-a", "feature-b", "origin/main"];
|
|
}
|
|
|
|
async getBranchLastModifiedMap(_ref: string, _dir: string): Promise<Map<string, Date>> {
|
|
const map = new Map<string, Date>();
|
|
map.set("backlog/tasks/task-1 - Main Task.md", new Date("2025-06-13"));
|
|
map.set("backlog/tasks/task-2 - Feature Task.md", new Date("2025-06-13"));
|
|
map.set("backlog/tasks/task-3 - New Task.md", new Date("2025-06-13"));
|
|
return map;
|
|
}
|
|
|
|
async listFilesInTree(ref: string, _path: string): Promise<string[]> {
|
|
// Main branch has task-1 and task-2
|
|
if (ref === "main") {
|
|
return ["backlog/tasks/task-1 - Main Task.md", "backlog/tasks/task-2 - Feature Task.md"];
|
|
}
|
|
// feature-a has task-1 and task-3 (task-3 is new)
|
|
if (ref === "feature-a") {
|
|
return ["backlog/tasks/task-1 - Main Task.md", "backlog/tasks/task-3 - New Task.md"];
|
|
}
|
|
// feature-b has task-2
|
|
if (ref === "feature-b") {
|
|
return ["backlog/tasks/task-2 - Feature Task.md"];
|
|
}
|
|
return [];
|
|
}
|
|
|
|
async showFile(_ref: string, file: string): Promise<string> {
|
|
if (file.includes("task-1")) {
|
|
return `---
|
|
id: task-1
|
|
title: Main Task
|
|
status: To Do
|
|
assignee: []
|
|
created_date: 2025-06-13
|
|
labels: []
|
|
dependencies: []
|
|
---\n\n## Description\n\nMain task`;
|
|
}
|
|
if (file.includes("task-2")) {
|
|
return `---
|
|
id: task-2
|
|
title: Feature Task
|
|
status: In Progress
|
|
assignee: []
|
|
created_date: 2025-06-13
|
|
labels: []
|
|
dependencies: []
|
|
---\n\n## Description\n\nFeature task`;
|
|
}
|
|
if (file.includes("task-3")) {
|
|
return `---
|
|
id: task-3
|
|
title: New Task
|
|
status: To Do
|
|
assignee: []
|
|
created_date: 2025-06-13
|
|
labels: []
|
|
dependencies: []
|
|
---\n\n## Description\n\nNew task from feature-a branch`;
|
|
}
|
|
return "";
|
|
}
|
|
}
|
|
|
|
describe("Local branch task discovery", () => {
|
|
let consoleDebugSpy: ReturnType<typeof spyOn>;
|
|
|
|
beforeEach(() => {
|
|
consoleDebugSpy = spyOn(console, "debug");
|
|
});
|
|
|
|
afterEach(() => {
|
|
consoleDebugSpy?.mockRestore();
|
|
});
|
|
|
|
describe("buildLocalBranchTaskIndex", () => {
|
|
it("should build index from local branches excluding current branch", async () => {
|
|
const mockGit = new MockGitOperations() as unknown as GitOperations;
|
|
const branches = ["main", "feature-a", "feature-b", "origin/main"];
|
|
|
|
const index = await buildLocalBranchTaskIndex(mockGit, branches, "main", "backlog");
|
|
|
|
// Should find task-3 from feature-a (not in main)
|
|
expect(index.has("task-3")).toBe(true);
|
|
const task3Entries = index.get("task-3");
|
|
expect(task3Entries?.length).toBe(1);
|
|
expect(task3Entries?.[0]?.branch).toBe("feature-a");
|
|
|
|
// Should find task-1 and task-2 from other branches
|
|
expect(index.has("task-1")).toBe(true);
|
|
expect(index.has("task-2")).toBe(true);
|
|
});
|
|
|
|
it("should exclude origin/ branches", async () => {
|
|
const mockGit = new MockGitOperations() as unknown as GitOperations;
|
|
const branches = ["main", "feature-a", "origin/feature-a"];
|
|
|
|
const index = await buildLocalBranchTaskIndex(mockGit, branches, "main", "backlog");
|
|
|
|
// Should only have entries from feature-a (local), not origin/feature-a
|
|
const task1Entries = index.get("task-1");
|
|
expect(task1Entries?.every((e) => e.branch === "feature-a")).toBe(true);
|
|
});
|
|
|
|
it("should exclude current branch", async () => {
|
|
const mockGit = new MockGitOperations() as unknown as GitOperations;
|
|
const branches = ["main", "feature-a"];
|
|
|
|
const index = await buildLocalBranchTaskIndex(mockGit, branches, "main", "backlog");
|
|
|
|
// task-1 should only be from feature-a, not main
|
|
const task1Entries = index.get("task-1");
|
|
expect(task1Entries?.every((e) => e.branch !== "main")).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("loadLocalBranchTasks", () => {
|
|
it("should discover tasks from other local branches", async () => {
|
|
const mockGit = new MockGitOperations() as unknown as GitOperations;
|
|
|
|
const progressMessages: string[] = [];
|
|
const localBranchTasks = await loadLocalBranchTasks(mockGit, null, (msg: string) => {
|
|
progressMessages.push(msg);
|
|
});
|
|
|
|
// Should find task-3 which only exists in feature-a
|
|
const task3 = localBranchTasks.find((t) => t.id === "task-3");
|
|
expect(task3).toBeDefined();
|
|
expect(task3?.title).toBe("New Task");
|
|
expect(task3?.source).toBe("local-branch");
|
|
expect(task3?.branch).toBe("feature-a");
|
|
|
|
// Progress should mention other local branches
|
|
expect(progressMessages.some((msg) => msg.includes("other local branches"))).toBe(true);
|
|
});
|
|
|
|
it("should skip tasks that exist in filesystem when provided", async () => {
|
|
const mockGit = new MockGitOperations() as unknown as GitOperations;
|
|
|
|
// Simulate that task-1 already exists in filesystem
|
|
const localTasks: Task[] = [
|
|
{
|
|
id: "task-1",
|
|
title: "Main Task (local)",
|
|
status: "To Do",
|
|
assignee: [],
|
|
createdDate: "2025-06-13",
|
|
labels: [],
|
|
dependencies: [],
|
|
source: "local",
|
|
},
|
|
];
|
|
|
|
const localBranchTasks = await loadLocalBranchTasks(mockGit, null, undefined, localTasks);
|
|
|
|
// task-3 should be found (not in local tasks)
|
|
expect(localBranchTasks.some((t) => t.id === "task-3")).toBe(true);
|
|
|
|
// task-1 should not be hydrated since it exists locally
|
|
// (unless the remote version is newer, which in this mock it's not)
|
|
// The behavior depends on whether the remote version is newer
|
|
});
|
|
|
|
it("should return empty array when on detached HEAD", async () => {
|
|
const mockGit = {
|
|
getCurrentBranch: async () => "",
|
|
} as unknown as GitOperations;
|
|
|
|
const tasks = await loadLocalBranchTasks(mockGit, null);
|
|
expect(tasks).toEqual([]);
|
|
});
|
|
|
|
it("should return empty when only current branch exists", async () => {
|
|
const mockGit = {
|
|
getCurrentBranch: async () => "main",
|
|
listRecentBranches: async () => ["main"],
|
|
} as unknown as GitOperations;
|
|
|
|
const tasks = await loadLocalBranchTasks(mockGit, null);
|
|
expect(tasks).toEqual([]);
|
|
});
|
|
});
|
|
});
|