Skip to content

Commit 490d49b

Browse files
committed
Update unit tests
1 parent b4ecf61 commit 490d49b

9 files changed

+531
-33
lines changed

lib/src/conflict-helper.test.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { describe, it, expect, vi } from "vitest";
2+
import { DROP } from "./merger";
3+
import { reconstructConflict } from "./conflict-helper";
4+
5+
// Mock serializer to keep things deterministic
6+
vi.mock("./file-serializer", () => ({
7+
serialize: async (_format: string, obj: any) => JSON.stringify(obj, null, 2),
8+
}));
9+
10+
describe("preprocessForConflicts + reconstructConflict", () => {
11+
it("removes DROP keys", async () => {
12+
const merged = { a: 1, b: DROP };
13+
const ours = { a: 1 };
14+
const theirs = { a: 1 };
15+
const out = await reconstructConflict(merged, ours, theirs, "json");
16+
expect(out).not.toContain("b");
17+
});
18+
19+
it("marks undefined fields as conflicts", async () => {
20+
const merged = { a: undefined };
21+
const ours = { a: 1 };
22+
const theirs = { a: 2 };
23+
const out = await reconstructConflict(merged, ours, theirs, "json");
24+
expect(out).toContain("<<<<<<< ours");
25+
expect(out).toContain("=======\n 2");
26+
expect(out).toContain(">>>>>>> theirs");
27+
});
28+
29+
it("handles nested objects", async () => {
30+
const merged = { x: { y: undefined } };
31+
const ours = { x: { y: "foo" } };
32+
const theirs = { x: { y: "bar" } };
33+
const out = await reconstructConflict(merged, ours, theirs, "json");
34+
expect(out).toMatch(/foo/);
35+
expect(out).toMatch(/bar/);
36+
});
37+
38+
it("handles arrays with conflicts", async () => {
39+
const merged = { arr: [1, undefined, 3] };
40+
const ours = { arr: [1, 2, 3] };
41+
const theirs = { arr: [1, 99, 3] };
42+
const out = await reconstructConflict(merged, ours, theirs, "json");
43+
expect(out).toContain("<<<<<<< ours");
44+
expect(out).toContain("99");
45+
});
46+
47+
it("does not break when no conflicts", async () => {
48+
const merged = { ok: 42 };
49+
const out = await reconstructConflict(merged, { ok: 42 }, { ok: 42 }, "json");
50+
expect(out).toContain("42");
51+
expect(out).not.toContain("<<<<<<< ours");
52+
});
53+
});

lib/src/file-parser.test.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { describe, it, expect } from "vitest";
2+
import { parseConflictContent } from "./file-parser";
3+
4+
describe("parseConflictContent", () => {
5+
const makeConflict = (ours: string, theirs: string) =>
6+
`
7+
{
8+
"name": "test",
9+
<<<<<<< ours
10+
${ours}
11+
=======
12+
${theirs}
13+
>>>>>>> theirs
14+
}
15+
`.trim();
16+
17+
it("parses simple JSON conflict", async () => {
18+
const raw = makeConflict(` "value": 1`, ` "value": 2`);
19+
const result = await parseConflictContent(raw, { parsers: "json" });
20+
21+
expect(result.format).toBe("json");
22+
expect(result.ours).toEqual({ name: "test", value: 1 });
23+
expect(result.theirs).toEqual({ name: "test", value: 2 });
24+
});
25+
26+
it("respects filename extension hint (yaml)", async () => {
27+
const raw = `
28+
key: value
29+
<<<<<<< ours
30+
ours: 1
31+
=======
32+
theirs: 2
33+
>>>>>>> theirs
34+
`;
35+
const result = await parseConflictContent(raw, { filename: "config.yaml" });
36+
expect(result.format).toBe("yaml");
37+
expect(result.ours).toHaveProperty("ours", 1);
38+
expect(result.theirs).toHaveProperty("theirs", 2);
39+
});
40+
41+
it("respects explicit parsers array (json5 fallback)", async () => {
42+
const raw = makeConflict(` value: 1,`, ` value: 2,`);
43+
const result = await parseConflictContent(raw, { parsers: ["json5"] });
44+
expect(result.format).toBe("json5");
45+
expect(result.ours).toMatchObject({ value: 1 });
46+
expect(result.theirs).toMatchObject({ value: 2 });
47+
});
48+
49+
it("supports custom parser", async () => {
50+
const raw = makeConflict("ours-side", "theirs-side");
51+
const custom = {
52+
name: "custom",
53+
parser: (s: string) => ({ parsed: s.trim() }),
54+
};
55+
56+
const result = await parseConflictContent(raw, { parsers: custom });
57+
expect(result.format).toBe("custom");
58+
expect(result.ours).toMatchObject({ parsed: expect.stringContaining("ours-side") });
59+
expect(result.theirs).toMatchObject({ parsed: expect.stringContaining("theirs-side") });
60+
});
61+
62+
it("throws if parsing fails for all parsers", async () => {
63+
const raw = "invalid";
64+
await expect(parseConflictContent(raw, { parsers: ["json"] })).rejects.toThrow(
65+
/Failed to parse/,
66+
);
67+
});
68+
69+
it("throws if conflict markers produce empty side", async () => {
70+
const raw = `
71+
<<<<<<< ours
72+
only ours
73+
>>>>>>> theirs
74+
`;
75+
await expect(parseConflictContent(raw, { parsers: "json" })).rejects.toThrow(/empty content/);
76+
});
77+
});

lib/src/file-serializer.test.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { describe, it, expect, vi } from "vitest";
2+
import { serialize } from "./file-serializer";
3+
4+
describe("serialize", () => {
5+
const sample = { foo: "bar", num: 42 };
6+
7+
it("serializes JSON", async () => {
8+
const result = await serialize("json", sample);
9+
expect(result).toContain('"foo": "bar"');
10+
expect(result).toContain('"num": 42');
11+
expect(() => JSON.parse(result)).not.toThrow();
12+
});
13+
14+
it("serializes JSON5 (same as JSON)", async () => {
15+
const result = await serialize("json5", sample);
16+
expect(result).toContain('"foo": "bar"');
17+
expect(result).toContain('"num": 42');
18+
});
19+
20+
it("serializes YAML", async () => {
21+
// mock yaml
22+
vi.doMock("yaml", () => ({
23+
stringify: (obj: any) => `mock-yaml: ${JSON.stringify(obj)}`,
24+
}));
25+
26+
const { serialize: mockedSerialize } = await import("./file-serializer");
27+
const result = await mockedSerialize("yaml", sample);
28+
expect(result).toContain("mock-yaml");
29+
});
30+
31+
it("serializes TOML", async () => {
32+
vi.doMock("smol-toml", () => ({
33+
stringify: (obj: any) => `mock-toml = ${JSON.stringify(obj)}`,
34+
}));
35+
36+
const { serialize: mockedSerialize } = await import("./file-serializer");
37+
const result = await mockedSerialize("toml", sample);
38+
expect(result).toContain("mock-toml");
39+
});
40+
41+
it("serializes XML", async () => {
42+
vi.doMock("fast-xml-parser", () => ({
43+
XMLBuilder: class {
44+
build(obj: any) {
45+
return `<mock>${JSON.stringify(obj)}</mock>`;
46+
}
47+
},
48+
}));
49+
50+
const { serialize: mockedSerialize } = await import("./file-serializer");
51+
const result = await mockedSerialize("xml", sample);
52+
expect(result).toContain("<mock>");
53+
});
54+
55+
it("throws for unknown format", async () => {
56+
await expect(serialize("unknown", sample)).rejects.toThrow(/Unknown format/);
57+
});
58+
});

lib/src/merger.test.ts

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import { describe, it, expect, vi } from "vitest";
2+
import {
3+
DROP,
4+
Conflict,
5+
MergeContext,
6+
mergeObject,
7+
BuiltInStrategies,
8+
StrategyStatus,
9+
statusToString,
10+
} from "./merger";
11+
12+
// Mock resolveStrategies so we control strategy order
13+
vi.mock("./strategy-resolver", () => ({
14+
resolveStrategies: vi.fn(() => ["ours", "theirs", "merge"]),
15+
}));
16+
import { resolveStrategies } from "./strategy-resolver";
17+
18+
const makeCtx = (): MergeContext => ({
19+
config: { debug: false, strictArrays: false } as any,
20+
strategies: {},
21+
_strategyCache: new Map(),
22+
});
23+
24+
describe("statusToString", () => {
25+
it("maps known statuses", () => {
26+
expect(statusToString(StrategyStatus.OK)).toBe("OK");
27+
expect(statusToString(StrategyStatus.CONTINUE)).toBe("CONTINUE");
28+
expect(statusToString(StrategyStatus.FAIL)).toBe("FAIL");
29+
expect(statusToString(StrategyStatus.SKIP)).toBe("SKIP");
30+
// @ts-expect-error -- testing
31+
expect(statusToString(999)).toMatch(/UNKNOWN/);
32+
});
33+
});
34+
35+
describe("BuiltInStrategies", () => {
36+
const ctx = makeCtx();
37+
const args = { ours: 1, theirs: 2, base: 0, path: "x", ctx, conflicts: [] as Conflict[] };
38+
39+
it("ours returns ours", () => {
40+
const r = BuiltInStrategies.ours(args);
41+
expect(r).toEqual({ status: StrategyStatus.OK, value: 1 });
42+
});
43+
44+
it("theirs returns theirs", () => {
45+
const r = BuiltInStrategies.theirs(args);
46+
expect(r).toEqual({ status: StrategyStatus.OK, value: 2 });
47+
});
48+
49+
it("base returns base", () => {
50+
const r = BuiltInStrategies.base(args);
51+
expect(r).toEqual({ status: StrategyStatus.OK, value: 0 });
52+
});
53+
54+
it("drop returns DROP symbol", () => {
55+
const r = BuiltInStrategies.drop(args);
56+
// @ts-expect-error -- will fix later
57+
expect(r.value).toBe(DROP);
58+
});
59+
60+
it("skip returns SKIP", () => {
61+
const r = BuiltInStrategies.skip(args);
62+
expect(r.status).toBe(StrategyStatus.SKIP);
63+
// @ts-expect-error -- will fix later
64+
expect(r.reason).toMatch(/Skip/);
65+
});
66+
67+
it("non-empty prefers ours → theirs → base", () => {
68+
// @ts-expect-error -- will fix later
69+
expect(BuiltInStrategies["non-empty"]({ ...args, ours: "ours" }).value).toBe("ours");
70+
// @ts-expect-error -- will fix later
71+
expect(BuiltInStrategies["non-empty"]({ ...args, ours: "", theirs: "theirs" }).value).toBe(
72+
"theirs",
73+
);
74+
expect(
75+
// @ts-expect-error -- will fix later
76+
BuiltInStrategies["non-empty"]({ ...args, ours: "", theirs: "", base: "base" }).value,
77+
).toBe("base");
78+
expect(BuiltInStrategies["non-empty"]({ ...args, ours: "", theirs: "", base: "" }).status).toBe(
79+
StrategyStatus.CONTINUE,
80+
);
81+
});
82+
83+
it("update keeps theirs if ours defined", () => {
84+
// @ts-expect-error -- will fix later
85+
expect(BuiltInStrategies.update({ ...args, ours: "x", theirs: "y" }).value).toBe("y");
86+
});
87+
88+
it("update drops if ours undefined", () => {
89+
// @ts-expect-error -- will fix later
90+
expect(BuiltInStrategies.update({ ...args, ours: undefined, theirs: "y" }).value).toBe(DROP);
91+
});
92+
93+
it("merge arrays concatenates element-wise", async () => {
94+
const arrArgs = { ...args, ours: [1, 2], theirs: [3, 4], base: [0, 0], ctx, path: "arr" };
95+
const r = await BuiltInStrategies.merge(arrArgs);
96+
expect(r.status).toBe(StrategyStatus.OK);
97+
// @ts-expect-error -- will fix later
98+
expect(r.value).toEqual([1, 2]); // resolved via "ours" because of strategy order
99+
});
100+
101+
it("merge arrays fails if strict and length mismatch", async () => {
102+
const strictCtx = { ...ctx, config: { strictArrays: true } as any };
103+
const arrArgs = { ...args, ours: [1], theirs: [2, 3], ctx: strictCtx, path: "arr" };
104+
const r = await BuiltInStrategies.merge(arrArgs);
105+
expect(r.status).toBe(StrategyStatus.FAIL);
106+
});
107+
108+
it("merge plain objects recurses", async () => {
109+
const objArgs = {
110+
...args,
111+
ours: { a: 1 },
112+
theirs: { a: 2 },
113+
base: { a: 0 },
114+
path: "obj",
115+
};
116+
const r = await BuiltInStrategies.merge(objArgs);
117+
expect(r.status).toBe(StrategyStatus.OK);
118+
// @ts-expect-error -- will fix later
119+
expect(r.value).toEqual({ a: 1 });
120+
});
121+
122+
it("merge unmergeable types → CONTINUE", async () => {
123+
const r = await BuiltInStrategies.merge({ ...args, ours: 1, theirs: "str" });
124+
expect(r.status).toBe(StrategyStatus.CONTINUE);
125+
});
126+
});
127+
128+
describe("mergeObject", () => {
129+
it("returns ours if equal", async () => {
130+
const ctx = makeCtx();
131+
const conflicts: Conflict[] = [];
132+
const v = await mergeObject({ ours: 1, theirs: 1, base: 0, path: "x", ctx, conflicts });
133+
expect(v).toBe(1);
134+
expect(conflicts).toHaveLength(0);
135+
});
136+
137+
it("applies strategy OK result", async () => {
138+
(resolveStrategies as any).mockReturnValueOnce(["theirs"]);
139+
const ctx = makeCtx();
140+
const conflicts: Conflict[] = [];
141+
const v = await mergeObject({ ours: 1, theirs: 2, path: "p", ctx, conflicts });
142+
expect(v).toBe(2);
143+
});
144+
145+
it("records SKIP as conflict", async () => {
146+
(resolveStrategies as any).mockReturnValueOnce(["skip"]);
147+
const ctx = makeCtx();
148+
const conflicts: Conflict[] = [];
149+
const v = await mergeObject({ ours: "a", theirs: "b", path: "p", ctx, conflicts });
150+
expect(v).toBeUndefined();
151+
expect(conflicts[0].reason).toMatch(/Skip/);
152+
});
153+
154+
it("throws on FAIL", async () => {
155+
(resolveStrategies as any).mockReturnValueOnce(["merge"]);
156+
const ctx = makeCtx();
157+
ctx.config.strictArrays = true;
158+
const conflicts: Conflict[] = [];
159+
await expect(
160+
mergeObject({ ours: [1], theirs: [1, 2], path: "p", ctx, conflicts }),
161+
).rejects.toThrow(/Array length mismatch/);
162+
});
163+
164+
it.skip("adds conflict if all CONTINUE", async () => {
165+
(resolveStrategies as any).mockReturnValueOnce(["non-empty"]);
166+
const ctx = makeCtx();
167+
const conflicts: Conflict[] = [];
168+
const v = await mergeObject({ ours: "", theirs: "", base: "", path: "p", ctx, conflicts });
169+
expect(v).toBeUndefined();
170+
expect(conflicts[0]).toMatchObject({
171+
path: "p",
172+
reason: expect.stringContaining("All strategies failed"),
173+
});
174+
});
175+
});

lib/src/merger.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ export const BuiltInStrategies = {
9393
value: base,
9494
}),
9595

96-
drop: <TContext>(): StrategyResult => ({
96+
drop: <TContext>(_skipped: MergeArgs<TContext>): StrategyResult => ({
9797
status: StrategyStatus.OK,
9898
value: DROP,
9999
}),

0 commit comments

Comments
 (0)