dsh-config-manager 0.1.78 → 0.1.79

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 (290) hide show
  1. package/lib/adapters/credentials.d.ts +6 -2
  2. package/lib/adapters/credentials.js +19 -3
  3. package/lib/adapters/file-collection.js +2 -1
  4. package/lib/adapters/mcp.d.ts +48 -2
  5. package/lib/adapters/mcp.js +265 -3
  6. package/lib/adapters/messages.d.ts +3 -0
  7. package/lib/adapters/messages.js +7 -0
  8. package/lib/adapters/plugin-files.js +2 -1
  9. package/lib/adapters/plugins.js +105 -20
  10. package/lib/adapters/pnpm-workspace.d.ts +32 -0
  11. package/lib/adapters/pnpm-workspace.js +113 -2
  12. package/lib/adapters/prompts.d.ts +6 -2
  13. package/lib/adapters/prompts.js +17 -2
  14. package/lib/adapters/providers.d.ts +19 -2
  15. package/lib/adapters/providers.js +59 -3
  16. package/lib/adapters/settings.d.ts +6 -2
  17. package/lib/adapters/settings.js +13 -2
  18. package/lib/adapters/skills.d.ts +25 -1
  19. package/lib/adapters/skills.js +60 -0
  20. package/lib/adapters/ui.d.ts +6 -2
  21. package/lib/adapters/ui.js +13 -2
  22. package/lib/adapters/units.d.ts +18 -0
  23. package/lib/adapters/units.js +39 -0
  24. package/lib/adapters/workspaces.js +2 -1
  25. package/lib/client.d.ts +400 -112
  26. package/lib/client.js +2845 -1524
  27. package/lib/core/backup.js +20 -0
  28. package/lib/core/mcp-connector-store.d.ts +159 -0
  29. package/lib/core/mcp-connector-store.js +445 -0
  30. package/lib/core/messages-core.d.ts +2 -0
  31. package/lib/core/messages-core.js +4 -0
  32. package/lib/core/messages.d.ts +20 -0
  33. package/lib/core/messages.js +44 -0
  34. package/lib/core/phase3-host.js +18 -1
  35. package/lib/core/plugin-cli.d.ts +22 -2
  36. package/lib/core/plugin-cli.js +22 -3
  37. package/lib/core/recovery-orchestrator.d.ts +3 -1
  38. package/lib/core/recovery-orchestrator.js +75 -16
  39. package/lib/core/rollback-activity.d.ts +89 -0
  40. package/lib/core/rollback-activity.js +350 -0
  41. package/lib/core/rollback-ledger.d.ts +0 -9
  42. package/lib/core/rollback-ledger.js +37 -0
  43. package/lib/core/rollback-merge.d.ts +22 -2
  44. package/lib/core/rollback-merge.js +63 -13
  45. package/lib/core/rollback-point.d.ts +93 -0
  46. package/lib/core/rollback-point.js +420 -101
  47. package/lib/core/rollback-undo-journal.d.ts +123 -0
  48. package/lib/core/rollback-undo-journal.js +517 -0
  49. package/lib/core/self-update.js +2 -4
  50. package/lib/core/text-fidelity.d.ts +89 -0
  51. package/lib/core/text-fidelity.js +176 -0
  52. package/lib/core/types.d.ts +12 -3
  53. package/lib/core/update-check.js +2 -4
  54. package/lib/core/validator.d.ts +9 -0
  55. package/lib/core/validator.js +12 -0
  56. package/lib/foreign/bundle.js +42 -3
  57. package/lib/foreign/claude-code.js +358 -26
  58. package/lib/foreign/claude-sessions.d.ts +51 -8
  59. package/lib/foreign/claude-sessions.js +111 -13
  60. package/lib/foreign/read-claude-code.d.ts +33 -4
  61. package/lib/foreign/read-claude-code.js +446 -65
  62. package/lib/foreign/read-claude-plugins.d.ts +36 -0
  63. package/lib/foreign/read-claude-plugins.js +403 -0
  64. package/lib/foreign/registry.js +25 -2
  65. package/lib/foreign/truth-table.js +6 -4
  66. package/lib/foreign/types.d.ts +170 -2
  67. package/lib/index.d.ts +14 -5
  68. package/lib/index.js +60 -16
  69. package/lib/market/git-file-writer.js +7 -1
  70. package/lib/profiles/dsh-profile-launcher.d.ts +20 -0
  71. package/lib/profiles/dsh-profile-launcher.js +58 -1
  72. package/lib/profiles/dsh-profile-shared.d.ts +8 -2
  73. package/lib/profiles/dsh-profile-shared.js +8 -2
  74. package/lib/routes/foreign.js +8 -1
  75. package/lib/routes/index.d.ts +1 -1
  76. package/lib/routes/kit.d.ts +2 -2
  77. package/lib/routes/sync.js +173 -9
  78. package/lib/schema/manifest.d.ts +6 -1
  79. package/lib/schema/manifest.js +55 -0
  80. package/lib/schema/types.d.ts +47 -2
  81. package/lib/sync/autosync-config.d.ts +73 -0
  82. package/lib/sync/autosync-config.js +89 -0
  83. package/lib/sync/autosync-scheduler.d.ts +80 -6
  84. package/lib/sync/autosync-scheduler.js +263 -40
  85. package/lib/sync/gist/gist-transport.d.ts +15 -1
  86. package/lib/sync/gist/gist-transport.js +66 -5
  87. package/lib/sync/gist/messages.d.ts +3 -0
  88. package/lib/sync/gist/messages.js +7 -0
  89. package/lib/sync/git/git-transport.d.ts +42 -3
  90. package/lib/sync/git/git-transport.js +146 -3
  91. package/lib/sync/merge.d.ts +47 -14
  92. package/lib/sync/merge.js +242 -222
  93. package/lib/sync/messages.d.ts +8 -0
  94. package/lib/sync/messages.js +19 -0
  95. package/lib/sync/risk.d.ts +19 -2
  96. package/lib/sync/risk.js +10 -3
  97. package/lib/sync/s3/messages.d.ts +6 -0
  98. package/lib/sync/s3/messages.js +13 -0
  99. package/lib/sync/s3/s3-transport.d.ts +9 -1
  100. package/lib/sync/s3/s3-transport.js +78 -6
  101. package/lib/sync/sync-engine.d.ts +193 -4
  102. package/lib/sync/sync-engine.js +450 -37
  103. package/lib/sync/sync-history.d.ts +11 -0
  104. package/lib/sync/sync-history.js +20 -0
  105. package/lib/sync/sync-state.d.ts +43 -0
  106. package/lib/sync/sync-state.js +44 -0
  107. package/lib/sync/sync-watcher.d.ts +76 -0
  108. package/lib/sync/sync-watcher.js +202 -0
  109. package/lib/sync/text-merge.d.ts +85 -0
  110. package/lib/sync/text-merge.js +513 -0
  111. package/lib/sync/transport.d.ts +48 -0
  112. package/lib/sync/transport.js +12 -0
  113. package/lib/sync/watch-scope.d.ts +38 -0
  114. package/lib/sync/watch-scope.js +95 -0
  115. package/lib/sync/webdav/webdav-transport.d.ts +16 -2
  116. package/lib/sync/webdav/webdav-transport.js +128 -11
  117. package/lib/ui/foreign-view.js +10 -0
  118. package/lib/ui/i18n.d.ts +8 -0
  119. package/lib/ui/i18n.js +20 -0
  120. package/lib/ui/rollback-view.d.ts +12 -0
  121. package/lib/ui/rollback-view.js +2 -0
  122. package/lib/ui/sync-settings-view.d.ts +47 -0
  123. package/lib/ui/sync-settings-view.js +24 -0
  124. package/lib/ui/types.d.ts +21 -0
  125. package/lib/utils/env-lock.d.ts +1 -0
  126. package/lib/utils/env-lock.js +7 -4
  127. package/package.json +6 -6
  128. package/src/adapters/credentials.ts +22 -4
  129. package/src/adapters/file-collection.ts +2 -1
  130. package/src/adapters/mcp.test.ts +235 -1
  131. package/src/adapters/mcp.ts +276 -5
  132. package/src/adapters/messages.ts +7 -0
  133. package/src/adapters/plugin-files.ts +2 -1
  134. package/src/adapters/plugins.test.ts +176 -2
  135. package/src/adapters/plugins.ts +107 -21
  136. package/src/adapters/pnpm-workspace.test.ts +89 -1
  137. package/src/adapters/pnpm-workspace.ts +127 -2
  138. package/src/adapters/prompts.ts +20 -3
  139. package/src/adapters/providers.ts +62 -4
  140. package/src/adapters/settings.ts +16 -3
  141. package/src/adapters/skills.test.ts +141 -0
  142. package/src/adapters/skills.ts +58 -2
  143. package/src/adapters/ui.ts +16 -3
  144. package/src/adapters/units-entries.test.ts +190 -0
  145. package/src/adapters/units.ts +50 -0
  146. package/src/adapters/workspaces.ts +2 -1
  147. package/src/client/about/PluginUpdateDialog.tsx +4 -2
  148. package/src/client/about/ReleaseNotesDialog.tsx +4 -2
  149. package/src/client/chat/RollbackReviewDialog.tsx +3 -2
  150. package/src/client/common/Skeleton.tsx +32 -0
  151. package/src/client/common/routes.ts +9 -0
  152. package/src/client/config-manager.module.css +321 -11
  153. package/src/client/home/HomePanel.tsx +14 -6
  154. package/src/client/import/ForeignImportView.tsx +3 -1
  155. package/src/client/import/import-wizard-steps.tsx +2 -1
  156. package/src/client/library/ArtifactRow.tsx +12 -2
  157. package/src/client/library/LibraryActions.tsx +6 -3
  158. package/src/client/library/LibraryPanel.tsx +21 -3
  159. package/src/client/library/RestorePlanView.tsx +4 -2
  160. package/src/client/locales.ts +3 -0
  161. package/src/client/recovery/RecoveryPanel.tsx +54 -7
  162. package/src/client/recovery/RollbackCard.tsx +7 -2
  163. package/src/client/recovery/RollbackDiffPane.tsx +3 -2
  164. package/src/client/recovery/recovery-locales.ts +20 -2
  165. package/src/client/recovery/recovery-view.test.ts +39 -1
  166. package/src/client/recovery/recovery-view.ts +23 -1
  167. package/src/client/run-store.test.ts +1 -1
  168. package/src/client/run-store.ts +4 -0
  169. package/src/client/sync/AutosyncCard.tsx +49 -6
  170. package/src/client/sync/MergeModal.tsx +219 -0
  171. package/src/client/sync/MergePreviewView.tsx +306 -0
  172. package/src/client/sync/SyncActivityBar.tsx +31 -0
  173. package/src/client/sync/SyncConfirmView.tsx +2 -1
  174. package/src/client/sync/SyncPanel.tsx +130 -87
  175. package/src/client/sync/SyncProbeRow.tsx +162 -0
  176. package/src/client/sync/SyncSettingsView.test.ts +27 -2
  177. package/src/client/sync/WorkDirCard.tsx +44 -6
  178. package/src/client/sync/history-model.ts +4 -1
  179. package/src/client/sync/merge-view.test.ts +234 -0
  180. package/src/client/sync/merge-view.ts +371 -0
  181. package/src/client/sync/probe-view.test.ts +90 -0
  182. package/src/client/sync/probe-view.ts +63 -0
  183. package/src/client/sync/remote-device-view.test.ts +35 -0
  184. package/src/client/sync/remote-device-view.ts +25 -0
  185. package/src/client/sync/sync-api.ts +212 -2
  186. package/src/client/sync/sync-entry.test.ts +78 -0
  187. package/src/client/sync/sync-entry.ts +53 -0
  188. package/src/client/sync/sync-locales.ts +234 -2
  189. package/src/client/sync/sync-view.test.ts +18 -1
  190. package/src/client/sync/sync-view.ts +20 -0
  191. package/src/core/backup.test.ts +41 -0
  192. package/src/core/backup.ts +20 -0
  193. package/src/core/mcp-connector-store.test.ts +265 -0
  194. package/src/core/mcp-connector-store.ts +516 -0
  195. package/src/core/messages-core.ts +4 -0
  196. package/src/core/messages.ts +46 -0
  197. package/src/core/phase3-host.ts +18 -1
  198. package/src/core/phase3-production-integration.test.ts +4 -0
  199. package/src/core/plugin-cli.test.ts +26 -0
  200. package/src/core/plugin-cli.ts +30 -4
  201. package/src/core/recovery-orchestrator.test.ts +104 -5
  202. package/src/core/recovery-orchestrator.ts +74 -17
  203. package/src/core/rollback-activity.test.ts +311 -0
  204. package/src/core/rollback-activity.ts +367 -0
  205. package/src/core/rollback-cross-session.test.ts +694 -0
  206. package/src/core/rollback-interrupted.test.ts +137 -0
  207. package/src/core/rollback-ledger.ts +32 -0
  208. package/src/core/rollback-merge.test.ts +130 -0
  209. package/src/core/rollback-merge.ts +78 -14
  210. package/src/core/rollback-point.ts +468 -91
  211. package/src/core/rollback-undo-journal.test.ts +508 -0
  212. package/src/core/rollback-undo-journal.ts +571 -0
  213. package/src/core/self-update.ts +2 -5
  214. package/src/core/text-fidelity.test.ts +189 -0
  215. package/src/core/text-fidelity.ts +197 -0
  216. package/src/core/types.ts +11 -3
  217. package/src/core/update-check.ts +2 -5
  218. package/src/core/validator.ts +14 -0
  219. package/src/foreign/bundle-origin.test.ts +265 -0
  220. package/src/foreign/bundle.ts +45 -5
  221. package/src/foreign/claude-code.test.ts +268 -0
  222. package/src/foreign/claude-code.ts +406 -25
  223. package/src/foreign/claude-plugins.test.ts +234 -0
  224. package/src/foreign/claude-sessions.test.ts +141 -0
  225. package/src/foreign/claude-sessions.ts +179 -23
  226. package/src/foreign/file-budget.test.ts +4 -1
  227. package/src/foreign/read-claude-code.test.ts +320 -0
  228. package/src/foreign/read-claude-code.ts +490 -71
  229. package/src/foreign/read-claude-plugins.ts +465 -0
  230. package/src/foreign/registry.ts +25 -2
  231. package/src/foreign/session-ir.test.ts +65 -0
  232. package/src/foreign/truth-table.test.ts +81 -0
  233. package/src/foreign/truth-table.ts +6 -3
  234. package/src/foreign/types.ts +162 -2
  235. package/src/index.facade.test.ts +26 -4
  236. package/src/index.ts +76 -19
  237. package/src/market/git-file-writer.test.ts +4 -0
  238. package/src/market/git-file-writer.ts +7 -1
  239. package/src/profiles/dsh-profile-launcher.test.ts +68 -3
  240. package/src/profiles/dsh-profile-launcher.ts +56 -1
  241. package/src/profiles/dsh-profile-shared.ts +8 -2
  242. package/src/routes/foreign.ts +8 -1
  243. package/src/routes/index.ts +1 -1
  244. package/src/routes/kit.ts +2 -2
  245. package/src/routes/sync.ts +176 -9
  246. package/src/schema/manifest-schema.test.ts +87 -2
  247. package/src/schema/manifest.ts +57 -1
  248. package/src/schema/types.ts +45 -2
  249. package/src/sync/autosync-config.test.ts +73 -1
  250. package/src/sync/autosync-config.ts +124 -0
  251. package/src/sync/autosync-risk-tier.test.ts +38 -2
  252. package/src/sync/autosync-scheduler.test.ts +264 -12
  253. package/src/sync/autosync-scheduler.ts +296 -44
  254. package/src/sync/gist/gist-conditional-write.test.ts +223 -0
  255. package/src/sync/gist/gist-transport.ts +73 -7
  256. package/src/sync/gist/messages.ts +7 -0
  257. package/src/sync/git/git-probe.test.ts +80 -0
  258. package/src/sync/git/git-transport.test.ts +185 -0
  259. package/src/sync/git/git-transport.ts +178 -5
  260. package/src/sync/merge.test.ts +188 -55
  261. package/src/sync/merge.ts +282 -224
  262. package/src/sync/messages.ts +19 -0
  263. package/src/sync/risk.ts +29 -5
  264. package/src/sync/s3/messages.ts +13 -0
  265. package/src/sync/s3/s3-conditional-write.test.ts +330 -0
  266. package/src/sync/s3/s3-transport.ts +87 -9
  267. package/src/sync/sync-device-identity.test.ts +267 -0
  268. package/src/sync/sync-engine.test.ts +379 -0
  269. package/src/sync/sync-engine.ts +535 -36
  270. package/src/sync/sync-history.test.ts +16 -1
  271. package/src/sync/sync-history.ts +26 -0
  272. package/src/sync/sync-state.test.ts +64 -0
  273. package/src/sync/sync-state.ts +79 -0
  274. package/src/sync/sync-watcher.test.ts +197 -0
  275. package/src/sync/sync-watcher.ts +246 -0
  276. package/src/sync/text-merge.test.ts +358 -0
  277. package/src/sync/text-merge.ts +554 -0
  278. package/src/sync/transport.ts +57 -0
  279. package/src/sync/watch-scope.test.ts +122 -0
  280. package/src/sync/watch-scope.ts +120 -0
  281. package/src/sync/webdav/webdav-conditional-write.test.ts +360 -0
  282. package/src/sync/webdav/webdav-transport.ts +137 -14
  283. package/src/ui/foreign-view.test.ts +34 -0
  284. package/src/ui/foreign-view.ts +10 -0
  285. package/src/ui/i18n.ts +20 -0
  286. package/src/ui/rollback-view.ts +14 -0
  287. package/src/ui/sync-settings-view.test.ts +28 -1
  288. package/src/ui/sync-settings-view.ts +43 -0
  289. package/src/ui/types.ts +22 -0
  290. package/src/utils/env-lock.ts +8 -5
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * m-sync-flow:三方合并引擎测试。
3
3
  * 覆盖:
4
- * - 分区级四象限(useRemote / keepLocal / skip / conflict)
5
- * - JSON 分区键级精细合并(非重叠键自动合、双侧均改且不同留冲突)
6
- * - FilesSection 文件级精细合并
7
- * - MergePlan 输出形状
4
+ * - 分区级四象限(useRemote / keepLocal / skip / conflict)+ 精细合并成功的 'merge' 决策
5
+ * - JSON 分区递归合并(嵌套对象/未知字段保留/数组保守冲突/冲突路径到叶子)
6
+ * - FilesSection 逐文件合并(文本行级三方合并 / 二进制保留冲突 / 删除-修改冲突)
7
+ * - MergePlan 输出形状与 MergeConflict.reason
8
8
  * - 无祖先退化为整分区冲突
9
9
  */
10
10
  import test from 'node:test';
@@ -22,13 +22,22 @@ const settings = (ns: Record<string, { value: unknown; revision: number }>): Set
22
22
 
23
23
  const files = (entries: Array<[string, string]>): FilesSection => ({
24
24
  version: 1,
25
- files: entries.map(([p, c]) => ({ relativePath: p, data: new TextEncoder().encode(c), contentHash: '' })),
25
+ files: entries.map(([pth, c]) => ({ relativePath: pth, data: new TextEncoder().encode(c), contentHash: '' })),
26
26
  });
27
27
 
28
+ const binFiles = (entries: Array<[string, Uint8Array]>): FilesSection => ({
29
+ version: 1,
30
+ files: entries.map(([pth, data]) => ({ relativePath: pth, data, contentHash: '' })),
31
+ });
32
+
33
+ const decode = (s: FilesSection, path: string): string | undefined => {
34
+ const f = s.files.find((x) => x.relativePath === path);
35
+ return f === undefined ? undefined : new TextDecoder().decode(f.data);
36
+ };
37
+
28
38
  test('merge: 本地==远端 → skip', () => {
29
39
  const s = settings({ general: { value: { theme: 'dark' }, revision: 1 } });
30
- const a = s;
31
- const plan = merge({ settings: s as SectionData }, { settings: s as SectionData }, { settings: a as SectionData });
40
+ const plan = merge({ settings: s as SectionData }, { settings: s as SectionData }, { settings: s as SectionData });
32
41
  assert.equal(plan.sections.length, 1);
33
42
  assert.equal(plan.sections[0]!.decision, 'skip');
34
43
  assert.equal(plan.sections[0]!.conflicts.length, 0);
@@ -36,10 +45,9 @@ test('merge: 本地==远端 → skip', () => {
36
45
 
37
46
  test('merge: 本地==祖先 & 远端!=祖先 → useRemote', () => {
38
47
  const anc = settings({ general: { value: { theme: 'dark' }, revision: 1 } });
39
- const loc = anc;
40
48
  const rem = settings({ general: { value: { theme: 'light' }, revision: 2 } });
41
49
  const plan = merge(
42
- { settings: loc as SectionData },
50
+ { settings: anc as SectionData },
43
51
  { settings: rem as SectionData },
44
52
  { settings: anc as SectionData },
45
53
  );
@@ -50,17 +58,16 @@ test('merge: 本地==祖先 & 远端!=祖先 → useRemote', () => {
50
58
  test('merge: 本地!=祖先 & 远端==祖先 → keepLocal', () => {
51
59
  const anc = settings({ general: { value: { theme: 'dark' }, revision: 1 } });
52
60
  const loc = settings({ general: { value: { theme: 'custom' }, revision: 2 } });
53
- const rem = anc;
54
61
  const plan = merge(
55
62
  { settings: loc as SectionData },
56
- { settings: rem as SectionData },
63
+ { settings: anc as SectionData },
57
64
  { settings: anc as SectionData },
58
65
  );
59
66
  assert.equal(plan.sections[0]!.decision, 'keepLocal');
60
67
  assert.deepEqual((plan.sections[0]!.merged as SettingsSection).namespaces.general!.value, { theme: 'custom' });
61
68
  });
62
69
 
63
- test('merge: 双侧相对祖先都改且分区 hash 不同 → conflict', () => {
70
+ test('merge: 双侧相对祖先都改且同处不同值 → conflict(路径精确到叶子)', () => {
64
71
  const anc = settings({ general: { value: { theme: 'dark' }, revision: 1 } });
65
72
  const loc = settings({ general: { value: { theme: 'blue' }, revision: 2 } });
66
73
  const rem = settings({ general: { value: { theme: 'red' }, revision: 2 } });
@@ -70,7 +77,11 @@ test('merge: 双侧相对祖先都改且分区 hash 不同 → conflict', () =>
70
77
  { settings: anc as SectionData },
71
78
  );
72
79
  assert.equal(plan.sections[0]!.decision, 'conflict');
73
- assert.ok(plan.sections[0]!.conflicts.length >= 1);
80
+ assert.equal(plan.sections[0]!.merged, undefined, '冲突时绝不回传半成品 merged');
81
+ assert.equal(plan.sections[0]!.conflicts.length, 1);
82
+ assert.equal(plan.sections[0]!.conflicts[0]!.path, 'namespaces.general.value.theme');
83
+ assert.equal(plan.sections[0]!.conflicts[0]!.kind, 'key');
84
+ assert.equal(plan.sections[0]!.conflicts[0]!.reason, 'both-modified');
74
85
  });
75
86
 
76
87
  test('merge: 无 ancestor → 整分区 conflict(不猜测)', () => {
@@ -78,20 +89,18 @@ test('merge: 无 ancestor → 整分区 conflict(不猜测)', () => {
78
89
  const rem = settings({ general: { value: { theme: 'light' }, revision: 2 } });
79
90
  const plan = merge({ settings: loc as SectionData }, { settings: rem as SectionData });
80
91
  assert.equal(plan.sections[0]!.decision, 'conflict');
92
+ assert.equal(plan.sections[0]!.conflicts[0]!.reason, 'no-ancestor');
81
93
  });
82
94
 
83
- test('merge: JSON 分区键级精细合并 —— 非重叠键各自变更自动合', () => {
84
- // 祖先:{ general: {theme:'dark'}, editor: {fontSize:12} }
95
+ test('merge: JSON 非重叠键各自变更 → decision=merge + autoMerged 计数(不再谎报 useRemote)', () => {
85
96
  const anc = settings({
86
97
  general: { value: { theme: 'dark' }, revision: 1 },
87
98
  editor: { value: { fontSize: 12 }, revision: 1 },
88
99
  });
89
- // 本地:改 editor
90
100
  const loc = settings({
91
101
  general: { value: { theme: 'dark' }, revision: 1 },
92
102
  editor: { value: { fontSize: 14 }, revision: 2 },
93
103
  });
94
- // 远端:改 general
95
104
  const rem = settings({
96
105
  general: { value: { theme: 'light' }, revision: 2 },
97
106
  editor: { value: { fontSize: 12 }, revision: 1 },
@@ -101,29 +110,60 @@ test('merge: JSON 分区键级精细合并 —— 非重叠键各自变更自动
101
110
  { settings: rem as SectionData },
102
111
  { settings: anc as SectionData },
103
112
  );
104
- assert.equal(plan.sections[0]!.decision, 'useRemote', '精细合并后无非冲突 → useRemote(承载合并结果)');
105
- const merged = plan.sections[0]!.merged as SettingsSection;
113
+ const r = plan.sections[0]!;
114
+ assert.equal(r.decision, 'merge');
115
+ assert.equal(r.conflicts.length, 0);
116
+ assert.ok((r.autoMerged ?? 0) > 0, 'autoMerged 必须如实统计并入的变更点');
117
+ const merged = r.merged as SettingsSection;
106
118
  assert.deepEqual(merged.namespaces.general!.value, { theme: 'light' });
107
119
  assert.deepEqual(merged.namespaces.editor!.value, { fontSize: 14 });
108
- assert.equal(plan.sections[0]!.conflicts.length, 0);
109
120
  });
110
121
 
111
- test('merge: JSON 分区键级精细合并 —— 双侧均改同一 key 且值不同 → 冲突', () => {
112
- const anc = settings({ general: { value: { theme: 'dark' }, revision: 1 } });
113
- const loc = settings({ general: { value: { theme: 'blue' }, revision: 2 } });
114
- const rem = settings({ general: { value: { theme: 'red' }, revision: 2 } });
115
- const plan = merge(
116
- { settings: loc as SectionData },
117
- { settings: rem as SectionData },
118
- { settings: anc as SectionData },
119
- );
120
- assert.equal(plan.sections[0]!.decision, 'conflict');
121
- assert.equal(plan.sections[0]!.conflicts.length, 1);
122
- assert.equal(plan.sections[0]!.conflicts[0]!.path, 'general');
123
- assert.equal(plan.sections[0]!.conflicts[0]!.kind, 'key');
122
+ test('merge: 深层嵌套 JSON 合并(不同叶子自动合,未知字段两侧都保留)', () => {
123
+ const anc: SectionData = { version: 1, ui: { theme: { color: 'dark', size: 12 }, extra: { a: 1 } } } as unknown as SectionData;
124
+ const loc: SectionData = { version: 1, ui: { theme: { color: 'blue', size: 12 }, extra: { a: 1 }, localOnly: true } } as unknown as SectionData;
125
+ const rem: SectionData = { version: 1, ui: { theme: { color: 'dark', size: 16 }, extra: { a: 1 }, remoteOnly: true } } as unknown as SectionData;
126
+ const plan = merge({ settings: loc }, { settings: rem }, { settings: anc });
127
+ const r = plan.sections[0]!;
128
+ assert.equal(r.decision, 'merge');
129
+ const merged = r.merged as unknown as { ui: Record<string, unknown> };
130
+ assert.deepEqual(merged.ui['theme'], { color: 'blue', size: 16 });
131
+ assert.equal(merged.ui['localOnly'], true, '本地新增的未知字段不得被静默丢弃');
132
+ assert.equal(merged.ui['remoteOnly'], true, '远端新增的未知字段不得被静默丢弃');
124
133
  });
125
134
 
126
- test('merge: FilesSection 文件级精细合并 —— 非重叠文件自动合', () => {
135
+ test('merge: 数组双侧都改且不同 → conflict(reason=array),不按索引猜', () => {
136
+ const anc: SectionData = { version: 1, plugins: [{ spec: 'a' }] } as unknown as SectionData;
137
+ const loc: SectionData = { version: 1, plugins: [{ spec: 'a' }, { spec: 'b' }] } as unknown as SectionData;
138
+ const rem: SectionData = { version: 1, plugins: [{ spec: 'c' }] } as unknown as SectionData;
139
+ const plan = merge({ plugins: loc }, { plugins: rem }, { plugins: anc });
140
+ const r = plan.sections[0]!;
141
+ assert.equal(r.decision, 'conflict');
142
+ assert.equal(r.conflicts[0]!.path, 'plugins');
143
+ assert.equal(r.conflicts[0]!.reason, 'array');
144
+ });
145
+
146
+ test('merge: 数组仅一侧改 → 采纳该侧(另一侧键的改动同时保住)', () => {
147
+ const anc: SectionData = { version: 1, plugins: [{ spec: 'a' }], note: 'x' } as unknown as SectionData;
148
+ const loc: SectionData = { version: 1, plugins: [{ spec: 'a' }, { spec: 'b' }], note: 'x' } as unknown as SectionData;
149
+ const rem: SectionData = { version: 1, plugins: [{ spec: 'a' }], note: 'y' } as unknown as SectionData;
150
+ const plan = merge({ plugins: loc }, { plugins: rem }, { plugins: anc });
151
+ const r = plan.sections[0]!;
152
+ assert.equal(r.decision, 'merge');
153
+ const merged = r.merged as unknown as { plugins: unknown[]; note: string };
154
+ assert.equal(merged.plugins.length, 2);
155
+ assert.equal(merged.note, 'y');
156
+ });
157
+
158
+ test('merge: 数组双侧改成相同结果 → 收敛为一份(不算冲突)', () => {
159
+ const anc: SectionData = { version: 1, plugins: [{ spec: 'a' }] } as unknown as SectionData;
160
+ const loc: SectionData = { version: 1, plugins: [{ spec: 'a' }, { spec: 'b' }] } as unknown as SectionData;
161
+ const rem: SectionData = { version: 1, plugins: [{ spec: 'a' }, { spec: 'b' }] } as unknown as SectionData;
162
+ const plan = merge({ plugins: loc }, { plugins: rem }, { plugins: anc });
163
+ assert.equal(plan.sections[0]!.decision, 'skip');
164
+ });
165
+
166
+ test('merge: FilesSection 非重叠文件自动合 → decision=merge', () => {
127
167
  const anc = files([['a.md', '# A old\n'], ['b.md', '# B\n']]);
128
168
  const loc = files([['a.md', '# A new local\n'], ['b.md', '# B\n']]);
129
169
  const rem = files([['a.md', '# A old\n'], ['b.md', '# B remote\n']]);
@@ -132,44 +172,126 @@ test('merge: FilesSection 文件级精细合并 —— 非重叠文件自动合'
132
172
  { skills: rem as SectionData },
133
173
  { skills: anc as SectionData },
134
174
  );
135
- assert.equal(plan.sections[0]!.decision, 'useRemote');
136
- const merged = plan.sections[0]!.merged as FilesSection;
137
- const byPath = new Map(merged.files.map((f) => [f.relativePath, new TextDecoder().decode(f.data)]));
138
- assert.equal(byPath.get('a.md'), '# A new local\n');
139
- assert.equal(byPath.get('b.md'), '# B remote\n');
175
+ const r = plan.sections[0]!;
176
+ assert.equal(r.decision, 'merge');
177
+ const merged = r.merged as FilesSection;
178
+ assert.equal(decode(merged, 'a.md'), '# A new local\n');
179
+ assert.equal(decode(merged, 'b.md'), '# B remote\n');
140
180
  });
141
181
 
142
- test('merge: FilesSection 文件级精细合并 —— 双侧均改同一文件 → 冲突', () => {
143
- const anc = files([['a.md', '# A old\n']]);
144
- const loc = files([['a.md', '# A local\n']]);
145
- const rem = files([['a.md', '# A remote\n']]);
182
+ test('merge: 同一文本文件两侧改不同行 → 行级三方合并成功', () => {
183
+ const anc = files([['a.md', 'line1\nline2\nline3\n']]);
184
+ const loc = files([['a.md', 'LINE1\nline2\nline3\n']]);
185
+ const rem = files([['a.md', 'line1\nline2\nLINE3\n']]);
146
186
  const plan = merge(
147
187
  { skills: loc as SectionData },
148
188
  { skills: rem as SectionData },
149
189
  { skills: anc as SectionData },
150
190
  );
151
- assert.equal(plan.sections[0]!.decision, 'conflict');
152
- assert.equal(plan.sections[0]!.conflicts[0]!.kind, 'file');
153
- assert.equal(plan.sections[0]!.conflicts[0]!.path, 'a.md');
191
+ const r = plan.sections[0]!;
192
+ assert.equal(r.decision, 'merge', '不同行的改动必须能自动合并');
193
+ assert.equal(decode(r.merged as FilesSection, 'a.md'), 'LINE1\nline2\nLINE3\n');
154
194
  });
155
195
 
156
- test('merge: 远端缺 + 本地等于祖先 → skip', () => {
157
- const anc = settings({ general: { value: { theme: 'dark' }, revision: 1 } });
196
+ test('merge: 同一文本文件两侧改同一行 → 冲突(不静默覆盖任何一侧)', () => {
197
+ const anc = files([['a.md', 'line1\nline2\n']]);
198
+ const loc = files([['a.md', 'line1\nLOCAL\n']]);
199
+ const rem = files([['a.md', 'line1\nREMOTE\n']]);
158
200
  const plan = merge(
159
- { settings: anc as SectionData },
160
- {}, // 远端无此分区
161
- { settings: anc as SectionData },
201
+ { skills: loc as SectionData },
202
+ { skills: rem as SectionData },
203
+ { skills: anc as SectionData },
162
204
  );
205
+ const r = plan.sections[0]!;
206
+ assert.equal(r.decision, 'conflict');
207
+ assert.equal(r.conflicts[0]!.kind, 'file');
208
+ assert.equal(r.conflicts[0]!.path, 'a.md');
209
+ assert.equal(r.conflicts[0]!.reason, 'both-modified');
210
+ });
211
+
212
+ test('merge: 二进制文件两侧都改 → 冲突 reason=binary(不做文本合并)', () => {
213
+ const anc = binFiles([['blob.bin', new Uint8Array([0, 1, 2])]]);
214
+ const loc = binFiles([['blob.bin', new Uint8Array([0, 9, 2])]]);
215
+ const rem = binFiles([['blob.bin', new Uint8Array([0, 7, 2])]]);
216
+ const plan = merge(
217
+ { skills: loc as SectionData },
218
+ { skills: rem as SectionData },
219
+ { skills: anc as SectionData },
220
+ );
221
+ const r = plan.sections[0]!;
222
+ assert.equal(r.decision, 'conflict');
223
+ assert.equal(r.conflicts[0]!.reason, 'binary');
224
+ assert.equal(r.conflicts[0]!.autoMergeAttempted, true);
225
+ });
226
+
227
+ test('merge: 文件级 本地改 / 远端删 → modify-delete 冲突(不直接删数据)', () => {
228
+ const anc = files([['a.md', '# A\n'], ['b.md', '# B\n']]);
229
+ const loc = files([['a.md', '# A local edit\n'], ['b.md', '# B\n']]);
230
+ const rem = files([['b.md', '# B\n']]);
231
+ const plan = merge(
232
+ { skills: loc as SectionData },
233
+ { skills: rem as SectionData },
234
+ { skills: anc as SectionData },
235
+ );
236
+ const r = plan.sections[0]!;
237
+ assert.equal(r.decision, 'conflict');
238
+ assert.equal(r.conflicts[0]!.path, 'a.md');
239
+ assert.equal(r.conflicts[0]!.reason, 'modify-delete');
240
+ });
241
+
242
+ test('merge: 文件级 本地删 / 远端改 → delete-modify 冲突', () => {
243
+ const anc = files([['a.md', '# A\n'], ['b.md', '# B\n']]);
244
+ const loc = files([['b.md', '# B\n']]);
245
+ const rem = files([['a.md', '# A remote edit\n'], ['b.md', '# B\n']]);
246
+ const plan = merge(
247
+ { skills: loc as SectionData },
248
+ { skills: rem as SectionData },
249
+ { skills: anc as SectionData },
250
+ );
251
+ const r = plan.sections[0]!;
252
+ assert.equal(r.decision, 'conflict');
253
+ assert.equal(r.conflicts[0]!.path, 'a.md');
254
+ assert.equal(r.conflicts[0]!.reason, 'delete-modify');
255
+ });
256
+
257
+ test('merge: 文件级 本地删 / 远端未改 → 跟随删除(无冲突)', () => {
258
+ const anc = files([['a.md', '# A\n'], ['b.md', '# B\n']]);
259
+ const loc = files([['b.md', '# B\n']]);
260
+ const rem = files([['a.md', '# A\n'], ['b.md', '# B changed\n']]);
261
+ const plan = merge(
262
+ { skills: loc as SectionData },
263
+ { skills: rem as SectionData },
264
+ { skills: anc as SectionData },
265
+ );
266
+ const r = plan.sections[0]!;
267
+ assert.equal(r.decision, 'merge');
268
+ const merged = r.merged as FilesSection;
269
+ assert.equal(decode(merged, 'a.md'), undefined, '本地已删且远端未改 → 跟随删除');
270
+ assert.equal(decode(merged, 'b.md'), '# B changed\n');
271
+ });
272
+
273
+ test('merge: 远端缺 + 本地等于祖先 → skip', () => {
274
+ const anc = settings({ general: { value: { theme: 'dark' }, revision: 1 } });
275
+ const plan = merge({ settings: anc as SectionData }, {}, { settings: anc as SectionData });
163
276
  assert.equal(plan.sections[0]!.decision, 'skip');
164
277
  });
165
278
 
279
+ test('merge: 远端缺 + 本地改了 → 冲突且不得丢弃本地改动', () => {
280
+ const anc = settings({ general: { value: { theme: 'dark' }, revision: 1 } });
281
+ const loc = settings({ general: { value: { theme: 'blue' }, revision: 2 } });
282
+ const plan = merge({ settings: loc as SectionData }, {}, { settings: anc as SectionData });
283
+ assert.equal(plan.sections[0]!.decision, 'conflict');
284
+ assert.equal(plan.sections[0]!.conflicts[0]!.reason, 'modify-delete');
285
+ assert.ok(plan.sections[0]!.conflicts[0]!.local !== undefined);
286
+ });
287
+
166
288
  test('merge: MergePlan 输出形状符合 contract', () => {
167
289
  const anc = settings({ general: { value: { theme: 'dark' }, revision: 1 } });
168
290
  const plan = merge({ settings: anc as SectionData }, { settings: anc as SectionData }, { settings: anc as SectionData });
169
291
  assert.ok(Array.isArray(plan.sections));
170
292
  const r = plan.sections[0]!;
171
293
  assert.equal(typeof r.id, 'string');
172
- assert.ok(['useRemote', 'keepLocal', 'skip', 'conflict'].includes(r.decision));
294
+ assert.ok(['useRemote', 'keepLocal', 'merge', 'skip', 'conflict'].includes(r.decision));
173
295
  assert.ok(Array.isArray(r.conflicts));
174
296
  });
175
297
 
@@ -177,7 +299,6 @@ test('merge: 多分区混合场景(settings skip / providers keepLocal / skill
177
299
  const sA = settings({ general: { value: { theme: 'dark' }, revision: 1 } });
178
300
  const pA: SectionData = { version: 1, providers: { openai: { route: 'https://api.openai.com/v1' } } };
179
301
  const fA = files([['a.md', '# A\n']]);
180
-
181
302
  const plan = merge(
182
303
  {
183
304
  settings: sA as SectionData,
@@ -197,8 +318,20 @@ test('merge: 多分区混合场景(settings skip / providers keepLocal / skill
197
318
  );
198
319
  const byId = new Map(plan.sections.map((s) => [s.id, s]));
199
320
  assert.equal(byId.get('settings')!.decision, 'skip');
200
- // providers:本地改了 route、远端 == 祖先 → keepLocal
201
321
  assert.equal(byId.get('providers')!.decision, 'keepLocal');
202
- // skills:本地 == 祖先、远端新增 b.md → useRemote
203
322
  assert.equal(byId.get('skills')!.decision, 'useRemote');
204
323
  });
324
+
325
+ test('merge: 类型不一致(一侧文件类 / 一侧 JSON)→ 整分区冲突 reason=section', () => {
326
+ const anc = settings({ general: { value: { theme: 'dark' }, revision: 1 } });
327
+ const loc = files([['a.md', '# A\n']]);
328
+ const rem = settings({ general: { value: { theme: 'light' }, revision: 2 } });
329
+ const plan = merge(
330
+ { skills: loc as SectionData },
331
+ { skills: rem as SectionData },
332
+ { skills: anc as SectionData },
333
+ );
334
+ const r = plan.sections[0]!;
335
+ assert.equal(r.decision, 'conflict');
336
+ assert.equal(r.conflicts[0]!.reason, 'section');
337
+ });