import { describe, expect, it } from "vitest"; import type { FileStatus } from "@/bindings"; import { aggregateCheck, buildChangeTree, commitRepos, initialCheck, planCommit, type CheckState, type ChangeNode, } from "../changes"; import { rootOf } from "../model"; const fs = (rel: string, staged: FileStatus["staged"], unstaged: FileStatus["unstaged"]): FileStatus => ({ path: `/r/${rel}`, relPath: rel, staged, unstaged, renamedFrom: null, }); const status = [ fs("src/main/kotlin/App.kt", null, "modified"), fs("src/main/kotlin/Partial.kt", "modified", "modified"), fs("src/test/AppTest.kt", "added", null), fs("README.md", "modified", null), fs("notes.txt", null, "untracked"), fs("conf.yml", null, "conflicted"), fs("out/", null, "ignored"), ]; const shape = (nodes: ChangeNode[], depth = 0): string[] => nodes.flatMap((n) => [`${" ".repeat(depth)}${n.label}`, ...shape(n.children ?? [], depth + 1)]); describe("buildChangeTree", () => { it("groups conflicts, changes and unversioned files (flat)", () => { expect(shape(buildChangeTree(status, "/r", false))).toEqual([ "Merge Conflicts", " conf.yml", "Changes", " App.kt", " AppTest.kt", " Partial.kt", " README.md", "Unversioned Files", " notes.txt", ]); }); it("groups by directory with compacted chains", () => { const changes = buildChangeTree(status, "/r", true).find((n) => n.group === "changes")!; expect(shape([changes])).toEqual([ "Changes", " src", " main/kotlin", " App.kt", " Partial.kt", " test", " AppTest.kt", " README.md", ]); expect(changes.children![0]!.files).toHaveLength(3); }); }); describe("several repositories", () => { const nested = [...status, fs("libs/core/Lib.kt", null, "modified"), fs("libs/core/new.txt", null, "untracked")]; const repos = ["/r", "/r/libs/core"]; it("groups each change group by repository", () => { expect(shape(buildChangeTree(nested, "/r", false, undefined, repos))).toEqual([ "Merge Conflicts", " r", " conf.yml", "Changes", " r", " App.kt", " AppTest.kt", " Partial.kt", " README.md", " libs/core", " Lib.kt", "Unversioned Files", " r", " notes.txt", " libs/core", " new.txt", ]); const changes = buildChangeTree(nested, "/r", true, undefined, repos)[1]!; expect(changes.children!.map((n) => [n.label, n.path])).toEqual([ ["r", "/r"], ["libs/core", "/r/libs/core"], ]); expect(shape(changes.children![1]!.children!)).toEqual(["Lib.kt"]); // One repository: no repository level. const single = buildChangeTree(status, "/r", false, undefined, ["/r"]); expect(shape(single)).toEqual(shape(buildChangeTree(status, "/r", false))); }); it("commits every repository with included changes, the selected one last", () => { const included = [nested[0]!, nested[7]!]; expect(commitRepos(included, repos, "/r")).toEqual(["/r/libs/core", "/r"]); expect(commitRepos(included.slice(0, 1), repos, "/r/libs/core")).toEqual(["/r"]); expect(commitRepos([], repos, "/r/libs/core")).toEqual(["/r/libs/core"]); }); it("routes a nested repository's own entry (submodule) to the enclosing repository", () => { expect(rootOf("/r/libs/core/Lib.kt", repos)).toBe("/r/libs/core"); expect(rootOf("/r/libs/core", repos)).toBe("/r"); expect(rootOf("/r/libs/core/", repos)).toBe("/r"); expect(rootOf("/r", repos)).toBe("/r"); expect(rootOf("/other", repos)).toBeNull(); }); }); describe("commit inclusion", () => { it("defaults: tracked changes included, partial files keep their staged part, unversioned excluded", () => { expect(initialCheck(status[0]!)).toBe("all"); expect(initialCheck(status[1]!)).toBe("staged"); expect(initialCheck(status[4]!)).toBe("none"); expect(initialCheck(status[5]!)).toBe("none"); }); it("plans staging so the index holds exactly the checked changes", () => { const checks = new Map([ ["/r/README.md", "none"], ["/r/notes.txt", "all"], ]); const plan = planCommit(status, checks); expect(plan.stage.sort()).toEqual(["/r/notes.txt", "/r/src/main/kotlin/App.kt"]); expect(plan.unstage).toEqual(["/r/README.md"]); expect(plan.included.map((f) => f.relPath).sort()).toEqual([ "notes.txt", "src/main/kotlin/App.kt", "src/main/kotlin/Partial.kt", "src/test/AppTest.kt", ]); }); it("aggregates checkbox state for groups", () => { const checks = new Map(); expect(aggregateCheck([status[0]!, status[3]!], checks)).toMatchObject({ checked: true, indeterminate: false }); expect(aggregateCheck([status[0]!, status[4]!], checks)).toMatchObject({ checked: false, indeterminate: true }); expect(aggregateCheck([status[1]!], checks)).toMatchObject({ checked: false, indeterminate: true }); }); });