dsh-config-manager 0.1.78 → 0.1.80
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 +401 -112
- package/lib/client.js +2899 -1525
- 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/ancestor.d.ts +22 -0
- package/lib/sync/ancestor.js +66 -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 +18 -1
- package/lib/sync/gist/gist-transport.js +42 -3
- package/lib/sync/gist/messages.d.ts +2 -0
- package/lib/sync/gist/messages.js +5 -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/s3/sigv4.js +11 -2
- package/lib/sync/sync-engine.d.ts +212 -8
- package/lib/sync/sync-engine.js +514 -58
- 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 +9 -0
- package/lib/ui/i18n.js +22 -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/api-base.test.ts +36 -0
- package/src/client/common/http.test.ts +12 -1
- package/src/client/common/http.ts +8 -2
- package/src/client/common/routes.ts +46 -3
- 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/market/market-api.test.ts +3 -2
- 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 +220 -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 +248 -0
- package/src/client/sync/merge-view.ts +398 -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.test.ts +5 -3
- 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/ancestor.ts +76 -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 +203 -0
- package/src/sync/gist/gist-transport.ts +53 -5
- package/src/sync/gist/messages.ts +5 -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.test.ts +2 -1
- package/src/sync/s3/s3-transport.ts +87 -9
- package/src/sync/s3/sigv4.test.ts +4 -1
- package/src/sync/s3/sigv4.ts +11 -2
- package/src/sync/sync-device-identity.test.ts +267 -0
- package/src/sync/sync-engine.test.ts +410 -19
- package/src/sync/sync-engine.ts +602 -57
- 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 +22 -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
|
@@ -18,6 +18,7 @@ import type { MutationLockPort, MutationLockToken } from '../utils/env-lock.ts';
|
|
|
18
18
|
import type { AutosyncConfig } from './autosync-config.ts';
|
|
19
19
|
import type { AutosyncHistoryEntry } from './sync-history.ts';
|
|
20
20
|
import type { MergePlan, MergeSectionResult } from './merge.ts';
|
|
21
|
+
import type { SyncSnapshotMeta } from './transport.ts';
|
|
21
22
|
import type { SectionId } from '../schema/types.ts';
|
|
22
23
|
import type { SyncEngine } from './sync-engine.ts';
|
|
23
24
|
import type { SyncConfig, SyncTransportType } from './sync-config.ts';
|
|
@@ -242,6 +243,48 @@ test('runOnce: 历史记录携带触发通道(transport=调用通道 git/webda
|
|
|
242
243
|
for (const e of entries) assert.ok(e.transport === 'git' || e.transport === 'webdav', `历史必须带 transport: ${JSON.stringify(e)}`);
|
|
243
244
|
});
|
|
244
245
|
|
|
246
|
+
test('runOnce: 缺省远端检测复用本轮列表(一轮至多一次远端 list,不再白跑一次网络往返)', async () => {
|
|
247
|
+
const cfg: AutosyncConfig = { enabled: true, interval: '30m', startupMinIntervalMs: 300000, consecutiveFailures: 0 };
|
|
248
|
+
const metas: SyncSnapshotMeta[] = [{
|
|
249
|
+
id: 'sync-001',
|
|
250
|
+
createdAt: '2026-08-16T12:00:00.000Z',
|
|
251
|
+
sections: {},
|
|
252
|
+
manifest: {
|
|
253
|
+
schemaVersion: 3, dshVersion: '0.0.0', platform: 'win32',
|
|
254
|
+
sectionIds: [], containsSecrets: false, encrypted: false,
|
|
255
|
+
},
|
|
256
|
+
}];
|
|
257
|
+
let listCalls = 0;
|
|
258
|
+
let seenListed: unknown = 'not-called';
|
|
259
|
+
const engine = {
|
|
260
|
+
listSnapshots: async () => { listCalls += 1; return metas; },
|
|
261
|
+
hasNewRemoteSnapshot: async (listed?: unknown) => { seenListed = listed; return false; },
|
|
262
|
+
hasLocalChanges: async () => false,
|
|
263
|
+
};
|
|
264
|
+
const { scheduler } = makeScheduler({ cfg, engine, history: [] });
|
|
265
|
+
await scheduler.runOnce('git');
|
|
266
|
+
assert.equal(listCalls, 1, '查加密快照已 list 过一次 → 检测必须复用,不得再 list');
|
|
267
|
+
assert.equal(seenListed, metas, '本轮列表要按引用原样透传给检测');
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test('runOnce: 列表拿不到(网络失败)时检测回退自己 list,行为与改造前一致', async () => {
|
|
271
|
+
const cfg: AutosyncConfig = { enabled: true, interval: '30m', startupMinIntervalMs: 300000, consecutiveFailures: 0 };
|
|
272
|
+
let detectCalls = 0;
|
|
273
|
+
let seenListed: unknown = 'not-called';
|
|
274
|
+
const engine = {
|
|
275
|
+
listSnapshots: async () => { throw new Error('network down'); },
|
|
276
|
+
hasNewRemoteSnapshot: async (listed?: unknown) => { detectCalls += 1; seenListed = listed; return false; },
|
|
277
|
+
hasLocalChanges: async () => false,
|
|
278
|
+
};
|
|
279
|
+
const { scheduler, getEntries } = makeScheduler({ cfg, engine, history: [] });
|
|
280
|
+
const result = await scheduler.runOnce('git');
|
|
281
|
+
assert.equal(detectCalls, 1, '列表失败不阻断检测');
|
|
282
|
+
assert.equal(seenListed, undefined, '没有可用列表 → 传 undefined,让引擎自己 list');
|
|
283
|
+
assert.equal(result.status, 'success');
|
|
284
|
+
assert.equal(result.skipReason, 'upToDate');
|
|
285
|
+
assert.equal(getEntries().length, 1);
|
|
286
|
+
});
|
|
287
|
+
|
|
245
288
|
test('runOnce: 未配置仓库 → skipped(unconfigured),写历史但不计失败', async () => {
|
|
246
289
|
const cfg: AutosyncConfig = { enabled: true, interval: '30m', startupMinIntervalMs: 300000, consecutiveFailures: 0 };
|
|
247
290
|
const scheduler = new AutoSyncScheduler({
|
|
@@ -308,10 +351,10 @@ test('runOnce: merge 抛错 → failed,连续失败计数 +1', async () => {
|
|
|
308
351
|
assert.ok(getEntries().some((e) => e.status === 'failed'), '写入失败历史');
|
|
309
352
|
});
|
|
310
353
|
|
|
311
|
-
test('issue #79
|
|
354
|
+
test('issue #79 + conflictPolicy=local(缺省):冲突只挡下行,本机版本强制推成新远端(partial,不谎报 success/failed)', async () => {
|
|
312
355
|
const cfg: AutosyncConfig = { enabled: true, interval: '30m', startupMinIntervalMs: 300000, consecutiveFailures: 1 };
|
|
313
356
|
const applyCalls: string[] = [];
|
|
314
|
-
const pushCalls:
|
|
357
|
+
const pushCalls: Array<{ force?: boolean } | undefined> = [];
|
|
315
358
|
const engine = {
|
|
316
359
|
hasNewRemoteSnapshot: async () => true,
|
|
317
360
|
hasLocalChanges: async () => true,
|
|
@@ -320,23 +363,56 @@ test('issue #79:远端冲突只挡下行 —— 不写本地,但仍推送本
|
|
|
320
363
|
applyCalls.push('apply');
|
|
321
364
|
return { ok: true, applied: [] as never[], restoreId: 'r1', rolledBack: false, review: [], warnings: [] };
|
|
322
365
|
},
|
|
323
|
-
push: async (
|
|
366
|
+
push: async (opts?: { force?: boolean }) => {
|
|
367
|
+
pushCalls.push(opts);
|
|
368
|
+
return { ok: true, snapshotId: 'snap-up', sections: [] as never, warnings: [] };
|
|
369
|
+
},
|
|
324
370
|
};
|
|
325
371
|
const { scheduler, getConfig, getEntries } = makeScheduler({ cfg, engine, history: [] });
|
|
326
372
|
const result = await scheduler.runOnce('git');
|
|
327
|
-
assert.equal(result.status, 'partial', '上行成功 +
|
|
328
|
-
assert.equal(result.skipReason, 'conflict');
|
|
373
|
+
assert.equal(result.status, 'partial', '上行成功 + 下行被冲突挡下 → partial');
|
|
374
|
+
assert.equal(result.skipReason, 'conflict-kept-local', '本机已按策略覆盖远端,与「待人工裁决」区分开');
|
|
329
375
|
assert.deepEqual(result.conflictedSections, ['settings']);
|
|
330
376
|
assert.equal(result.pushedSnapshotId, 'snap-up', '本机快照仍有上行出口(issue #79 主诉)');
|
|
331
377
|
assert.equal(applyCalls.length, 0, '冲突绝不写本机配置');
|
|
332
378
|
assert.equal(pushCalls.length, 1, '纯上行不被冲突连坐');
|
|
379
|
+
assert.equal(pushCalls[0]?.force, true, 'local 策略下必须带上 force,否则引擎会因「远端已推进」拒绝上传');
|
|
333
380
|
assert.equal(result.consecutiveFailures, 0, '冲突跳过不是失败 → 计数归零');
|
|
334
|
-
assert.equal(getConfig().lastRunSkipReason, 'conflict', '跳过原因落到状态(界面按码取字典)');
|
|
381
|
+
assert.equal(getConfig().lastRunSkipReason, 'conflict-kept-local', '跳过原因落到状态(界面按码取字典)');
|
|
335
382
|
assert.equal(getConfig().lastRunSkipCount, 1);
|
|
336
|
-
assert.ok(getEntries().some((e) => e.status === 'partial' && e.skipReason === 'conflict'), '写入 partial 历史');
|
|
383
|
+
assert.ok(getEntries().some((e) => e.status === 'partial' && e.skipReason === 'conflict-kept-local'), '写入 partial 历史');
|
|
337
384
|
assert.ok(getEntries().some((e) => e.pushedSnapshotId === 'snap-up'), '历史带上行快照 id');
|
|
338
385
|
});
|
|
339
386
|
|
|
387
|
+
test('issue #79 + conflictPolicy=ask:冲突只挡下行,绝不强推覆盖远端(partial + skipped 码 conflict)', async () => {
|
|
388
|
+
const cfg: AutosyncConfig = {
|
|
389
|
+
enabled: true, interval: '30m', startupMinIntervalMs: 300000, consecutiveFailures: 1,
|
|
390
|
+
conflictPolicy: 'ask',
|
|
391
|
+
};
|
|
392
|
+
const applyCalls: string[] = [];
|
|
393
|
+
const pushCalls: Array<{ force?: boolean } | undefined> = [];
|
|
394
|
+
const engine = {
|
|
395
|
+
hasNewRemoteSnapshot: async () => true,
|
|
396
|
+
hasLocalChanges: async () => true,
|
|
397
|
+
merge: async (): Promise<MergePlan> => makeMergePlan([['settings', 'conflict']]),
|
|
398
|
+
applyMergePlan: async () => {
|
|
399
|
+
applyCalls.push('apply');
|
|
400
|
+
return { ok: true, applied: [] as never[], restoreId: 'r1', rolledBack: false, review: [], warnings: [] };
|
|
401
|
+
},
|
|
402
|
+
push: async (opts?: { force?: boolean }) => {
|
|
403
|
+
pushCalls.push(opts);
|
|
404
|
+
return { ok: true, snapshotId: 'snap-up', sections: [] as never, warnings: [] };
|
|
405
|
+
},
|
|
406
|
+
};
|
|
407
|
+
const { scheduler, getConfig } = makeScheduler({ cfg, engine, history: [] });
|
|
408
|
+
const result = await scheduler.runOnce('git');
|
|
409
|
+
assert.equal(result.status, 'partial');
|
|
410
|
+
assert.equal(result.skipReason, 'conflict', 'ask 策略保留「待裁决」语义');
|
|
411
|
+
assert.equal(pushCalls[0]?.force, undefined, 'ask 策略绝不 force 覆盖远端');
|
|
412
|
+
assert.equal(applyCalls.length, 0, '冲突绝不写本机配置');
|
|
413
|
+
assert.equal(getConfig().lastRunSkipReason, 'conflict');
|
|
414
|
+
});
|
|
415
|
+
|
|
340
416
|
test('issue #79:冲突且本机无改动(非 startup)→ skipped(conflict) 且不带 push', async () => {
|
|
341
417
|
const cfg: AutosyncConfig = { enabled: true, interval: '30m', startupMinIntervalMs: 300000, consecutiveFailures: 0 };
|
|
342
418
|
const pushCalls: string[] = [];
|
|
@@ -377,6 +453,31 @@ test('issue #79:本轮成功必须清掉上一轮的错误文案与跳过码
|
|
|
377
453
|
assert.equal(saved.consecutiveFailures, 0, '成功归零连续失败');
|
|
378
454
|
});
|
|
379
455
|
|
|
456
|
+
test('push 被远端推进检测拒绝(conflict)→ skipped(conflict) 而非 failed;不累加连续失败、历史留下引擎说明', async () => {
|
|
457
|
+
const cfg: AutosyncConfig = { enabled: true, interval: '30m', startupMinIntervalMs: 300000, consecutiveFailures: 2 };
|
|
458
|
+
const engine = {
|
|
459
|
+
hasNewRemoteSnapshot: async () => false,
|
|
460
|
+
hasLocalChanges: async () => true,
|
|
461
|
+
push: async () => ({
|
|
462
|
+
ok: false, snapshotId: '', sections: [] as never, warnings: [],
|
|
463
|
+
conflict: true,
|
|
464
|
+
message: '远端最新快照 sync-other 与本地基线 sync-base 不一致:本次推送未执行',
|
|
465
|
+
}),
|
|
466
|
+
};
|
|
467
|
+
const { scheduler, getConfig, getEntries } = makeScheduler({ cfg, engine, history: [] });
|
|
468
|
+
const result = await scheduler.runOnce('git');
|
|
469
|
+
assert.equal(result.status, 'skipped', '有冲突待处理 ≠ 同步坏了');
|
|
470
|
+
assert.equal(result.skipReason, 'conflict');
|
|
471
|
+
assert.equal(result.consecutiveFailures, 0, '冲突跳过不计失败(否则会误发「连续失败」通知)');
|
|
472
|
+
assert.ok((result.error ?? '').includes('sync-other'), '必须把引擎的说明带给用户: ' + (result.error ?? ''));
|
|
473
|
+
assert.equal(getConfig().lastRunStatus, 'skipped');
|
|
474
|
+
assert.equal(getConfig().lastRunSkipReason, 'conflict');
|
|
475
|
+
assert.ok(
|
|
476
|
+
getEntries().some((e) => e.status === 'skipped' && e.skipReason === 'conflict' && (e.error ?? '').includes('sync-other')),
|
|
477
|
+
'历史里必须能看出「有远端冲突待合并」',
|
|
478
|
+
);
|
|
479
|
+
});
|
|
480
|
+
|
|
380
481
|
test('runOnce: 无冲突且无变化 → success(pull,unchanged),不上传(本地也无改动)', async () => {
|
|
381
482
|
const cfg: AutosyncConfig = { enabled: true, interval: '30m', startupMinIntervalMs: 300000, consecutiveFailures: 0 };
|
|
382
483
|
const engine = {
|
|
@@ -496,7 +597,8 @@ test('runOnce: 远端有新快照但本地无改动 → 只 pull 合并,不 pu
|
|
|
496
597
|
});
|
|
497
598
|
|
|
498
599
|
test('start(): 定时器触发后自动重排下一次(周期性后台同步)', async () => {
|
|
499
|
-
|
|
600
|
+
// realtime:false 关掉「远端轮询定时器」,本用例只测 interval 兜底排期与重排(每个通道 1 个定时器)。
|
|
601
|
+
const cfg: AutosyncConfig = { enabled: true, interval: '30m', startupMinIntervalMs: 300000, consecutiveFailures: 0, realtime: false };
|
|
500
602
|
const pending: Array<() => void> = [];
|
|
501
603
|
let timerSeq = 0;
|
|
502
604
|
const engine = {
|
|
@@ -597,12 +699,13 @@ test('runOnce: 远端最新快照为普通 → 加密检测不触发(listSnaps
|
|
|
597
699
|
});
|
|
598
700
|
|
|
599
701
|
test('双通道:git/webdav 同时 enabled → 各自独立排期;runOnce 写各自通道配置', async () => {
|
|
600
|
-
|
|
601
|
-
const
|
|
702
|
+
// realtime:false → 只有 interval 兜底定时器(realtime 时每通道还会多一个远端轮询定时器)。
|
|
703
|
+
const gitCfg: AutosyncConfig = { enabled: true, interval: '30m', startupMinIntervalMs: 300000, consecutiveFailures: 0, realtime: false };
|
|
704
|
+
const webdavCfg: AutosyncConfig = { enabled: true, interval: '5m', startupMinIntervalMs: 300000, consecutiveFailures: 0, realtime: false };
|
|
602
705
|
// 全通道配置由唯一枚举派生(新增通道不得手写漏项);其余通道保持默认关闭
|
|
603
706
|
const configs = {} as Record<SyncTransportType, AutosyncConfig>;
|
|
604
707
|
for (const ch of SYNC_CHANNELS) {
|
|
605
|
-
configs[ch] = { enabled: false, interval: '30m', startupMinIntervalMs: 300000, consecutiveFailures: 0 };
|
|
708
|
+
configs[ch] = { enabled: false, interval: '30m', startupMinIntervalMs: 300000, consecutiveFailures: 0, realtime: false };
|
|
606
709
|
}
|
|
607
710
|
configs.git = gitCfg;
|
|
608
711
|
configs.webdav = webdavCfg;
|
|
@@ -664,7 +767,8 @@ test('t32:排期覆盖 SYNC_CHANNELS 的每个通道(长度取自枚举,
|
|
|
664
767
|
now: () => new Date(1_000_000_000_000),
|
|
665
768
|
readConfig: async (channel: SyncTransportType) => {
|
|
666
769
|
seen.push(channel);
|
|
667
|
-
|
|
770
|
+
// realtime:false → 每个通道只排一个 interval 定时器,断言直接对应「通道数」。
|
|
771
|
+
return { enabled: true, interval: '30m', startupMinIntervalMs: 300000, consecutiveFailures: 0, realtime: false };
|
|
668
772
|
},
|
|
669
773
|
writeConfig: async () => {},
|
|
670
774
|
readSyncConfigFn: async () => null,
|
|
@@ -680,3 +784,151 @@ test('t32:排期覆盖 SYNC_CHANNELS 的每个通道(长度取自枚举,
|
|
|
680
784
|
scheduler.stop();
|
|
681
785
|
});
|
|
682
786
|
|
|
787
|
+
/* ---------------- m-autosync-realtime:即时通道(本地改动监听 + 远端轮询) ---------------- */
|
|
788
|
+
|
|
789
|
+
interface RealtimeHarness {
|
|
790
|
+
scheduler: AutoSyncScheduler;
|
|
791
|
+
timers: Array<() => void>;
|
|
792
|
+
/** 模拟监听器发现本地改动 */
|
|
793
|
+
fireLocalChange: (changed?: readonly string[]) => void;
|
|
794
|
+
/** 监听器生命周期(started/stopped/suspended/resumed 计数) */
|
|
795
|
+
watcher: { started: boolean; stopped: boolean; suspended: number; resumed: number };
|
|
796
|
+
engineCalls: { merge: number; push: number };
|
|
797
|
+
/** 推进夹具时钟(间隔下限的判定依赖真实时间流逝) */
|
|
798
|
+
advance: (ms: number) => void;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function makeRealtimeScheduler(cfg: AutosyncConfig, opts: { minTriggerGapMs?: number } = {}): RealtimeHarness {
|
|
802
|
+
const timers: Array<() => void> = [];
|
|
803
|
+
const watcher = { started: false, stopped: false, suspended: 0, resumed: 0 };
|
|
804
|
+
const engineCalls = { merge: 0, push: 0 };
|
|
805
|
+
let flush: ((changed: readonly string[]) => void) | null = null;
|
|
806
|
+
let clockMs = 1_000_000_000_000;
|
|
807
|
+
const engine = {
|
|
808
|
+
hasNewRemoteSnapshot: async () => false,
|
|
809
|
+
hasLocalChanges: async () => true,
|
|
810
|
+
merge: async (): Promise<MergePlan> => { engineCalls.merge += 1; return makeMergePlan([]); },
|
|
811
|
+
push: async () => {
|
|
812
|
+
engineCalls.push += 1;
|
|
813
|
+
return { ok: true, snapshotId: 'snap-' + engineCalls.push, sections: [] as never, warnings: [] };
|
|
814
|
+
},
|
|
815
|
+
};
|
|
816
|
+
const scheduler = new AutoSyncScheduler({
|
|
817
|
+
syncDir: '/tmp',
|
|
818
|
+
host: { log: nullLogger() },
|
|
819
|
+
makeSyncEngine: () => engine as unknown as SyncEngine,
|
|
820
|
+
msg: (k: string) => k,
|
|
821
|
+
runs: new RunRegistry(),
|
|
822
|
+
now: () => new Date(clockMs),
|
|
823
|
+
// 只有 git 通道开启:其余通道回退 enabled:false,否则 refreshTimers 会给四个通道都排定时器
|
|
824
|
+
readConfig: async (channel) => (channel === 'git' ? cfg : { ...cfg, enabled: false }),
|
|
825
|
+
writeConfig: async () => {},
|
|
826
|
+
readSyncConfigFn: async () => ({ schemaVersion: 2, transport: 'git', git: { repoUrl: 'git@github.com:foo/bar.git' } }),
|
|
827
|
+
readHistoryFn: async () => ({ schemaVersion: 1, autosyncEntries: [], updatedAt: '' }),
|
|
828
|
+
appendHistoryFn: async () => {},
|
|
829
|
+
setTimer: (fn) => { timers.push(fn); return timers.length as unknown as ReturnType<typeof setTimeout>; },
|
|
830
|
+
clearTimer: () => {},
|
|
831
|
+
homeDir: '/home/u/.dsh',
|
|
832
|
+
minTriggerGapMs: opts.minTriggerGapMs ?? 3000,
|
|
833
|
+
makeWatcher: (wopts) => {
|
|
834
|
+
flush = wopts.onFlush;
|
|
835
|
+
return {
|
|
836
|
+
start: () => { watcher.started = true; },
|
|
837
|
+
stop: () => { watcher.stopped = true; },
|
|
838
|
+
setTargets: () => {},
|
|
839
|
+
suspend: () => { watcher.suspended += 1; },
|
|
840
|
+
resume: () => { watcher.resumed += 1; },
|
|
841
|
+
isStarted: () => watcher.started,
|
|
842
|
+
attachmentCount: () => 1,
|
|
843
|
+
flushNow: () => {},
|
|
844
|
+
} as never;
|
|
845
|
+
},
|
|
846
|
+
});
|
|
847
|
+
return {
|
|
848
|
+
scheduler, timers, watcher, engineCalls,
|
|
849
|
+
advance: (ms: number) => { clockMs += ms; },
|
|
850
|
+
fireLocalChange: (changed = ['settings.yaml']) => {
|
|
851
|
+
assert.ok(flush !== null, '监听器尚未创建(start() 未跑或 realtime 关闭)');
|
|
852
|
+
flush!(changed);
|
|
853
|
+
},
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
const tick = (): Promise<void> => new Promise((resolve) => setTimeout(resolve, 0));
|
|
858
|
+
|
|
859
|
+
test('realtime:本地改动 → 立刻触发一轮(不等 interval),runOnce 期间挂起监听、结束后恢复', async () => {
|
|
860
|
+
const cfg: AutosyncConfig = { enabled: true, interval: '24h', startupMinIntervalMs: 300000, consecutiveFailures: 0 };
|
|
861
|
+
// gap 归零:本用例只测「监听回调 → 立刻跑一轮」,间隔合并另有用例
|
|
862
|
+
const h = makeRealtimeScheduler(cfg, { minTriggerGapMs: 0 });
|
|
863
|
+
h.scheduler.start();
|
|
864
|
+
await tick();
|
|
865
|
+
await tick();
|
|
866
|
+
assert.equal(h.watcher.started, true, '开启实时同步必须挂上本地改动监听');
|
|
867
|
+
const pushesBefore = h.engineCalls.push;
|
|
868
|
+
const suspendedBefore = h.watcher.suspended;
|
|
869
|
+
const resumedBefore = h.watcher.resumed;
|
|
870
|
+
h.fireLocalChange();
|
|
871
|
+
await tick();
|
|
872
|
+
await tick();
|
|
873
|
+
assert.equal(h.engineCalls.push, pushesBefore + 1, '本地一改就上行(不必等 24h 兜底定时器)');
|
|
874
|
+
assert.equal(h.watcher.suspended, suspendedBefore + 1, '同步自家写入必须被挂起,否则自激');
|
|
875
|
+
assert.equal(h.watcher.resumed, resumedBefore + 1, '收尾必须恢复监听');
|
|
876
|
+
h.scheduler.stop();
|
|
877
|
+
assert.equal(h.watcher.stopped, true, 'stop() 必须关掉监听');
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
test('realtime:间隔下限(minTriggerGapMs)内的第二次触发排成延迟触发,不丢改动', async () => {
|
|
881
|
+
const cfg: AutosyncConfig = { enabled: true, interval: '24h', startupMinIntervalMs: 300000, consecutiveFailures: 0 };
|
|
882
|
+
const h = makeRealtimeScheduler(cfg, { minTriggerGapMs: 3000 });
|
|
883
|
+
h.scheduler.start();
|
|
884
|
+
await tick();
|
|
885
|
+
await tick();
|
|
886
|
+
const before = h.timers.length;
|
|
887
|
+
const pushesBefore = h.engineCalls.push;
|
|
888
|
+
h.fireLocalChange(['settings.yaml']);
|
|
889
|
+
await tick();
|
|
890
|
+
assert.equal(h.engineCalls.push, pushesBefore, '距上轮不足 gap 时不得立刻再跑(避免连续保存打满远端)');
|
|
891
|
+
assert.equal(h.timers.length, before + 1, 'gap 内触发必须排一个延迟定时器(绝不丢用户改动)');
|
|
892
|
+
// 同一通道再来一次:复用同一枚定时器(合并成一轮),不得堆积
|
|
893
|
+
h.fireLocalChange(['skills']);
|
|
894
|
+
await tick();
|
|
895
|
+
assert.equal(h.timers.length, before + 1, '同一通道 gap 内只保留一枚定时器(连续保存合并成一轮)');
|
|
896
|
+
const gapTimer = h.timers[h.timers.length - 1];
|
|
897
|
+
assert.ok(gapTimer !== undefined);
|
|
898
|
+
h.advance(3000); // 真实时间里这 3 秒是自然流逝的;夹具时钟被钉死,必须手动推进
|
|
899
|
+
gapTimer();
|
|
900
|
+
await tick();
|
|
901
|
+
await tick();
|
|
902
|
+
assert.equal(h.engineCalls.push, pushesBefore + 1, '延迟触发到点后必须补跑(两处改动合成一轮)');
|
|
903
|
+
h.scheduler.stop();
|
|
904
|
+
});
|
|
905
|
+
|
|
906
|
+
test('realtime=false:不挂监听、不排远端轮询定时器(退回纯 interval 兜底)', async () => {
|
|
907
|
+
const cfg: AutosyncConfig = { enabled: true, interval: '30m', startupMinIntervalMs: 300000, consecutiveFailures: 0, realtime: false };
|
|
908
|
+
const h = makeRealtimeScheduler(cfg);
|
|
909
|
+
h.scheduler.start();
|
|
910
|
+
await tick();
|
|
911
|
+
assert.equal(h.watcher.started, false, 'realtime 关闭时不得挂监听');
|
|
912
|
+
assert.equal(h.timers.length, 1, '只排 interval 兜底定时器(无远端轮询)');
|
|
913
|
+
h.scheduler.stop();
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
test('realtime:远端轮询定时器按 remoteCheckMs 排期,到点触发一轮后立刻重排', async () => {
|
|
917
|
+
const cfg: AutosyncConfig = {
|
|
918
|
+
enabled: true, interval: '24h', startupMinIntervalMs: 300000, consecutiveFailures: 0,
|
|
919
|
+
remoteCheckMs: 60_000,
|
|
920
|
+
};
|
|
921
|
+
const h = makeRealtimeScheduler(cfg);
|
|
922
|
+
h.scheduler.start();
|
|
923
|
+
await tick();
|
|
924
|
+
// interval 兜底 1 个 + 远端轮询 1 个
|
|
925
|
+
assert.equal(h.timers.length, 2, '每通道排 interval + 远端轮询两枚定时器');
|
|
926
|
+
const poll = h.timers[1];
|
|
927
|
+
assert.ok(poll !== undefined);
|
|
928
|
+
poll();
|
|
929
|
+
await tick();
|
|
930
|
+
await tick();
|
|
931
|
+
assert.ok(h.timers.length >= 3, '轮询到点后必须立刻重排下一次');
|
|
932
|
+
h.scheduler.stop();
|
|
933
|
+
});
|
|
934
|
+
|