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.
- package/lib/adapters/credentials.d.ts +6 -2
- package/lib/adapters/credentials.js +19 -3
- package/lib/adapters/file-collection.js +2 -1
- package/lib/adapters/mcp.d.ts +48 -2
- package/lib/adapters/mcp.js +265 -3
- package/lib/adapters/messages.d.ts +3 -0
- package/lib/adapters/messages.js +7 -0
- package/lib/adapters/plugin-files.js +2 -1
- package/lib/adapters/plugins.js +105 -20
- package/lib/adapters/pnpm-workspace.d.ts +32 -0
- package/lib/adapters/pnpm-workspace.js +113 -2
- package/lib/adapters/prompts.d.ts +6 -2
- package/lib/adapters/prompts.js +17 -2
- package/lib/adapters/providers.d.ts +19 -2
- package/lib/adapters/providers.js +59 -3
- package/lib/adapters/settings.d.ts +6 -2
- package/lib/adapters/settings.js +13 -2
- package/lib/adapters/skills.d.ts +25 -1
- package/lib/adapters/skills.js +60 -0
- package/lib/adapters/ui.d.ts +6 -2
- package/lib/adapters/ui.js +13 -2
- package/lib/adapters/units.d.ts +18 -0
- package/lib/adapters/units.js +39 -0
- package/lib/adapters/workspaces.js +2 -1
- package/lib/client.d.ts +400 -112
- package/lib/client.js +2845 -1524
- package/lib/core/backup.js +20 -0
- package/lib/core/mcp-connector-store.d.ts +159 -0
- package/lib/core/mcp-connector-store.js +445 -0
- package/lib/core/messages-core.d.ts +2 -0
- package/lib/core/messages-core.js +4 -0
- package/lib/core/messages.d.ts +20 -0
- package/lib/core/messages.js +44 -0
- package/lib/core/phase3-host.js +18 -1
- package/lib/core/plugin-cli.d.ts +22 -2
- package/lib/core/plugin-cli.js +22 -3
- package/lib/core/recovery-orchestrator.d.ts +3 -1
- package/lib/core/recovery-orchestrator.js +75 -16
- package/lib/core/rollback-activity.d.ts +89 -0
- package/lib/core/rollback-activity.js +350 -0
- package/lib/core/rollback-ledger.d.ts +0 -9
- package/lib/core/rollback-ledger.js +37 -0
- package/lib/core/rollback-merge.d.ts +22 -2
- package/lib/core/rollback-merge.js +63 -13
- package/lib/core/rollback-point.d.ts +93 -0
- package/lib/core/rollback-point.js +420 -101
- package/lib/core/rollback-undo-journal.d.ts +123 -0
- package/lib/core/rollback-undo-journal.js +517 -0
- package/lib/core/self-update.js +2 -4
- package/lib/core/text-fidelity.d.ts +89 -0
- package/lib/core/text-fidelity.js +176 -0
- package/lib/core/types.d.ts +12 -3
- package/lib/core/update-check.js +2 -4
- package/lib/core/validator.d.ts +9 -0
- package/lib/core/validator.js +12 -0
- package/lib/foreign/bundle.js +42 -3
- package/lib/foreign/claude-code.js +358 -26
- package/lib/foreign/claude-sessions.d.ts +51 -8
- package/lib/foreign/claude-sessions.js +111 -13
- package/lib/foreign/read-claude-code.d.ts +33 -4
- package/lib/foreign/read-claude-code.js +446 -65
- package/lib/foreign/read-claude-plugins.d.ts +36 -0
- package/lib/foreign/read-claude-plugins.js +403 -0
- package/lib/foreign/registry.js +25 -2
- package/lib/foreign/truth-table.js +6 -4
- package/lib/foreign/types.d.ts +170 -2
- package/lib/index.d.ts +14 -5
- package/lib/index.js +60 -16
- package/lib/market/git-file-writer.js +7 -1
- package/lib/profiles/dsh-profile-launcher.d.ts +20 -0
- package/lib/profiles/dsh-profile-launcher.js +58 -1
- package/lib/profiles/dsh-profile-shared.d.ts +8 -2
- package/lib/profiles/dsh-profile-shared.js +8 -2
- package/lib/routes/foreign.js +8 -1
- package/lib/routes/index.d.ts +1 -1
- package/lib/routes/kit.d.ts +2 -2
- package/lib/routes/sync.js +173 -9
- package/lib/schema/manifest.d.ts +6 -1
- package/lib/schema/manifest.js +55 -0
- package/lib/schema/types.d.ts +47 -2
- package/lib/sync/autosync-config.d.ts +73 -0
- package/lib/sync/autosync-config.js +89 -0
- package/lib/sync/autosync-scheduler.d.ts +80 -6
- package/lib/sync/autosync-scheduler.js +263 -40
- package/lib/sync/gist/gist-transport.d.ts +15 -1
- package/lib/sync/gist/gist-transport.js +66 -5
- package/lib/sync/gist/messages.d.ts +3 -0
- package/lib/sync/gist/messages.js +7 -0
- package/lib/sync/git/git-transport.d.ts +42 -3
- package/lib/sync/git/git-transport.js +146 -3
- package/lib/sync/merge.d.ts +47 -14
- package/lib/sync/merge.js +242 -222
- package/lib/sync/messages.d.ts +8 -0
- package/lib/sync/messages.js +19 -0
- package/lib/sync/risk.d.ts +19 -2
- package/lib/sync/risk.js +10 -3
- package/lib/sync/s3/messages.d.ts +6 -0
- package/lib/sync/s3/messages.js +13 -0
- package/lib/sync/s3/s3-transport.d.ts +9 -1
- package/lib/sync/s3/s3-transport.js +78 -6
- package/lib/sync/sync-engine.d.ts +193 -4
- package/lib/sync/sync-engine.js +450 -37
- package/lib/sync/sync-history.d.ts +11 -0
- package/lib/sync/sync-history.js +20 -0
- package/lib/sync/sync-state.d.ts +43 -0
- package/lib/sync/sync-state.js +44 -0
- package/lib/sync/sync-watcher.d.ts +76 -0
- package/lib/sync/sync-watcher.js +202 -0
- package/lib/sync/text-merge.d.ts +85 -0
- package/lib/sync/text-merge.js +513 -0
- package/lib/sync/transport.d.ts +48 -0
- package/lib/sync/transport.js +12 -0
- package/lib/sync/watch-scope.d.ts +38 -0
- package/lib/sync/watch-scope.js +95 -0
- package/lib/sync/webdav/webdav-transport.d.ts +16 -2
- package/lib/sync/webdav/webdav-transport.js +128 -11
- package/lib/ui/foreign-view.js +10 -0
- package/lib/ui/i18n.d.ts +8 -0
- package/lib/ui/i18n.js +20 -0
- package/lib/ui/rollback-view.d.ts +12 -0
- package/lib/ui/rollback-view.js +2 -0
- package/lib/ui/sync-settings-view.d.ts +47 -0
- package/lib/ui/sync-settings-view.js +24 -0
- package/lib/ui/types.d.ts +21 -0
- package/lib/utils/env-lock.d.ts +1 -0
- package/lib/utils/env-lock.js +7 -4
- package/package.json +6 -6
- package/src/adapters/credentials.ts +22 -4
- package/src/adapters/file-collection.ts +2 -1
- package/src/adapters/mcp.test.ts +235 -1
- package/src/adapters/mcp.ts +276 -5
- package/src/adapters/messages.ts +7 -0
- package/src/adapters/plugin-files.ts +2 -1
- package/src/adapters/plugins.test.ts +176 -2
- package/src/adapters/plugins.ts +107 -21
- package/src/adapters/pnpm-workspace.test.ts +89 -1
- package/src/adapters/pnpm-workspace.ts +127 -2
- package/src/adapters/prompts.ts +20 -3
- package/src/adapters/providers.ts +62 -4
- package/src/adapters/settings.ts +16 -3
- package/src/adapters/skills.test.ts +141 -0
- package/src/adapters/skills.ts +58 -2
- package/src/adapters/ui.ts +16 -3
- package/src/adapters/units-entries.test.ts +190 -0
- package/src/adapters/units.ts +50 -0
- package/src/adapters/workspaces.ts +2 -1
- package/src/client/about/PluginUpdateDialog.tsx +4 -2
- package/src/client/about/ReleaseNotesDialog.tsx +4 -2
- package/src/client/chat/RollbackReviewDialog.tsx +3 -2
- package/src/client/common/Skeleton.tsx +32 -0
- package/src/client/common/routes.ts +9 -0
- package/src/client/config-manager.module.css +321 -11
- package/src/client/home/HomePanel.tsx +14 -6
- package/src/client/import/ForeignImportView.tsx +3 -1
- package/src/client/import/import-wizard-steps.tsx +2 -1
- package/src/client/library/ArtifactRow.tsx +12 -2
- package/src/client/library/LibraryActions.tsx +6 -3
- package/src/client/library/LibraryPanel.tsx +21 -3
- package/src/client/library/RestorePlanView.tsx +4 -2
- package/src/client/locales.ts +3 -0
- package/src/client/recovery/RecoveryPanel.tsx +54 -7
- package/src/client/recovery/RollbackCard.tsx +7 -2
- package/src/client/recovery/RollbackDiffPane.tsx +3 -2
- package/src/client/recovery/recovery-locales.ts +20 -2
- package/src/client/recovery/recovery-view.test.ts +39 -1
- package/src/client/recovery/recovery-view.ts +23 -1
- package/src/client/run-store.test.ts +1 -1
- package/src/client/run-store.ts +4 -0
- package/src/client/sync/AutosyncCard.tsx +49 -6
- package/src/client/sync/MergeModal.tsx +219 -0
- package/src/client/sync/MergePreviewView.tsx +306 -0
- package/src/client/sync/SyncActivityBar.tsx +31 -0
- package/src/client/sync/SyncConfirmView.tsx +2 -1
- package/src/client/sync/SyncPanel.tsx +130 -87
- package/src/client/sync/SyncProbeRow.tsx +162 -0
- package/src/client/sync/SyncSettingsView.test.ts +27 -2
- package/src/client/sync/WorkDirCard.tsx +44 -6
- package/src/client/sync/history-model.ts +4 -1
- package/src/client/sync/merge-view.test.ts +234 -0
- package/src/client/sync/merge-view.ts +371 -0
- package/src/client/sync/probe-view.test.ts +90 -0
- package/src/client/sync/probe-view.ts +63 -0
- package/src/client/sync/remote-device-view.test.ts +35 -0
- package/src/client/sync/remote-device-view.ts +25 -0
- package/src/client/sync/sync-api.ts +212 -2
- package/src/client/sync/sync-entry.test.ts +78 -0
- package/src/client/sync/sync-entry.ts +53 -0
- package/src/client/sync/sync-locales.ts +234 -2
- package/src/client/sync/sync-view.test.ts +18 -1
- package/src/client/sync/sync-view.ts +20 -0
- package/src/core/backup.test.ts +41 -0
- package/src/core/backup.ts +20 -0
- package/src/core/mcp-connector-store.test.ts +265 -0
- package/src/core/mcp-connector-store.ts +516 -0
- package/src/core/messages-core.ts +4 -0
- package/src/core/messages.ts +46 -0
- package/src/core/phase3-host.ts +18 -1
- package/src/core/phase3-production-integration.test.ts +4 -0
- package/src/core/plugin-cli.test.ts +26 -0
- package/src/core/plugin-cli.ts +30 -4
- package/src/core/recovery-orchestrator.test.ts +104 -5
- package/src/core/recovery-orchestrator.ts +74 -17
- package/src/core/rollback-activity.test.ts +311 -0
- package/src/core/rollback-activity.ts +367 -0
- package/src/core/rollback-cross-session.test.ts +694 -0
- package/src/core/rollback-interrupted.test.ts +137 -0
- package/src/core/rollback-ledger.ts +32 -0
- package/src/core/rollback-merge.test.ts +130 -0
- package/src/core/rollback-merge.ts +78 -14
- package/src/core/rollback-point.ts +468 -91
- package/src/core/rollback-undo-journal.test.ts +508 -0
- package/src/core/rollback-undo-journal.ts +571 -0
- package/src/core/self-update.ts +2 -5
- package/src/core/text-fidelity.test.ts +189 -0
- package/src/core/text-fidelity.ts +197 -0
- package/src/core/types.ts +11 -3
- package/src/core/update-check.ts +2 -5
- package/src/core/validator.ts +14 -0
- package/src/foreign/bundle-origin.test.ts +265 -0
- package/src/foreign/bundle.ts +45 -5
- package/src/foreign/claude-code.test.ts +268 -0
- package/src/foreign/claude-code.ts +406 -25
- package/src/foreign/claude-plugins.test.ts +234 -0
- package/src/foreign/claude-sessions.test.ts +141 -0
- package/src/foreign/claude-sessions.ts +179 -23
- package/src/foreign/file-budget.test.ts +4 -1
- package/src/foreign/read-claude-code.test.ts +320 -0
- package/src/foreign/read-claude-code.ts +490 -71
- package/src/foreign/read-claude-plugins.ts +465 -0
- package/src/foreign/registry.ts +25 -2
- package/src/foreign/session-ir.test.ts +65 -0
- package/src/foreign/truth-table.test.ts +81 -0
- package/src/foreign/truth-table.ts +6 -3
- package/src/foreign/types.ts +162 -2
- package/src/index.facade.test.ts +26 -4
- package/src/index.ts +76 -19
- package/src/market/git-file-writer.test.ts +4 -0
- package/src/market/git-file-writer.ts +7 -1
- package/src/profiles/dsh-profile-launcher.test.ts +68 -3
- package/src/profiles/dsh-profile-launcher.ts +56 -1
- package/src/profiles/dsh-profile-shared.ts +8 -2
- package/src/routes/foreign.ts +8 -1
- package/src/routes/index.ts +1 -1
- package/src/routes/kit.ts +2 -2
- package/src/routes/sync.ts +176 -9
- package/src/schema/manifest-schema.test.ts +87 -2
- package/src/schema/manifest.ts +57 -1
- package/src/schema/types.ts +45 -2
- package/src/sync/autosync-config.test.ts +73 -1
- package/src/sync/autosync-config.ts +124 -0
- package/src/sync/autosync-risk-tier.test.ts +38 -2
- package/src/sync/autosync-scheduler.test.ts +264 -12
- package/src/sync/autosync-scheduler.ts +296 -44
- package/src/sync/gist/gist-conditional-write.test.ts +223 -0
- package/src/sync/gist/gist-transport.ts +73 -7
- package/src/sync/gist/messages.ts +7 -0
- package/src/sync/git/git-probe.test.ts +80 -0
- package/src/sync/git/git-transport.test.ts +185 -0
- package/src/sync/git/git-transport.ts +178 -5
- package/src/sync/merge.test.ts +188 -55
- package/src/sync/merge.ts +282 -224
- package/src/sync/messages.ts +19 -0
- package/src/sync/risk.ts +29 -5
- package/src/sync/s3/messages.ts +13 -0
- package/src/sync/s3/s3-conditional-write.test.ts +330 -0
- package/src/sync/s3/s3-transport.ts +87 -9
- package/src/sync/sync-device-identity.test.ts +267 -0
- package/src/sync/sync-engine.test.ts +379 -0
- package/src/sync/sync-engine.ts +535 -36
- package/src/sync/sync-history.test.ts +16 -1
- package/src/sync/sync-history.ts +26 -0
- package/src/sync/sync-state.test.ts +64 -0
- package/src/sync/sync-state.ts +79 -0
- package/src/sync/sync-watcher.test.ts +197 -0
- package/src/sync/sync-watcher.ts +246 -0
- package/src/sync/text-merge.test.ts +358 -0
- package/src/sync/text-merge.ts +554 -0
- package/src/sync/transport.ts +57 -0
- package/src/sync/watch-scope.test.ts +122 -0
- package/src/sync/watch-scope.ts +120 -0
- package/src/sync/webdav/webdav-conditional-write.test.ts +360 -0
- package/src/sync/webdav/webdav-transport.ts +137 -14
- package/src/ui/foreign-view.test.ts +34 -0
- package/src/ui/foreign-view.ts +10 -0
- package/src/ui/i18n.ts +20 -0
- package/src/ui/rollback-view.ts +14 -0
- package/src/ui/sync-settings-view.test.ts +28 -1
- package/src/ui/sync-settings-view.ts +43 -0
- package/src/ui/types.ts +22 -0
- package/src/utils/env-lock.ts +8 -5
package/src/sync/merge.test.ts
CHANGED
|
@@ -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(([
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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.
|
|
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
|
-
|
|
105
|
-
|
|
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
|
|
112
|
-
const anc =
|
|
113
|
-
const loc =
|
|
114
|
-
const rem =
|
|
115
|
-
const plan = merge(
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
);
|
|
120
|
-
assert.equal(
|
|
121
|
-
assert.equal(
|
|
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:
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
const
|
|
138
|
-
assert.equal(
|
|
139
|
-
assert.equal(
|
|
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:
|
|
143
|
-
const anc = files([['a.md', '
|
|
144
|
-
const loc = files([['a.md', '
|
|
145
|
-
const rem = files([['a.md', '
|
|
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
|
-
|
|
152
|
-
assert.equal(
|
|
153
|
-
assert.equal(
|
|
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:
|
|
157
|
-
const anc =
|
|
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
|
-
{
|
|
160
|
-
{},
|
|
161
|
-
{
|
|
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
|
+
});
|