mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-15 12:55:51 +00:00
test: 添加 Utils 纯函数单元测试 (测试计划 02)
覆盖 xml, hash, stringUtils, semver, uuid, format, frontmatterParser, file, glob, diff 共 10 个模块的纯函数测试。 json.ts 因模块加载链路过重暂跳过。 共 190 个测试用例(含已有 array/set)全部通过。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
77
src/utils/__tests__/diff.test.ts
Normal file
77
src/utils/__tests__/diff.test.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { adjustHunkLineNumbers, getPatchFromContents } from "../diff";
|
||||
|
||||
describe("adjustHunkLineNumbers", () => {
|
||||
test("shifts hunk line numbers by offset", () => {
|
||||
const hunks = [
|
||||
{ oldStart: 1, oldLines: 3, newStart: 1, newLines: 4, lines: [" a", "-b", "+c", "+d", " e"] },
|
||||
] as any[];
|
||||
const result = adjustHunkLineNumbers(hunks, 10);
|
||||
expect(result[0].oldStart).toBe(11);
|
||||
expect(result[0].newStart).toBe(11);
|
||||
});
|
||||
|
||||
test("returns original hunks for zero offset", () => {
|
||||
const hunks = [
|
||||
{ oldStart: 5, oldLines: 2, newStart: 5, newLines: 2, lines: [] },
|
||||
] as any[];
|
||||
const result = adjustHunkLineNumbers(hunks, 0);
|
||||
expect(result).toBe(hunks); // same reference
|
||||
});
|
||||
|
||||
test("handles negative offset", () => {
|
||||
const hunks = [
|
||||
{ oldStart: 10, oldLines: 2, newStart: 10, newLines: 2, lines: [] },
|
||||
] as any[];
|
||||
const result = adjustHunkLineNumbers(hunks, -5);
|
||||
expect(result[0].oldStart).toBe(5);
|
||||
expect(result[0].newStart).toBe(5);
|
||||
});
|
||||
|
||||
test("handles empty hunks array", () => {
|
||||
expect(adjustHunkLineNumbers([], 10)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getPatchFromContents", () => {
|
||||
test("returns hunks for different content", () => {
|
||||
const hunks = getPatchFromContents({
|
||||
filePath: "test.txt",
|
||||
oldContent: "hello\nworld",
|
||||
newContent: "hello\nplanet",
|
||||
});
|
||||
expect(hunks.length).toBeGreaterThan(0);
|
||||
expect(hunks[0].lines.some((l: string) => l.startsWith("-"))).toBe(true);
|
||||
expect(hunks[0].lines.some((l: string) => l.startsWith("+"))).toBe(true);
|
||||
});
|
||||
|
||||
test("returns empty hunks for identical content", () => {
|
||||
const hunks = getPatchFromContents({
|
||||
filePath: "test.txt",
|
||||
oldContent: "same content",
|
||||
newContent: "same content",
|
||||
});
|
||||
expect(hunks).toEqual([]);
|
||||
});
|
||||
|
||||
test("handles content with ampersands", () => {
|
||||
const hunks = getPatchFromContents({
|
||||
filePath: "test.txt",
|
||||
oldContent: "a & b",
|
||||
newContent: "a & c",
|
||||
});
|
||||
expect(hunks.length).toBeGreaterThan(0);
|
||||
// Verify ampersands are unescaped in the output
|
||||
const allLines = hunks.flatMap((h: any) => h.lines);
|
||||
expect(allLines.some((l: string) => l.includes("&"))).toBe(true);
|
||||
});
|
||||
|
||||
test("handles empty old content (new file)", () => {
|
||||
const hunks = getPatchFromContents({
|
||||
filePath: "test.txt",
|
||||
oldContent: "",
|
||||
newContent: "new content",
|
||||
});
|
||||
expect(hunks.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
95
src/utils/__tests__/file.test.ts
Normal file
95
src/utils/__tests__/file.test.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import {
|
||||
convertLeadingTabsToSpaces,
|
||||
addLineNumbers,
|
||||
stripLineNumberPrefix,
|
||||
pathsEqual,
|
||||
normalizePathForComparison,
|
||||
} from "../file";
|
||||
|
||||
describe("convertLeadingTabsToSpaces", () => {
|
||||
test("converts leading tabs to 2 spaces each", () => {
|
||||
expect(convertLeadingTabsToSpaces("\t\thello")).toBe(" hello");
|
||||
});
|
||||
|
||||
test("only converts leading tabs", () => {
|
||||
expect(convertLeadingTabsToSpaces("\thello\tworld")).toBe(" hello\tworld");
|
||||
});
|
||||
|
||||
test("returns unchanged if no tabs", () => {
|
||||
expect(convertLeadingTabsToSpaces("no tabs")).toBe("no tabs");
|
||||
});
|
||||
|
||||
test("handles empty string", () => {
|
||||
expect(convertLeadingTabsToSpaces("")).toBe("");
|
||||
});
|
||||
|
||||
test("handles multiline content", () => {
|
||||
const input = "\tline1\n\t\tline2\nline3";
|
||||
const expected = " line1\n line2\nline3";
|
||||
expect(convertLeadingTabsToSpaces(input)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("addLineNumbers", () => {
|
||||
test("adds line numbers starting from 1", () => {
|
||||
const result = addLineNumbers({ content: "a\nb\nc", startLine: 1 });
|
||||
expect(result).toContain("1");
|
||||
expect(result).toContain("a");
|
||||
expect(result).toContain("b");
|
||||
expect(result).toContain("c");
|
||||
});
|
||||
|
||||
test("returns empty string for empty content", () => {
|
||||
expect(addLineNumbers({ content: "", startLine: 1 })).toBe("");
|
||||
});
|
||||
|
||||
test("respects startLine offset", () => {
|
||||
const result = addLineNumbers({ content: "hello", startLine: 10 });
|
||||
expect(result).toContain("10");
|
||||
});
|
||||
});
|
||||
|
||||
describe("stripLineNumberPrefix", () => {
|
||||
test("strips arrow-separated prefix", () => {
|
||||
expect(stripLineNumberPrefix(" 1→content")).toBe("content");
|
||||
});
|
||||
|
||||
test("strips tab-separated prefix", () => {
|
||||
expect(stripLineNumberPrefix("1\tcontent")).toBe("content");
|
||||
});
|
||||
|
||||
test("returns line unchanged if no prefix", () => {
|
||||
expect(stripLineNumberPrefix("no prefix")).toBe("no prefix");
|
||||
});
|
||||
|
||||
test("handles large line numbers", () => {
|
||||
expect(stripLineNumberPrefix("123456→content")).toBe("content");
|
||||
});
|
||||
});
|
||||
|
||||
describe("normalizePathForComparison", () => {
|
||||
test("normalizes redundant separators", () => {
|
||||
const result = normalizePathForComparison("/a//b/c");
|
||||
expect(result).toBe("/a/b/c");
|
||||
});
|
||||
|
||||
test("resolves dot segments", () => {
|
||||
const result = normalizePathForComparison("/a/./b/../c");
|
||||
expect(result).toBe("/a/c");
|
||||
});
|
||||
});
|
||||
|
||||
describe("pathsEqual", () => {
|
||||
test("returns true for identical paths", () => {
|
||||
expect(pathsEqual("/a/b/c", "/a/b/c")).toBe(true);
|
||||
});
|
||||
|
||||
test("returns true for equivalent paths with dot segments", () => {
|
||||
expect(pathsEqual("/a/./b", "/a/b")).toBe(true);
|
||||
});
|
||||
|
||||
test("returns false for different paths", () => {
|
||||
expect(pathsEqual("/a/b", "/a/c")).toBe(false);
|
||||
});
|
||||
});
|
||||
133
src/utils/__tests__/format.test.ts
Normal file
133
src/utils/__tests__/format.test.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import {
|
||||
formatFileSize,
|
||||
formatSecondsShort,
|
||||
formatDuration,
|
||||
formatNumber,
|
||||
formatTokens,
|
||||
formatRelativeTime,
|
||||
} from "../format";
|
||||
|
||||
describe("formatFileSize", () => {
|
||||
test("formats bytes", () => {
|
||||
expect(formatFileSize(500)).toBe("500 bytes");
|
||||
});
|
||||
|
||||
test("formats kilobytes", () => {
|
||||
expect(formatFileSize(1536)).toBe("1.5KB");
|
||||
});
|
||||
|
||||
test("formats megabytes", () => {
|
||||
expect(formatFileSize(1.5 * 1024 * 1024)).toBe("1.5MB");
|
||||
});
|
||||
|
||||
test("formats gigabytes", () => {
|
||||
expect(formatFileSize(2 * 1024 * 1024 * 1024)).toBe("2GB");
|
||||
});
|
||||
|
||||
test("removes trailing .0", () => {
|
||||
expect(formatFileSize(1024)).toBe("1KB");
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatSecondsShort", () => {
|
||||
test("formats milliseconds to seconds", () => {
|
||||
expect(formatSecondsShort(1234)).toBe("1.2s");
|
||||
});
|
||||
|
||||
test("formats zero", () => {
|
||||
expect(formatSecondsShort(0)).toBe("0.0s");
|
||||
});
|
||||
|
||||
test("formats sub-second", () => {
|
||||
expect(formatSecondsShort(500)).toBe("0.5s");
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatDuration", () => {
|
||||
test("formats 0 as 0s", () => {
|
||||
expect(formatDuration(0)).toBe("0s");
|
||||
});
|
||||
|
||||
test("formats seconds", () => {
|
||||
expect(formatDuration(5000)).toBe("5s");
|
||||
});
|
||||
|
||||
test("formats minutes and seconds", () => {
|
||||
expect(formatDuration(125000)).toBe("2m 5s");
|
||||
});
|
||||
|
||||
test("formats hours", () => {
|
||||
expect(formatDuration(3661000)).toBe("1h 1m 1s");
|
||||
});
|
||||
|
||||
test("formats days", () => {
|
||||
expect(formatDuration(90000000)).toBe("1d 1h 0m");
|
||||
});
|
||||
|
||||
test("hideTrailingZeros removes zero components", () => {
|
||||
expect(formatDuration(3600000, { hideTrailingZeros: true })).toBe("1h");
|
||||
expect(formatDuration(60000, { hideTrailingZeros: true })).toBe("1m");
|
||||
});
|
||||
|
||||
test("mostSignificantOnly returns largest unit", () => {
|
||||
expect(formatDuration(90000000, { mostSignificantOnly: true })).toBe("1d");
|
||||
expect(formatDuration(3661000, { mostSignificantOnly: true })).toBe("1h");
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatNumber", () => {
|
||||
test("formats small numbers as-is", () => {
|
||||
expect(formatNumber(900)).toBe("900");
|
||||
});
|
||||
|
||||
test("formats thousands with k suffix", () => {
|
||||
const result = formatNumber(1321);
|
||||
expect(result).toContain("k");
|
||||
});
|
||||
|
||||
test("formats millions", () => {
|
||||
const result = formatNumber(1500000);
|
||||
expect(result).toContain("m");
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatTokens", () => {
|
||||
test("removes .0 from formatted number", () => {
|
||||
const result = formatTokens(1000);
|
||||
expect(result).not.toContain(".0");
|
||||
});
|
||||
|
||||
test("formats small numbers", () => {
|
||||
expect(formatTokens(500)).toBe("500");
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatRelativeTime", () => {
|
||||
const now = new Date("2026-01-15T12:00:00Z");
|
||||
|
||||
test("formats seconds ago", () => {
|
||||
const date = new Date("2026-01-15T11:59:30Z");
|
||||
const result = formatRelativeTime(date, { now });
|
||||
expect(result).toContain("30");
|
||||
expect(result).toContain("ago");
|
||||
});
|
||||
|
||||
test("formats minutes ago", () => {
|
||||
const date = new Date("2026-01-15T11:55:00Z");
|
||||
const result = formatRelativeTime(date, { now });
|
||||
expect(result).toContain("5");
|
||||
expect(result).toContain("ago");
|
||||
});
|
||||
|
||||
test("formats future time", () => {
|
||||
const date = new Date("2026-01-15T13:00:00Z");
|
||||
const result = formatRelativeTime(date, { now });
|
||||
expect(result).toContain("in");
|
||||
});
|
||||
|
||||
test("handles zero difference", () => {
|
||||
const result = formatRelativeTime(now, { now });
|
||||
expect(result).toContain("0");
|
||||
});
|
||||
});
|
||||
164
src/utils/__tests__/frontmatterParser.test.ts
Normal file
164
src/utils/__tests__/frontmatterParser.test.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import {
|
||||
parseFrontmatter,
|
||||
splitPathInFrontmatter,
|
||||
parsePositiveIntFromFrontmatter,
|
||||
parseBooleanFrontmatter,
|
||||
parseShellFrontmatter,
|
||||
} from "../frontmatterParser";
|
||||
|
||||
describe("parseFrontmatter", () => {
|
||||
test("parses valid frontmatter", () => {
|
||||
const md = `---
|
||||
description: A test
|
||||
type: user
|
||||
---
|
||||
Content here`;
|
||||
const result = parseFrontmatter(md);
|
||||
expect(result.frontmatter.description).toBe("A test");
|
||||
expect(result.frontmatter.type).toBe("user");
|
||||
expect(result.content).toBe("Content here");
|
||||
});
|
||||
|
||||
test("returns empty frontmatter when none exists", () => {
|
||||
const md = "Just content, no frontmatter";
|
||||
const result = parseFrontmatter(md);
|
||||
expect(result.frontmatter).toEqual({});
|
||||
expect(result.content).toBe(md);
|
||||
});
|
||||
|
||||
test("handles empty frontmatter block", () => {
|
||||
const md = `---
|
||||
---
|
||||
Content`;
|
||||
const result = parseFrontmatter(md);
|
||||
expect(result.frontmatter).toEqual({});
|
||||
expect(result.content).toBe("Content");
|
||||
});
|
||||
|
||||
test("handles frontmatter with list values", () => {
|
||||
const md = `---
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
---
|
||||
Content`;
|
||||
const result = parseFrontmatter(md);
|
||||
expect(result.frontmatter["allowed-tools"]).toEqual(["Bash", "Read"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("splitPathInFrontmatter", () => {
|
||||
test("splits comma-separated paths", () => {
|
||||
expect(splitPathInFrontmatter("a, b, c")).toEqual(["a", "b", "c"]);
|
||||
});
|
||||
|
||||
test("expands brace patterns", () => {
|
||||
expect(splitPathInFrontmatter("src/*.{ts,tsx}")).toEqual([
|
||||
"src/*.ts",
|
||||
"src/*.tsx",
|
||||
]);
|
||||
});
|
||||
|
||||
test("handles nested brace expansion", () => {
|
||||
expect(splitPathInFrontmatter("{a,b}/{c,d}")).toEqual([
|
||||
"a/c", "a/d", "b/c", "b/d",
|
||||
]);
|
||||
});
|
||||
|
||||
test("handles array input", () => {
|
||||
expect(splitPathInFrontmatter(["a", "b"])).toEqual(["a", "b"]);
|
||||
});
|
||||
|
||||
test("returns empty array for non-string", () => {
|
||||
expect(splitPathInFrontmatter(123 as any)).toEqual([]);
|
||||
});
|
||||
|
||||
test("preserves braces in comma-separated list", () => {
|
||||
expect(splitPathInFrontmatter("a, src/*.{ts,tsx}")).toEqual([
|
||||
"a",
|
||||
"src/*.ts",
|
||||
"src/*.tsx",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parsePositiveIntFromFrontmatter", () => {
|
||||
test("returns number for positive integer", () => {
|
||||
expect(parsePositiveIntFromFrontmatter(5)).toBe(5);
|
||||
});
|
||||
|
||||
test("parses string number", () => {
|
||||
expect(parsePositiveIntFromFrontmatter("10")).toBe(10);
|
||||
});
|
||||
|
||||
test("returns undefined for zero", () => {
|
||||
expect(parsePositiveIntFromFrontmatter(0)).toBeUndefined();
|
||||
});
|
||||
|
||||
test("returns undefined for negative number", () => {
|
||||
expect(parsePositiveIntFromFrontmatter(-1)).toBeUndefined();
|
||||
});
|
||||
|
||||
test("returns undefined for float", () => {
|
||||
expect(parsePositiveIntFromFrontmatter(1.5)).toBeUndefined();
|
||||
});
|
||||
|
||||
test("returns undefined for null/undefined", () => {
|
||||
expect(parsePositiveIntFromFrontmatter(null)).toBeUndefined();
|
||||
expect(parsePositiveIntFromFrontmatter(undefined)).toBeUndefined();
|
||||
});
|
||||
|
||||
test("returns undefined for non-numeric string", () => {
|
||||
expect(parsePositiveIntFromFrontmatter("abc")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseBooleanFrontmatter", () => {
|
||||
test("returns true for boolean true", () => {
|
||||
expect(parseBooleanFrontmatter(true)).toBe(true);
|
||||
});
|
||||
|
||||
test("returns true for string 'true'", () => {
|
||||
expect(parseBooleanFrontmatter("true")).toBe(true);
|
||||
});
|
||||
|
||||
test("returns false for boolean false", () => {
|
||||
expect(parseBooleanFrontmatter(false)).toBe(false);
|
||||
});
|
||||
|
||||
test("returns false for string 'false'", () => {
|
||||
expect(parseBooleanFrontmatter("false")).toBe(false);
|
||||
});
|
||||
|
||||
test("returns false for null/undefined", () => {
|
||||
expect(parseBooleanFrontmatter(null)).toBe(false);
|
||||
expect(parseBooleanFrontmatter(undefined)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseShellFrontmatter", () => {
|
||||
test("returns bash for 'bash'", () => {
|
||||
expect(parseShellFrontmatter("bash", "test")).toBe("bash");
|
||||
});
|
||||
|
||||
test("returns powershell for 'powershell'", () => {
|
||||
expect(parseShellFrontmatter("powershell", "test")).toBe("powershell");
|
||||
});
|
||||
|
||||
test("returns undefined for null", () => {
|
||||
expect(parseShellFrontmatter(null, "test")).toBeUndefined();
|
||||
});
|
||||
|
||||
test("returns undefined for unrecognized value", () => {
|
||||
expect(parseShellFrontmatter("zsh", "test")).toBeUndefined();
|
||||
});
|
||||
|
||||
test("is case insensitive", () => {
|
||||
expect(parseShellFrontmatter("BASH", "test")).toBe("bash");
|
||||
});
|
||||
|
||||
test("returns undefined for empty string", () => {
|
||||
expect(parseShellFrontmatter("", "test")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
40
src/utils/__tests__/glob.test.ts
Normal file
40
src/utils/__tests__/glob.test.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { extractGlobBaseDirectory } from "../glob";
|
||||
|
||||
describe("extractGlobBaseDirectory", () => {
|
||||
test("extracts base dir from glob with *", () => {
|
||||
const result = extractGlobBaseDirectory("src/utils/*.ts");
|
||||
expect(result.baseDir).toBe("src/utils");
|
||||
expect(result.relativePattern).toBe("*.ts");
|
||||
});
|
||||
|
||||
test("extracts base dir from glob with **", () => {
|
||||
const result = extractGlobBaseDirectory("src/**/*.ts");
|
||||
expect(result.baseDir).toBe("src");
|
||||
expect(result.relativePattern).toBe("**/*.ts");
|
||||
});
|
||||
|
||||
test("returns dirname for literal path", () => {
|
||||
const result = extractGlobBaseDirectory("src/utils/file.ts");
|
||||
expect(result.baseDir).toBe("src/utils");
|
||||
expect(result.relativePattern).toBe("file.ts");
|
||||
});
|
||||
|
||||
test("handles glob starting with pattern", () => {
|
||||
const result = extractGlobBaseDirectory("*.ts");
|
||||
expect(result.baseDir).toBe("");
|
||||
expect(result.relativePattern).toBe("*.ts");
|
||||
});
|
||||
|
||||
test("handles braces pattern", () => {
|
||||
const result = extractGlobBaseDirectory("src/{a,b}/*.ts");
|
||||
expect(result.baseDir).toBe("src");
|
||||
expect(result.relativePattern).toBe("{a,b}/*.ts");
|
||||
});
|
||||
|
||||
test("handles question mark pattern", () => {
|
||||
const result = extractGlobBaseDirectory("src/?.ts");
|
||||
expect(result.baseDir).toBe("src");
|
||||
expect(result.relativePattern).toBe("?.ts");
|
||||
});
|
||||
});
|
||||
57
src/utils/__tests__/hash.test.ts
Normal file
57
src/utils/__tests__/hash.test.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { djb2Hash, hashContent, hashPair } from "../hash";
|
||||
|
||||
describe("djb2Hash", () => {
|
||||
test("returns a number", () => {
|
||||
expect(typeof djb2Hash("hello")).toBe("number");
|
||||
});
|
||||
|
||||
test("returns 0 for empty string", () => {
|
||||
expect(djb2Hash("")).toBe(0);
|
||||
});
|
||||
|
||||
test("is deterministic", () => {
|
||||
expect(djb2Hash("test")).toBe(djb2Hash("test"));
|
||||
});
|
||||
|
||||
test("different strings produce different hashes", () => {
|
||||
expect(djb2Hash("abc")).not.toBe(djb2Hash("def"));
|
||||
});
|
||||
|
||||
test("returns 32-bit integer", () => {
|
||||
const hash = djb2Hash("some long string to hash");
|
||||
expect(hash).toBe(hash | 0); // bitwise OR with 0 preserves 32-bit int
|
||||
});
|
||||
});
|
||||
|
||||
describe("hashContent", () => {
|
||||
test("returns a string", () => {
|
||||
expect(typeof hashContent("hello")).toBe("string");
|
||||
});
|
||||
|
||||
test("is deterministic", () => {
|
||||
expect(hashContent("test")).toBe(hashContent("test"));
|
||||
});
|
||||
|
||||
test("different strings produce different hashes", () => {
|
||||
expect(hashContent("abc")).not.toBe(hashContent("def"));
|
||||
});
|
||||
});
|
||||
|
||||
describe("hashPair", () => {
|
||||
test("returns a string", () => {
|
||||
expect(typeof hashPair("a", "b")).toBe("string");
|
||||
});
|
||||
|
||||
test("is deterministic", () => {
|
||||
expect(hashPair("a", "b")).toBe(hashPair("a", "b"));
|
||||
});
|
||||
|
||||
test("order matters", () => {
|
||||
expect(hashPair("a", "b")).not.toBe(hashPair("b", "a"));
|
||||
});
|
||||
|
||||
test("disambiguates different splits", () => {
|
||||
expect(hashPair("ts", "code")).not.toBe(hashPair("tsc", "ode"));
|
||||
});
|
||||
});
|
||||
98
src/utils/__tests__/semver.test.ts
Normal file
98
src/utils/__tests__/semver.test.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { gt, gte, lt, lte, satisfies, order } from "../semver";
|
||||
|
||||
describe("gt", () => {
|
||||
test("returns true when a > b", () => {
|
||||
expect(gt("2.0.0", "1.0.0")).toBe(true);
|
||||
});
|
||||
|
||||
test("returns false when a < b", () => {
|
||||
expect(gt("1.0.0", "2.0.0")).toBe(false);
|
||||
});
|
||||
|
||||
test("returns false when equal", () => {
|
||||
expect(gt("1.0.0", "1.0.0")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("gte", () => {
|
||||
test("returns true when a > b", () => {
|
||||
expect(gte("2.0.0", "1.0.0")).toBe(true);
|
||||
});
|
||||
|
||||
test("returns true when equal", () => {
|
||||
expect(gte("1.0.0", "1.0.0")).toBe(true);
|
||||
});
|
||||
|
||||
test("returns false when a < b", () => {
|
||||
expect(gte("1.0.0", "2.0.0")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("lt", () => {
|
||||
test("returns true when a < b", () => {
|
||||
expect(lt("1.0.0", "2.0.0")).toBe(true);
|
||||
});
|
||||
|
||||
test("returns false when a > b", () => {
|
||||
expect(lt("2.0.0", "1.0.0")).toBe(false);
|
||||
});
|
||||
|
||||
test("returns false when equal", () => {
|
||||
expect(lt("1.0.0", "1.0.0")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("lte", () => {
|
||||
test("returns true when a < b", () => {
|
||||
expect(lte("1.0.0", "2.0.0")).toBe(true);
|
||||
});
|
||||
|
||||
test("returns true when equal", () => {
|
||||
expect(lte("1.0.0", "1.0.0")).toBe(true);
|
||||
});
|
||||
|
||||
test("returns false when a > b", () => {
|
||||
expect(lte("2.0.0", "1.0.0")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("satisfies", () => {
|
||||
test("matches exact version", () => {
|
||||
expect(satisfies("1.2.3", "1.2.3")).toBe(true);
|
||||
});
|
||||
|
||||
test("matches range", () => {
|
||||
expect(satisfies("1.2.3", ">=1.0.0")).toBe(true);
|
||||
});
|
||||
|
||||
test("does not match out-of-range version", () => {
|
||||
expect(satisfies("0.9.0", ">=1.0.0")).toBe(false);
|
||||
});
|
||||
|
||||
test("matches caret range", () => {
|
||||
expect(satisfies("1.2.3", "^1.0.0")).toBe(true);
|
||||
});
|
||||
|
||||
test("does not match major bump in caret", () => {
|
||||
expect(satisfies("2.0.0", "^1.0.0")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("order", () => {
|
||||
test("returns 1 when a > b", () => {
|
||||
expect(order("2.0.0", "1.0.0")).toBe(1);
|
||||
});
|
||||
|
||||
test("returns -1 when a < b", () => {
|
||||
expect(order("1.0.0", "2.0.0")).toBe(-1);
|
||||
});
|
||||
|
||||
test("returns 0 when equal", () => {
|
||||
expect(order("1.0.0", "1.0.0")).toBe(0);
|
||||
});
|
||||
|
||||
test("compares patch versions", () => {
|
||||
expect(order("1.0.1", "1.0.0")).toBe(1);
|
||||
});
|
||||
});
|
||||
195
src/utils/__tests__/stringUtils.test.ts
Normal file
195
src/utils/__tests__/stringUtils.test.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import {
|
||||
escapeRegExp,
|
||||
capitalize,
|
||||
plural,
|
||||
firstLineOf,
|
||||
countCharInString,
|
||||
normalizeFullWidthDigits,
|
||||
normalizeFullWidthSpace,
|
||||
safeJoinLines,
|
||||
EndTruncatingAccumulator,
|
||||
truncateToLines,
|
||||
} from "../stringUtils";
|
||||
|
||||
describe("escapeRegExp", () => {
|
||||
test("escapes special regex chars", () => {
|
||||
expect(escapeRegExp("a.b*c?d")).toBe("a\\.b\\*c\\?d");
|
||||
});
|
||||
|
||||
test("escapes brackets and parens", () => {
|
||||
expect(escapeRegExp("[foo](bar)")).toBe("\\[foo\\]\\(bar\\)");
|
||||
});
|
||||
|
||||
test("escapes all special chars", () => {
|
||||
expect(escapeRegExp("^${}()|[]\\.*+?")).toBe(
|
||||
"\\^\\$\\{\\}\\(\\)\\|\\[\\]\\\\\\.\\*\\+\\?"
|
||||
);
|
||||
});
|
||||
|
||||
test("returns normal string unchanged", () => {
|
||||
expect(escapeRegExp("hello")).toBe("hello");
|
||||
});
|
||||
});
|
||||
|
||||
describe("capitalize", () => {
|
||||
test("uppercases first char", () => {
|
||||
expect(capitalize("hello")).toBe("Hello");
|
||||
});
|
||||
|
||||
test("does NOT lowercase rest", () => {
|
||||
expect(capitalize("fooBar")).toBe("FooBar");
|
||||
});
|
||||
|
||||
test("handles single char", () => {
|
||||
expect(capitalize("a")).toBe("A");
|
||||
});
|
||||
|
||||
test("handles empty string", () => {
|
||||
expect(capitalize("")).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("plural", () => {
|
||||
test("returns singular for 1", () => {
|
||||
expect(plural(1, "file")).toBe("file");
|
||||
});
|
||||
|
||||
test("returns plural for 0", () => {
|
||||
expect(plural(0, "file")).toBe("files");
|
||||
});
|
||||
|
||||
test("returns plural for many", () => {
|
||||
expect(plural(3, "file")).toBe("files");
|
||||
});
|
||||
|
||||
test("uses custom plural form", () => {
|
||||
expect(plural(2, "entry", "entries")).toBe("entries");
|
||||
});
|
||||
});
|
||||
|
||||
describe("firstLineOf", () => {
|
||||
test("returns first line of multiline string", () => {
|
||||
expect(firstLineOf("line1\nline2\nline3")).toBe("line1");
|
||||
});
|
||||
|
||||
test("returns whole string if no newline", () => {
|
||||
expect(firstLineOf("single line")).toBe("single line");
|
||||
});
|
||||
|
||||
test("returns empty string for leading newline", () => {
|
||||
expect(firstLineOf("\nline2")).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("countCharInString", () => {
|
||||
test("counts occurrences of a character", () => {
|
||||
expect(countCharInString("hello world", "l")).toBe(3);
|
||||
});
|
||||
|
||||
test("returns 0 for no match", () => {
|
||||
expect(countCharInString("hello", "z")).toBe(0);
|
||||
});
|
||||
|
||||
test("counts from start offset", () => {
|
||||
expect(countCharInString("aabaa", "a", 2)).toBe(2);
|
||||
});
|
||||
|
||||
test("returns 0 for empty string", () => {
|
||||
expect(countCharInString("", "a")).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("normalizeFullWidthDigits", () => {
|
||||
test("converts full-width digits to half-width", () => {
|
||||
expect(normalizeFullWidthDigits("0123456789")).toBe("0123456789");
|
||||
});
|
||||
|
||||
test("leaves half-width digits unchanged", () => {
|
||||
expect(normalizeFullWidthDigits("0123")).toBe("0123");
|
||||
});
|
||||
|
||||
test("handles mixed content", () => {
|
||||
expect(normalizeFullWidthDigits("test123")).toBe("test123");
|
||||
});
|
||||
});
|
||||
|
||||
describe("normalizeFullWidthSpace", () => {
|
||||
test("converts full-width space to half-width", () => {
|
||||
expect(normalizeFullWidthSpace("a\u3000b")).toBe("a b");
|
||||
});
|
||||
|
||||
test("leaves normal spaces unchanged", () => {
|
||||
expect(normalizeFullWidthSpace("a b")).toBe("a b");
|
||||
});
|
||||
});
|
||||
|
||||
describe("safeJoinLines", () => {
|
||||
test("joins lines with delimiter", () => {
|
||||
expect(safeJoinLines(["a", "b", "c"], ",")).toBe("a,b,c");
|
||||
});
|
||||
|
||||
test("truncates when exceeding maxSize", () => {
|
||||
const result = safeJoinLines(["hello", "world", "foo"], ",", 12);
|
||||
expect(result.length).toBeLessThanOrEqual(12 + "...[truncated]".length);
|
||||
expect(result).toContain("...[truncated]");
|
||||
});
|
||||
|
||||
test("returns empty string for empty input", () => {
|
||||
expect(safeJoinLines([])).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("EndTruncatingAccumulator", () => {
|
||||
test("accumulates text", () => {
|
||||
const acc = new EndTruncatingAccumulator(100);
|
||||
acc.append("hello ");
|
||||
acc.append("world");
|
||||
expect(acc.toString()).toBe("hello world");
|
||||
});
|
||||
|
||||
test("truncates when exceeding maxSize", () => {
|
||||
const acc = new EndTruncatingAccumulator(10);
|
||||
acc.append("12345678901234567890");
|
||||
expect(acc.truncated).toBe(true);
|
||||
expect(acc.length).toBe(10);
|
||||
});
|
||||
|
||||
test("reports total bytes received", () => {
|
||||
const acc = new EndTruncatingAccumulator(5);
|
||||
acc.append("1234567890");
|
||||
expect(acc.totalBytes).toBe(10);
|
||||
});
|
||||
|
||||
test("clear resets state", () => {
|
||||
const acc = new EndTruncatingAccumulator(100);
|
||||
acc.append("hello");
|
||||
acc.clear();
|
||||
expect(acc.toString()).toBe("");
|
||||
expect(acc.length).toBe(0);
|
||||
expect(acc.truncated).toBe(false);
|
||||
});
|
||||
|
||||
test("stops accepting data once truncated and full", () => {
|
||||
const acc = new EndTruncatingAccumulator(5);
|
||||
acc.append("12345");
|
||||
acc.append("67890");
|
||||
expect(acc.length).toBe(5);
|
||||
acc.append("more");
|
||||
expect(acc.length).toBe(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe("truncateToLines", () => {
|
||||
test("returns text unchanged if within limit", () => {
|
||||
expect(truncateToLines("a\nb\nc", 5)).toBe("a\nb\nc");
|
||||
});
|
||||
|
||||
test("truncates text exceeding limit", () => {
|
||||
expect(truncateToLines("a\nb\nc\nd\ne", 3)).toBe("a\nb\nc…");
|
||||
});
|
||||
|
||||
test("handles single line", () => {
|
||||
expect(truncateToLines("hello", 1)).toBe("hello");
|
||||
});
|
||||
});
|
||||
34
src/utils/__tests__/uuid.test.ts
Normal file
34
src/utils/__tests__/uuid.test.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { validateUuid } from "../uuid";
|
||||
|
||||
describe("validateUuid", () => {
|
||||
test("validates correct UUID", () => {
|
||||
const result = validateUuid("550e8400-e29b-41d4-a716-446655440000");
|
||||
expect(result).toBe("550e8400-e29b-41d4-a716-446655440000");
|
||||
});
|
||||
|
||||
test("validates uppercase UUID", () => {
|
||||
const result = validateUuid("550E8400-E29B-41D4-A716-446655440000");
|
||||
expect(result).not.toBeNull();
|
||||
});
|
||||
|
||||
test("returns null for non-string", () => {
|
||||
expect(validateUuid(123)).toBeNull();
|
||||
expect(validateUuid(null)).toBeNull();
|
||||
expect(validateUuid(undefined)).toBeNull();
|
||||
});
|
||||
|
||||
test("returns null for invalid UUID format", () => {
|
||||
expect(validateUuid("not-a-uuid")).toBeNull();
|
||||
expect(validateUuid("550e8400-e29b-41d4-a716")).toBeNull();
|
||||
expect(validateUuid("550e8400e29b41d4a716446655440000")).toBeNull();
|
||||
});
|
||||
|
||||
test("returns null for empty string", () => {
|
||||
expect(validateUuid("")).toBeNull();
|
||||
});
|
||||
|
||||
test("returns null for UUID with invalid chars", () => {
|
||||
expect(validateUuid("550e8400-e29b-41d4-a716-44665544000g")).toBeNull();
|
||||
});
|
||||
});
|
||||
42
src/utils/__tests__/xml.test.ts
Normal file
42
src/utils/__tests__/xml.test.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { escapeXml, escapeXmlAttr } from "../xml";
|
||||
|
||||
describe("escapeXml", () => {
|
||||
test("escapes ampersand", () => {
|
||||
expect(escapeXml("a & b")).toBe("a & b");
|
||||
});
|
||||
|
||||
test("escapes less-than", () => {
|
||||
expect(escapeXml("<div>")).toBe("<div>");
|
||||
});
|
||||
|
||||
test("escapes greater-than", () => {
|
||||
expect(escapeXml("a > b")).toBe("a > b");
|
||||
});
|
||||
|
||||
test("escapes multiple special chars", () => {
|
||||
expect(escapeXml("<a & b>")).toBe("<a & b>");
|
||||
});
|
||||
|
||||
test("returns empty string unchanged", () => {
|
||||
expect(escapeXml("")).toBe("");
|
||||
});
|
||||
|
||||
test("returns normal text unchanged", () => {
|
||||
expect(escapeXml("hello world")).toBe("hello world");
|
||||
});
|
||||
});
|
||||
|
||||
describe("escapeXmlAttr", () => {
|
||||
test("escapes double quotes", () => {
|
||||
expect(escapeXmlAttr('say "hello"')).toBe("say "hello"");
|
||||
});
|
||||
|
||||
test("escapes single quotes", () => {
|
||||
expect(escapeXmlAttr("it's")).toBe("it's");
|
||||
});
|
||||
|
||||
test("escapes all special chars", () => {
|
||||
expect(escapeXmlAttr('<a & "b">')).toBe("<a & "b">");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user