Skip to content

Commit 2419a20

Browse files
that-github-userunknownclaude
authored
Add unit tests for display utilities and git operations (#46)
- 9 tests for padRight (ANSI handling, edge cases) and formatDuration - 6 tests for git lifecycle: getRepoRoot, createWorktree, getDiff, getDiffStats, removeWorktree, cleanupBranches - Export padRight and formatDuration for testability - Total: 56 tests passing (up from 41) Closes #23 Co-authored-by: unknown <that-github-user@github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 08fc8f2 commit 2419a20

File tree

3 files changed

+108
-2
lines changed

3 files changed

+108
-2
lines changed

src/utils/display.test.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import assert from "node:assert/strict";
2+
import { describe, it } from "node:test";
3+
import { formatDuration, padRight } from "./display.js";
4+
5+
describe("padRight", () => {
6+
it("pads plain text to target length", () => {
7+
assert.equal(padRight("hello", 10), "hello ");
8+
});
9+
10+
it("does not pad if already at target length", () => {
11+
assert.equal(padRight("hello", 5), "hello");
12+
});
13+
14+
it("does not truncate if longer than target", () => {
15+
assert.equal(padRight("hello world", 5), "hello world");
16+
});
17+
18+
it("handles ANSI codes in length calculation", () => {
19+
const colored = "\x1b[32mok\x1b[0m"; // "ok" in green
20+
const padded = padRight(colored, 6);
21+
// Visual length is 2 ("ok"), so should add 4 spaces
22+
assert.ok(padded.endsWith(" "));
23+
});
24+
25+
it("handles empty string", () => {
26+
assert.equal(padRight("", 5), " ");
27+
});
28+
});
29+
30+
describe("formatDuration", () => {
31+
it("formats seconds", () => {
32+
assert.equal(formatDuration(5000), "5s");
33+
assert.equal(formatDuration(45000), "45s");
34+
});
35+
36+
it("formats minutes and seconds", () => {
37+
assert.equal(formatDuration(65000), "1m5s");
38+
assert.equal(formatDuration(125000), "2m5s");
39+
});
40+
41+
it("handles zero", () => {
42+
assert.equal(formatDuration(0), "0s");
43+
});
44+
45+
it("rounds to nearest second", () => {
46+
assert.equal(formatDuration(1499), "1s");
47+
assert.equal(formatDuration(1500), "2s");
48+
});
49+
});

src/utils/display.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,15 +99,15 @@ export function displayApplyInstructions(result: EnsembleResult): void {
9999
console.log();
100100
}
101101

102-
function padRight(str: string, len: number): string {
102+
export function padRight(str: string, len: number): string {
103103
// Strip ANSI codes for length calculation
104104
// biome-ignore lint/suspicious/noControlCharactersInRegex: intentional ANSI escape sequence matching
105105
const stripped = str.replace(/\x1b\[[0-9;]*m/g, "");
106106
const padding = Math.max(0, len - stripped.length);
107107
return str + " ".repeat(padding);
108108
}
109109

110-
function formatDuration(ms: number): string {
110+
export function formatDuration(ms: number): string {
111111
const seconds = Math.round(ms / 1000);
112112
if (seconds < 60) return `${seconds}s`;
113113
const minutes = Math.floor(seconds / 60);

src/utils/git.test.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import assert from "node:assert/strict";
2+
import { after, before, describe, it } from "node:test";
3+
import {
4+
cleanupBranches,
5+
createWorktree,
6+
getDiff,
7+
getDiffStats,
8+
getRepoRoot,
9+
removeWorktree,
10+
} from "./git.js";
11+
12+
describe("getRepoRoot", () => {
13+
it("returns the repository root", async () => {
14+
const root = await getRepoRoot();
15+
assert.ok(root.length > 0);
16+
assert.ok(root.includes("thinktank"));
17+
});
18+
});
19+
20+
describe("worktree lifecycle", () => {
21+
let worktreePath: string;
22+
23+
it("creates a worktree", async () => {
24+
worktreePath = await createWorktree(99);
25+
assert.ok(worktreePath.length > 0);
26+
assert.ok(worktreePath.includes("thinktank-agent-99"));
27+
});
28+
29+
it("gets empty diff for unchanged worktree", async () => {
30+
const diff = await getDiff(worktreePath);
31+
assert.equal(diff, "");
32+
});
33+
34+
it("gets empty diff stats for unchanged worktree", async () => {
35+
const stats = await getDiffStats(worktreePath);
36+
assert.deepEqual(stats.filesChanged, []);
37+
assert.equal(stats.linesAdded, 0);
38+
assert.equal(stats.linesRemoved, 0);
39+
});
40+
41+
it("removes the worktree", async () => {
42+
await removeWorktree(worktreePath);
43+
// Should not throw
44+
});
45+
46+
after(async () => {
47+
// Clean up any leftover branches
48+
await cleanupBranches().catch(() => {});
49+
});
50+
});
51+
52+
describe("cleanupBranches", () => {
53+
it("runs without error even when no thinktank branches exist", async () => {
54+
await cleanupBranches();
55+
// Should not throw
56+
});
57+
});

0 commit comments

Comments
 (0)