@gitwand/core 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (219) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +52 -0
  3. package/dist/__tests__/bench.bench.d.ts +14 -0
  4. package/dist/__tests__/bench.bench.d.ts.map +1 -0
  5. package/dist/__tests__/bench.bench.js +137 -0
  6. package/dist/__tests__/bench.bench.js.map +1 -0
  7. package/dist/__tests__/confidence-v14.test.d.ts +13 -0
  8. package/dist/__tests__/confidence-v14.test.d.ts.map +1 -0
  9. package/dist/__tests__/confidence-v14.test.js +284 -0
  10. package/dist/__tests__/confidence-v14.test.js.map +1 -0
  11. package/dist/__tests__/config.test.d.ts +2 -0
  12. package/dist/__tests__/config.test.d.ts.map +1 -0
  13. package/dist/__tests__/config.test.js +317 -0
  14. package/dist/__tests__/config.test.js.map +1 -0
  15. package/dist/__tests__/corpus.d.ts +36 -0
  16. package/dist/__tests__/corpus.d.ts.map +1 -0
  17. package/dist/__tests__/corpus.js +541 -0
  18. package/dist/__tests__/corpus.js.map +1 -0
  19. package/dist/__tests__/corpus.test.d.ts +17 -0
  20. package/dist/__tests__/corpus.test.d.ts.map +1 -0
  21. package/dist/__tests__/corpus.test.js +179 -0
  22. package/dist/__tests__/corpus.test.js.map +1 -0
  23. package/dist/__tests__/diff.test.d.ts +10 -0
  24. package/dist/__tests__/diff.test.d.ts.map +1 -0
  25. package/dist/__tests__/diff.test.js +178 -0
  26. package/dist/__tests__/diff.test.js.map +1 -0
  27. package/dist/__tests__/format-resolvers.test.d.ts +2 -0
  28. package/dist/__tests__/format-resolvers.test.d.ts.map +1 -0
  29. package/dist/__tests__/format-resolvers.test.js +577 -0
  30. package/dist/__tests__/format-resolvers.test.js.map +1 -0
  31. package/dist/__tests__/imports-extended.test.d.ts +2 -0
  32. package/dist/__tests__/imports-extended.test.d.ts.map +1 -0
  33. package/dist/__tests__/imports-extended.test.js +94 -0
  34. package/dist/__tests__/imports-extended.test.js.map +1 -0
  35. package/dist/__tests__/lockfile-resolvers.test.d.ts +2 -0
  36. package/dist/__tests__/lockfile-resolvers.test.d.ts.map +1 -0
  37. package/dist/__tests__/lockfile-resolvers.test.js +200 -0
  38. package/dist/__tests__/lockfile-resolvers.test.js.map +1 -0
  39. package/dist/__tests__/patterns/insertion-at-boundary.test.d.ts +10 -0
  40. package/dist/__tests__/patterns/insertion-at-boundary.test.d.ts.map +1 -0
  41. package/dist/__tests__/patterns/insertion-at-boundary.test.js +185 -0
  42. package/dist/__tests__/patterns/insertion-at-boundary.test.js.map +1 -0
  43. package/dist/__tests__/patterns/reorder-only.test.d.ts +10 -0
  44. package/dist/__tests__/patterns/reorder-only.test.d.ts.map +1 -0
  45. package/dist/__tests__/patterns/reorder-only.test.js +181 -0
  46. package/dist/__tests__/patterns/reorder-only.test.js.map +1 -0
  47. package/dist/__tests__/phase-7-2-3b.test.d.ts +6 -0
  48. package/dist/__tests__/phase-7-2-3b.test.d.ts.map +1 -0
  49. package/dist/__tests__/phase-7-2-3b.test.js +730 -0
  50. package/dist/__tests__/phase-7-2-3b.test.js.map +1 -0
  51. package/dist/__tests__/resolver.test.d.ts +2 -0
  52. package/dist/__tests__/resolver.test.d.ts.map +1 -0
  53. package/dist/__tests__/resolver.test.js +927 -0
  54. package/dist/__tests__/resolver.test.js.map +1 -0
  55. package/dist/__tests__/resolvers/cargo.test.d.ts +10 -0
  56. package/dist/__tests__/resolvers/cargo.test.d.ts.map +1 -0
  57. package/dist/__tests__/resolvers/cargo.test.js +158 -0
  58. package/dist/__tests__/resolvers/cargo.test.js.map +1 -0
  59. package/dist/__tests__/resolvers/dockerfile.test.d.ts +8 -0
  60. package/dist/__tests__/resolvers/dockerfile.test.d.ts.map +1 -0
  61. package/dist/__tests__/resolvers/dockerfile.test.js +120 -0
  62. package/dist/__tests__/resolvers/dockerfile.test.js.map +1 -0
  63. package/dist/__tests__/resolvers/dotenv.test.d.ts +9 -0
  64. package/dist/__tests__/resolvers/dotenv.test.d.ts.map +1 -0
  65. package/dist/__tests__/resolvers/dotenv.test.js +113 -0
  66. package/dist/__tests__/resolvers/dotenv.test.js.map +1 -0
  67. package/dist/__tests__/resolvers/improvements-v14.test.d.ts +8 -0
  68. package/dist/__tests__/resolvers/improvements-v14.test.d.ts.map +1 -0
  69. package/dist/__tests__/resolvers/improvements-v14.test.js +306 -0
  70. package/dist/__tests__/resolvers/improvements-v14.test.js.map +1 -0
  71. package/dist/__tests__/validation.test.d.ts +12 -0
  72. package/dist/__tests__/validation.test.d.ts.map +1 -0
  73. package/dist/__tests__/validation.test.js +136 -0
  74. package/dist/__tests__/validation.test.js.map +1 -0
  75. package/dist/classifier.d.ts +21 -0
  76. package/dist/classifier.d.ts.map +1 -0
  77. package/dist/classifier.js +127 -0
  78. package/dist/classifier.js.map +1 -0
  79. package/dist/config.d.ts +108 -0
  80. package/dist/config.d.ts.map +1 -0
  81. package/dist/config.js +200 -0
  82. package/dist/config.js.map +1 -0
  83. package/dist/diff.d.ts +69 -0
  84. package/dist/diff.d.ts.map +1 -0
  85. package/dist/diff.js +328 -0
  86. package/dist/diff.js.map +1 -0
  87. package/dist/index.d.ts +47 -0
  88. package/dist/index.d.ts.map +1 -0
  89. package/dist/index.js +38 -0
  90. package/dist/index.js.map +1 -0
  91. package/dist/parser.d.ts +39 -0
  92. package/dist/parser.d.ts.map +1 -0
  93. package/dist/parser.js +164 -0
  94. package/dist/parser.js.map +1 -0
  95. package/dist/patterns/complex.d.ts +5 -0
  96. package/dist/patterns/complex.d.ts.map +1 -0
  97. package/dist/patterns/complex.js +27 -0
  98. package/dist/patterns/complex.js.map +1 -0
  99. package/dist/patterns/delete-no-change.d.ts +4 -0
  100. package/dist/patterns/delete-no-change.d.ts.map +1 -0
  101. package/dist/patterns/delete-no-change.js +75 -0
  102. package/dist/patterns/delete-no-change.js.map +1 -0
  103. package/dist/patterns/insertion-at-boundary.d.ts +22 -0
  104. package/dist/patterns/insertion-at-boundary.d.ts.map +1 -0
  105. package/dist/patterns/insertion-at-boundary.js +164 -0
  106. package/dist/patterns/insertion-at-boundary.js.map +1 -0
  107. package/dist/patterns/non-overlapping.d.ts +4 -0
  108. package/dist/patterns/non-overlapping.d.ts.map +1 -0
  109. package/dist/patterns/non-overlapping.js +28 -0
  110. package/dist/patterns/non-overlapping.js.map +1 -0
  111. package/dist/patterns/one-side-change.d.ts +4 -0
  112. package/dist/patterns/one-side-change.d.ts.map +1 -0
  113. package/dist/patterns/one-side-change.js +45 -0
  114. package/dist/patterns/one-side-change.js.map +1 -0
  115. package/dist/patterns/reorder-only.d.ts +14 -0
  116. package/dist/patterns/reorder-only.d.ts.map +1 -0
  117. package/dist/patterns/reorder-only.js +81 -0
  118. package/dist/patterns/reorder-only.js.map +1 -0
  119. package/dist/patterns/same-change.d.ts +4 -0
  120. package/dist/patterns/same-change.d.ts.map +1 -0
  121. package/dist/patterns/same-change.js +25 -0
  122. package/dist/patterns/same-change.js.map +1 -0
  123. package/dist/patterns/utils.d.ts +70 -0
  124. package/dist/patterns/utils.d.ts.map +1 -0
  125. package/dist/patterns/utils.js +206 -0
  126. package/dist/patterns/utils.js.map +1 -0
  127. package/dist/patterns/value-only-change.d.ts +4 -0
  128. package/dist/patterns/value-only-change.d.ts.map +1 -0
  129. package/dist/patterns/value-only-change.js +34 -0
  130. package/dist/patterns/value-only-change.js.map +1 -0
  131. package/dist/patterns/whitespace-only.d.ts +4 -0
  132. package/dist/patterns/whitespace-only.d.ts.map +1 -0
  133. package/dist/patterns/whitespace-only.js +32 -0
  134. package/dist/patterns/whitespace-only.js.map +1 -0
  135. package/dist/resolver/assemble.d.ts +25 -0
  136. package/dist/resolver/assemble.d.ts.map +1 -0
  137. package/dist/resolver/assemble.js +170 -0
  138. package/dist/resolver/assemble.js.map +1 -0
  139. package/dist/resolver/format-dispatch.d.ts +40 -0
  140. package/dist/resolver/format-dispatch.d.ts.map +1 -0
  141. package/dist/resolver/format-dispatch.js +51 -0
  142. package/dist/resolver/format-dispatch.js.map +1 -0
  143. package/dist/resolver/generated-detection.d.ts +48 -0
  144. package/dist/resolver/generated-detection.d.ts.map +1 -0
  145. package/dist/resolver/generated-detection.js +123 -0
  146. package/dist/resolver/generated-detection.js.map +1 -0
  147. package/dist/resolver/index.d.ts +26 -0
  148. package/dist/resolver/index.d.ts.map +1 -0
  149. package/dist/resolver/index.js +147 -0
  150. package/dist/resolver/index.js.map +1 -0
  151. package/dist/resolver/policy.d.ts +53 -0
  152. package/dist/resolver/policy.d.ts.map +1 -0
  153. package/dist/resolver/policy.js +99 -0
  154. package/dist/resolver/policy.js.map +1 -0
  155. package/dist/resolver/validation.d.ts +28 -0
  156. package/dist/resolver/validation.d.ts.map +1 -0
  157. package/dist/resolver/validation.js +96 -0
  158. package/dist/resolver/validation.js.map +1 -0
  159. package/dist/resolver.d.ts +18 -0
  160. package/dist/resolver.d.ts.map +1 -0
  161. package/dist/resolver.js +18 -0
  162. package/dist/resolver.js.map +1 -0
  163. package/dist/resolvers/cargo.d.ts +34 -0
  164. package/dist/resolvers/cargo.d.ts.map +1 -0
  165. package/dist/resolvers/cargo.js +262 -0
  166. package/dist/resolvers/cargo.js.map +1 -0
  167. package/dist/resolvers/css.d.ts +60 -0
  168. package/dist/resolvers/css.d.ts.map +1 -0
  169. package/dist/resolvers/css.js +531 -0
  170. package/dist/resolvers/css.js.map +1 -0
  171. package/dist/resolvers/dispatcher.d.ts +78 -0
  172. package/dist/resolvers/dispatcher.d.ts.map +1 -0
  173. package/dist/resolvers/dispatcher.js +290 -0
  174. package/dist/resolvers/dispatcher.js.map +1 -0
  175. package/dist/resolvers/dockerfile.d.ts +24 -0
  176. package/dist/resolvers/dockerfile.d.ts.map +1 -0
  177. package/dist/resolvers/dockerfile.js +221 -0
  178. package/dist/resolvers/dockerfile.js.map +1 -0
  179. package/dist/resolvers/dotenv.d.ts +27 -0
  180. package/dist/resolvers/dotenv.d.ts.map +1 -0
  181. package/dist/resolvers/dotenv.js +114 -0
  182. package/dist/resolvers/dotenv.js.map +1 -0
  183. package/dist/resolvers/imports.d.ts +63 -0
  184. package/dist/resolvers/imports.d.ts.map +1 -0
  185. package/dist/resolvers/imports.js +513 -0
  186. package/dist/resolvers/imports.js.map +1 -0
  187. package/dist/resolvers/json.d.ts +48 -0
  188. package/dist/resolvers/json.d.ts.map +1 -0
  189. package/dist/resolvers/json.js +363 -0
  190. package/dist/resolvers/json.js.map +1 -0
  191. package/dist/resolvers/lockfile-npm.d.ts +38 -0
  192. package/dist/resolvers/lockfile-npm.d.ts.map +1 -0
  193. package/dist/resolvers/lockfile-npm.js +267 -0
  194. package/dist/resolvers/lockfile-npm.js.map +1 -0
  195. package/dist/resolvers/lockfile-pnpm.d.ts +44 -0
  196. package/dist/resolvers/lockfile-pnpm.d.ts.map +1 -0
  197. package/dist/resolvers/lockfile-pnpm.js +277 -0
  198. package/dist/resolvers/lockfile-pnpm.js.map +1 -0
  199. package/dist/resolvers/lockfile-yarn.d.ts +40 -0
  200. package/dist/resolvers/lockfile-yarn.d.ts.map +1 -0
  201. package/dist/resolvers/lockfile-yarn.js +184 -0
  202. package/dist/resolvers/lockfile-yarn.js.map +1 -0
  203. package/dist/resolvers/markdown.d.ts +64 -0
  204. package/dist/resolvers/markdown.d.ts.map +1 -0
  205. package/dist/resolvers/markdown.js +335 -0
  206. package/dist/resolvers/markdown.js.map +1 -0
  207. package/dist/resolvers/vue.d.ts +65 -0
  208. package/dist/resolvers/vue.d.ts.map +1 -0
  209. package/dist/resolvers/vue.js +258 -0
  210. package/dist/resolvers/vue.js.map +1 -0
  211. package/dist/resolvers/yaml.d.ts +65 -0
  212. package/dist/resolvers/yaml.d.ts.map +1 -0
  213. package/dist/resolvers/yaml.js +405 -0
  214. package/dist/resolvers/yaml.js.map +1 -0
  215. package/dist/types.d.ts +256 -0
  216. package/dist/types.d.ts.map +1 -0
  217. package/dist/types.js +8 -0
  218. package/dist/types.js.map +1 -0
  219. package/package.json +57 -0
@@ -0,0 +1,927 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { resolve } from "../resolver.js";
3
+ import { mergeNonOverlapping, lcs, computeDiff } from "../diff.js";
4
+ /**
5
+ * Fixtures de fichiers avec conflits Git.
6
+ * Chaque fixture simule un cas réel.
7
+ */
8
+ // ═══════════════════════════════════════════════════════════════
9
+ // BASIC PATTERNS
10
+ // ═══════════════════════════════════════════════════════════════
11
+ const CONFLICT_SAME_CHANGE = `import { useState } from "react";
12
+ <<<<<<< ours
13
+ import { useEffect } from "react";
14
+ ||||||| base
15
+ =======
16
+ import { useEffect } from "react";
17
+ >>>>>>> theirs
18
+ export default function App() {}`;
19
+ const CONFLICT_ONE_SIDE = `const config = {
20
+ <<<<<<< ours
21
+ port: 3000,
22
+ host: "localhost",
23
+ ||||||| base
24
+ port: 3000,
25
+ =======
26
+ port: 3000,
27
+ >>>>>>> theirs
28
+ };`;
29
+ const CONFLICT_DELETE_NO_CHANGE = `function main() {
30
+ <<<<<<< ours
31
+ ||||||| base
32
+ console.log("debug");
33
+ =======
34
+ console.log("debug");
35
+ >>>>>>> theirs
36
+ return true;
37
+ }`;
38
+ const CONFLICT_WHITESPACE = `<<<<<<< ours
39
+ function hello() {
40
+ return "world";
41
+ }
42
+ =======
43
+ function hello() {
44
+ return "world";
45
+ }
46
+ >>>>>>> theirs`;
47
+ // ═══════════════════════════════════════════════════════════════
48
+ // NON-OVERLAPPING PATTERNS
49
+ // ═══════════════════════════════════════════════════════════════
50
+ const CONFLICT_NON_OVERLAPPING_IMPORTS = `<<<<<<< ours
51
+ import React from "react";
52
+ import { useState } from "react";
53
+ import { useEffect } from "react";
54
+ import axios from "axios";
55
+ ||||||| base
56
+ import React from "react";
57
+ import { useState } from "react";
58
+ import axios from "axios";
59
+ =======
60
+ import React from "react";
61
+ import { useState } from "react";
62
+ import axios from "axios";
63
+ import dayjs from "dayjs";
64
+ >>>>>>> theirs`;
65
+ const CONFLICT_NON_OVERLAPPING_CODE = `<<<<<<< ours
66
+ const API_URL = "https://api.example.com";
67
+ const TIMEOUT = 5000;
68
+ const RETRIES = 3;
69
+ const DEBUG = false;
70
+ ||||||| base
71
+ const API_URL = "https://api.example.com";
72
+ const TIMEOUT = 5000;
73
+ const RETRIES = 3;
74
+ =======
75
+ const API_URL = "https://api.production.com";
76
+ const TIMEOUT = 5000;
77
+ const RETRIES = 3;
78
+ >>>>>>> theirs`;
79
+ const CONFLICT_OVERLAPPING = `<<<<<<< ours
80
+ const API_URL = "https://staging.example.com";
81
+ const TIMEOUT = 10000;
82
+ ||||||| base
83
+ const API_URL = "https://api.example.com";
84
+ const TIMEOUT = 5000;
85
+ =======
86
+ const API_URL = "https://production.example.com";
87
+ const TIMEOUT = 3000;
88
+ >>>>>>> theirs`;
89
+ // ═══════════════════════════════════════════════════════════════
90
+ // COMPLEX / EDGE CASES
91
+ // ═══════════════════════════════════════════════════════════════
92
+ const CONFLICT_COMPLEX = `<<<<<<< ours
93
+ function calculate(a: number, b: number) {
94
+ return a + b;
95
+ }
96
+ =======
97
+ function calculate(x: number, y: number): number {
98
+ return x * y;
99
+ }
100
+ >>>>>>> theirs`;
101
+ const MULTIPLE_CONFLICTS = `import React from "react";
102
+ <<<<<<< ours
103
+ import { useState } from "react";
104
+ ||||||| base
105
+ =======
106
+ import { useState } from "react";
107
+ >>>>>>> theirs
108
+
109
+ const App = () => {
110
+ <<<<<<< ours
111
+ const title = "Hello GitWand";
112
+ ||||||| base
113
+ const title = "Hello World";
114
+ =======
115
+ const title = "Hello World";
116
+ >>>>>>> theirs
117
+ return <h1>{title}</h1>;
118
+ };`;
119
+ // ═══════════════════════════════════════════════════════════════
120
+ // REALISTIC FIXTURES — from real-world project patterns
121
+ // ═══════════════════════════════════════════════════════════════
122
+ /** package.json version bump — both branches bump the same version */
123
+ const REAL_PACKAGE_JSON_SAME = `{
124
+ "name": "my-app",
125
+ <<<<<<< ours
126
+ "version": "2.1.0",
127
+ ||||||| base
128
+ "version": "2.0.0",
129
+ =======
130
+ "version": "2.1.0",
131
+ >>>>>>> theirs
132
+ "license": "MIT"
133
+ }`;
134
+ /** package.json — different deps added by each branch (non-overlapping) */
135
+ const REAL_PACKAGE_JSON_DEPS = `{
136
+ "dependencies": {
137
+ <<<<<<< ours
138
+ "axios": "^1.6.0",
139
+ "react": "^18.2.0",
140
+ "react-dom": "^18.2.0",
141
+ "zustand": "^4.5.0"
142
+ ||||||| base
143
+ "axios": "^1.6.0",
144
+ "react": "^18.2.0",
145
+ "react-dom": "^18.2.0"
146
+ =======
147
+ "axios": "^1.6.0",
148
+ "dayjs": "^1.11.0",
149
+ "react": "^18.2.0",
150
+ "react-dom": "^18.2.0"
151
+ >>>>>>> theirs
152
+ }
153
+ }`;
154
+ /** Laravel PHP — route files, two devs adding routes in different groups */
155
+ const REAL_LARAVEL_ROUTES = `<?php
156
+ use Illuminate\\Support\\Facades\\Route;
157
+
158
+ <<<<<<< ours
159
+ Route::get('/dashboard', [DashboardController::class, 'index']);
160
+ Route::get('/dashboard/stats', [DashboardController::class, 'stats']);
161
+ Route::get('/users', [UserController::class, 'index']);
162
+ ||||||| base
163
+ Route::get('/dashboard', [DashboardController::class, 'index']);
164
+ Route::get('/users', [UserController::class, 'index']);
165
+ =======
166
+ Route::get('/dashboard', [DashboardController::class, 'index']);
167
+ Route::get('/users', [UserController::class, 'index']);
168
+ Route::get('/users/{id}/profile', [UserController::class, 'profile']);
169
+ >>>>>>> theirs`;
170
+ /** Vue SFC — one branch changes template, other changes script */
171
+ const REAL_VUE_SFC_NONOVERLAP = `<template>
172
+ <<<<<<< ours
173
+ <div class="container">
174
+ <h1>{{ title }}</h1>
175
+ <p>{{ description }}</p>
176
+ <UserList :users="users" />
177
+ </div>
178
+ ||||||| base
179
+ <div class="container">
180
+ <h1>{{ title }}</h1>
181
+ <UserList :users="users" />
182
+ </div>
183
+ =======
184
+ <div class="container">
185
+ <h1>{{ title }}</h1>
186
+ <UserList :users="users" />
187
+ </div>
188
+ >>>>>>> theirs
189
+ </template>`;
190
+ /** CSS — conflicting media queries (complex) */
191
+ const REAL_CSS_COMPLEX = `<<<<<<< ours
192
+ .header {
193
+ display: flex;
194
+ justify-content: space-between;
195
+ padding: 1rem 2rem;
196
+ }
197
+ =======
198
+ .header {
199
+ display: grid;
200
+ grid-template-columns: auto 1fr auto;
201
+ padding: 0.5rem 1rem;
202
+ }
203
+ >>>>>>> theirs`;
204
+ /** Typical .env.example — one side adds, other doesn't touch */
205
+ const REAL_ENV_ONE_SIDE = `APP_NAME=MyApp
206
+ <<<<<<< ours
207
+ APP_ENV=production
208
+ APP_DEBUG=false
209
+ APP_URL=https://myapp.com
210
+ SENTRY_DSN=https://sentry.io/xxx
211
+ ||||||| base
212
+ APP_ENV=production
213
+ APP_DEBUG=false
214
+ APP_URL=https://myapp.com
215
+ =======
216
+ APP_ENV=production
217
+ APP_DEBUG=false
218
+ APP_URL=https://myapp.com
219
+ >>>>>>> theirs
220
+ DB_HOST=localhost`;
221
+ /** Mixed file: 3 conflicts — 2 resolvable, 1 complex */
222
+ const REAL_MIXED_FILE = `import express from "express";
223
+ <<<<<<< ours
224
+ import cors from "cors";
225
+ import helmet from "helmet";
226
+ ||||||| base
227
+ import cors from "cors";
228
+ =======
229
+ import cors from "cors";
230
+ >>>>>>> theirs
231
+
232
+ const app = express();
233
+
234
+ <<<<<<< ours
235
+ app.use(cors({ origin: "https://myapp.com" }));
236
+ ||||||| base
237
+ app.use(cors());
238
+ =======
239
+ app.use(cors());
240
+ >>>>>>> theirs
241
+
242
+ <<<<<<< ours
243
+ app.get("/health", (req, res) => {
244
+ res.json({ status: "ok", version: "2.0" });
245
+ });
246
+ =======
247
+ app.get("/health", (req, res) => {
248
+ res.json({ healthy: true, uptime: process.uptime() });
249
+ });
250
+ >>>>>>> theirs
251
+
252
+ app.listen(3000);`;
253
+ // ═══════════════════════════════════════════════════════════════
254
+ // TESTS
255
+ // ═══════════════════════════════════════════════════════════════
256
+ describe("@gitwand/core resolve", () => {
257
+ describe("same_change", () => {
258
+ it("resolves when both branches made the same edit", () => {
259
+ const result = resolve(CONFLICT_SAME_CHANGE, "app.tsx");
260
+ expect(result.stats.totalConflicts).toBe(1);
261
+ expect(result.stats.autoResolved).toBe(1);
262
+ expect(result.stats.remaining).toBe(0);
263
+ expect(result.mergedContent).toContain('import { useEffect } from "react"');
264
+ expect(result.mergedContent).not.toContain("<<<<<<<");
265
+ expect(result.hunks[0].type).toBe("same_change");
266
+ expect(result.hunks[0].confidence.label).toBe("certain");
267
+ });
268
+ });
269
+ describe("one_side_change", () => {
270
+ it("resolves by taking the side that changed (ours)", () => {
271
+ const result = resolve(CONFLICT_ONE_SIDE, "config.ts");
272
+ expect(result.stats.autoResolved).toBe(1);
273
+ expect(result.mergedContent).toContain('host: "localhost"');
274
+ expect(result.hunks[0].type).toBe("one_side_change");
275
+ });
276
+ });
277
+ describe("delete_no_change", () => {
278
+ it("resolves by accepting the deletion", () => {
279
+ const result = resolve(CONFLICT_DELETE_NO_CHANGE, "main.ts");
280
+ expect(result.stats.autoResolved).toBe(1);
281
+ expect(result.mergedContent).not.toContain("console.log");
282
+ expect(result.hunks[0].type).toBe("delete_no_change");
283
+ });
284
+ });
285
+ describe("whitespace_only", () => {
286
+ it("resolves whitespace-only conflicts", () => {
287
+ const result = resolve(CONFLICT_WHITESPACE, "hello.ts");
288
+ expect(result.stats.autoResolved).toBe(1);
289
+ expect(result.hunks[0].type).toBe("whitespace_only");
290
+ });
291
+ it("skips whitespace if option is disabled", () => {
292
+ const result = resolve(CONFLICT_WHITESPACE, "hello.ts", {
293
+ resolveWhitespace: false,
294
+ });
295
+ expect(result.stats.autoResolved).toBe(0);
296
+ expect(result.stats.remaining).toBe(1);
297
+ });
298
+ });
299
+ describe("non_overlapping", () => {
300
+ it("merges imports added at different locations", () => {
301
+ const result = resolve(CONFLICT_NON_OVERLAPPING_IMPORTS, "imports.ts");
302
+ expect(result.stats.autoResolved).toBe(1);
303
+ expect(result.hunks[0].type).toBe("non_overlapping");
304
+ expect(result.hunks[0].confidence.label).toBe("high");
305
+ expect(result.mergedContent).toContain("useEffect");
306
+ expect(result.mergedContent).toContain("dayjs");
307
+ expect(result.mergedContent).toContain("React");
308
+ expect(result.mergedContent).toContain("useState");
309
+ expect(result.mergedContent).toContain("axios");
310
+ });
311
+ it("merges code modifications at different locations", () => {
312
+ const result = resolve(CONFLICT_NON_OVERLAPPING_CODE, "config.ts");
313
+ expect(result.stats.autoResolved).toBe(1);
314
+ expect(result.hunks[0].type).toBe("non_overlapping");
315
+ expect(result.mergedContent).toContain("DEBUG");
316
+ expect(result.mergedContent).toContain("production.com");
317
+ });
318
+ it("does NOT merge when modifications overlap", () => {
319
+ const result = resolve(CONFLICT_OVERLAPPING, "config.ts");
320
+ expect(result.hunks[0].type).toBe("complex");
321
+ expect(result.stats.autoResolved).toBe(0);
322
+ });
323
+ it("skips if option is disabled", () => {
324
+ const result = resolve(CONFLICT_NON_OVERLAPPING_IMPORTS, "imports.ts", {
325
+ resolveNonOverlapping: false,
326
+ });
327
+ expect(result.stats.autoResolved).toBe(0);
328
+ });
329
+ });
330
+ describe("complex", () => {
331
+ it("does not resolve complex conflicts", () => {
332
+ const result = resolve(CONFLICT_COMPLEX, "calc.ts");
333
+ expect(result.stats.autoResolved).toBe(0);
334
+ expect(result.stats.remaining).toBe(1);
335
+ expect(result.mergedContent).toBeNull();
336
+ expect(result.hunks[0].type).toBe("complex");
337
+ });
338
+ });
339
+ describe("multiple conflicts in one file", () => {
340
+ it("resolves trivial conflicts and leaves others", () => {
341
+ const result = resolve(MULTIPLE_CONFLICTS, "app.tsx");
342
+ expect(result.stats.totalConflicts).toBe(2);
343
+ expect(result.resolutions[0].autoResolved).toBe(true);
344
+ expect(result.resolutions[1].autoResolved).toBe(true);
345
+ expect(result.stats.autoResolved).toBe(2);
346
+ });
347
+ });
348
+ describe("clean file (no conflicts)", () => {
349
+ it("returns content as-is", () => {
350
+ const clean = 'const x = 42;\nconsole.log(x);\n';
351
+ const result = resolve(clean, "clean.ts");
352
+ expect(result.stats.totalConflicts).toBe(0);
353
+ expect(result.mergedContent).toBe(clean);
354
+ });
355
+ });
356
+ // ═════════════════════════════════════════════════════════════
357
+ // REALISTIC SCENARIOS
358
+ // ═════════════════════════════════════════════════════════════
359
+ describe("real-world: package.json", () => {
360
+ it("resolves identical version bumps (same_change)", () => {
361
+ const result = resolve(REAL_PACKAGE_JSON_SAME, "package.json");
362
+ expect(result.stats.autoResolved).toBe(1);
363
+ expect(result.hunks[0].type).toBe("same_change");
364
+ expect(result.mergedContent).toContain('"version": "2.1.0"');
365
+ });
366
+ it("merges different dependencies added by each branch", () => {
367
+ const result = resolve(REAL_PACKAGE_JSON_DEPS, "package.json");
368
+ expect(result.stats.autoResolved).toBe(1);
369
+ expect(result.hunks[0].type).toBe("non_overlapping");
370
+ expect(result.mergedContent).toContain("zustand");
371
+ expect(result.mergedContent).toContain("dayjs");
372
+ expect(result.mergedContent).toContain("react");
373
+ });
374
+ });
375
+ describe("real-world: Laravel routes", () => {
376
+ it("merges routes added in different locations", () => {
377
+ const result = resolve(REAL_LARAVEL_ROUTES, "routes/web.php");
378
+ expect(result.stats.autoResolved).toBe(1);
379
+ expect(result.hunks[0].type).toBe("non_overlapping");
380
+ expect(result.mergedContent).toContain("stats");
381
+ expect(result.mergedContent).toContain("profile");
382
+ });
383
+ });
384
+ describe("real-world: Vue SFC", () => {
385
+ it("resolves template change as one_side_change", () => {
386
+ const result = resolve(REAL_VUE_SFC_NONOVERLAP, "MyComponent.vue");
387
+ expect(result.stats.autoResolved).toBe(1);
388
+ expect(result.hunks[0].type).toBe("one_side_change");
389
+ expect(result.mergedContent).toContain("description");
390
+ });
391
+ });
392
+ describe("real-world: CSS conflicts", () => {
393
+ it("does NOT auto-resolve conflicting CSS architectures", () => {
394
+ const result = resolve(REAL_CSS_COMPLEX, "styles.css");
395
+ expect(result.stats.autoResolved).toBe(0);
396
+ expect(result.hunks[0].type).toBe("complex");
397
+ });
398
+ });
399
+ describe("real-world: .env one-side addition", () => {
400
+ it("resolves when one branch adds a new env variable", () => {
401
+ const result = resolve(REAL_ENV_ONE_SIDE, ".env.example");
402
+ expect(result.stats.autoResolved).toBe(1);
403
+ expect(result.hunks[0].type).toBe("one_side_change");
404
+ expect(result.mergedContent).toContain("SENTRY_DSN");
405
+ expect(result.mergedContent).toContain("DB_HOST");
406
+ });
407
+ });
408
+ describe("real-world: mixed file (resolvable + complex)", () => {
409
+ it("resolves 2 out of 3 conflicts, leaves the complex one", () => {
410
+ const result = resolve(REAL_MIXED_FILE, "server.ts");
411
+ expect(result.stats.totalConflicts).toBe(3);
412
+ // First: one_side_change (ours added helmet)
413
+ expect(result.resolutions[0].autoResolved).toBe(true);
414
+ // Second: one_side_change (ours changed cors config)
415
+ expect(result.resolutions[1].autoResolved).toBe(true);
416
+ // Third: complex (different health endpoint implementations)
417
+ expect(result.resolutions[2].autoResolved).toBe(false);
418
+ expect(result.resolutions[2].hunk.type).toBe("complex");
419
+ expect(result.stats.autoResolved).toBe(2);
420
+ expect(result.stats.remaining).toBe(1);
421
+ // mergedContent is null because one conflict remains
422
+ expect(result.mergedContent).toBeNull();
423
+ });
424
+ });
425
+ // ═════════════════════════════════════════════════════════════
426
+ // DIFF UTILITIES
427
+ // ═════════════════════════════════════════════════════════════
428
+ describe("diff utilities", () => {
429
+ it("lcs finds the longest common subsequence", () => {
430
+ const a = ["a", "b", "c", "d"];
431
+ const b = ["a", "x", "c", "d"];
432
+ const result = lcs(a, b);
433
+ expect(result).toEqual([[0, 0], [2, 2], [3, 3]]);
434
+ });
435
+ it("lcs handles empty arrays", () => {
436
+ expect(lcs([], ["a"])).toEqual([]);
437
+ expect(lcs(["a"], [])).toEqual([]);
438
+ expect(lcs([], [])).toEqual([]);
439
+ });
440
+ it("lcs handles identical arrays", () => {
441
+ const a = ["a", "b", "c"];
442
+ const result = lcs(a, a);
443
+ expect(result).toEqual([[0, 0], [1, 1], [2, 2]]);
444
+ });
445
+ it("computeDiff identifies additions and removals", () => {
446
+ const base = ["a", "b", "c"];
447
+ const branch = ["a", "x", "b", "c"];
448
+ const diff = computeDiff(base, branch);
449
+ const adds = diff.filter((d) => d.type === "add");
450
+ expect(adds.length).toBe(1);
451
+ expect(adds[0].line).toBe("x");
452
+ });
453
+ it("computeDiff handles complete replacement", () => {
454
+ const base = ["a", "b"];
455
+ const branch = ["x", "y"];
456
+ const diff = computeDiff(base, branch);
457
+ const removes = diff.filter((d) => d.type === "remove");
458
+ const adds = diff.filter((d) => d.type === "add");
459
+ expect(removes.length).toBe(2);
460
+ expect(adds.length).toBe(2);
461
+ });
462
+ it("mergeNonOverlapping merges additions at different locations", () => {
463
+ const base = ["a", "b", "c"];
464
+ const ours = ["a", "x", "b", "c"];
465
+ const theirs = ["a", "b", "c", "y"];
466
+ const result = mergeNonOverlapping(base, ours, theirs);
467
+ expect(result).not.toBeNull();
468
+ expect(result).toEqual(["a", "x", "b", "c", "y"]);
469
+ });
470
+ it("mergeNonOverlapping returns null on overlapping edits", () => {
471
+ const base = ["a", "b", "c"];
472
+ const ours = ["a", "X", "c"];
473
+ const theirs = ["a", "Y", "c"];
474
+ const result = mergeNonOverlapping(base, ours, theirs);
475
+ expect(result).toBeNull();
476
+ });
477
+ it("mergeNonOverlapping handles one side adding, other side deleting elsewhere", () => {
478
+ const base = ["a", "b", "c", "d"];
479
+ const ours = ["a", "b", "c", "d", "e"]; // added "e" at end
480
+ const theirs = ["a", "c", "d"]; // removed "b"
481
+ const result = mergeNonOverlapping(base, ours, theirs);
482
+ expect(result).not.toBeNull();
483
+ expect(result).toEqual(["a", "c", "d", "e"]);
484
+ });
485
+ });
486
+ // ═════════════════════════════════════════════════════════════
487
+ // STATS & REPORTING
488
+ // ═════════════════════════════════════════════════════════════
489
+ describe("stats and reporting", () => {
490
+ it("provides human-readable explanations for each hunk", () => {
491
+ const result = resolve(CONFLICT_ONE_SIDE, "config.ts");
492
+ expect(result.hunks[0].explanation).toBeTruthy();
493
+ expect(typeof result.hunks[0].explanation).toBe("string");
494
+ expect(result.hunks[0].explanation.length).toBeGreaterThan(10);
495
+ });
496
+ it("provides stats by type", () => {
497
+ const result = resolve(MULTIPLE_CONFLICTS, "app.tsx");
498
+ expect(result.stats.byType).toBeDefined();
499
+ expect(typeof result.stats.byType).toBe("object");
500
+ });
501
+ it("counts correctly with mixed resolvable/non-resolvable", () => {
502
+ const result = resolve(REAL_MIXED_FILE, "server.ts");
503
+ expect(result.stats.totalConflicts).toBe(3);
504
+ expect(result.stats.autoResolved).toBe(2);
505
+ expect(result.stats.remaining).toBe(1);
506
+ expect(result.stats.autoResolved + result.stats.remaining).toBe(result.stats.totalConflicts);
507
+ });
508
+ });
509
+ // ═════════════════════════════════════════════════════════════
510
+ // DIFF2 IMPROVEMENTS (no base)
511
+ // ═════════════════════════════════════════════════════════════
512
+ describe("diff2: value_only_change", () => {
513
+ it("detects hash-only differences in build manifest", () => {
514
+ const manifest = `{
515
+ <<<<<<< HEAD
516
+ "_Foo-DIwZRTuY.js": {
517
+ "file": "assets/Foo-DIwZRTuY.js",
518
+ =======
519
+ "_Foo-Bv7I4tRv.js": {
520
+ "file": "assets/Foo-Bv7I4tRv.js",
521
+ >>>>>>> master
522
+ "name": "Foo"
523
+ }
524
+ }`;
525
+ const result = resolve(manifest, "build/manifest.json");
526
+ expect(result.hunks[0].type).toBe("value_only_change");
527
+ expect(result.hunks[0].confidence.label).toBe("high");
528
+ expect(result.stats.autoResolved).toBe(1);
529
+ // Should take theirs
530
+ expect(result.mergedContent).toContain("Bv7I4tRv");
531
+ expect(result.mergedContent).not.toContain("DIwZRTuY");
532
+ });
533
+ it("detects version-only changes in diff2", () => {
534
+ const lockEntry = `<<<<<<< HEAD
535
+ "version": "3.2.1",
536
+ "resolved": "https://registry.npmjs.org/foo/-/foo-3.2.1.tgz",
537
+ "integrity": "sha512-abc123def456"
538
+ =======
539
+ "version": "3.3.0",
540
+ "resolved": "https://registry.npmjs.org/foo/-/foo-3.3.0.tgz",
541
+ "integrity": "sha512-xyz789ghi012"
542
+ >>>>>>> master`;
543
+ const result = resolve(lockEntry, "package-lock.json");
544
+ expect(result.hunks[0].type).toBe("value_only_change");
545
+ expect(result.stats.autoResolved).toBe(1);
546
+ });
547
+ it("does NOT classify as value_only when structure differs", () => {
548
+ const diff = `<<<<<<< HEAD
549
+ export const API_URL = "https://staging.example.com";
550
+ export const TIMEOUT = 5000;
551
+ export const DEBUG = true;
552
+ =======
553
+ export const API_URL = "https://production.example.com";
554
+ export const TIMEOUT = 5000;
555
+ >>>>>>> master`;
556
+ const result = resolve(diff, "config.ts");
557
+ // Different number of lines → not value_only
558
+ expect(result.hunks[0].type).toBe("complex");
559
+ });
560
+ });
561
+ describe("diff2: delete_no_change (sans base)", () => {
562
+ it("detects deletion by ours (empty ours, non-empty theirs)", () => {
563
+ const diff = `before
564
+ <<<<<<< HEAD
565
+ =======
566
+ some old code;
567
+ >>>>>>> master
568
+ after`;
569
+ const result = resolve(diff, "file.ts", { minConfidence: "medium" });
570
+ expect(result.hunks[0].type).toBe("delete_no_change");
571
+ expect(result.hunks[0].confidence.label).toBe("medium");
572
+ expect(result.stats.autoResolved).toBe(1);
573
+ expect(result.mergedContent).toBe("before\nafter");
574
+ });
575
+ it("detects deletion by theirs (non-empty ours, empty theirs)", () => {
576
+ const diff = `before
577
+ <<<<<<< HEAD
578
+ some old code;
579
+ =======
580
+ >>>>>>> master
581
+ after`;
582
+ const result = resolve(diff, "file.ts", { minConfidence: "medium" });
583
+ expect(result.hunks[0].type).toBe("delete_no_change");
584
+ expect(result.stats.autoResolved).toBe(1);
585
+ });
586
+ });
587
+ describe("generated file detection", () => {
588
+ it("reclassifies truly complex conflicts in .min.js as generated_file", () => {
589
+ // Minified code with structural differences (not just value changes)
590
+ const minJs = `<<<<<<< HEAD
591
+ !function(){var a=1;console.log(a);doStuff()}();
592
+ =======
593
+ !function(){var b=2;alert(b);doOther();cleanup()}();
594
+ >>>>>>> master`;
595
+ const result = resolve(minJs, "public/dist/app.min.js", { minConfidence: "medium" });
596
+ expect(result.hunks[0].type).toBe("generated_file");
597
+ expect(result.stats.autoResolved).toBe(1);
598
+ });
599
+ it("reclassifies complex conflicts in package-lock.json as generated_file", () => {
600
+ const lockJson = `<<<<<<< HEAD
601
+ "node_modules/foo": {
602
+ "version": "1.0.0",
603
+ "requires": { "bar": "^2.0" }
604
+ }
605
+ =======
606
+ "node_modules/foo": {
607
+ "version": "1.1.0",
608
+ "requires": { "bar": "^2.0", "baz": "^1.0" }
609
+ }
610
+ >>>>>>> master`;
611
+ const result = resolve(lockJson, "package-lock.json", { minConfidence: "medium" });
612
+ expect(result.hunks[0].type).toBe("generated_file");
613
+ expect(result.stats.autoResolved).toBe(1);
614
+ });
615
+ it("reclassifies complex in build/manifest.json as generated_file", () => {
616
+ const manifest = `<<<<<<< HEAD
617
+ "resources/js/app.js": {
618
+ "file": "assets/app-abc.js",
619
+ "css": ["assets/app-abc.css"]
620
+ }
621
+ =======
622
+ "resources/js/app.js": {
623
+ "file": "assets/app-xyz.js",
624
+ "css": ["assets/app-xyz.css"],
625
+ "extra": true
626
+ }
627
+ >>>>>>> master`;
628
+ const result = resolve(manifest, "public/build/manifest.json", { minConfidence: "medium" });
629
+ expect(result.hunks[0].type).toBe("generated_file");
630
+ expect(result.stats.autoResolved).toBe(1);
631
+ });
632
+ it("does NOT mark normal .ts files as generated", () => {
633
+ const ts = `<<<<<<< HEAD
634
+ const x = 1;
635
+ =======
636
+ const x = 2;
637
+ >>>>>>> master`;
638
+ const result = resolve(ts, "src/utils/config.ts");
639
+ expect(result.hunks[0].type).not.toBe("generated_file");
640
+ });
641
+ // ─── P2.4 — user-provided generated patterns ─────────
642
+ it("P2.4: reclassifie via un pattern user (*.generated.ts)", () => {
643
+ const ts = `<<<<<<< HEAD
644
+ export const foo = { a: 1, b: 2, c: 3 };
645
+ export function bar() { return 1; }
646
+ =======
647
+ export const foo = { a: 1, b: 999, d: 4 };
648
+ export function bar() { return 2; }
649
+ >>>>>>> master`;
650
+ // Sans config : pas reclassifié (fichier non dans les built-ins)
651
+ const baseline = resolve(ts, "src/schema.generated.ts", { minConfidence: "medium" });
652
+ expect(baseline.hunks[0].type).not.toBe("generated_file");
653
+ // Avec user pattern : reclassifié
654
+ const withConfig = resolve(ts, "src/schema.generated.ts", {
655
+ minConfidence: "medium",
656
+ generatedFiles: ["**/*.generated.ts"],
657
+ });
658
+ expect(withConfig.hunks[0].type).toBe("generated_file");
659
+ expect(withConfig.hunks[0].confidence.boosters[0]).toMatch(/user pattern: /);
660
+ });
661
+ it("P2.4: les built-ins gardent la priorité sur les user patterns", () => {
662
+ // package-lock.json matche un built-in ; un user pattern ne doit pas
663
+ // écraser le label descriptif ("npm lockfile" > "user pattern: ...").
664
+ const lockJson = `<<<<<<< HEAD
665
+ "node_modules/foo": { "version": "1.0.0" }
666
+ =======
667
+ "node_modules/foo": { "version": "1.1.0", "extra": true }
668
+ >>>>>>> master`;
669
+ const result = resolve(lockJson, "package-lock.json", {
670
+ minConfidence: "medium",
671
+ generatedFiles: ["**/*"], // user pattern qui matche tout
672
+ });
673
+ expect(result.hunks[0].type).toBe("generated_file");
674
+ // Le booster doit refléter le built-in, pas le user pattern
675
+ expect(result.hunks[0].confidence.boosters[0]).toContain("npm lockfile");
676
+ expect(result.hunks[0].confidence.boosters[0]).not.toContain("user pattern:");
677
+ });
678
+ it("P2.4: un tableau vide de user patterns n'altère pas la détection", () => {
679
+ const ts = `<<<<<<< HEAD
680
+ const x = 1;
681
+ =======
682
+ const x = 2;
683
+ >>>>>>> master`;
684
+ const result = resolve(ts, "src/foo.ts", { generatedFiles: [] });
685
+ expect(result.hunks[0].type).not.toBe("generated_file");
686
+ });
687
+ it("P2.4: match glob sur basename (*.pb.go sans chemin)", () => {
688
+ const go = `<<<<<<< HEAD
689
+ var Foo = []byte{0x01, 0x02}
690
+ var Bar = []byte{0x03}
691
+ =======
692
+ var Foo = []byte{0x01, 0x02, 0x03}
693
+ var Bar = []byte{0x04, 0x05}
694
+ >>>>>>> master`;
695
+ const result = resolve(go, "proto/user.pb.go", {
696
+ minConfidence: "medium",
697
+ generatedFiles: ["*.pb.go"],
698
+ });
699
+ expect(result.hunks[0].type).toBe("generated_file");
700
+ });
701
+ });
702
+ // ═══════════════════════════════════════════════════════════════
703
+ // PHASE 7.1 — DecisionTrace
704
+ // ═══════════════════════════════════════════════════════════════
705
+ describe("Phase 7.1 — DecisionTrace", () => {
706
+ it("chaque hunk a une trace avec le type sélectionné", () => {
707
+ const result = resolve(CONFLICT_SAME_CHANGE, "App.tsx");
708
+ const hunk = result.hunks[0];
709
+ expect(hunk.trace).toBeDefined();
710
+ expect(hunk.trace.selected).toBe("same_change");
711
+ // CONFLICT_SAME_CHANGE a un base vide (||||||| base\n======= avec rien entre)
712
+ // donc hasBase = false. On vérifie juste que c'est un boolean.
713
+ expect(typeof hunk.trace.hasBase).toBe("boolean");
714
+ });
715
+ it("la trace contient des étapes évaluées", () => {
716
+ const result = resolve(CONFLICT_SAME_CHANGE, "App.tsx");
717
+ const { trace } = result.hunks[0];
718
+ expect(trace.steps.length).toBeGreaterThan(0);
719
+ // L'étape gagnante doit avoir passed: true
720
+ const winner = trace.steps.find((s) => s.passed);
721
+ expect(winner).toBeDefined();
722
+ expect(winner.type).toBe("same_change");
723
+ });
724
+ it("les étapes rejetées ont passed: false avec une raison", () => {
725
+ const result = resolve(CONFLICT_ONE_SIDE, "config.ts");
726
+ const { trace } = result.hunks[0];
727
+ const rejected = trace.steps.filter((s) => !s.passed);
728
+ // same_change doit être rejeté avant d'arriver à one_side_change
729
+ expect(rejected.some((s) => s.type === "same_change")).toBe(true);
730
+ rejected.forEach((s) => {
731
+ expect(s.reason.length).toBeGreaterThan(0);
732
+ });
733
+ });
734
+ it("la trace d'un conflit complexe passe par plusieurs rejets", () => {
735
+ const complex = `<<<<<<< ours
736
+ function foo() {
737
+ return 42;
738
+ }
739
+ ||||||| base
740
+ function foo() {
741
+ return 0;
742
+ }
743
+ =======
744
+ function foo() {
745
+ return "hello";
746
+ }
747
+ >>>>>>> theirs`;
748
+ const result = resolve(complex, "utils.ts");
749
+ const { trace } = result.hunks[0];
750
+ expect(trace.selected).toBe("complex");
751
+ // Doit avoir testé plusieurs types avant d'arriver à complex
752
+ expect(trace.steps.length).toBeGreaterThan(3);
753
+ // Le dernier step doit être complex avec passed: true
754
+ const lastStep = trace.steps[trace.steps.length - 1];
755
+ expect(lastStep.type).toBe("complex");
756
+ expect(lastStep.passed).toBe(true);
757
+ });
758
+ it("la trace a un résumé lisible non vide", () => {
759
+ const result = resolve(CONFLICT_WHITESPACE, "styles.css");
760
+ expect(result.hunks[0].trace.summary.length).toBeGreaterThan(10);
761
+ });
762
+ it("chaque HunkResolution a une resolutionReason", () => {
763
+ const result = resolve(CONFLICT_ONE_SIDE, "config.ts");
764
+ const res = result.resolutions[0];
765
+ expect(res.resolutionReason).toBeDefined();
766
+ expect(res.resolutionReason.length).toBeGreaterThan(0);
767
+ });
768
+ it("resolutionReason explique pourquoi un conflit complexe n'est pas résolu", () => {
769
+ const complex = `<<<<<<< ours
770
+ return 42;
771
+ ||||||| base
772
+ return 0;
773
+ =======
774
+ return "hello";
775
+ >>>>>>> theirs`;
776
+ // Passer minConfidence: "low" pour bypasser le filtre de confiance
777
+ // et atteindre la branche "complex" dans resolveHunk
778
+ const result = resolve(complex, "utils.ts", { minConfidence: "low" });
779
+ const res = result.resolutions[0];
780
+ expect(res.autoResolved).toBe(false);
781
+ expect(res.resolutionReason).toMatch(/complexe|manuelle/i);
782
+ });
783
+ it("mode explainOnly : classi fie sans résoudre", () => {
784
+ const result = resolve(CONFLICT_SAME_CHANGE, "App.tsx", { explainOnly: true });
785
+ // Doit classer correctement
786
+ expect(result.hunks[0].type).toBe("same_change");
787
+ // Mais pas résoudre
788
+ expect(result.resolutions[0].autoResolved).toBe(false);
789
+ expect(result.resolutions[0].resolvedLines).toBeNull();
790
+ expect(result.mergedContent).toBeNull();
791
+ // La raison doit mentionner explainOnly
792
+ expect(result.resolutions[0].resolutionReason).toMatch(/explain-only/i);
793
+ });
794
+ it("mode explainOnly préserve les traces", () => {
795
+ const result = resolve(CONFLICT_NON_OVERLAPPING_IMPORTS, "imports.ts", { explainOnly: true });
796
+ result.hunks.forEach((hunk) => {
797
+ expect(hunk.trace).toBeDefined();
798
+ expect(hunk.trace.steps.length).toBeGreaterThan(0);
799
+ });
800
+ });
801
+ it("trace diff2 : hasBase = false", () => {
802
+ const diff2 = `<<<<<<< ours
803
+ const x = 1;
804
+ =======
805
+ const x = 2;
806
+ >>>>>>> theirs`;
807
+ const result = resolve(diff2, "config.ts");
808
+ expect(result.hunks[0].trace.hasBase).toBe(false);
809
+ });
810
+ it("trace diff3 : hasBase = true quand la base est non vide", () => {
811
+ // CONFLICT_ONE_SIDE a une base non vide ("port: 3000")
812
+ const result = resolve(CONFLICT_ONE_SIDE, "config.ts");
813
+ expect(result.hunks[0].trace.hasBase).toBe(true);
814
+ });
815
+ });
816
+ // ═══════════════════════════════════════════════════════════════
817
+ // PHASE 7.2 — Validation post-merge
818
+ // ═══════════════════════════════════════════════════════════════
819
+ describe("Phase 7.2 — Validation post-merge", () => {
820
+ it("un fichier entièrement résolu passe la validation", () => {
821
+ const result = resolve(CONFLICT_SAME_CHANGE, "App.tsx");
822
+ expect(result.validation.isValid).toBe(true);
823
+ expect(result.validation.hasResidualMarkers).toBe(false);
824
+ expect(result.validation.syntaxError).toBeNull();
825
+ });
826
+ it("un fichier non résolu a une validation vide (contenu null)", () => {
827
+ const complex = `<<<<<<< ours
828
+ return 42;
829
+ ||||||| base
830
+ return 0;
831
+ =======
832
+ return "hello";
833
+ >>>>>>> theirs`;
834
+ const result = resolve(complex, "utils.ts");
835
+ // mergedContent est null donc on n'applique pas la validation
836
+ expect(result.mergedContent).toBeNull();
837
+ expect(result.validation.isValid).toBe(true); // validation vide = OK (pas de contenu à valider)
838
+ expect(result.validation.hasResidualMarkers).toBe(false);
839
+ });
840
+ it("détecte les marqueurs résiduels dans le contenu fusionné", () => {
841
+ // Simuler un fichier qui a été résolu mais contient encore un marqueur
842
+ // On fait ça en injectant directement dans le contenu
843
+ // La seule façon réelle est un fichier non entièrement résolu…
844
+ // mais la validation s'applique au mergedContent (qui est null si non résolu).
845
+ // Donc on teste la fonction via un fichier sans conflits mais avec "marqueurs" dans le texte.
846
+ const withFakeMarker = `function test() {
847
+ <<<<<<< ours
848
+ return 1;
849
+ >>>>>>> theirs
850
+ }`;
851
+ // Un fichier avec vrais marqueurs → ne sera pas entièrement résolu
852
+ // On ne peut pas facilement tester hasResidualMarkers = true sans un bug de résolution.
853
+ // On teste plutôt que la validation tourne bien sur du contenu propre.
854
+ const result = resolve(CONFLICT_ONE_SIDE, "config.ts");
855
+ expect(result.validation).toBeDefined();
856
+ expect(typeof result.validation.isValid).toBe("boolean");
857
+ expect(Array.isArray(result.validation.residualMarkerLines)).toBe(true);
858
+ });
859
+ it("détecte les erreurs de syntaxe JSON", () => {
860
+ // Un fichier JSON avec conflit résolu mais résultat invalide JSON
861
+ // On test avec un fichier .json dont le contenu résolu serait mal formé
862
+ const jsonConflict = `{
863
+ <<<<<<< ours
864
+ "version": "1.0.0",
865
+ "name": "my-app",
866
+ ||||||| base
867
+ "version": "1.0.0",
868
+ =======
869
+ "version": "1.0.0",
870
+ >>>>>>> theirs
871
+ }`;
872
+ // Le fichier restera non résolu (theirs = base, ours a changé → one_side_change)
873
+ // Dans ce cas on ne valide pas le JSON (mergedContent null ou null)
874
+ const result = resolve(jsonConflict, "package.json");
875
+ // Soit résolu avec contenu valide, soit non résolu → dans tous les cas validation cohérente
876
+ if (result.mergedContent !== null) {
877
+ expect(result.validation).toBeDefined();
878
+ }
879
+ });
880
+ it("valide un JSON bien formé sans erreur", () => {
881
+ const jsonConflict = `{
882
+ "name": "test",
883
+ <<<<<<< ours
884
+ "version": "2.0.0",
885
+ ||||||| base
886
+ "version": "1.0.0",
887
+ =======
888
+ "version": "1.0.0",
889
+ >>>>>>> theirs
890
+ "main": "index.js"
891
+ }`;
892
+ const result = resolve(jsonConflict, "package.json");
893
+ if (result.mergedContent !== null) {
894
+ expect(result.validation.syntaxError).toBeNull();
895
+ expect(result.validation.isValid).toBe(true);
896
+ }
897
+ });
898
+ it("détecte une erreur de syntaxe dans un JSON invalide résolu", () => {
899
+ // Contenu avec marqueur de conflit résolu mais JSON mal formé (virgule finale)
900
+ const brokenJson = `{
901
+ <<<<<<< ours
902
+ "a": 1,
903
+ "b": 2,
904
+ ||||||| base
905
+ "a": 1,
906
+ =======
907
+ "a": 1,
908
+ >>>>>>> theirs
909
+ }`;
910
+ const result = resolve(brokenJson, "config.json");
911
+ // Si résolu, le JSON avec trailing comma sera invalide
912
+ if (result.mergedContent !== null) {
913
+ // Le résultat peut ou non être valide selon la résolution
914
+ expect(typeof result.validation.isValid).toBe("boolean");
915
+ if (!result.validation.isValid) {
916
+ expect(result.validation.syntaxError).not.toBeNull();
917
+ }
918
+ }
919
+ });
920
+ it("ne tente pas de valider JSON pour les fichiers .ts", () => {
921
+ const result = resolve(CONFLICT_SAME_CHANGE, "App.tsx");
922
+ // Pas de validation JSON sur les .tsx
923
+ expect(result.validation.syntaxError).toBeNull();
924
+ });
925
+ });
926
+ });
927
+ //# sourceMappingURL=resolver.test.js.map