Skip to content

Commit 971ec3b

Browse files
committed
docs(changeset): Improve error handling and unit tests. Add performance enhancemnts
1 parent 3b1eb92 commit 971ec3b

File tree

5 files changed

+239
-51
lines changed

5 files changed

+239
-51
lines changed

.changeset/warm-rocks-clap.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"git-json-resolver": patch
3+
---
4+
5+
Improve error handling and unit tests. Add performance enhancemnts

lib/src/index.test.ts

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2+
import { resolveConflicts } from "./index";
3+
import { execSync } from "node:child_process";
4+
import fs from "node:fs/promises";
5+
6+
// Mock all dependencies
7+
vi.mock("node:child_process");
8+
vi.mock("node:fs/promises");
9+
vi.mock("./file-parser");
10+
vi.mock("./file-serializer");
11+
vi.mock("./merger");
12+
vi.mock("./normalizer");
13+
vi.mock("./utils");
14+
vi.mock("./conflict-helper");
15+
vi.mock("./logger");
16+
17+
import { parseConflictContent } from "./file-parser";
18+
import { serialize } from "./file-serializer";
19+
import { mergeObject } from "./merger";
20+
import { normalizeConfig } from "./normalizer";
21+
import { backupFile, listMatchingFiles } from "./utils";
22+
import { reconstructConflict } from "./conflict-helper";
23+
import { createLogger } from "./logger";
24+
25+
const mockExecSync = vi.mocked(execSync);
26+
const mockFs = vi.mocked(fs);
27+
const mockParseConflictContent = vi.mocked(parseConflictContent);
28+
const mockSerialize = vi.mocked(serialize);
29+
const mockMergeObject = vi.mocked(mergeObject);
30+
const mockNormalizeConfig = vi.mocked(normalizeConfig);
31+
const mockBackupFile = vi.mocked(backupFile);
32+
const mockListMatchingFiles = vi.mocked(listMatchingFiles);
33+
const mockReconstructConflict = vi.mocked(reconstructConflict);
34+
const mockCreateLogger = vi.mocked(createLogger);
35+
36+
describe("resolveConflicts", () => {
37+
const mockLogger = {
38+
info: vi.fn(),
39+
debug: vi.fn(),
40+
warn: vi.fn(),
41+
flush: vi.fn(),
42+
};
43+
44+
beforeEach(() => {
45+
vi.clearAllMocks();
46+
mockCreateLogger.mockReturnValue(mockLogger);
47+
mockNormalizeConfig.mockResolvedValue({
48+
debug: false,
49+
customStrategies: {},
50+
} as any);
51+
mockListMatchingFiles.mockResolvedValue([]);
52+
});
53+
54+
afterEach(() => {
55+
vi.clearAllMocks();
56+
});
57+
58+
it("processes files without conflicts successfully", async () => {
59+
const config = { defaultStrategy: "ours" as const };
60+
const fileEntry = { filePath: "test.json", content: "content" };
61+
const parsedContent = { theirs: { a: 1 }, ours: { a: 2 }, format: "json" };
62+
const mergedResult = { a: 2 };
63+
64+
mockListMatchingFiles.mockResolvedValue([fileEntry]);
65+
mockParseConflictContent.mockResolvedValue(parsedContent);
66+
mockMergeObject.mockResolvedValue(mergedResult);
67+
mockSerialize.mockResolvedValue('{"a":2}');
68+
mockBackupFile.mockResolvedValue("backup/test.json");
69+
70+
await resolveConflicts(config);
71+
72+
expect(mockNormalizeConfig).toHaveBeenCalledWith(config);
73+
expect(mockListMatchingFiles).toHaveBeenCalled();
74+
expect(mockParseConflictContent).toHaveBeenCalledWith("content", { filename: "test.json" });
75+
expect(mockMergeObject).toHaveBeenCalledWith({
76+
ours: { a: 2 },
77+
theirs: { a: 1 },
78+
base: undefined,
79+
filePath: "test.json",
80+
conflicts: [],
81+
path: "",
82+
ctx: expect.objectContaining({
83+
config: expect.any(Object),
84+
strategies: {},
85+
_strategyCache: expect.any(Map),
86+
}),
87+
});
88+
expect(mockBackupFile).toHaveBeenCalledWith("test.json", undefined);
89+
expect(mockSerialize).toHaveBeenCalledWith("json", mergedResult);
90+
expect(mockFs.writeFile).toHaveBeenCalledWith("test.json", '{"a":2}', "utf8");
91+
expect(mockExecSync).toHaveBeenCalledWith("git add test.json");
92+
expect(mockLogger.flush).toHaveBeenCalled();
93+
});
94+
95+
it("handles files with conflicts", async () => {
96+
const config = { writeConflictSidecar: true };
97+
const conflicts = [{ path: "a", reason: "test conflict" }];
98+
const reconstructedContent = "conflict content";
99+
100+
mockListMatchingFiles.mockResolvedValue([{ filePath: "test.json", content: "content" }]);
101+
mockParseConflictContent.mockResolvedValue({ theirs: {}, ours: {}, format: "json" });
102+
mockMergeObject.mockImplementation(({ conflicts: conflictsArray }) => {
103+
conflictsArray.push(...conflicts);
104+
return Promise.resolve({});
105+
});
106+
mockReconstructConflict.mockResolvedValue(reconstructedContent);
107+
108+
await resolveConflicts(config);
109+
110+
expect(mockReconstructConflict).toHaveBeenCalledWith({}, {}, {}, "json");
111+
expect(mockFs.writeFile).toHaveBeenCalledWith("test.json", reconstructedContent, "utf8");
112+
expect(mockFs.writeFile).toHaveBeenCalledWith(
113+
"test.json.conflict.json",
114+
JSON.stringify(conflicts, null, 2)
115+
);
116+
expect(mockExecSync).not.toHaveBeenCalled();
117+
});
118+
119+
it("handles git staging errors gracefully", async () => {
120+
mockListMatchingFiles.mockResolvedValue([{ filePath: "test.json", content: "content" }]);
121+
mockParseConflictContent.mockResolvedValue({ theirs: {}, ours: {}, format: "json" });
122+
mockMergeObject.mockResolvedValue({});
123+
mockSerialize.mockResolvedValue("{}");
124+
mockExecSync.mockImplementation(() => {
125+
throw new Error("git error");
126+
});
127+
128+
await resolveConflicts({});
129+
130+
expect(mockLogger.warn).toHaveBeenCalledWith("test.json", "Failed to stage file: Error: git error");
131+
});
132+
133+
it("enables debug logging when configured", async () => {
134+
const config = { debug: true };
135+
mockNormalizeConfig.mockResolvedValue({ debug: true, customStrategies: {} } as any);
136+
mockListMatchingFiles.mockResolvedValue([{ filePath: "test.json", content: "content" }]);
137+
mockParseConflictContent.mockResolvedValue({ theirs: {}, ours: {}, format: "json" });
138+
mockMergeObject.mockResolvedValue({});
139+
140+
await resolveConflicts(config);
141+
142+
expect(mockLogger.info).toHaveBeenCalledWith(
143+
"all",
144+
expect.stringContaining("normalizedConfig")
145+
);
146+
expect(mockLogger.debug).toHaveBeenCalledWith(
147+
"test.json",
148+
expect.stringContaining("merged")
149+
);
150+
});
151+
152+
it("processes multiple files concurrently", async () => {
153+
const files = [
154+
{ filePath: "file1.json", content: "content1" },
155+
{ filePath: "file2.json", content: "content2" },
156+
];
157+
158+
mockListMatchingFiles.mockResolvedValue(files);
159+
mockParseConflictContent.mockResolvedValue({ theirs: {}, ours: {}, format: "json" });
160+
mockMergeObject.mockResolvedValue({});
161+
mockSerialize.mockResolvedValue("{}");
162+
163+
await resolveConflicts({});
164+
165+
expect(mockParseConflictContent).toHaveBeenCalledTimes(2);
166+
expect(mockMergeObject).toHaveBeenCalledTimes(2);
167+
expect(mockBackupFile).toHaveBeenCalledTimes(2);
168+
});
169+
170+
it("uses custom backup directory", async () => {
171+
const config = { backupDir: "custom-backup" };
172+
mockListMatchingFiles.mockResolvedValue([{ filePath: "test.json", content: "content" }]);
173+
mockParseConflictContent.mockResolvedValue({ theirs: {}, ours: {}, format: "json" });
174+
mockMergeObject.mockResolvedValue({});
175+
176+
await resolveConflicts(config);
177+
178+
expect(mockBackupFile).toHaveBeenCalledWith("test.json", "custom-backup");
179+
});
180+
});

lib/src/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@ export * from "./types";
1313

1414
const _strategyCache = new Map<string, string[]>();
1515

16-
let globalLogger: ReturnType<typeof createLogger>;
16+
let globalLogger: Awaited<ReturnType<typeof createLogger>>;
1717

1818
export const resolveConflicts = async <T extends string = InbuiltMergeStrategies>(
1919
config: Config<T>,
2020
) => {
21-
globalLogger = createLogger(config.loggerConfig);
21+
globalLogger = await createLogger(config.loggerConfig);
2222
const normalizedConfig: NormalizedConfig = await normalizeConfig<T>(config);
2323
const filesEntries = await listMatchingFiles(normalizedConfig);
2424
if (normalizedConfig.debug) {
@@ -68,5 +68,5 @@ export const resolveConflicts = async <T extends string = InbuiltMergeStrategies
6868
}
6969
}),
7070
);
71-
globalLogger.flush();
71+
await globalLogger.flush();
7272
};

lib/src/logger.test.ts

Lines changed: 38 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -18,101 +18,98 @@ afterEach(() => {
1818
}
1919
});
2020

21-
describe("createLogger", () => {
22-
it("creates logger with default config", () => {
23-
const logger = createLogger();
21+
describe("createLogger", async () => {
22+
it("creates logger with default config", async () => {
23+
const logger = await createLogger();
2424
expect(logger).toHaveProperty("info");
2525
expect(logger).toHaveProperty("warn");
2626
expect(logger).toHaveProperty("error");
2727
expect(logger).toHaveProperty("debug");
2828
expect(logger).toHaveProperty("flush");
2929
});
3030

31-
it("logs to console for stdout levels", () => {
31+
it("logs to console for stdout levels", async () => {
3232
const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
3333
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
34-
35-
const logger = createLogger({ levels: { stdout: ["warn", "error"] } });
36-
34+
35+
const logger = await createLogger({ levels: { stdout: ["warn", "error"] } });
36+
3737
logger.warn("test", "warning message");
3838
logger.error("test", "error message");
3939
logger.info("test", "info message");
40-
40+
4141
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining("[WARN] warning message"));
4242
expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining("[ERROR] error message"));
4343
expect(consoleSpy).not.toHaveBeenCalledWith(expect.stringContaining("[INFO] info message"));
4444
});
4545

46-
it("writes to files in memory mode", () => {
47-
const logger = createLogger({
48-
mode: "memory",
46+
it("writes to files in memory mode", async () => {
47+
const logger = await createLogger({
48+
mode: "memory",
4949
logDir: TEST_LOG_DIR,
50-
levels: { file: ["info", "warn"] }
50+
levels: { file: ["info", "warn"] },
5151
});
52-
52+
5353
logger.info("test", "info message");
5454
logger.warn("test", "warning message");
55-
logger.flush();
56-
55+
await logger.flush();
56+
5757
const files = fs.readdirSync(TEST_LOG_DIR);
5858
expect(files.length).toBeGreaterThan(0);
59-
59+
6060
const logContent = fs.readFileSync(path.join(TEST_LOG_DIR, files[0]), "utf8");
6161
expect(logContent).toContain("[INFO] info message");
6262
expect(logContent).toContain("[WARN] warning message");
6363
});
6464

65-
it("writes to single file when singleFile is true", () => {
66-
const logger = createLogger({
67-
mode: "memory",
65+
it("writes to single file when singleFile is true", async () => {
66+
const logger = await createLogger({
67+
mode: "memory",
6868
logDir: TEST_LOG_DIR,
69-
singleFile: true
69+
singleFile: true,
7070
});
71-
71+
7272
logger.info("file1", "message1");
7373
logger.info("file2", "message2");
74-
logger.flush();
75-
74+
await logger.flush();
75+
7676
const files = fs.readdirSync(TEST_LOG_DIR);
7777
expect(files.length).toBe(1);
7878
expect(files[0]).toMatch(/combined-/);
7979
});
8080

81-
it("creates log directory if it doesn't exist", () => {
82-
const logger = createLogger({ logDir: TEST_LOG_DIR });
81+
it("creates log directory if it doesn't exist", async () => {
82+
const logger = await createLogger({ logDir: TEST_LOG_DIR });
8383
expect(fs.existsSync(TEST_LOG_DIR)).toBe(true);
8484
});
8585

8686
it("handles stream mode", async () => {
87-
const logger = createLogger({
88-
mode: "stream",
89-
logDir: TEST_LOG_DIR
87+
const logger = await createLogger({
88+
mode: "stream",
89+
logDir: TEST_LOG_DIR,
9090
});
91-
91+
9292
logger.info("test", "stream message");
93-
logger.flush();
94-
95-
// Wait for stream to close
96-
await new Promise(resolve => setTimeout(resolve, 10));
97-
93+
await logger.flush();
94+
9895
const files = fs.readdirSync(TEST_LOG_DIR);
9996
expect(files.length).toBeGreaterThan(0);
10097
});
10198

102-
it("filters logs by level", () => {
103-
const logger = createLogger({
99+
it("filters logs by level", async () => {
100+
const logger = await createLogger({
104101
mode: "memory",
105102
logDir: TEST_LOG_DIR,
106-
levels: { file: ["error"] }
103+
levels: { file: ["error"] },
107104
});
108-
105+
109106
logger.info("test", "info");
110107
logger.error("test", "error");
111-
logger.flush();
112-
108+
await logger.flush();
109+
113110
const files = fs.readdirSync(TEST_LOG_DIR);
114111
const content = fs.readFileSync(path.join(TEST_LOG_DIR, files[0]), "utf8");
115112
expect(content).toContain("error");
116113
expect(content).not.toContain("info");
117114
});
118-
});
115+
});

0 commit comments

Comments
 (0)