dsh-config-manager 0.1.71 → 0.1.73
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/README.md +149 -98
- package/README.zh-CN.md +245 -88
- package/lib/adapters/agent-instructions.js +1 -1
- package/lib/adapters/file-collection.js +1 -1
- package/lib/adapters/index.d.ts +2 -1
- package/lib/adapters/index.js +6 -1
- package/lib/adapters/link-report.d.ts +1 -0
- package/lib/adapters/link-report.js +27 -3
- package/lib/adapters/messages.d.ts +21 -0
- package/lib/adapters/messages.js +57 -0
- package/lib/adapters/plugin-data.d.ts +114 -0
- package/lib/adapters/plugin-data.js +309 -0
- package/lib/adapters/plugin-files.js +3 -2
- package/lib/adapters/plugins.js +4 -3
- package/lib/adapters/prompts.d.ts +1 -0
- package/lib/adapters/prompts.js +7 -1
- package/lib/adapters/self.d.ts +8 -2
- package/lib/adapters/self.js +3 -2
- package/lib/adapters/sessions.d.ts +1 -0
- package/lib/adapters/sessions.js +4 -2
- package/lib/adapters/test-helpers.d.ts +9 -0
- package/lib/adapters/test-helpers.js +14 -0
- package/lib/adapters/ui.d.ts +2 -2
- package/lib/adapters/ui.js +25 -25
- package/lib/adapters/workspaces.d.ts +1 -0
- package/lib/adapters/workspaces.js +7 -3
- package/lib/cli/actions.d.ts +9 -1
- package/lib/cli/actions.js +5 -2
- package/lib/cli/help.js +23 -10
- package/lib/cli/index.d.ts +23 -0
- package/lib/cli/index.js +90 -32
- package/lib/cli/sessions-inspect.js +3 -1
- package/lib/cli/sessions-repair.js +25 -6
- package/lib/cli/web/page.d.ts +12 -0
- package/lib/cli/web/page.js +21 -12
- package/lib/cli/web/routes.js +2 -2
- package/lib/cli/web/server.js +4 -4
- package/lib/client.d.ts +1442 -494
- package/lib/client.js +15326 -8520
- package/lib/core/analyzer.d.ts +11 -2
- package/lib/core/analyzer.js +31 -8
- package/lib/core/backup-plan.d.ts +10 -0
- package/lib/core/backup-plan.js +51 -20
- package/lib/core/backup-verify.js +24 -23
- package/lib/core/backup.d.ts +25 -1
- package/lib/core/backup.js +99 -28
- package/lib/core/boot-rescue.js +4 -3
- package/lib/core/boot-safety.js +2 -1
- package/lib/core/command-rollback.d.ts +182 -0
- package/lib/core/command-rollback.js +293 -0
- package/lib/core/consult-source.js +7 -6
- package/lib/core/disk-usage.d.ts +74 -10
- package/lib/core/disk-usage.js +67 -4
- package/lib/core/engine-locale.d.ts +87 -0
- package/lib/core/engine-locale.js +125 -0
- package/lib/core/exporter.js +3 -3
- package/lib/core/fork-reconcile.d.ts +128 -0
- package/lib/core/fork-reconcile.js +150 -0
- package/lib/core/journal.js +6 -5
- package/lib/core/local-plugin-host.js +3 -2
- package/lib/core/local-plugin-pack.js +11 -6
- package/lib/core/messages-core.d.ts +294 -0
- package/lib/core/messages-core.js +619 -0
- package/lib/core/messages.d.ts +61 -2
- package/lib/core/messages.js +118 -13
- package/lib/core/migration-consult.js +21 -20
- package/lib/core/migration-history.js +19 -5
- package/lib/core/model-tools.d.ts +0 -22
- package/lib/core/model-tools.js +50 -27
- package/lib/core/phase3-host.js +30 -5
- package/lib/core/plugin-cli.js +18 -19
- package/lib/core/plugin-data-registry.d.ts +124 -0
- package/lib/core/plugin-data-registry.js +198 -0
- package/lib/core/reconcile.d.ts +2 -14
- package/lib/core/reconcile.js +87 -17
- package/lib/core/recovery-orchestrator.js +19 -18
- package/lib/core/reinstall.d.ts +4 -0
- package/lib/core/reinstall.js +26 -21
- package/lib/core/restore.js +20 -17
- package/lib/core/rollback-gate.d.ts +33 -0
- package/lib/core/rollback-gate.js +57 -0
- package/lib/core/rollback-ignore-rules.d.ts +31 -0
- package/lib/core/rollback-ignore-rules.js +140 -0
- package/lib/core/rollback-ledger.d.ts +248 -0
- package/lib/core/rollback-ledger.js +1282 -0
- package/lib/core/rollback-point.d.ts +1391 -0
- package/lib/core/rollback-point.js +3503 -0
- package/lib/core/rollback.js +6 -4
- package/lib/core/run-registry.js +6 -5
- package/lib/core/session-health.d.ts +15 -0
- package/lib/core/session-meta.d.ts +42 -2
- package/lib/core/session-meta.js +140 -1
- package/lib/core/session-title.d.ts +28 -0
- package/lib/core/session-title.js +185 -0
- package/lib/core/startup-barrier.d.ts +24 -0
- package/lib/core/startup-barrier.js +22 -0
- package/lib/core/transaction-coordinator.js +4 -3
- package/lib/core/types.d.ts +37 -1
- package/lib/core/validator.js +5 -4
- package/lib/core/verify-recovery.js +31 -30
- package/lib/foreign/bundle.js +3 -2
- package/lib/foreign/continue.js +2 -1
- package/lib/foreign/crush.js +2 -1
- package/lib/foreign/gemini.js +2 -1
- package/lib/foreign/goose.js +2 -1
- package/lib/foreign/kilocode.js +2 -1
- package/lib/foreign/kimi.js +2 -1
- package/lib/foreign/messages.d.ts +15 -0
- package/lib/foreign/messages.js +40 -0
- package/lib/foreign/mimocode.js +2 -1
- package/lib/foreign/openclaw.js +2 -1
- package/lib/foreign/opencode.js +2 -1
- package/lib/foreign/pi.js +2 -1
- package/lib/foreign/qoder.js +2 -1
- package/lib/foreign/qwen.js +2 -1
- package/lib/foreign/registry.d.ts +1 -1
- package/lib/foreign/registry.js +8 -7
- package/lib/foreign/teleagent.js +2 -1
- package/lib/foreign/workbuddy.js +2 -1
- package/lib/foreign/zcode.js +2 -1
- package/lib/foreign/zed.js +2 -1
- package/lib/index.d.ts +227 -7
- package/lib/index.js +799 -54
- package/lib/market/git-file-writer.js +20 -19
- package/lib/market/github-repos.js +35 -19
- package/lib/market/index-parser.js +31 -30
- package/lib/market/messages-market.d.ts +138 -0
- package/lib/market/messages-market.js +305 -0
- package/lib/market/my-repo.d.ts +2 -2
- package/lib/market/my-repo.js +32 -29
- package/lib/market/prepare.js +18 -15
- package/lib/market/security.js +20 -19
- package/lib/market/types.js +2 -1
- package/lib/market/url.js +2 -1
- package/lib/migrations/index.js +6 -5
- package/lib/migrations/messages-migrations.d.ts +32 -0
- package/lib/migrations/messages-migrations.js +58 -0
- package/lib/profiles/dsh-profile-launcher.js +2 -1
- package/lib/profiles/dsh-profile-runtime.js +2 -1
- package/lib/profiles/messages.d.ts +7 -0
- package/lib/profiles/messages.js +27 -0
- package/lib/routes/backup.js +18 -2
- package/lib/routes/import.js +3 -3
- package/lib/routes/index.d.ts +1 -1
- package/lib/routes/kit.d.ts +3 -2
- package/lib/routes/kit.js +6 -1
- package/lib/routes/market.js +1 -1
- package/lib/routes/messages.d.ts +43 -0
- package/lib/routes/messages.js +102 -0
- package/lib/routes/prefs.js +26 -3
- package/lib/routes/profiles.js +12 -6
- package/lib/routes/recovery.d.ts +21 -0
- package/lib/routes/recovery.js +478 -94
- package/lib/routes/snapshots.js +51 -4
- package/lib/routes/sync.js +79 -17
- package/lib/schema/config.d.ts +11 -1
- package/lib/schema/config.js +52 -20
- package/lib/schema/index.d.ts +8 -6
- package/lib/schema/index.js +8 -6
- package/lib/schema/manifest.js +19 -18
- package/lib/schema/messages.d.ts +47 -0
- package/lib/schema/messages.js +110 -0
- package/lib/schema/section-registry.d.ts +42 -15
- package/lib/schema/section-registry.js +67 -32
- package/lib/schema/types.d.ts +1 -1
- package/lib/schema/versions.js +6 -5
- package/lib/security/encryption.js +16 -15
- package/lib/security/integrity.js +8 -7
- package/lib/security/messages.d.ts +30 -0
- package/lib/security/messages.js +77 -0
- package/lib/security/redaction.js +12 -0
- package/lib/security/secret-scanner.js +16 -13
- package/lib/security/vault.js +2 -1
- package/lib/security/zip-security.js +4 -3
- package/lib/sync/ancestor.js +2 -1
- package/lib/sync/autosync-config.d.ts +11 -0
- package/lib/sync/autosync-config.js +12 -0
- package/lib/sync/autosync-scheduler.d.ts +11 -1
- package/lib/sync/autosync-scheduler.js +115 -76
- package/lib/sync/backup-files.js +3 -2
- package/lib/sync/backup-scheduler.js +6 -5
- package/lib/sync/blob-store.js +2 -1
- package/lib/sync/git/git-transport.d.ts +55 -0
- package/lib/sync/git/git-transport.js +152 -2
- package/lib/sync/github-auth.js +10 -9
- package/lib/sync/layout.js +13 -12
- package/lib/sync/messages.d.ts +63 -0
- package/lib/sync/messages.js +149 -0
- package/lib/sync/review-queue.js +6 -5
- package/lib/sync/s3/messages.d.ts +7 -0
- package/lib/sync/s3/messages.js +16 -0
- package/lib/sync/s3/s3-providers.d.ts +4 -1
- package/lib/sync/s3/s3-providers.js +5 -5
- package/lib/sync/s3/s3-transport.js +1 -1
- package/lib/sync/s3/sigv4.js +2 -1
- package/lib/sync/snapshot-crypto.js +6 -5
- package/lib/sync/snapshot-json.js +3 -2
- package/lib/sync/sync-engine.d.ts +58 -22
- package/lib/sync/sync-engine.js +119 -48
- package/lib/sync/sync-history.js +7 -6
- package/lib/sync/sync-selection.d.ts +15 -4
- package/lib/sync/sync-selection.js +17 -5
- package/lib/sync/ui-prefs.d.ts +22 -0
- package/lib/sync/ui-prefs.js +28 -0
- package/lib/sync/webdav/webdav-transport.d.ts +1 -0
- package/lib/sync/webdav/webdav-transport.js +4 -1
- package/lib/ui/artifact-view.js +4 -3
- package/lib/ui/commands.d.ts +1 -1
- package/lib/ui/commands.js +10 -0
- package/lib/ui/diff-view.d.ts +10 -0
- package/lib/ui/diff-view.js +17 -0
- package/lib/ui/disk-usage-view.d.ts +33 -4
- package/lib/ui/disk-usage-view.js +29 -1
- package/lib/ui/errors.d.ts +1 -1
- package/lib/ui/errors.js +4 -3
- package/lib/ui/export-flow.js +26 -20
- package/lib/ui/foreign-view.d.ts +5 -1
- package/lib/ui/foreign-view.js +26 -2
- package/lib/ui/i18n.d.ts +68 -1
- package/lib/ui/i18n.js +120 -2
- package/lib/ui/import-wizard.d.ts +7 -0
- package/lib/ui/import-wizard.js +13 -10
- package/lib/ui/progress.d.ts +2 -2
- package/lib/ui/progress.js +1 -2
- package/lib/ui/report.d.ts +5 -5
- package/lib/ui/report.js +6 -6
- package/lib/ui/rollback-view.d.ts +815 -0
- package/lib/ui/rollback-view.js +738 -0
- package/lib/ui/selection-model.d.ts +41 -0
- package/lib/ui/selection-model.js +30 -0
- package/lib/ui/session-inventory-view.d.ts +97 -3
- package/lib/ui/session-inventory-view.js +76 -2
- package/lib/ui/session-issue-labels.d.ts +43 -0
- package/lib/ui/session-issue-labels.js +48 -0
- package/lib/ui/sync-settings-view.d.ts +33 -0
- package/lib/ui/sync-settings-view.js +40 -0
- package/lib/ui/test-helpers.js +2 -1
- package/lib/ui/types.d.ts +8 -3
- package/lib/ui/types.js +1 -1
- package/lib/utils/atomic-write.js +6 -5
- package/lib/utils/bundle-scan.d.ts +11 -1
- package/lib/utils/bundle-scan.js +12 -1
- package/lib/utils/env-lock.d.ts +10 -0
- package/lib/utils/env-lock.js +73 -46
- package/lib/utils/json.js +6 -5
- package/lib/utils/messages-utils.d.ts +96 -0
- package/lib/utils/messages-utils.js +216 -0
- package/lib/utils/proxy.js +18 -5
- package/lib/utils/recursive-walk.d.ts +22 -0
- package/lib/utils/recursive-walk.js +75 -1
- package/lib/utils/session-log-repair.js +10 -9
- package/lib/utils/zip.js +27 -26
- package/package.json +12 -2
- package/src/adapters/agent-instructions.ts +1 -1
- package/src/adapters/file-collection.test.ts +1 -0
- package/src/adapters/file-collection.ts +1 -1
- package/src/adapters/files.test.ts +27 -0
- package/src/adapters/index.ts +6 -1
- package/src/adapters/link-report.test.ts +22 -2
- package/src/adapters/link-report.ts +29 -4
- package/src/adapters/messages.ts +61 -0
- package/src/adapters/plugin-data.test.ts +225 -0
- package/src/adapters/plugin-data.ts +351 -0
- package/src/adapters/plugin-files.ts +3 -2
- package/src/adapters/plugins.ts +4 -3
- package/src/adapters/prompts.ts +7 -1
- package/src/adapters/self.test.ts +26 -0
- package/src/adapters/self.ts +10 -3
- package/src/adapters/sessions.ts +4 -2
- package/src/adapters/settings.test.ts +38 -1
- package/src/adapters/test-helpers.ts +15 -0
- package/src/adapters/ui.test.ts +2 -2
- package/src/adapters/ui.ts +27 -26
- package/src/adapters/workspaces.ts +7 -3
- package/src/cli/actions.test.ts +28 -0
- package/src/cli/actions.ts +11 -2
- package/src/cli/help.ts +23 -10
- package/src/cli/index-guards.test.ts +193 -0
- package/src/cli/index.ts +106 -31
- package/src/cli/sessions-inspect.test.ts +15 -1
- package/src/cli/sessions-inspect.ts +3 -1
- package/src/cli/sessions-repair.test.ts +84 -0
- package/src/cli/sessions-repair.ts +33 -7
- package/src/cli/web/console-route-parity.test.ts +115 -0
- package/src/cli/web/page.ts +23 -12
- package/src/cli/web/routes.ts +2 -1
- package/src/cli/web/server.ts +4 -4
- package/src/cli/web/web.test.ts +2 -2
- package/src/client/ConfigManagerSection.tsx +30 -2
- package/src/client/about/AboutPanel.tsx +2 -2
- package/src/client/about/PluginUpdateDialog.tsx +1 -1
- package/src/client/about/ReleaseNotesDialog.tsx +2 -2
- package/src/client/about/about-view.test.ts +13 -3
- package/src/client/about/about-view.ts +10 -6
- package/src/client/about/release-notes-view.test.ts +14 -4
- package/src/client/about/release-notes-view.ts +4 -2
- package/src/client/api.ts +18 -6
- package/src/client/chat/ChatEntryView.tsx +411 -0
- package/src/client/chat/RollbackDiffTab.tsx +108 -0
- package/src/client/chat/RollbackFileIcon.tsx +96 -0
- package/src/client/chat/RollbackReviewDialog.tsx +607 -0
- package/src/client/chat/chat-entry-view.test.ts +1174 -0
- package/src/client/chat/chat-entry-view.ts +758 -0
- package/src/client/chat/chat-locales.ts +152 -0
- package/src/client/chat/chat-node-registration.ts +64 -0
- package/src/client/chat/chat-point-window.ts +183 -0
- package/src/client/chat/chat-slots.ts +107 -0
- package/src/client/chat/rollback-diff-tab.test.ts +105 -0
- package/src/client/chat/rollback-diff-tab.ts +174 -0
- package/src/client/chat/rollback-entry-view.test.ts +1469 -0
- package/src/client/chat/rollback-entry-view.ts +946 -0
- package/src/client/chat/sidebar-slots.ts +59 -0
- package/src/client/common/ConfirmDialog.tsx +7 -3
- package/src/client/common/ContentPicker.tsx +48 -10
- package/src/client/common/DiffTable.tsx +133 -0
- package/src/client/common/ErrorBanner.tsx +4 -4
- package/src/client/common/Icon.tsx +2 -0
- package/src/client/common/Modal.tsx +8 -1
- package/src/client/common/ProgressBar.tsx +8 -2
- package/src/client/common/ReportView.tsx +4 -4
- package/src/client/common/RunsCenter.tsx +1 -0
- package/src/client/common/client-route-parity.test.ts +84 -0
- package/src/client/common/info-hint-guard.test.ts +46 -13
- package/src/client/common/picker-lazy-units.test.ts +42 -0
- package/src/client/common/plan-text-redaction.test.ts +17 -6
- package/src/client/common/progress-view-edge.test.ts +8 -7
- package/src/client/common/progress-view.test.ts +9 -9
- package/src/client/common/progress-view.ts +2 -2
- package/src/client/common/routes.ts +38 -0
- package/src/client/common/section-labels.test.ts +1 -0
- package/src/client/common/section-labels.ts +2 -0
- package/src/client/common/ui.tsx +73 -10
- package/src/client/config-manager.module.css +1109 -31
- package/src/client/css-scale-guard.test.ts +30 -0
- package/src/client/environment/EnvironmentPanel.tsx +11 -4
- package/src/client/environment/maintenance/DiskUsageCard.tsx +23 -2
- package/src/client/export/ExportView.tsx +13 -8
- package/src/client/history/HistoryPanel.tsx +4 -3
- package/src/client/history/history-api.ts +2 -2
- package/src/client/history/history-locales.ts +5 -1
- package/src/client/home/HomePanel.tsx +29 -8
- package/src/client/import/ForeignImportView.tsx +4 -5
- package/src/client/import/ImportWizardView.tsx +5 -3
- package/src/client/import/import-wizard-split.test.ts +3 -1
- package/src/client/import/import-wizard-steps.tsx +9 -9
- package/src/client/index.ts +232 -3
- package/src/client/library/LibraryActions.tsx +32 -7
- package/src/client/library/LibraryPanel.tsx +23 -3
- package/src/client/library/RestorePlanView.tsx +9 -66
- package/src/client/locales.ts +62 -9
- package/src/client/lucide-icons.d.ts +1 -0
- package/src/client/market/MarketPanel.tsx +13 -2
- package/src/client/market/MyConfigsView.tsx +5 -4
- package/src/client/market/MyConfigsWizard.tsx +10 -8
- package/src/client/market/market-api.test.ts +9 -8
- package/src/client/market/market-api.ts +2 -2
- package/src/client/market/market-locales.ts +3 -0
- package/src/client/market/market-view.test.ts +6 -6
- package/src/client/market/market-view.ts +3 -3
- package/src/client/market/my-configs-api.ts +2 -2
- package/src/client/market/my-configs-view.test.ts +23 -22
- package/src/client/market/my-configs-view.ts +8 -8
- package/src/client/recovery/IncompleteCopiesSection.tsx +2 -1
- package/src/client/recovery/RecoveryPanel.tsx +442 -283
- package/src/client/recovery/RollbackCard.tsx +859 -0
- package/src/client/recovery/RollbackDiffPane.tsx +69 -0
- package/src/client/recovery/RollbackFileChanges.tsx +163 -0
- package/src/client/recovery/incident-api.ts +2 -2
- package/src/client/recovery/recovery-api.ts +144 -6
- package/src/client/recovery/recovery-locales.test.ts +299 -0
- package/src/client/recovery/recovery-locales.ts +520 -149
- package/src/client/recovery/recovery-panel-layout.test.ts +132 -2
- package/src/client/recovery/rollback-enabled.ts +41 -0
- package/src/client/recovery/rollback-file-changes.test.ts +114 -0
- package/src/client/recovery/session-dialog-mount.test.ts +54 -0
- package/src/client/run-store.ts +84 -21
- package/src/client/sync/AutosyncCard.tsx +25 -1
- package/src/client/sync/ChannelClearConfirmDialog.tsx +6 -3
- package/src/client/sync/ChannelConfigDialog.tsx +10 -10
- package/src/client/sync/DecryptPasswordCard.tsx +2 -2
- package/src/client/sync/SecurityOptionsCard.tsx +27 -17
- package/src/client/sync/SyncChannelEntryCard.tsx +14 -2
- package/src/client/sync/SyncConfirmView.tsx +46 -8
- package/src/client/sync/SyncLogList.tsx +1 -1
- package/src/client/sync/SyncPanel.tsx +31 -23
- package/src/client/sync/SyncRepositoryPicker.tsx +3 -5
- package/src/client/sync/SyncSectionPickerDialog.tsx +11 -2
- package/src/client/sync/SyncSettingsView.test.ts +20 -1
- package/src/client/sync/UnreadableSnapshotsBanner.tsx +1 -1
- package/src/client/sync/WorkDirCard.tsx +112 -0
- package/src/client/sync/history-model.test.ts +42 -9
- package/src/client/sync/history-model.ts +30 -15
- package/src/client/sync/sync-api.test.ts +57 -33
- package/src/client/sync/sync-api.ts +80 -6
- package/src/client/sync/sync-locales.ts +104 -3
- package/src/client/sync/sync-view-v2.test.ts +65 -13
- package/src/client/sync/sync-view.test.ts +107 -52
- package/src/client/sync/sync-view.ts +124 -41
- package/src/core/analyzer.ts +31 -9
- package/src/core/backup-plan.test.ts +48 -5
- package/src/core/backup-plan.ts +51 -20
- package/src/core/backup-verify.ts +24 -23
- package/src/core/backup.test.ts +76 -0
- package/src/core/backup.ts +105 -29
- package/src/core/boot-rescue.ts +4 -3
- package/src/core/boot-safety.ts +2 -1
- package/src/core/command-rollback.test.ts +536 -0
- package/src/core/command-rollback.ts +406 -0
- package/src/core/consult-source.ts +7 -6
- package/src/core/disk-usage.test.ts +257 -1
- package/src/core/disk-usage.ts +134 -14
- package/src/core/engine-locale.test.ts +593 -0
- package/src/core/engine-locale.ts +175 -0
- package/src/core/exporter.ts +3 -3
- package/src/core/fork-reconcile.test.ts +279 -0
- package/src/core/fork-reconcile.ts +234 -0
- package/src/core/journal.ts +6 -5
- package/src/core/local-plugin-host.ts +3 -2
- package/src/core/local-plugin-pack.ts +11 -6
- package/src/core/messages-core.ts +661 -0
- package/src/core/messages.ts +125 -17
- package/src/core/migration-consult.ts +21 -20
- package/src/core/migration-history.ts +19 -5
- package/src/core/model-tools.ts +28 -27
- package/src/core/phase3-host.ts +21 -5
- package/src/core/phase3-p2b-fingerprint.test.ts +22 -0
- package/src/core/plugin-cli.ts +18 -19
- package/src/core/plugin-data-registry.test.ts +113 -0
- package/src/core/plugin-data-registry.ts +261 -0
- package/src/core/reconcile.test.ts +117 -0
- package/src/core/reconcile.ts +85 -17
- package/src/core/recovery-orchestrator.ts +19 -18
- package/src/core/reinstall.ts +26 -21
- package/src/core/restore.ts +20 -17
- package/src/core/rollback-file-diff.test.ts +159 -0
- package/src/core/rollback-gate.test.ts +66 -0
- package/src/core/rollback-gate.ts +61 -0
- package/src/core/rollback-ignore-rules.ts +126 -0
- package/src/core/rollback-ledger.test.ts +655 -0
- package/src/core/rollback-ledger.ts +1314 -0
- package/src/core/rollback-point.test.ts +2853 -0
- package/src/core/rollback-point.ts +4340 -0
- package/src/core/rollback.ts +5 -3
- package/src/core/run-registry.ts +6 -5
- package/src/core/session-health.ts +15 -0
- package/src/core/session-meta.test.ts +117 -1
- package/src/core/session-meta.ts +149 -2
- package/src/core/session-title.test.ts +127 -0
- package/src/core/session-title.ts +189 -0
- package/src/core/startup-barrier.test.ts +69 -0
- package/src/core/startup-barrier.ts +27 -0
- package/src/core/transaction-coordinator.ts +4 -3
- package/src/core/types.ts +37 -1
- package/src/core/validator.ts +5 -4
- package/src/core/verify-recovery.ts +31 -30
- package/src/foreign/bundle.ts +3 -2
- package/src/foreign/continue.ts +2 -1
- package/src/foreign/crush.ts +2 -1
- package/src/foreign/file-budget.test.ts +2 -0
- package/src/foreign/gemini.ts +2 -1
- package/src/foreign/goose.ts +2 -1
- package/src/foreign/kilocode.ts +2 -1
- package/src/foreign/kimi.ts +2 -1
- package/src/foreign/messages.ts +44 -0
- package/src/foreign/mimocode.ts +2 -1
- package/src/foreign/openclaw.ts +2 -1
- package/src/foreign/opencode.ts +2 -1
- package/src/foreign/pi.ts +2 -1
- package/src/foreign/qoder.ts +2 -1
- package/src/foreign/qwen.ts +2 -1
- package/src/foreign/registry.test.ts +1 -1
- package/src/foreign/registry.ts +8 -7
- package/src/foreign/teleagent.ts +2 -1
- package/src/foreign/workbuddy.ts +2 -1
- package/src/foreign/zcode.ts +2 -1
- package/src/foreign/zed.ts +2 -1
- package/src/index.facade.test.ts +340 -1
- package/src/index.ts +876 -57
- package/src/market/git-file-writer.ts +20 -19
- package/src/market/github-repos.test.ts +23 -0
- package/src/market/github-repos.ts +37 -19
- package/src/market/index-parser.ts +31 -30
- package/src/market/messages-market.ts +325 -0
- package/src/market/my-repo.ts +32 -29
- package/src/market/prepare.ts +18 -15
- package/src/market/security.ts +20 -19
- package/src/market/types.ts +2 -1
- package/src/market/url.ts +2 -1
- package/src/migrations/index.ts +6 -5
- package/src/migrations/messages-migrations.ts +59 -0
- package/src/profiles/dsh-profile-launcher.ts +2 -1
- package/src/profiles/dsh-profile-runtime.ts +2 -1
- package/src/profiles/messages.ts +31 -0
- package/src/routes/backup.ts +18 -2
- package/src/routes/import.ts +3 -3
- package/src/routes/index.ts +1 -1
- package/src/routes/kit.ts +9 -3
- package/src/routes/market.ts +1 -1
- package/src/routes/messages.ts +106 -0
- package/src/routes/prefs.ts +31 -2
- package/src/routes/profiles.ts +12 -6
- package/src/routes/recovery.test.ts +405 -1
- package/src/routes/recovery.ts +477 -94
- package/src/routes/snapshots.ts +51 -4
- package/src/routes/sync.ts +82 -17
- package/src/schema/config.ts +53 -20
- package/src/schema/index.ts +7 -5
- package/src/schema/manifest-schema.test.ts +4 -4
- package/src/schema/manifest.ts +19 -18
- package/src/schema/messages.ts +120 -0
- package/src/schema/registry.test.ts +28 -7
- package/src/schema/section-registry.ts +96 -42
- package/src/schema/types.ts +2 -1
- package/src/schema/versions.ts +6 -5
- package/src/security/encryption.ts +16 -15
- package/src/security/integrity.ts +8 -7
- package/src/security/messages.ts +87 -0
- package/src/security/redaction.test.ts +17 -1
- package/src/security/redaction.ts +13 -0
- package/src/security/secret-scanner.ts +19 -12
- package/src/security/security.test.ts +47 -0
- package/src/security/vault.ts +2 -1
- package/src/security/zip-security.ts +4 -3
- package/src/sync/ancestor.ts +2 -1
- package/src/sync/autosync-config.ts +21 -0
- package/src/sync/autosync-scheduler.test.ts +63 -7
- package/src/sync/autosync-scheduler.ts +118 -78
- package/src/sync/backup-files.ts +3 -2
- package/src/sync/backup-scheduler.ts +6 -5
- package/src/sync/blob-store.ts +2 -1
- package/src/sync/git/git-transport.test.ts +170 -8
- package/src/sync/git/git-transport.ts +176 -2
- package/src/sync/github-auth.ts +10 -9
- package/src/sync/layout.ts +13 -12
- package/src/sync/messages.ts +153 -0
- package/src/sync/review-queue.ts +6 -5
- package/src/sync/s3/messages.ts +16 -0
- package/src/sync/s3/s3-providers.ts +9 -6
- package/src/sync/s3/s3-transport.ts +1 -1
- package/src/sync/s3/sigv4.ts +3 -1
- package/src/sync/snapshot-crypto.ts +6 -5
- package/src/sync/snapshot-json.ts +3 -2
- package/src/sync/sync-credentials.test.ts +119 -0
- package/src/sync/sync-engine-download-tombstone.test.ts +1 -1
- package/src/sync/sync-engine.test.ts +226 -8
- package/src/sync/sync-engine.ts +154 -52
- package/src/sync/sync-history.ts +7 -6
- package/src/sync/sync-selection.test.ts +12 -2
- package/src/sync/sync-selection.ts +18 -5
- package/src/sync/transport.test.ts +1 -0
- package/src/sync/ui-prefs.test.ts +44 -1
- package/src/sync/ui-prefs.ts +39 -0
- package/src/sync/webdav/webdav-transport.ts +4 -1
- package/src/ui/artifact-view.ts +4 -3
- package/src/ui/commands.test.ts +1 -0
- package/src/ui/commands.ts +11 -1
- package/src/ui/diff-view.test.ts +9 -1
- package/src/ui/diff-view.ts +22 -0
- package/src/ui/disk-usage-view.test.ts +113 -6
- package/src/ui/disk-usage-view.ts +63 -7
- package/src/ui/errors.ts +4 -3
- package/src/ui/export-flow.ts +26 -20
- package/src/ui/foreign-view.test.ts +43 -0
- package/src/ui/foreign-view.ts +30 -3
- package/src/ui/i18n.ts +131 -2
- package/src/ui/import-wizard.ts +18 -10
- package/src/ui/progress.ts +2 -2
- package/src/ui/report.ts +7 -6
- package/src/ui/rollback-view.test.ts +836 -0
- package/src/ui/rollback-view.ts +1405 -0
- package/src/ui/selection-model.test.ts +33 -1
- package/src/ui/selection-model.ts +53 -0
- package/src/ui/session-inventory-view.test.ts +109 -0
- package/src/ui/session-inventory-view.ts +138 -3
- package/src/ui/session-issue-labels.test.ts +51 -0
- package/src/ui/session-issue-labels.ts +70 -0
- package/src/ui/sync-settings-view.test.ts +36 -0
- package/src/ui/sync-settings-view.ts +49 -0
- package/src/ui/test-helpers.ts +2 -1
- package/src/ui/types.ts +10 -4
- package/src/utils/atomic-write.ts +6 -5
- package/src/utils/bundle-scan.test.ts +2 -2
- package/src/utils/bundle-scan.ts +12 -1
- package/src/utils/env-lock.ts +83 -49
- package/src/utils/json.ts +7 -5
- package/src/utils/messages-utils.ts +232 -0
- package/src/utils/proxy.test.ts +25 -0
- package/src/utils/proxy.ts +19 -5
- package/src/utils/recursive-walk.test.ts +43 -0
- package/src/utils/recursive-walk.ts +75 -1
- package/src/utils/session-log-repair.ts +10 -9
- package/src/utils/zip.ts +27 -26
- package/lib/core/checkpoint.d.ts +0 -362
- package/lib/core/checkpoint.js +0 -1309
- package/src/core/checkpoint.test.ts +0 -665
- package/src/core/checkpoint.ts +0 -1614
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
import { RunRegistry, type RunState } from './run-registry.ts';
|
|
30
30
|
import type { HostContext, Snapshot } from './types.ts';
|
|
31
31
|
import type { MsgFunc } from './messages.ts';
|
|
32
|
+
import { engineMsg } from './messages-core.ts';
|
|
32
33
|
|
|
33
34
|
/** 路由注入的恢复执行器(restore / rollback 引擎;测试注入 mock)。 */
|
|
34
35
|
export interface RecoveryExecutorFns {
|
|
@@ -195,8 +196,8 @@ export function createRecoveryOrchestrator(deps: RecoveryOrchestratorDeps): Reco
|
|
|
195
196
|
performRestore: executors.performRestore, performRollback: executors.performRollback,
|
|
196
197
|
}, true);
|
|
197
198
|
if (result === 'failed') {
|
|
198
|
-
runs.fail(runId, 'recovery
|
|
199
|
-
return { status: 400, body: { error: 'recovery
|
|
199
|
+
runs.fail(runId, engineMsg('core.recovery.executeFailed'));
|
|
200
|
+
return { status: 400, body: { error: engineMsg('core.recovery.executeFailed'), runId } };
|
|
200
201
|
}
|
|
201
202
|
runs.finish(runId, { decision: decisionKind, state: 'RECOVERING' });
|
|
202
203
|
return { status: 200, body: { ok: true, operationId, decision: decisionKind, state: 'RECOVERING', runId } };
|
|
@@ -290,10 +291,10 @@ export function createRecoveryOrchestrator(deps: RecoveryOrchestratorDeps): Reco
|
|
|
290
291
|
if (userConfirmed !== true) return { status: 400, body: { error: 'userConfirmed required' } };
|
|
291
292
|
const j = await store.loadActive(operationId);
|
|
292
293
|
if (j === null) return { status: 404, body: { error: 'operation not found' } };
|
|
293
|
-
if (j.state !== 'NEEDS_ATTENTION') return { status: 409, body: { error:
|
|
294
|
-
if (j.snapshotId === null || j.snapshotId === '') return { status: 400, body: { error: '
|
|
294
|
+
if (j.state !== 'NEEDS_ATTENTION') return { status: 409, body: { error: engineMsg('core.recovery.stateNotAllowed', { state: j.state, action: 'confirm' }) } };
|
|
295
|
+
if (j.snapshotId === null || j.snapshotId === '') return { status: 400, body: { error: engineMsg('core.recovery.noTrustedSnapshot') } };
|
|
295
296
|
const verdict = await validateSnapshotVerdict(j.snapshotId);
|
|
296
|
-
if (verdict !== 'TRUSTED_OPERATION_SNAPSHOT') return { status: 400, body: { error:
|
|
297
|
+
if (verdict !== 'TRUSTED_OPERATION_SNAPSHOT') return { status: 400, body: { error: engineMsg('core.recovery.snapshotUntrusted', { verdict }) } };
|
|
297
298
|
// journal 保持 NEEDS_ATTENTION(confirm 不转 RECOVERING)
|
|
298
299
|
return { status: 200, body: { ok: true, operationId, snapshotId: j.snapshotId, verdict } };
|
|
299
300
|
},
|
|
@@ -303,12 +304,12 @@ export function createRecoveryOrchestrator(deps: RecoveryOrchestratorDeps): Reco
|
|
|
303
304
|
if (userConfirmed !== true) return { status: 400, body: { error: 'userConfirmed required' } };
|
|
304
305
|
const j = await store.loadActive(operationId);
|
|
305
306
|
if (j === null) return { status: 404, body: { error: 'operation not found' } };
|
|
306
|
-
if (j.state !== 'NEEDS_ATTENTION' && j.state !== 'RECOVERING') return { status: 409, body: { error:
|
|
307
|
-
if (j.snapshotId === null || j.snapshotId === '') return { status: 400, body: { error: '
|
|
307
|
+
if (j.state !== 'NEEDS_ATTENTION' && j.state !== 'RECOVERING') return { status: 409, body: { error: engineMsg('core.recovery.stateNotAllowed', { state: j.state, action: 'execute' }) } };
|
|
308
|
+
if (j.snapshotId === null || j.snapshotId === '') return { status: 400, body: { error: engineMsg('core.recovery.noTrustedSnapshot') } };
|
|
308
309
|
const d = await decision(j);
|
|
309
|
-
if (d === 'needs-attention') return { status: 400, body: { error: 'recovery
|
|
310
|
+
if (d === 'needs-attention') return { status: 400, body: { error: engineMsg('core.recovery.needsAttentionNoAuto') } };
|
|
310
311
|
const verdict = await validateSnapshotVerdict(j.snapshotId);
|
|
311
|
-
if (verdict !== 'TRUSTED_OPERATION_SNAPSHOT') return { status: 400, body: { error:
|
|
312
|
+
if (verdict !== 'TRUSTED_OPERATION_SNAPSHOT') return { status: 400, body: { error: engineMsg('core.recovery.snapshotUntrusted', { verdict }) } };
|
|
312
313
|
return runExecution(operationId, j, d, makeExecutors);
|
|
313
314
|
},
|
|
314
315
|
|
|
@@ -316,13 +317,13 @@ export function createRecoveryOrchestrator(deps: RecoveryOrchestratorDeps): Reco
|
|
|
316
317
|
if (!isValidOperationId(operationId)) return { status: 400, body: { error: 'invalid operationId' } };
|
|
317
318
|
const j = await store.loadActive(operationId);
|
|
318
319
|
if (j === null) return { status: 404, body: { error: 'operation not found' } };
|
|
319
|
-
if (j.state !== 'RECOVERING') return { status: 409, body: { error:
|
|
320
|
-
if (j.snapshotId === null || j.snapshotId === '') return { status: 400, body: { error: '
|
|
320
|
+
if (j.state !== 'RECOVERING') return { status: 409, body: { error: engineMsg('core.recovery.stateNotAllowed', { state: j.state, action: 'verify' }) } };
|
|
321
|
+
if (j.snapshotId === null || j.snapshotId === '') return { status: 400, body: { error: engineMsg('core.recovery.noTrustedSnapshotShort') } };
|
|
321
322
|
let snap: Snapshot;
|
|
322
323
|
try {
|
|
323
324
|
snap = await new FileSnapshotStore({ dir: snapshotsDir }).load(j.snapshotId);
|
|
324
325
|
} catch {
|
|
325
|
-
return { status: 400, body: { error: '
|
|
326
|
+
return { status: 400, body: { error: engineMsg('core.recovery.snapshotLoadFailed') } };
|
|
326
327
|
}
|
|
327
328
|
const verification = await verifyRecovery(snap, host, { snapshotsRoot: snapshotsDir, environmentFingerprint: getEnvironmentFingerprint(), expectedOperationId: j.operationId });
|
|
328
329
|
const terminal = recoveryTerminalState(verification.verdict);
|
|
@@ -351,12 +352,12 @@ export function createRecoveryOrchestrator(deps: RecoveryOrchestratorDeps): Reco
|
|
|
351
352
|
if (userConfirmed !== true) return { status: 400, body: { error: 'userConfirmed required' } };
|
|
352
353
|
const j = await store.loadActive(operationId);
|
|
353
354
|
if (j === null) return { status: 404, body: { error: 'operation not found' } };
|
|
354
|
-
if (j.state !== 'NEEDS_ATTENTION') return { status: 409, body: { error:
|
|
355
|
-
if (j.snapshotId === null || j.snapshotId === '') return { status: 400, body: { error: '
|
|
355
|
+
if (j.state !== 'NEEDS_ATTENTION') return { status: 409, body: { error: engineMsg('core.recovery.stateNotAllowed', { state: j.state, action: 'retry' }) } };
|
|
356
|
+
if (j.snapshotId === null || j.snapshotId === '') return { status: 400, body: { error: engineMsg('core.recovery.noTrustedSnapshotShort') } };
|
|
356
357
|
const d = await decision(j);
|
|
357
|
-
if (d === 'needs-attention') return { status: 400, body: { error: 'recovery
|
|
358
|
+
if (d === 'needs-attention') return { status: 400, body: { error: engineMsg('core.recovery.needsAttentionNoRetry') } };
|
|
358
359
|
const verdict = await validateSnapshotVerdict(j.snapshotId);
|
|
359
|
-
if (verdict !== 'TRUSTED_OPERATION_SNAPSHOT') return { status: 400, body: { error:
|
|
360
|
+
if (verdict !== 'TRUSTED_OPERATION_SNAPSHOT') return { status: 400, body: { error: engineMsg('core.recovery.snapshotUntrusted', { verdict }) } };
|
|
360
361
|
return runExecution(operationId, j, d, makeExecutors);
|
|
361
362
|
},
|
|
362
363
|
|
|
@@ -367,7 +368,7 @@ export function createRecoveryOrchestrator(deps: RecoveryOrchestratorDeps): Reco
|
|
|
367
368
|
if (j === null) return { status: 404, body: { error: 'operation not found' } };
|
|
368
369
|
// dismiss → quarantine(用户放弃;不删除 snapshot/journal evidence,不强行 RECOVERED)
|
|
369
370
|
const result = await executeRecovery(store, { operationId, action: 'dismiss', snapshotId: j.snapshotId }, true);
|
|
370
|
-
if (result === 'failed') return { status: 400, body: { error: '
|
|
371
|
+
if (result === 'failed') return { status: 400, body: { error: engineMsg('core.recovery.dismissFailed') } };
|
|
371
372
|
// issue #56:放弃恢复 = 该 incident 结案 —— 若没有别的未解决 incident,必须同时解除保护。
|
|
372
373
|
// 此前这里只 quarantine 不清 SAFE MODE,而唯一的清除点挂在 verify(ROLLED_BACK) 上,
|
|
373
374
|
// 事务已被移出 active/ ⇒ 那条分支永远走不到:界面显示「暂无待处理」,写操作却持续 423,
|
|
@@ -429,7 +430,7 @@ export function createRecoveryOrchestrator(deps: RecoveryOrchestratorDeps): Reco
|
|
|
429
430
|
}
|
|
430
431
|
if (!res.ok) {
|
|
431
432
|
// 拒绝是正常结果(活锁/无法证明 stale/二次验证失败),不是 500
|
|
432
|
-
return { status: 200, body: { ok: false, removed: false, state: res.state, reason: res.detail ?? '
|
|
433
|
+
return { status: 200, body: { ok: false, removed: false, state: res.state, reason: res.detail ?? engineMsg('core.recovery.lockNotStale') } };
|
|
433
434
|
}
|
|
434
435
|
// 回收成功后 stale 分类已消失;锁态交由客户端重新拉取 status 刷新
|
|
435
436
|
return { status: 200, body: { ok: true, removed: res.removed, state: res.state } };
|
package/src/core/reinstall.ts
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* 显式亮出独立开关删除。
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
+
import { engineMsg } from './messages-core.ts';
|
|
24
25
|
import os from 'node:os'
|
|
25
26
|
import path from 'node:path'
|
|
26
27
|
|
|
@@ -40,39 +41,43 @@ export interface ReinstallItemDef {
|
|
|
40
41
|
defaultOn: boolean
|
|
41
42
|
}
|
|
42
43
|
|
|
44
|
+
/**
|
|
45
|
+
* 类别清单。label/desc 写成 **getter**(访问时才解析语言):本常量在模块加载期求值,
|
|
46
|
+
* 若直接调用 engineMsg 会把文案冻结成加载时的语言 —— 正是本次 i18n 改造要修的根因。
|
|
47
|
+
*/
|
|
43
48
|
export const REINSTALL_ITEMS: readonly ReinstallItemDef[] = [
|
|
44
49
|
{
|
|
45
50
|
id: 'program',
|
|
46
|
-
label
|
|
47
|
-
desc
|
|
51
|
+
get label() { return engineMsg('core.reinstall.programLabel') },
|
|
52
|
+
get desc() { return engineMsg('core.reinstall.programDesc') },
|
|
48
53
|
destructive: false,
|
|
49
54
|
defaultOn: true,
|
|
50
55
|
},
|
|
51
56
|
{
|
|
52
57
|
id: 'cache',
|
|
53
|
-
label
|
|
54
|
-
desc
|
|
58
|
+
get label() { return engineMsg('core.reinstall.cacheLabel') },
|
|
59
|
+
get desc() { return engineMsg('core.reinstall.cacheDesc') },
|
|
55
60
|
destructive: false,
|
|
56
61
|
defaultOn: true,
|
|
57
62
|
},
|
|
58
63
|
{
|
|
59
64
|
id: 'settings',
|
|
60
|
-
label
|
|
61
|
-
desc
|
|
65
|
+
get label() { return engineMsg('core.reinstall.settingsLabel') },
|
|
66
|
+
get desc() { return engineMsg('core.reinstall.settingsDesc') },
|
|
62
67
|
destructive: true,
|
|
63
68
|
defaultOn: false,
|
|
64
69
|
},
|
|
65
70
|
{
|
|
66
71
|
id: 'plugins',
|
|
67
|
-
label
|
|
68
|
-
desc
|
|
72
|
+
get label() { return engineMsg('core.reinstall.pluginsLabel') },
|
|
73
|
+
get desc() { return engineMsg('core.reinstall.pluginsDesc') },
|
|
69
74
|
destructive: true,
|
|
70
75
|
defaultOn: false,
|
|
71
76
|
},
|
|
72
77
|
{
|
|
73
78
|
id: 'data',
|
|
74
|
-
label
|
|
75
|
-
desc
|
|
79
|
+
get label() { return engineMsg('core.reinstall.dataLabel') },
|
|
80
|
+
get desc() { return engineMsg('core.reinstall.dataDesc') },
|
|
76
81
|
destructive: true,
|
|
77
82
|
defaultOn: false,
|
|
78
83
|
},
|
|
@@ -216,22 +221,22 @@ export async function buildReinstallPlan(
|
|
|
216
221
|
const globalRoot = await resolveNpmGlobalRoot(exec)
|
|
217
222
|
const dshGlobalDir = path.join(globalRoot, '@deepseek-ai', 'dsh')
|
|
218
223
|
steps.push({
|
|
219
|
-
label: '
|
|
224
|
+
label: engineMsg('core.reinstall.stepUninstallGlobal'),
|
|
220
225
|
command: 'npm uninstall -g @deepseek-ai/dsh',
|
|
221
226
|
dangerous: false,
|
|
222
227
|
})
|
|
223
228
|
steps.push({
|
|
224
|
-
label: '
|
|
229
|
+
label: engineMsg('core.reinstall.stepCleanGlobal'),
|
|
225
230
|
command: rmCommand(dshGlobalDir, platform),
|
|
226
231
|
dangerous: false,
|
|
227
232
|
})
|
|
228
233
|
steps.push({
|
|
229
|
-
label:
|
|
234
|
+
label: engineMsg('core.reinstall.stepInstallGlobal', { version: versionSpec }),
|
|
230
235
|
command: `npm install -g @deepseek-ai/dsh@${versionSpec}`,
|
|
231
236
|
dangerous: false,
|
|
232
237
|
})
|
|
233
238
|
steps.push({
|
|
234
|
-
label: '
|
|
239
|
+
label: engineMsg('core.reinstall.stepVerifyVersion'),
|
|
235
240
|
command: 'dsh --version',
|
|
236
241
|
dangerous: false,
|
|
237
242
|
})
|
|
@@ -241,13 +246,13 @@ export async function buildReinstallPlan(
|
|
|
241
246
|
const pnpmStore = resolvePnpmStoreDir(env, platform)
|
|
242
247
|
if (pnpmStore !== null) {
|
|
243
248
|
steps.push({
|
|
244
|
-
label: '
|
|
249
|
+
label: engineMsg('core.reinstall.stepCleanPnpmStore'),
|
|
245
250
|
command: rmCommand(pnpmStore, platform),
|
|
246
251
|
dangerous: false,
|
|
247
252
|
})
|
|
248
253
|
}
|
|
249
254
|
steps.push({
|
|
250
|
-
label: '
|
|
255
|
+
label: engineMsg('core.reinstall.stepCleanNpmCache'),
|
|
251
256
|
command: 'npm cache clean --force',
|
|
252
257
|
dangerous: false,
|
|
253
258
|
})
|
|
@@ -259,33 +264,33 @@ export async function buildReinstallPlan(
|
|
|
259
264
|
// 但历史会话/凭据在此备份,作为抢救兜底)。
|
|
260
265
|
if (wipeConfig) {
|
|
261
266
|
steps.push({
|
|
262
|
-
label: '
|
|
267
|
+
label: engineMsg('core.reinstall.stepBackupHome'),
|
|
263
268
|
command: copyDirCommand(dshHome, `${dshHome}.reinstall-backup`, platform),
|
|
264
269
|
dangerous: true,
|
|
265
270
|
})
|
|
266
271
|
}
|
|
267
272
|
if (selection.has('settings')) {
|
|
268
273
|
steps.push({
|
|
269
|
-
label: '
|
|
274
|
+
label: engineMsg('core.reinstall.stepWipeSettings'),
|
|
270
275
|
command: rmCommand(path.join(dshHome, 'settings.yaml'), platform),
|
|
271
276
|
dangerous: true,
|
|
272
277
|
})
|
|
273
278
|
}
|
|
274
279
|
if (selection.has('plugins')) {
|
|
275
280
|
steps.push({
|
|
276
|
-
label: '
|
|
281
|
+
label: engineMsg('core.reinstall.stepWipePlugins'),
|
|
277
282
|
command: rmCommand(path.join(dshHome, 'profiles'), platform),
|
|
278
283
|
dangerous: true,
|
|
279
284
|
})
|
|
280
285
|
}
|
|
281
286
|
if (selection.has('data')) {
|
|
282
287
|
steps.push({
|
|
283
|
-
label: '
|
|
288
|
+
label: engineMsg('core.reinstall.stepWipeSessions'),
|
|
284
289
|
command: rmCommand(path.join(dshHome, 'sessions'), platform),
|
|
285
290
|
dangerous: true,
|
|
286
291
|
})
|
|
287
292
|
steps.push({
|
|
288
|
-
label: '
|
|
293
|
+
label: engineMsg('core.reinstall.stepWipeCredentials'),
|
|
289
294
|
command: rmCommand(path.join(dshHome, '.credentials.yaml'), platform),
|
|
290
295
|
dangerous: true,
|
|
291
296
|
})
|
package/src/core/restore.ts
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* cordis.patch.yml);workspaceRecord 离线无法经 DSH storages 恢复 → 如实提示。
|
|
21
21
|
* 旧快照(无 beforePlugins / hostFileBackups)兼容:缺基线的部分只提示不动作。
|
|
22
22
|
*/
|
|
23
|
+
import { engineMsg } from './messages-core.ts';
|
|
23
24
|
import fs from 'node:fs/promises';
|
|
24
25
|
import path from 'node:path';
|
|
25
26
|
|
|
@@ -281,10 +282,10 @@ export interface RestoreSnapshotValidation {
|
|
|
281
282
|
/** 递归检查快照目录内关键文件非 symlink(F25:拒绝 symlink 化 metadata/blob 写穿)。 */
|
|
282
283
|
async function assertNoSnapshotSymlink(dir: string, rel: string): Promise<string | null> {
|
|
283
284
|
const p = path.join(dir, rel);
|
|
284
|
-
if (!isWithinHome(dir, p)) return
|
|
285
|
+
if (!isWithinHome(dir, p)) return engineMsg('core.restore.pathEscaped', { rel });
|
|
285
286
|
try {
|
|
286
287
|
const st = await fs.lstat(p);
|
|
287
|
-
if (st.isSymbolicLink()) return
|
|
288
|
+
if (st.isSymbolicLink()) return engineMsg('core.restore.symlinkCriticalFile', { rel });
|
|
288
289
|
return null;
|
|
289
290
|
} catch {
|
|
290
291
|
// 缺失由 verify/logic 处理(此处不判缺失)
|
|
@@ -310,12 +311,12 @@ export async function validateSnapshotForRestore(
|
|
|
310
311
|
env?: { environmentFingerprint?: string },
|
|
311
312
|
): Promise<RestoreSnapshotValidation> {
|
|
312
313
|
const id = path.basename(snapshotDir);
|
|
313
|
-
if (!isValidSnapshotId(id)) return { verdict: 'INVALID', reason: '
|
|
314
|
+
if (!isValidSnapshotId(id)) return { verdict: 'INVALID', reason: engineMsg('core.restore.invalidSnapshotId') };
|
|
314
315
|
|
|
315
316
|
// 目录边界:snapshotDir 必须紧邻 snapshotsRoot 下的一层
|
|
316
317
|
const rel = path.relative(snapshotsRoot, snapshotDir).split(path.sep).join('/');
|
|
317
318
|
if (rel === '' || rel.includes('/') || !isWithinHome(snapshotsRoot, snapshotDir)) {
|
|
318
|
-
return { verdict: 'INVALID', reason: '
|
|
319
|
+
return { verdict: 'INVALID', reason: engineMsg('core.restore.snapshotDirOutOfBounds') };
|
|
319
320
|
}
|
|
320
321
|
|
|
321
322
|
// 关键文件 symlink 检查(F25)
|
|
@@ -339,7 +340,7 @@ export async function validateSnapshotForRestore(
|
|
|
339
340
|
const blobEntries = await fs.readdir(blobsDir, { withFileTypes: true }).catch(() => [] as Array<{ name: string; isSymbolicLink(): boolean }>);
|
|
340
341
|
for (const be of blobEntries) {
|
|
341
342
|
if (be.isSymbolicLink()) {
|
|
342
|
-
return { verdict: 'UNSAFE_PATH', reason:
|
|
343
|
+
return { verdict: 'UNSAFE_PATH', reason: engineMsg('core.restore.blobSymlink', { name: be.name }) };
|
|
343
344
|
}
|
|
344
345
|
}
|
|
345
346
|
|
|
@@ -352,9 +353,9 @@ export async function validateSnapshotForRestore(
|
|
|
352
353
|
} catch {
|
|
353
354
|
/* verifySnapshot 已报缺失 */
|
|
354
355
|
}
|
|
355
|
-
if (snapshot === null || typeof snapshot !== 'object') return { verdict: 'CORRUPT', reason: '
|
|
356
|
+
if (snapshot === null || typeof snapshot !== 'object') return { verdict: 'CORRUPT', reason: engineMsg('core.restore.snapshotJsonUnparsable') };
|
|
356
357
|
if (!manifestExists && snapshot.readiness === undefined) {
|
|
357
|
-
return { verdict: 'LEGACY_REQUIRES_CONFIRMATION', reason: '
|
|
358
|
+
return { verdict: 'LEGACY_REQUIRES_CONFIRMATION', reason: engineMsg('core.restore.legacyNeedsConfirm') };
|
|
358
359
|
}
|
|
359
360
|
|
|
360
361
|
// 结构 + 完整性(磁盘重读,不信任内存)
|
|
@@ -364,18 +365,18 @@ export async function validateSnapshotForRestore(
|
|
|
364
365
|
// 环境绑定(WRONG_ENVIRONMENT)
|
|
365
366
|
if (env?.environmentFingerprint && snapshot.readiness === 'READY' && snapshot.environmentFingerprint
|
|
366
367
|
&& snapshot.environmentFingerprint !== env.environmentFingerprint) {
|
|
367
|
-
return { verdict: 'WRONG_ENVIRONMENT', reason: '
|
|
368
|
+
return { verdict: 'WRONG_ENVIRONMENT', reason: engineMsg('core.restore.envFingerprintMismatch') };
|
|
368
369
|
}
|
|
369
370
|
|
|
370
371
|
// provenance 分类
|
|
371
372
|
const hasBinding = snapshot.operationId !== undefined || snapshot.environmentFingerprint !== undefined || snapshot.ownerInstanceId !== undefined;
|
|
372
373
|
if (snapshot.readiness === undefined) {
|
|
373
|
-
return { verdict: 'LEGACY_REQUIRES_CONFIRMATION', reason: '
|
|
374
|
+
return { verdict: 'LEGACY_REQUIRES_CONFIRMATION', reason: engineMsg('core.restore.legacyNoReadiness') };
|
|
374
375
|
}
|
|
375
376
|
if (!hasBinding) {
|
|
376
|
-
return { verdict: 'TRUSTED_MANUAL_LOCAL', reason: '
|
|
377
|
+
return { verdict: 'TRUSTED_MANUAL_LOCAL', reason: engineMsg('core.restore.trustedManualLocal') };
|
|
377
378
|
}
|
|
378
|
-
return { verdict: 'TRUSTED_OPERATION_SNAPSHOT', reason: '
|
|
379
|
+
return { verdict: 'TRUSTED_OPERATION_SNAPSHOT', reason: engineMsg('core.restore.trustedOperationSnapshot') };
|
|
379
380
|
}
|
|
380
381
|
|
|
381
382
|
|
|
@@ -434,9 +435,11 @@ export async function planRestore(opts: RestoreOptions): Promise<RestorePlan> {
|
|
|
434
435
|
const currentExists = await fileExists(abs);
|
|
435
436
|
if (backup.existed) {
|
|
436
437
|
if (backup.blobPath === '') {
|
|
438
|
+
// unreadable = 拍快照时文件存在但读不出来:本次**只跳过并如实说明**,绝不因为
|
|
439
|
+
// 「快照里没有字节」就把它当成「当时不存在」(那会在恢复时删掉用户真实的文件)。
|
|
437
440
|
actions.push({
|
|
438
441
|
kind: 'skip',
|
|
439
|
-
description: msg('restore.hostNoBlobInfo', { path: backup.relPath }),
|
|
442
|
+
description: msg(backup.unreadable === true ? 'restore.hostUnreadable' : 'restore.hostNoBlobInfo', { path: backup.relPath }),
|
|
440
443
|
target: backup.relPath,
|
|
441
444
|
});
|
|
442
445
|
continue;
|
|
@@ -850,7 +853,7 @@ export async function listSnapshots(
|
|
|
850
853
|
// 其余(EACCES / EPERM / ENOTDIR…)经 out.unreadable 回传,由调用方如实展示。
|
|
851
854
|
const code = (error as { code?: string }).code;
|
|
852
855
|
if (code !== 'ENOENT' && out !== undefined) {
|
|
853
|
-
out.unreadable = '
|
|
856
|
+
out.unreadable = engineMsg('core.restore.snapshotDirUnreadable', { code: String(code ?? 'unknown'), dir });
|
|
854
857
|
}
|
|
855
858
|
return [];
|
|
856
859
|
}
|
|
@@ -891,11 +894,11 @@ export function isValidSnapshotId(id: unknown): id is string {
|
|
|
891
894
|
* 不存在视为成功(幂等)。返回是否实际删除。 */
|
|
892
895
|
export async function deleteSnapshot(snapshotsDir: string, id: string): Promise<boolean> {
|
|
893
896
|
if (!isValidSnapshotId(id)) {
|
|
894
|
-
throw new Error(
|
|
897
|
+
throw new Error(engineMsg('core.restore.illegalSnapshotId', { id: JSON.stringify(id) }));
|
|
895
898
|
}
|
|
896
899
|
const target = path.join(snapshotsDir, id);
|
|
897
900
|
if (!target.startsWith(path.resolve(snapshotsDir) + path.sep)) {
|
|
898
|
-
throw new Error(
|
|
901
|
+
throw new Error(engineMsg('core.restore.snapshotPathEscaped', { id }));
|
|
899
902
|
}
|
|
900
903
|
try {
|
|
901
904
|
await fs.stat(target);
|
|
@@ -910,12 +913,12 @@ export async function deleteSnapshot(snapshotsDir: string, id: string): Promise<
|
|
|
910
913
|
* 只接受合法 id;快照不存在抛错。 */
|
|
911
914
|
export async function setSnapshotPinned(snapshotsDir: string, id: string, pinned: boolean): Promise<boolean> {
|
|
912
915
|
if (!isValidSnapshotId(id)) {
|
|
913
|
-
throw new Error(
|
|
916
|
+
throw new Error(engineMsg('core.restore.illegalSnapshotId', { id: JSON.stringify(id) }));
|
|
914
917
|
}
|
|
915
918
|
const file = path.join(snapshotsDir, id, 'snapshot.json');
|
|
916
919
|
const resolvedRoot = path.resolve(snapshotsDir) + path.sep;
|
|
917
920
|
if (!file.startsWith(resolvedRoot)) {
|
|
918
|
-
throw new Error(
|
|
921
|
+
throw new Error(engineMsg('core.restore.snapshotPathEscaped', { id }));
|
|
919
922
|
}
|
|
920
923
|
const snapshot = parseJsonSafe(await fs.readFile(file, 'utf8')) as Snapshot;
|
|
921
924
|
snapshot.pinned = pinned;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 回滚点「单文件前后对照」(`RollbackPointEngine.fileDiff`)单测。
|
|
3
|
+
*
|
|
4
|
+
* 纪律与本仓既有 core 测试同款:真实临时目录 + 真实读写,不 mock 文件系统。
|
|
5
|
+
*
|
|
6
|
+
* 覆盖的是这个端点的**收窄与诚实性**(它是新的只读读取面,用户 2026-10-12 的「查看」数据源):
|
|
7
|
+
* · 方向必须是 before = **当前磁盘** / after = **回退点快照**(读反了整屏差异都是反的);
|
|
8
|
+
* · 只认**记录在案**的路径(未记录 ⇒ file-not-recorded,绝不变成任意路径读取器);
|
|
9
|
+
* · 二进制 / 超限 / 读不出一律结构化 reason + 空 hunks(「不知道」不得显示成「没有差异」);
|
|
10
|
+
* · 全程**零写入**(工作区字节不变、对象仓不新增)。
|
|
11
|
+
*/
|
|
12
|
+
import test from 'node:test'
|
|
13
|
+
import assert from 'node:assert/strict'
|
|
14
|
+
import fs from 'node:fs/promises'
|
|
15
|
+
import os from 'node:os'
|
|
16
|
+
import path from 'node:path'
|
|
17
|
+
|
|
18
|
+
import { RollbackPointEngine } from './rollback-point.ts'
|
|
19
|
+
|
|
20
|
+
interface Fixture {
|
|
21
|
+
root: string
|
|
22
|
+
homeDir: string
|
|
23
|
+
dataDir: string
|
|
24
|
+
workspaceDir: string
|
|
25
|
+
cleanup: () => Promise<void>
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function makeFixture(): Promise<Fixture> {
|
|
29
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'dsh-cm-rollback-file-diff-'))
|
|
30
|
+
const homeDir = path.join(root, 'home')
|
|
31
|
+
const dataDir = path.join(homeDir, 'dsh-config-manager')
|
|
32
|
+
const workspaceDir = path.join(root, 'ws')
|
|
33
|
+
await fs.mkdir(workspaceDir, { recursive: true })
|
|
34
|
+
await fs.writeFile(path.join(workspaceDir, 'a.txt'), 'A1\nA2\n')
|
|
35
|
+
return { root, homeDir, dataDir, workspaceDir, cleanup: async () => { await fs.rm(root, { recursive: true, force: true }) } }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function makeEngine(fx: Fixture): RollbackPointEngine {
|
|
39
|
+
return new RollbackPointEngine({ dataDir: fx.dataDir, homeDir: fx.homeDir, sessionQuiescentMs: 0 })
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 留一个点(只带工作区态),返回点 id。 */
|
|
43
|
+
async function capture(engine: RollbackPointEngine, fx: Fixture): Promise<string> {
|
|
44
|
+
const captured = await engine.capturePoint({ chunks: [fx.workspaceDir], sessionId: 'session-1111', turnSeq: 1 })
|
|
45
|
+
const id = captured.point?.id
|
|
46
|
+
assert.ok(typeof id === 'string' && id !== '', 'fixture 必须先真的留下一个点:' + JSON.stringify(captured))
|
|
47
|
+
return id as string
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
test('fileDiff:before = 当前磁盘、after = 回退点快照(方向不许读反)', async () => {
|
|
51
|
+
const fx = await makeFixture()
|
|
52
|
+
try {
|
|
53
|
+
const engine = makeEngine(fx)
|
|
54
|
+
const id = await capture(engine, fx)
|
|
55
|
+
const file = path.join(fx.workspaceDir, 'a.txt')
|
|
56
|
+
// 留点**之后**改文件:回退会把 A2 删掉、把 A3 加回来
|
|
57
|
+
await fs.writeFile(file, 'A1\nA3\n')
|
|
58
|
+
const diff = await engine.fileDiff(id, file)
|
|
59
|
+
assert.equal(diff.ok, true, JSON.stringify(diff))
|
|
60
|
+
assert.equal(diff.status, 'overwrite')
|
|
61
|
+
assert.equal(diff.before.exists, true)
|
|
62
|
+
assert.equal(diff.after.exists, true)
|
|
63
|
+
assert.equal(diff.identical, false)
|
|
64
|
+
// 方向证据:删掉的是当前独有的 A3,新增的是快照里的 A2
|
|
65
|
+
assert.equal(diff.removed, 1)
|
|
66
|
+
assert.equal(diff.added, 1)
|
|
67
|
+
const texts = diff.hunks.flatMap((hunk) => hunk.lines.map((line) => line.kind + ':' + line.text))
|
|
68
|
+
assert.ok(texts.includes('del:A3'), '当前独有的行必须是删除:' + JSON.stringify(texts))
|
|
69
|
+
assert.ok(texts.includes('add:A2'), '快照独有的行必须是新增:' + JSON.stringify(texts))
|
|
70
|
+
} finally {
|
|
71
|
+
await fx.cleanup()
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test('fileDiff:当前文件不存在(ENOENT)→ status=create,新增 = 快照全部行', async () => {
|
|
76
|
+
const fx = await makeFixture()
|
|
77
|
+
try {
|
|
78
|
+
const engine = makeEngine(fx)
|
|
79
|
+
const id = await capture(engine, fx)
|
|
80
|
+
const file = path.join(fx.workspaceDir, 'a.txt')
|
|
81
|
+
await fs.rm(file)
|
|
82
|
+
const diff = await engine.fileDiff(id, file)
|
|
83
|
+
assert.equal(diff.ok, true)
|
|
84
|
+
assert.equal(diff.status, 'create')
|
|
85
|
+
assert.equal(diff.before.exists, false)
|
|
86
|
+
assert.equal(diff.after.exists, true)
|
|
87
|
+
assert.equal(diff.removed, 0)
|
|
88
|
+
assert.equal(diff.added, diff.after.lines)
|
|
89
|
+
} finally {
|
|
90
|
+
await fx.cleanup()
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
test('fileDiff:未记录在案的路径 → file-not-recorded(这条端点不是任意路径读取器)', async () => {
|
|
95
|
+
const fx = await makeFixture()
|
|
96
|
+
try {
|
|
97
|
+
const engine = makeEngine(fx)
|
|
98
|
+
const id = await capture(engine, fx)
|
|
99
|
+
const outside = path.join(fx.root, 'outside.txt')
|
|
100
|
+
await fs.writeFile(outside, 'secret\n')
|
|
101
|
+
const diff = await engine.fileDiff(id, outside)
|
|
102
|
+
assert.equal(diff.ok, false)
|
|
103
|
+
assert.equal(diff.code, 'file-not-recorded')
|
|
104
|
+
assert.deepEqual(diff.hunks, [], '未记录的路径不得给出任何差异内容')
|
|
105
|
+
} finally {
|
|
106
|
+
await fx.cleanup()
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
test('fileDiff:二进制 → reason=binary 且 hunks 空(不把「不知道」说成「没有差异」)', async () => {
|
|
111
|
+
const fx = await makeFixture()
|
|
112
|
+
try {
|
|
113
|
+
const engine = makeEngine(fx)
|
|
114
|
+
const binary = path.join(fx.workspaceDir, 'blob.bin')
|
|
115
|
+
await fs.writeFile(binary, Buffer.from([0, 1, 2, 3, 0, 9]))
|
|
116
|
+
const id = await capture(engine, fx)
|
|
117
|
+
await fs.writeFile(binary, Buffer.from([0, 5, 6, 7, 0, 9]))
|
|
118
|
+
const diff = await engine.fileDiff(id, binary)
|
|
119
|
+
assert.equal(diff.ok, true)
|
|
120
|
+
assert.equal(diff.reason, 'binary')
|
|
121
|
+
assert.deepEqual(diff.hunks, [])
|
|
122
|
+
assert.equal(diff.identical, false, '无法逐行对比不得被说成「一致」')
|
|
123
|
+
} finally {
|
|
124
|
+
await fx.cleanup()
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
test('fileDiff:未知 id → record-not-found(结构化码,不抛)', async () => {
|
|
129
|
+
const fx = await makeFixture()
|
|
130
|
+
try {
|
|
131
|
+
const engine = makeEngine(fx)
|
|
132
|
+
await capture(engine, fx)
|
|
133
|
+
const diff = await engine.fileDiff('nope', path.join(fx.workspaceDir, 'a.txt'))
|
|
134
|
+
assert.equal(diff.ok, false)
|
|
135
|
+
assert.equal(diff.code, 'record-not-found')
|
|
136
|
+
} finally {
|
|
137
|
+
await fx.cleanup()
|
|
138
|
+
}
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
test('fileDiff:零写入 —— 对照前后工作区字节与对象仓条目都不变', async () => {
|
|
142
|
+
const fx = await makeFixture()
|
|
143
|
+
try {
|
|
144
|
+
const engine = makeEngine(fx)
|
|
145
|
+
const id = await capture(engine, fx)
|
|
146
|
+
const file = path.join(fx.workspaceDir, 'a.txt')
|
|
147
|
+
await fs.writeFile(file, 'A1\nCHANGED\n')
|
|
148
|
+
const before = await fs.readFile(file)
|
|
149
|
+
const objectsDir = path.join(engine.storeDir, 'objects')
|
|
150
|
+
const objectsBefore = await fs.readdir(objectsDir)
|
|
151
|
+
const ledgerBefore = await fs.readFile(path.join(engine.storeDir, 'index.json'), 'utf8')
|
|
152
|
+
await engine.fileDiff(id, file)
|
|
153
|
+
assert.deepEqual(await fs.readFile(file), before, '只读对照不得改动工作区文件')
|
|
154
|
+
assert.deepEqual(await fs.readdir(objectsDir), objectsBefore, '只读对照不得写对象仓')
|
|
155
|
+
assert.equal(await fs.readFile(path.join(engine.storeDir, 'index.json'), 'utf8'), ledgerBefore, '只读对照不得写台账')
|
|
156
|
+
} finally {
|
|
157
|
+
await fx.cleanup()
|
|
158
|
+
}
|
|
159
|
+
})
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 回合自动留点开关的**进程内**语义(issue #88 / #89,src/core/rollback-gate.ts)。
|
|
3
|
+
*
|
|
4
|
+
* 钉住四条:① 缺省关闭;② 变化才通知(同值不通知,避免写偏好时抖动 attach/detach);
|
|
5
|
+
* ③ 监听器抛错不影响开关本身与其它监听器;④ 解绑幂等。
|
|
6
|
+
*
|
|
7
|
+
* 为什么值得单测:宿主那条 `session/event` 订阅链的 attach/detach 完全由这里驱动 ——
|
|
8
|
+
* 一旦「关了还通知」或「通知丢了」,就是「界面说关了、宿主还在每回合写台账」这类静默故障。
|
|
9
|
+
*/
|
|
10
|
+
import test from 'node:test'
|
|
11
|
+
import assert from 'node:assert/strict'
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
isRollbackEnabled,
|
|
15
|
+
onRollbackEnabledChange,
|
|
16
|
+
resetRollbackGateForTest,
|
|
17
|
+
setRollbackEnabled,
|
|
18
|
+
} from './rollback-gate.ts'
|
|
19
|
+
|
|
20
|
+
test('G-01 缺省关闭 + 置真置假都生效', () => {
|
|
21
|
+
resetRollbackGateForTest()
|
|
22
|
+
assert.equal(isRollbackEnabled(), false, '缺省必须是关闭(fail-closed)')
|
|
23
|
+
setRollbackEnabled(true)
|
|
24
|
+
assert.equal(isRollbackEnabled(), true)
|
|
25
|
+
setRollbackEnabled(false)
|
|
26
|
+
assert.equal(isRollbackEnabled(), false)
|
|
27
|
+
resetRollbackGateForTest()
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test('G-02 同值不通知;变化按顺序通知一次', () => {
|
|
31
|
+
resetRollbackGateForTest()
|
|
32
|
+
const seen: boolean[] = []
|
|
33
|
+
const off = onRollbackEnabledChange((value) => { seen.push(value) })
|
|
34
|
+
setRollbackEnabled(false) // 与缺省同值 → 不通知
|
|
35
|
+
assert.deepEqual(seen, [], '同值不得通知')
|
|
36
|
+
setRollbackEnabled(true)
|
|
37
|
+
setRollbackEnabled(true) // 重复同值
|
|
38
|
+
setRollbackEnabled(false)
|
|
39
|
+
assert.deepEqual(seen, [true, false], '只在真实变化时通知一次')
|
|
40
|
+
off()
|
|
41
|
+
resetRollbackGateForTest()
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('G-03 监听器抛错不影响开关与其它监听器', () => {
|
|
45
|
+
resetRollbackGateForTest()
|
|
46
|
+
const seen: boolean[] = []
|
|
47
|
+
const offBad = onRollbackEnabledChange(() => { throw new Error('boom') })
|
|
48
|
+
const offGood = onRollbackEnabledChange((value) => { seen.push(value) })
|
|
49
|
+
setRollbackEnabled(true)
|
|
50
|
+
assert.equal(isRollbackEnabled(), true, '坏监听器不得拦住状态切换')
|
|
51
|
+
assert.deepEqual(seen, [true], '坏监听器不得吃掉后面监听器的通知')
|
|
52
|
+
offBad()
|
|
53
|
+
offGood()
|
|
54
|
+
resetRollbackGateForTest()
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('G-04 解绑幂等:解绑后再改状态不通知已解绑者', () => {
|
|
58
|
+
resetRollbackGateForTest()
|
|
59
|
+
const seen: boolean[] = []
|
|
60
|
+
const off = onRollbackEnabledChange((value) => { seen.push(value) })
|
|
61
|
+
off()
|
|
62
|
+
off() // 幂等
|
|
63
|
+
setRollbackEnabled(true)
|
|
64
|
+
assert.deepEqual(seen, [])
|
|
65
|
+
resetRollbackGateForTest()
|
|
66
|
+
})
|